/* v2_app/assets/styles.css — app styling. Dash auto-serves this assets/ folder. */

/* ============================================================================
   Design tokens — mirror of theme.py for the CSS layer.
   Source of truth is Source_Code/_MAIN/v2_app/theme.py; these :root variables
   exist so plain CSS rules (media queries, hover, utilities) can use the same
   values. If a token changes in theme.py, change it here too.
   ============================================================================ */
:root {
    /* Brand + interactive */
    --forest: #3d4c41;
    --forest-dark: #2f3a32;
    --forest-tint: #eef1ee;
    --rust: #c56b24;
    --rust-dark: #a85718;
    --rust-tint: #fdf1e6;   /* pale rust wash for the Premium column / preselect */
    --stone: #bdb5aa;
    --danger: #7C2723;      /* true errors only (sign-in failure, declined card) */
    --danger-tint: #F3E1E0; /* pale danger wash (disabled destructive button) */
    --warning: #854F0B;      /* amber = ATTENTION, not error (mirrors theme.WARNING) */
    --warning-tint: #FAEEDA; /* pale amber wash (canceled-checkout banner) */
    --info: #0C447C;         /* neutral notice (mirrors theme.INFO fg) */
    --info-tint: #E2ECF5;    /* pale blue wash (mirrors theme.INFO bg) */

    /* Surfaces + UI greys */
    --page-bg: #f8f6f3;
    --panel-bg: #ffffff;
    --text: #2b2b2b;
    --text-secondary: #5f5e5a;
    --text-tertiary: #9a9a96;
    --border: #e7e4df;
    --border-strong: #cfccc5;

    /* Bands tab geo-base map (mirrors theme.BAND_MAP_*; keep in sync). Used as the
       .bands-map-area backing so a letterbox gap reads as water, like the map's
       own ocean fill. */
    --band-map-ocean: #e9eff2;

    /* Typography */
    --font-display: Merriweather, Georgia, "Times New Roman", serif;
    --font-body: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    --fs-display: 28px;
    --fs-display-sm: 22px;   /* phones: long region titles wrap to 2 lines */
    --fs-h1: 22px;
    --fs-h2: 18px;
    --fs-body-lg: 15px;
    --fs-body: 14px;
    --fs-small: 13px;
    --fs-caption: 12px;
    --fs-micro: 11px;

    /* Spacing */
    --sp-1: 4px;
    --sp-2: 8px;
    --sp-3: 12px;
    --sp-4: 16px;
    --sp-5: 24px;
    --sp-6: 32px;
    --touch-min: 44px;
    --mobile-ad-h: 0px;    /* retired: the bottom ad strip is gone, so the mobile
                              "Explore regions" peek now sits flush at the bottom */

    /* Radius */
    --radius-sm: 8px;
    --radius-md: 12px;
    --radius-lg: 16px;
    --radius-pill: 999px;

    /* Elevation */
    --shadow-sm: 0 1px 2px rgba(45,45,45,0.06), 0 1px 3px rgba(45,45,45,0.10);
    --shadow-md: 0 4px 12px rgba(45,45,45,0.12);
    --shadow-lg: 0 12px 32px rgba(45,45,45,0.18);
    --shadow-sheet: 0 -2px 12px rgba(45,45,45,0.18);   /* top-directed (bottom sheet) */
}

/* Base page. Body defaults to Inter (--font-body); only elements explicitly
   given --font-display render in Merriweather. */
html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    background-color: #f8f6f3;
    font-family: var(--font-body);
    color: var(--text);
}

/* ============================================================================
   Base-map home shell — the hero map zone
   The shell is a single full-viewport map zone; EVERY piece of chrome floats
   over it as an island (brand, control island, legend, rail, and the ad island
   below). The old full-width top sponsor band ("the lid") is retired — the
   rotating ad now floats top-center as its own island over the map.
   ============================================================================ */
.app-shell {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
}
.map-zone {
    position: relative;
    flex: 1 1 auto;
    overflow: hidden;
}

/* Floating ad island — the rotating carousel, centered at the very top of the
   map. Card chrome (PANEL_BG, hairline border, RADIUS_MD, SHADOW_SM, SP_2
   padding) comes inline from theme.island_style(); this rule owns placement +
   stacking only. z-index 450 keeps it above the map (0) but BELOW every
   summoned or denser layer — the legend/attribution (470), the drawer scrim
   (480), slide-out panel (495) and rail (500), the control island + brand (600),
   the date panel (750), and Bootstrap's modals/lightbox (1050+) — so overlays
   the user opens always cover the ad, never the reverse. */
.ad-island {
    position: absolute;
    top: var(--sp-3);
    left: 50%;
    transform: translateX(-50%);
    z-index: 450;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* The .ad-slot is the sized box the carousel fills. It MUST carry a definite
   width (not flex:1) so the sliding track's translateX(-index*100%) yields
   uniform slides. Width = creative aspect ratio x slot height, per breakpoint
   below; flex 0 1 auto + min-width:0 let it shrink (never grow) when the
   max-width guard bites on narrow windows. */
.ad-slot {
    flex: 0 1 auto;
    min-height: 0; min-width: 0;
}
/* Desktop creative is 1200x100 (12:1). At the island's 72px slot height a 12:1
   image renders 864px wide, so an 864px slot fits it with NO side gutter. The
   max-width keeps the centered island clear of the 184px desktop left rail on
   narrower windows: 100vw minus (rail 184 + gutter 12 + island pad 8) mirrored
   on BOTH sides — symmetric, so the island stays centered and the creative
   simply letterboxes inside the slot when the guard bites. */
.ad-slot.desktop-only {
    width: 864px;
    height: 72px;
    max-width: calc(100vw - 408px);
}

/* Rotating ad carousel (both breakpoints live in the floating top island — the
   desktop creative on wide screens, the mobile creative on phones).
   Static markup from controls.build_ad_carousel(); assets/ad_carousel.js
   slides the track sideways on a browser timer — pure client-side, no Dash.
   Banners fit-by-height (object-fit: contain, never cropped) and center in
   the slide, so a 12:1 desktop or 3.2:1 mobile creative sits cleanly with
   side whitespace. */
.ad-carousel {
    width: 100%; height: 100%; min-height: 0;
    overflow: hidden;
    display: flex; align-items: center; justify-content: center;
}
.ad-carousel-track {
    display: flex; height: 100%; width: 100%; min-height: 0;
    transition: transform 500ms ease;
    will-change: transform;
}
.ad-slide {
    flex: 0 0 100%; height: 100%; min-height: 0;
    display: flex; align-items: center; justify-content: center;
}
/* The anchor needs a definite height (not max-height) so the img's
   max-height:100% inside it can resolve — otherwise a linked banner escapes
   the strip. */
.ad-slide a { display: flex; align-items: center; justify-content: center;
              max-width: 100%; height: 100%; }
.ad-slide img { max-width: 100%; max-height: 100%; object-fit: contain; }
@media (prefers-reduced-motion: reduce) {
    .ad-carousel-track { transition: none; }
}

@media (max-width: 768px) {
    /* Mobile home layout (measured on 390x844): the ad island sits top-center
       with its TOP edge 15px from the top of the map (was 44px). The account
       button (.acct-entry-island) is TOP-aligned at the same 15px — the two are no
       longer vertically centered on each other, they simply share a top line. On a
       notched phone 15px is safe: the app always runs in a browser tab (no
       viewport-fit=cover, no standalone/PWA manifest), whose page starts below
       the status bar. Only if viewport-fit=cover or standalone mode is ever
       added would this need env(safe-area-inset-top). Card chrome comes inline from theme.island_style() (white, 8px
       padding, 1px border); the radius + a softer two-layer shadow are the Design
       mockup values (source of truth, no matching theme token — mobile-only, so
       overridden here with !important over the inline island_style rather than
       restyling the shared island). */
    .ad-island {
        top: 15px;
        border-radius: 14px !important;
        box-shadow: 0 1px 2px rgba(45,45,45,.06), 0 2px 6px rgba(45,45,45,.08) !important;
    }
    /* Mobile creative is 320x100 (3.2:1). At a 70px slot a 3.2:1 image renders
       224px wide, so a 224px slot fits it with no side gutter; the max-width
       guard keeps the island inside very narrow phones (creative letterboxes).
       The island is center-anchored (left:50% / translateX(-50%)), so on small
       phones its right edge would reach the top-right 44px account button
       (.acct-entry-island, right:14px -> left edge at 100vw-58). Because the
       island is centered, pulling its right edge in means trimming total width
       on BOTH sides: capping the slot at 100vw-150 keeps the island (slot+18
       chrome) clear of the button with a small gap down to 320px, while it just
       letterboxes the creative (object-fit: contain) instead of overflowing. */
    .ad-slot.mobile-only {
        width: 224px;
        height: 70px;
        max-width: calc(100vw - 150px);
    }
}

/* Tabular figures — apply .tnum to any element whose digits should align in
   columns: the species-breakdown grid, the stat strip, the Reports counts. */
.tnum {
    font-feature-settings: "tnum" 1;
    font-variant-numeric: tabular-nums;
}

/* Region modal title: Merriweather display size, stepped down on phones so the
   longest region names wrap to 2 lines (not 3) at ~430px. (Applied when the
   title gets className="region-modal-title" in Phase 2.) */
.region-modal-title {
    font-family: var(--font-display);
    font-size: var(--fs-display);
    line-height: 1.2;
}

@media (max-width: 768px) {
    .region-modal-title { font-size: var(--fs-display-sm); }
}

/* Desktop vs mobile show/hide.
   Desktop (default): show the drawer, hide the bottom sheet peek.
   Mobile (<= 768px): hide the drawer, show the bottom sheet peek. */
.desktop-only { display: block; }
.mobile-only  { display: none; }

@media (max-width: 768px) {
    .desktop-only { display: none !important; }
    .mobile-only  { display: block !important; }
}

/* Keep map controls tappable above the map */
.dash-graph { z-index: 0; }

/* Region-list card — the shared white card that carries the region rows on both
   surfaces (the desktop panel's warm content area + the mobile sheet's warm
   ground), so the rows lift off the ground as one lifted card of clean divided
   rows instead of flat Bootstrap rows. Same card chrome as §6.3. overflow:hidden
   clips the first/last row's hover tint to the rounded corners. */
.drawer-region-card {
    background-color: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    overflow: hidden;
}

/* Rows are divided by a warm hairline; the last row drops its divider so it
   meets the card's rounded bottom cleanly. */
.drawer-region-card .drawer-region-row { border-bottom: 1px solid var(--border); }
.drawer-region-card .drawer-region-row:last-child { border-bottom: none; }

/* Clickable region rows in the drawer / bottom sheet */
.drawer-region-row:hover {
    background-color: var(--forest-tint);
    border-left-color: var(--forest) !important;   /* ties the row to the map highlight */
}

/* Left-rail nav items: warm-grey hover (a step off the active FOREST_TINT wash,
   which the callback sets inline). Inactive rows carry no inline background, so
   this plain rule takes effect; an active row's inline FOREST_TINT still wins. */
.rail-nav-row:hover { background-color: #f3f2ee; }

/* Mobile sheet drill-in: card press feedback + horizontal slide between the
   category list and a region list.
   A freshly-navigated pane is rendered as "sheet-pane slide-pending-{dir}";
   sheet_nav.js swaps the marker for "sheet-in-{dir}" on the next frame so the
   keyframe runs cleanly from its opacity:0 "from" state (no flash). The base
   ".sheet-pane" and the "slide-pending-*" marker MUST stay visible at rest —
   never give them opacity:0 — so a pane is always shown if the JS never runs. */
.sheet-cat-card:active { background-color: var(--forest-tint); }
@keyframes sheet-in-right {
    from { transform: translateX(40px); opacity: 0; }
    to   { transform: none; opacity: 1; }
}
@keyframes sheet-in-left {
    from { transform: translateX(-40px); opacity: 0; }
    to   { transform: none; opacity: 1; }
}
/* ~350ms ease-in-out emphasizes the directional drill-in / back slide. */
.sheet-in-right { animation: sheet-in-right .35s ease-in-out both; }
.sheet-in-left  { animation: sheet-in-left .35s ease-in-out both; }
@media (prefers-reduced-motion: reduce) {
    .sheet-in-right, .sheet-in-left { animation: none; }
}

/* Mobile bottom sheet (dbc.Offcanvas placement="bottom"). Bootstrap's default is
   a cool-grey slab pinned to 30vh; this makes it read as the app's lifted sheet:
   warm PAGE_BG ground, rounded TOP corners (RADIUS_LG), a warm top border, and
   the top-directed sheet shadow so it lifts off the map.
   Land-on-a-list (Design 2a): the sheet holds a fixed height with a static header
   + pill row on top and ONE scrolling region list beneath. 64vh ≈ 542px on an
   844px screen (40px shorter than the old 70vh menu), fits the curated lists, and
   stays clear of the map/ad above. The inner column (.sheet-inner) confines
   scrolling to the list (.sheet-list), so the header + pills never scroll away. */
.offcanvas.offcanvas-bottom {
    height: 64vh;
    background-color: var(--page-bg);
    border-top: 1px solid var(--border);
    border-top-left-radius: var(--radius-lg);
    border-top-right-radius: var(--radius-lg);
    box-shadow: var(--shadow-sheet);
}
/* Body fills the sheet as a flex column (no padding — the header + list own it),
   so .sheet-inner can stretch and the region list becomes the only scroller. */
.offcanvas.offcanvas-bottom .offcanvas-body {
    padding: 0; display: flex; flex-direction: column; min-height: 0;
}

/* ---- Land-on-a-list sheet: header + pills + region list ---------------------- */
.sheet-inner { display: flex; flex-direction: column; height: 100%; min-height: 0; }
/* #sheet-body holds the pills (fixed) + the region list (flex-fills + scrolls),
   so it must be a flex column that fills the inner and can shrink below content. */
.sheet-body { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.sheet-header { flex-shrink: 0; }
.sheet-handle {
    width: 40px; height: 4px; background: var(--border-strong);
    border-radius: 2px; margin: 8px auto 0;
}
.sheet-header-row {
    display: flex; align-items: center; justify-content: space-between;
    padding: 6px 6px 10px 16px;
}
.sheet-title {
    font-family: var(--font-display); font-size: var(--fs-h1); font-weight: 600;
    color: var(--text); line-height: 1.2;
}
.sheet-close {
    width: var(--touch-min); height: var(--touch-min); display: flex;
    align-items: center; justify-content: center; border: none; background: none;
    cursor: pointer; color: var(--text-secondary); font-size: 18px; padding: 0;
}
/* Category pills: one horizontally-scrolling row; the wrap masks the right edge
   to signal the row scrolls. The scroller's extra right padding lets the LAST
   pill clear the fade zone at scroll-end (otherwise it stays permanently dimmed). */
.sheet-pills-wrap {
    flex-shrink: 0; padding-bottom: var(--sp-3);
    -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent);
    mask-image: linear-gradient(90deg, #000 88%, transparent);
}
.sheet-pills {
    display: flex; gap: 6px; overflow-x: auto; padding: 0 48px 0 12px;
    scrollbar-width: none;
}
.sheet-pills::-webkit-scrollbar { display: none; }
.sheet-pill {
    flex-shrink: 0; display: inline-flex; align-items: center; gap: 5px;
    font-family: inherit; font-size: var(--fs-small); font-weight: 500;
    color: var(--text); padding: 7px 13px; border: 1px solid var(--border);
    border-radius: 999px; background: var(--panel-bg); white-space: nowrap;
    cursor: pointer;
}
.sheet-pill.on {
    background: var(--forest); color: var(--panel-bg); border-color: var(--forest);
    font-weight: 600;
}
.sheet-pill-lock { font-size: 10px; color: var(--text-tertiary); }
.sheet-pill.on .sheet-pill-lock { color: #cfd8d0; }   /* muted tint legible on forest */
/* Region list: a white card that fills the rest of the sheet and scrolls, with a
   soft bottom fade signalling more. The list is the sole scroll container. */
.sheet-list-wrap { position: relative; flex: 1 1 auto; min-height: 0; margin: 0 var(--sp-3); }
.sheet-list {
    height: 100%; overflow-y: auto; background: var(--panel-bg);
    border: 1px solid var(--border); border-bottom: none;
    border-radius: var(--radius-md) var(--radius-md) 0 0;
    box-shadow: var(--shadow-sm);
    padding: var(--sp-2) var(--sp-3) 28px;
}
/* The region rows already come wrapped in .drawer-region-card (shared with the
   desktop panel); inside the sheet's own card that would double the frame, so
   strip the inner card's chrome here only (mobile-scoped, desktop untouched). */
.sheet-list .drawer-region-card { border: none; border-radius: 0; box-shadow: none; }
/* Locked categories render a gated teaser whose sticky CTA bleeds to the card
   base; tighten the list's bottom padding so no white strip shows below the CTA. */
.sheet-list:has(> .pw-gate) { padding-bottom: var(--sp-3); }
.sheet-list-fade {
    position: absolute; left: 1px; right: 1px; bottom: 0; height: 40px;
    background: linear-gradient(180deg, rgba(255,255,255,0), var(--panel-bg));
    pointer-events: none; border-radius: 0 0 var(--radius-md) var(--radius-md);
}

/* Map attribution + the data-accuracy notice. The "i" carries a short "About this
   data" label on desktop (bottom-RIGHT of the map; the control card holds the
   bottom-left) and collapses to a bare 30px disc on mobile (riding 15px above the
   control card as the dock's first child). On hover (desktop) or tap/focus
   (mobile) it opens .attr-panel below it, holding the disclaimer AND the required
   map credit (Carto's tiles need the credit to stay reachable). */
.map-attribution {
    position: absolute;
    z-index: 470;    /* below the drawer scrim (480) + panel (495) so an open drawer covers it; above the map + 450 ad layer */
    right: 14px; left: auto;
    bottom: 14px;
}
/* The pointer promises a response, so give the pill a hover/open cue; keyboard
   focus shows a ring (focus-visible only, so a mouse/tap doesn't leave one).
   !important beats the inline border. */
.map-attribution:hover, .map-attribution:focus-within { border-color: var(--border-strong) !important; }
.map-attribution:focus { outline: none; }
.map-attribution:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; }

/* The pop-up panel: hidden at rest, revealed on hover/focus. Desktop = a card
   floating just above the "i", right-aligned; the padding-bottom keeps the hover
   bridge unbroken so the card stays open while the mouse travels onto it. */
.map-attribution .attr-panel { display: none; }
.map-attribution:hover .attr-panel,
.map-attribution:focus .attr-panel,
.map-attribution:focus-within .attr-panel {
    display: block;
    position: absolute;
    right: 0;
    bottom: 100%;
    padding-bottom: 8px;
    width: 300px;
    cursor: default;
}
.attr-panel-card {
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    padding: 14px 16px;
    text-align: left;
}
.attr-panel-title {
    font-family: var(--font-display); font-weight: 600;
    font-size: var(--fs-body-lg); color: var(--text); margin-bottom: 7px;
}
.attr-panel-body {
    font-family: var(--font-body); font-size: var(--fs-small);
    color: var(--text-secondary); line-height: 1.5; margin: 0;
}
.attr-panel-rule { height: 1px; background: var(--border); margin: 12px 0 10px; }
.attr-panel-credit { font-size: var(--fs-micro); color: var(--text-tertiary); }

@media (max-width: 768px) {
    /* Mobile: the "i" is a compact 30px disc, the FIRST child of the bottom-center
       control-card dock (a flex column). align-self keeps it a 30px circle pinned
       to the dock's left edge; the dock's 15px gap gives the founder's 15px space
       between the "i" and the card below it. The label is hidden; tapping the disc
       opens the panel as a card floating UP over the map. (The disc sits ABOVE the
       control card in the dock, so an upward card clears it — a fixed bottom sheet
       would be trapped under the dock's own z-index:600 stacking context and paint
       behind the control card.) position:relative anchors the panel to the disc. */
    .map-attribution {
        position: relative;
        left: auto; right: auto; bottom: auto;
        align-self: flex-start;
        width: 30px; height: 30px; box-sizing: border-box;
        padding: 0 !important;   /* beats the inline island padding so the disc is a true 30px circle */
        justify-content: center;
    }
    /* 44px tap target (STYLE_GUIDE §3 TOUCH_MIN) without growing the visible 30px
       disc: an invisible inset overlay extends the hit area 7px all around. It's the
       only way to reach the legal notice on mobile, so it must be glove-tappable. */
    .map-attribution::before { content: ""; position: absolute; inset: -7px; }
    /* Bump the glyph so it reads inside the 30px disc (lost at the desktop 11px). */
    .map-attribution .bi { font-size: var(--fs-body) !important; }
    .map-attribution .attr-label { display: none; }
    /* Anchor left (the disc hugs the dock's left edge) and match the control-card
       width below it; otherwise inherit the desktop reveal (absolute, opens upward). */
    .map-attribution:hover .attr-panel,
    .map-attribution:focus .attr-panel,
    .map-attribution:focus-within .attr-panel {
        left: 0; right: auto;
        width: calc(100vw - 28px);
    }
}

/* Hide the map library's built-in attribution control — our own editable
   .map-attribution element carries the (hover/tap-revealed) credit instead. */
.mapboxgl-ctrl-attrib, .maplibregl-ctrl-attrib { display: none !important; }

/* ============================================================================
   "Finish your upgrade" nudge (Decision 150 option C) — onboarding.build_finish_
   upgrade_nudge(). Desktop: a compact card bottom-RIGHT over the map (clear of the
   bottom-left control card). Mobile: a bar across the bottom, sitting ABOVE the
   control-card dock (which is pinned at bottom:53) rather than the mockup's 72 so
   it never overlaps the controls. Desktop bottom is 64 (not 24) so the card clears
   the "About this data" attribution pill that shares the bottom-right corner. The
   subline shows on both breakpoints. Non-blocking (no backdrop); z-index clears the
   map chrome, below the modals. */
.fu-nudge {
    position: absolute; z-index: 560;
    right: 24px; bottom: 64px; width: 364px;
    background: var(--panel-bg); border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: 0 12px 32px rgba(45,45,45,0.16), 0 3px 8px rgba(45,45,45,0.08);
    padding: 20px; display: flex; flex-direction: column; gap: var(--sp-2);
}
.fu-nudge[hidden] { display: none; }
.fu-nudge-close {
    position: absolute; top: 12px; right: 12px; width: 36px; height: 36px;
    display: flex; align-items: center; justify-content: center;
    border: none; background: transparent; color: var(--text-secondary);
    border-radius: 999px; cursor: pointer; font-size: 16px;
}
.fu-nudge-close:hover { background: var(--page-bg); color: var(--text); }
.fu-nudge-close:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; }
.fu-nudge-headline {
    font-family: var(--font-display); font-weight: 600;
    font-size: var(--fs-h2); line-height: 1.3; color: var(--text); padding-right: 36px;
}
.fu-nudge-sub { font-size: var(--fs-small); line-height: 1.5; color: var(--text-secondary); }
.fu-nudge-actions { display: flex; align-items: center; gap: var(--sp-3); margin-top: var(--sp-1); }
.fu-nudge-cta {
    height: 44px; padding: 0 var(--sp-4); font-size: var(--fs-body); box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--forest); color: #fff; border: none;
    border-radius: var(--radius-md); font-weight: 600; text-decoration: none;
    cursor: pointer; font-family: var(--font-body); white-space: nowrap;
}
.fu-nudge-cta:hover { background: var(--forest-dark); }
.fu-nudge-cta:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; }
.fu-nudge-later {
    min-height: 44px; padding: 0 8px; font-size: var(--fs-body);
    background: transparent; border: none; color: var(--text-secondary);
    text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
    font-weight: 500; font-family: var(--font-body);
}
.fu-nudge-later:hover { color: var(--text); }
.fu-nudge-later:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; }

@media (max-width: 768px) {
    .fu-nudge {
        left: 12px; right: 12px; width: auto;
        bottom: 200px;   /* clears the control-card dock (pinned at bottom:53) */
        border-radius: var(--radius-lg); padding: 16px; gap: 12px;
    }
    .fu-nudge-close { width: 44px; height: 44px; top: 6px; right: 6px; }
    .fu-nudge-headline { line-height: 1.28; padding-right: 44px; }
    .fu-nudge-actions { flex-direction: column; align-items: stretch; gap: var(--sp-1); margin-top: 2px; }
    /* width:100% + horizontal padding needs border-box (base rule) so the button
       never spills past the card's padding box. Full-width + the 44px token height
       keeps the tap target without making the card button-heavy. */
    .fu-nudge-cta { width: 100%; height: var(--touch-min); font-size: 15px; }
    .fu-nudge-later { align-self: center; }
}

/* ============================================================================
   Region modal shell — redesigned (Phase 2)
   Replaces the old .modal-body-fixed / .modal-tabs-scroll / region-modal-*
   rules. Uses the token CSS variables from the :root block.
   ============================================================================ */

/* ---- Shell + body ground --------------------------------------------------
   The body sits on the warm PAGE_BG ground so white content cards lift off it.
   Desktop keeps a fixed body height (consistent size across tabs); mobile is
   full-screen, so the body flexes to fill. */
.region-modal-body {
    background-color: var(--page-bg);
    overflow-y: auto;
    padding: var(--sp-3);
    height: 520px;                 /* desktop: stable height across tabs */
    display: flex;                 /* so the active tab's root can fill the body */
    flex-direction: column;
}

/* The per-tab content wrapper fills the body so a tab root can flex to full
   height (Overview map card) while a taller tab (Reports, etc.) overflows and
   lets the body scroll — flex-shrink 0 keeps it from collapsing under content. */
#modal-tab-content {
    flex: 1 0 auto;
    display: flex;
    flex-direction: column;
}

/* ---- Empty state ("no data yet") -----------------------------------------
   The shared empty-state card (empty_state.build). The wrapper fills a
   flex-column tab body and TOP-aligns the card so its top sits at the same
   height whether or not a credits footer follows it (Summary/Outlook center
   above their footer; Trends has none) — top-align keeps every empty tab's
   card starting at the same Y instead of jumping between tabs. The art caps its
   own width and lets its baked-in whitespace act as the padding above the
   headline. */
.empty-state-wrap {
    flex: 1 1 auto;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: center;
}
.empty-state-art {
    width: 100%;
    max-width: 315px;
    height: auto;
    /* Reserve the image box BEFORE the PNG decodes (art is 622x472). Without
       this the card lays out at text height, then the image pops in and expands
       it — a two-step appearance on mobile. The ratio lets the browser size the
       height from the width immediately, so the card opens full-height at once. */
    aspect-ratio: 622 / 472;
    margin: 0;
    display: block;
}

/* The pinned header is a flex-none band; the strip too; the body takes the
   rest. On desktop these live inside the centered lg dialog. */
.region-modal-header {
    background-color: var(--panel-bg);
    flex: 0 0 auto;
    position: relative;
    z-index: 3;
    transition: box-shadow .2s ease;
}

.region-modal-title {
    font-family: var(--font-display);
    font-size: var(--fs-display);
    font-weight: 600;
    color: var(--text);
    line-height: 1.2;
}

/* ---- Tab strip ------------------------------------------------------------
   Horizontal scroll on narrow screens instead of wrapping; hide the scrollbar
   but keep touch momentum. The strip is the bottom of the pinned header band, so
   it carries a permanent downward shadow that casts onto the scrolling body
   below (z-index puts it above the body; the title header above covers the
   upward blur). */
.modal-tab-strip {
    flex: 0 0 auto;
    background-color: var(--panel-bg);
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;          /* Firefox */
    position: relative;
    z-index: 2;
    box-shadow: var(--shadow-sm);
}
.modal-tab-strip::-webkit-scrollbar { display: none; }   /* WebKit */

/* ---- Tab buttons (CSS-driven active state) --------------------------------
   The active-tab highlight is flipped CLIENTSIDE by toggling .is-active (see
   app.py), so the underline/weight move the instant a tab is tapped while the
   tab content streams from the server. Values mirror the old inline style in
   modal.py: padding SP_3/SP_2 (12px/8px), FS_BODY (14px), FW 400->600,
   TEXT_SECONDARY->FOREST, a 2px bottom border transparent->FOREST, TOUCH_MIN. */
.modal-tab-btn {
    border: none;
    background: none;
    cursor: pointer;
    padding: 12px 8px;
    margin-right: 8px;
    font-family: inherit;
    font-size: 14px;
    font-weight: 400;
    color: var(--text-secondary);
    border-bottom: 2px solid transparent;
    white-space: nowrap;
    min-height: 44px;
    transition: color .15s ease, border-color .15s ease;
}
.modal-tab-btn.is-active {
    font-weight: 600;
    color: var(--forest);
    border-bottom-color: var(--forest);
}
/* Dimmed label for a tab whose content is an empty placeholder (no reports /
   nothing to summarize). Still fully clickable — just a quieter color so users
   gravitate to tabs that DO have data. :not(.is-active) so the active tab the
   user has tapped into always keeps its forest highlight, even when empty. */
.modal-tab-btn--empty:not(.is-active) {
    color: #c9c6c0;
}

/* ---- Instant loading placeholder ------------------------------------------
   Written into the modal body CLIENTSIDE the moment a tab or region changes, so
   stale content can't linger during the server round-trip (see app.py). The
   server callback overwrites it with the real content when it returns. */
.modal-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 48px 0;
    min-height: 160px;
}
.modal-spinner {
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 3px solid var(--border);
    border-top-color: var(--forest);
    animation: modal-spin .7s linear infinite;
}
@keyframes modal-spin { to { transform: rotate(360deg); } }

/* ---- Species segmented control (CSS-driven active state) -------------------
   The active chip is flipped CLIENTSIDE by toggling .is-active off the
   active-species store (see app.py), so the highlight is instant while the map /
   drawer rebuild server-side. Values mirror the old inline style in controls.py:
   padding 0 8px, 12px, FW 500->600, TEXT_SECONDARY->white, FOREST fill on active,
   a 1px BORDER left divider that goes transparent under the active cell so the
   joined control never shifts width as the selection moves. */
.species-segment > .seg-cell {
    padding: 0 8px;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    border: none;
    outline: none;
    min-height: 30px;
    white-space: nowrap;
    font-family: inherit;
    background-color: transparent;
    color: var(--text-secondary);
    border-left: 1px solid var(--border);
}
.species-segment > .seg-cell:first-child { border-left: none; }
.species-segment > .seg-cell.is-active {
    background-color: var(--forest);
    color: #ffffff;
    font-weight: 600;
    border-left-color: transparent;
}

/* ---- Footer CTA -----------------------------------------------------------
   Slim pinned bar; visibility toggled inline by a callback. */
.region-modal-footer {
    flex: 0 0 auto;
}

/* ---- Desktop dialog -------------------------------------------------------
   Make the content a column flexbox so header / strip / body / footer stack
   with the body scrolling. */
.region-modal-content {
    display: flex;
    flex-direction: column;
    border-radius: var(--radius-lg);
    overflow: hidden;
}

/* ---- Mobile: full-screen slide-up ----------------------------------------
   dbc fullscreen="md-down" already makes the dialog full-viewport; we pin the
   width definitely so the tab strip's own overflow-x scrolls instead of
   stretching the whole modal, and let the body flex to fill. */
@media (max-width: 768px) {
    .region-modal-shell .modal-dialog {
        max-width: 100% !important;
        width: 100% !important;
        margin: 0 !important;
    }
    .region-modal-content {
        min-height: 100vh;
        border-radius: 0;
        border: 0;
    }
    .region-modal-body {
        height: auto;
        flex: 1 1 auto;
        min-height: 0;
    }
    /* The subtitle is collapsible here (assets/modal.js drives it). Give it a
       transition so the collapse is smooth. */
    .region-modal-subtitle {
        transition: opacity .12s ease, max-height .12s ease;
    }

    /* Nudge the pinned header down on the fullscreen mobile modal so the region
       title clears the top edge AND gets a little breathing room from the ceiling
       (the title_row's own 12px top padding alone sat too tight). The safe-area
       term is 0 on our non-cover viewport but future-proofs a notch. */
    .region-modal-header {
        padding-top: calc(env(safe-area-inset-top, 0px) + 14px);
    }

    /* "More tabs →" edge fade for the horizontally-scrolling tab strip. The mask
       sits on the scroll element itself, so it stays fixed at the edges while the
       strip scrolls. assets/tab_fade.js toggles .has-more-right / .has-more-left
       only while there's more to scroll that way, so the fade RETRACTS at the end
       and the last tab (Band Insights) is never left dimmed. Mobile-scoped: on
       desktop every tab fits the lg dialog, so neither class is set anyway and the
       strip is unchanged. --tab-fade-w is the fade width (one-line tuning knob). */
    .modal-tab-strip { --tab-fade-w: 72px; }
    .modal-tab-strip.has-more-right {
        -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - var(--tab-fade-w)), transparent);
                mask-image: linear-gradient(90deg, #000 calc(100% - var(--tab-fade-w)), transparent);
    }
    .modal-tab-strip.has-more-left {
        -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--tab-fade-w));
                mask-image: linear-gradient(90deg, transparent, #000 var(--tab-fade-w));
    }
    .modal-tab-strip.has-more-left.has-more-right {
        -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--tab-fade-w), #000 calc(100% - var(--tab-fade-w)), transparent);
                mask-image: linear-gradient(90deg, transparent, #000 var(--tab-fade-w), #000 calc(100% - var(--tab-fade-w)), transparent);
    }
    /* mask-clip is border-box, so the strip's own box-shadow (painted OUTSIDE the
       box) is masked away whenever a fade is active — which on phones is at rest.
       Drop the shadow while masked so it doesn't pop in/out as the user scrolls to
       the end; the inline hairline border (modal.py) still frames the strip. */
    .modal-tab-strip.has-more-right,
    .modal-tab-strip.has-more-left {
        box-shadow: none;
    }
}

/* ---- Photo lightbox: stack above the region modal -------------------------
   The expanded photo modal opens on top of the region modal. The order we need
   (bottom -> top): region modal -> photo backdrop (dims the region) -> photo card.
   Bootstrap gives .modal z-index 1055 and .modal-backdrop 1050. We raise the
   photo backdrop to 1085 so it dims the region modal behind it. But the
   lightbox's CONTENT lives inside its .modal WRAPPER, so the WRAPPER (not the
   inner .modal-dialog) must sit above that 1085 backdrop — otherwise the
   backdrop paints a grey wash over the whole photo card (image + caption).
   modalClassName on the dbc.Modal puts .photo-lightbox-shell on the wrapper;
   !important beats the inline z-index older dbc sets when a second modal stacks.
   (The old rule targeted #photo-lightbox, which is the .modal-dialog — its
   z-index only nests inside the 1055 wrapper and never clears the backdrop.)
   Backdrop tone stays the default dark. */
.photo-lightbox-shell { z-index: 1090 !important; }
.photo-lightbox-backdrop { z-index: 1085 !important; }

/* House-system card: warm surface, rounded, elevated. overflow:hidden clips the
   photo to the rounded top; position:relative anchors the close disc. */
.photo-lightbox-content {
    background-color: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-md);
    overflow: hidden;
    position: relative;
}
.photo-lightbox-body { padding: 0; }          /* photo flush to the card top */
/* Fixed-size media box: same footprint for every photo, whatever its aspect
   ratio (modal never resizes to the image). No white frame, no dark letterbox. */
.photo-lightbox-media {
    position: relative; overflow: hidden; height: 62vh;
}
/* Back layer: the same photo, cover-cropped, scaled up and blurred to fill the
   box edge-to-edge. Decorative only (aria-hidden on the img). */
.photo-lightbox-img-blur {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; transform: scale(1.2);
    filter: blur(30px) brightness(0.92);
}
/* Front layer: the real photo, shown whole (contain) with a soft drop shadow
   so it lifts off the blurred fill. */
.photo-lightbox-img {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: contain;
    filter: drop-shadow(0 2px 12px rgba(0,0,0,.4));
}
.photo-lightbox-caption { padding: var(--sp-4) var(--sp-5) var(--sp-5); }
/* Location = headline (content); date = quiet sub-line */
.photo-lightbox-loc {
    font-family: var(--font-display); font-size: var(--fs-h2);
    font-weight: 600; color: var(--text); line-height: 1.3;
}
.photo-lightbox-date {
    font-size: var(--fs-small); color: var(--text-secondary); margin-top: 2px;
}
/* Divider separates content (what/where/when) from attribution (who) */
.photo-lightbox-divider {
    border: 0; border-top: 1px solid #f0ede8; margin: var(--sp-3) 0;
}
.photo-lightbox-credit { font-size: var(--fs-caption); color: var(--text-secondary); }
.photo-lightbox-credit-name { font-weight: 600; color: var(--text); }
/* Close disc: translucent light disc so the ✕ reads against any photo */
.photo-lightbox-close {
    position: absolute; top: 10px; right: 10px; z-index: 5;
    width: 32px; height: 32px; border-radius: 50%; border: none;
    background-color: rgba(255,255,255,0.72);
    color: var(--text); font-size: 14px; line-height: 1; padding: 0;
    display: flex; align-items: center; justify-content: center; cursor: pointer;
    box-shadow: 0 1px 3px rgba(45,45,45,0.20);
}
.photo-lightbox-close:hover { background-color: rgba(255,255,255,0.92); }

/* ---- Reports tab: collapsible SECTIONS (Level 0) --------------------------
   Native <details>. The header is a full-width "shelf" bar (Merriweather name +
   quiet count + a right-side caret) — structural chrome, deliberately a
   different order of control from the in-card "Species breakdown" pill below. */
.rpt-section { border-top: var(--border) solid 1px; }
.rpt-section:first-child { border-top: 0; }
summary.rpt-bar {
    list-style: none; cursor: pointer;
    display: flex; align-items: center; justify-content: space-between;
    padding: var(--sp-3) 2px;
}
summary.rpt-bar::-webkit-details-marker { display: none; }
.rpt-count { color: var(--text-tertiary); font-size: var(--fs-small); margin-left: var(--sp-2); }
.rpt-caret { color: var(--text-secondary); font-size: var(--fs-small);
             transition: transform .15s ease; }
.rpt-section[open] > summary.rpt-bar .rpt-caret { transform: rotate(180deg); }
.rpt-body { padding-bottom: var(--sp-3); }

/* ---- Reports tab: location card "Species breakdown" pill (Level 1) --------
   An affordance ON the card — small labeled Inter pill (forest tint), going
   solid forest when open. The label names what it opens so it can't be confused
   with the section bar's bare caret. */
summary.species-pill {
    list-style: none; cursor: pointer; display: inline-flex; align-items: center;
    gap: 5px; margin-top: var(--sp-2);
    font-size: var(--fs-caption); font-weight: 600; color: var(--forest);
    background-color: var(--forest-tint); border-radius: var(--radius-pill);
    padding: 3px 10px;
}
summary.species-pill::-webkit-details-marker { display: none; }
.species-caret { font-size: 10px; transition: transform .15s ease; }
.species-details[open] > summary.species-pill { background-color: var(--forest); color: #fff; }
.species-details[open] > summary.species-pill .species-caret { transform: rotate(180deg); }

/* ---- Reports tab: location card "Earlier reports" pill (Level 1) ----------
   The card's second in-card affordance — same pill geometry as the species
   pill but neutral stone (secondary text on a warm-grey fill with a hairline
   border). It goes SOLID when open — like the species pill — but in the STONE
   family (dark stone fill + white text) rather than forest, so the two pills
   both read as "active" when open yet stay visually distinct. The caret
   rotates. */
summary.history-pill {
    list-style: none; cursor: pointer; display: inline-flex; align-items: center;
    gap: 5px; margin-top: var(--sp-2);
    font-size: var(--fs-caption); font-weight: 600; color: var(--text-secondary);
    background-color: #f6f4f1; border: 1px solid var(--border);
    border-radius: var(--radius-pill); padding: 3px 10px;
}
summary.history-pill::-webkit-details-marker { display: none; }
.history-details[open] > summary.history-pill {
    background-color: var(--text-secondary); color: #fff;
    border-color: var(--text-secondary);
}
.history-details[open] > summary.history-pill .species-caret { transform: rotate(180deg); }

/* Compact history rows: one quiet line per older report, not a card */
.history-list { display: flex; flex-direction: column; gap: var(--sp-1); }
.history-row { font-size: var(--fs-small); color: var(--text-secondary); line-height: 1.45; }

/* ---- Reports tab: pill accordion (mutual exclusion + full-width panels) ----
   The two pills (Species breakdown + Earlier reports) are PURE <details>
   toggles: each holds only its <summary>, so opening one just flips [open] on it
   and the pills never move. Their panels live as plain sibling divs AFTER the
   .rpt-pill-row in the card (.rpt-panel-species then .rpt-panel-history). A panel
   reveals when its matching pill is open, via :has() on the row + the ~ sibling
   combinator — the pills hold their exact closed position and the open panel
   eases in below at full card width. pill_accordion.js still keeps only one pill
   open at a time, so at most one panel is ever revealed.

   :has() and the ~ reveal need an evergreen browser (this app's audience). */
.species-details, .history-details { flex: 0 0 auto; }

/* Panels: collapsed by default (0fr grid row, hidden). The inner div clips its
   overflow while the row height eases 0fr -> 1fr. On CLOSE, visibility flips to
   hidden only after the height finishes collapsing (0s delay at .15s) so the
   content isn't interactable while it shrinks. */
.rpt-panel {
    display: grid; grid-template-rows: 0fr; visibility: hidden;
    transition: grid-template-rows .15s ease-out, visibility 0s .15s;
}
.rpt-panel > .rpt-panel-inner { min-height: 0; overflow: hidden; }

/* Reveal the panel whose matching pill is open (accordion => at most one). */
.rpt-pill-row:has(.species-details[open]) ~ .rpt-panel-species,
.rpt-pill-row:has(.history-details[open]) ~ .rpt-panel-history {
    grid-template-rows: 1fr; visibility: visible;
    transition: grid-template-rows .15s ease-out;
}

/* Suppress the browser focus ring on a MOUSE click; keep it for keyboard nav. */
summary.species-pill:focus:not(:focus-visible),
summary.history-pill:focus:not(:focus-visible) { outline: none; }

/* Respect reduced-motion: instant open/close, no caret spin. */
@media (prefers-reduced-motion: reduce) {
    .rpt-panel,
    .rpt-pill-row:has(.species-details[open]) ~ .rpt-panel-species,
    .rpt-pill-row:has(.history-details[open]) ~ .rpt-panel-history { transition: none; }
    .species-caret, .history-pill .species-caret { transition: none; }
}

/* ---- Reports tab: species breakdown donut + keyed legend ------------------
   A CSS conic-gradient donut (built in reports.py) beside a keyed legend. The
   chip carries the species color; the name stays standard text. Wraps below the
   donut on very narrow widths. */
.species-chart {
    display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap;
}
.species-legend { display: flex; flex-direction: column; gap: var(--sp-1); }
.species-legend-row {
    display: flex; align-items: center; gap: var(--sp-2);
}
.species-legend-chip {
    width: 10px; height: 10px; border-radius: 2px; flex: 0 0 auto;
    display: inline-block;
}

/* ---- Overview tab cards ---------------------------------------------------
   The map container drives the map height: capped on desktop so it reads as one
   card in the stack, a bit taller on phones (held afield). The map now meets
   the card edge crisply (no edge-fade — see overview.py); the photo scroll
   strip hides its scrollbar. */
.ov-map { height: 340px; }        /* hero map now the carousel has left */
@media (max-width: 768px) {
    /* Floor only: the mobile map card sets an inline minHeight of 420px (the
       _MAP_H_MOBILE the mobile figure was fit to), which overrides this height
       upward. Kept as a lower bound so the map never collapses. */
    .ov-map { height: 360px; }
}

/* ---- Overview per-breakpoint map cards ------------------------------------
   One mini-map figure can't serve both breakpoints: fit to the desktop width it
   crops width-bound regions on a ~390px phone. So the Overview tab renders TWO
   cards — one fit to the desktop width/height, one fit to the narrower mobile
   dimensions, each with its own pin de-overlap at its own zoom — and these
   wrappers show exactly one at the app's 768px breakpoint. NOT reusing
   .desktop-only/.mobile-only: their !important display:block would fight the
   card's own flex column layout. */
.ov-bp-desktop { display: flex; flex-direction: column; flex: 1 0 auto; min-height: 0; }
.ov-bp-mobile  { display: none; }
@media (max-width: 768px) {
    .ov-bp-desktop { display: none; }
    .ov-bp-mobile  { display: flex; flex-direction: column; flex: 1 0 auto; min-height: 0; }
}

/* Modal subtitle dropped — the Overview map card carries the region identity
   now. Hide the empty header element so it never takes vertical space on any
   tab. */
.region-modal-subtitle:empty { display: none; }

/* Overview legend footer: reflowing chips grouped by type, compact. Each chip
   (numbered pin + name) is an unbreakable inline unit that wraps but never
   splits across rows. */
.ov-legend-group { margin-bottom: var(--sp-2); }
.ov-legend-group:last-child { margin-bottom: 0; }
.ov-legend-group-label {
    font-size: var(--fs-micro); letter-spacing: .5px; text-transform: uppercase;
    color: var(--text-tertiary); margin-bottom: var(--sp-2);
}
.ov-legend-flow { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-2); }
.ov-legend-chip {
    display: inline-flex; align-items: center; gap: 6px;
    white-space: nowrap; font-size: var(--fs-micro); color: var(--text);
}
.ov-legend-pin {
    display: inline-flex; align-items: center; justify-content: center;
    width: 15px; height: 15px; border-radius: 50%; flex: 0 0 auto;
    color: #fff; font-size: 9px; font-weight: 600;
}

/* ---- Overview pin popover --------------------------------------------------
   Desktop: a transparent hotspot overlay sits over the (static) map — see the
   ".ov-hot-*" rules further down — with one invisible hotspot per pin and a
   PRE-RENDERED copy of this popover as its sibling, revealed by pure CSS
   hover. Mobile: a hotspot tap instead fires the ov-pin-popover Dash callback,
   which re-uses this same class, re-presented centered on a dim scrim (the
   photo-viewer pattern) via the media overrides below — the inline anchor
   styles set by that callback are deliberately beaten with !important there. */
.ov-pin-popover {
    position: absolute; z-index: 30;
    /* max-width sized so the qualitative/photo footer caption ("Based on
       verifiable or authoritative qualitative data.", ~300px at fs-micro) and
       the spelled-out tier words ("Moderately High") sit on one line. The
       min(…, calc(100vw - 24px)) guard keeps it inside even a small phone
       viewport (never wider than the screen minus a 12px margin each side). */
    width: max-content; max-width: min(336px, calc(100vw - 24px));
    min-width: 150px; padding: var(--sp-3);
    background-color: var(--panel-bg); border: 1px solid var(--border);
    border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
}
/* Arrow: a small rotated square on the edge facing the pin */
.ov-pin-popover::after {
    content: ""; position: absolute; left: 50%; width: 10px; height: 10px;
    background-color: var(--panel-bg); border: 1px solid var(--border);
    transform: translateX(-50%) rotate(45deg);
}
.ov-pop-above::after { bottom: -6px; border-top: 0; border-left: 0; }
.ov-pop-below::after { top: -6px; border-bottom: 0; border-right: 0; }

.ov-pop-name {
    font-family: var(--font-display); font-size: var(--fs-body);
    font-weight: 600; color: var(--text); margin-bottom: 3px;
}
.ov-pop-line {
    display: flex; align-items: center; flex-wrap: wrap; gap: 2px;
    font-size: var(--fs-caption); color: var(--text); margin-bottom: var(--sp-2);
}
/* The "generalized, not exact" sub-header hugs the title above it and takes this
   SP_2 gap below to set it apart from the body. In CSS (not inline) so the
   :last-child inset rule below can still zero it when an approx pin has no body. */
.ov-pop-approx { margin-bottom: var(--sp-2); }
/* Uniform inset fix: whichever content child ends up last (name-only, a
   trailing .ov-pop-line, or the species-bar block) should not add its own
   trailing margin on top of the popover's padding — otherwise variants that
   happen to end on an .ov-pop-line (margin-bottom: var(--sp-2)) get a taller
   bottom inset than the species-bar variant, which ends on an unmargined div.
   Desktop hover popovers (pre-rendered by _hotspot_overlay, no close button)
   can key off :last-child directly. Mobile tap popovers (cb_pin_popover in
   app.py) append an .ov-pop-close button as the final DOM child, but it's
   position:absolute (out of flow, top/right-anchored) — so for those, the
   real last CONTENT child is the second-to-last DOM child instead. */
.ov-pin-popover:not(.ov-pop-mobile) > :last-child {
    margin-bottom: 0;
}
.ov-pin-popover.ov-pop-mobile > :nth-last-child(2) {
    margin-bottom: 0;
}
.ov-pop-bar {
    display: flex; height: 12px; border-radius: var(--radius-pill);
    overflow: hidden; position: relative;
    /* One continuous 0.5px perimeter as a REAL border (box-sizing keeps the bar
       12px tall, matches theme.GOOSE_GROUP_EDGE #111). A single geometric stroke
       renders the top and bottom identically and follows the pill radius at the
       caps, so the bottom edge can't read thinner than the top — which it did
       when the perimeter was a stacked inset-shadow overlay sitting on top of
       each slice's own inset edge (two 0.5px draws that mis-aligned by a
       subpixel). The internal slice dividers are now a single 0.5px border-right
       per slice (set inline in overview.py), so every black line on the bar is
       one 0.5px stroke instead of two abutting edges reading as ~1px. */
    border: 0.5px solid #111; box-sizing: border-box;
}
/* border-box so a slice's 0.5px divider border doesn't widen the slice. */
.ov-pop-bar > div { box-sizing: border-box; }
.ov-pop-bar-caption {
    font-size: var(--fs-micro); color: var(--text-secondary);
    line-height: 1.5; margin-top: var(--sp-1);
}
/* Close affordance: mobile-only (desktop dismisses on unhover) */
.ov-pop-close {
    display: none; position: absolute; top: 6px; right: 6px;
    width: 26px; height: 26px; border-radius: 50%; border: none;
    background-color: var(--forest-tint); color: var(--text);
    font-size: 12px; line-height: 1; cursor: pointer;
    align-items: center; justify-content: center;
}
/* Scrim behind the popover — mobile only (same dim language as the photo
   viewer). position:fixed so it covers the screen, not just the map card. */
.ov-pin-scrim {
    position: fixed; inset: 0; z-index: 1390;
    background-color: rgba(0, 0, 0, 0.5);
}
@media (min-width: 769px) {
    .ov-pin-scrim { display: none !important; }
}
@media (max-width: 768px) {
    /* Re-present the popover as a centered sheet on the scrim; the inline
       anchor styles from the callback are overridden on purpose. */
    .ov-pin-popover {
        position: fixed !important; z-index: 1400;
        left: 50% !important; top: 50% !important;
        transform: translate(-50%, -50%) !important;
        /* Match the base max-width so the footer caption fits one line here too;
           the calc(100vw - 24px) guard keeps a 12px margin each side on a small
           phone (the base max-width still caps it, so it never exceeds 336px). */
        width: min(336px, calc(100vw - 24px));
        /* When the species dropdown is open the popover grows tall (name + count +
           pill + blurred bar + full E-1 CTA ~ 400px); cap it to the viewport and
           scroll inside so it never overflows a short phone. */
        max-height: calc(100vh - 24px);
        overflow-y: auto;
    }
    .ov-pin-popover::after { display: none; }   /* no arrow when centered */
    .ov-pop-close { display: inline-flex; }
    /* Breathing room between the "Species breakdown" toggle and the revealed CTA
       content in the mobile tap popover. Scoped to .ov-pop-mobile so the
       Reports-tab species pill and the desktop popover are untouched. */
    .ov-pop-mobile .ov-pop-species[open] .ov-pop-species-body { margin-top: var(--sp-3); }
}
@media (min-width: 769px) {
    /* Desktop uses the CSS hover popovers (see .ov-hot-pop below); this
       callback-driven instance is the mobile tap path only — hide it here so
       it can't stack a duplicate popover on top of the hover one. */
    .ov-pop-mobile { display: none !important; }
}

/* ---- Overview pin hotspots (desktop CSS-hover popover) ---------------------
   A transparent layer drawn over the static map graph (built by
   overview._hotspot_overlay). It intentionally catches every mouse/touch
   event itself — that's what actually blocks mapbox panning, since
   dragmode=False alone does not stop it (a known plotly gap). One invisible
   hitbox per pin (slightly larger than the 18px pin), each paired with a
   PRE-RENDERED popover as an ADJACENT SIBLING (not a child), so the
   `:hover + ` selector below can reveal it. */
.ov-hot-overlay {
    position: absolute; inset: 0; z-index: 10; pointer-events: auto;
}
.ov-hot {
    position: absolute; width: 22px; height: 22px; border-radius: 50%;
    cursor: pointer; z-index: 11;
}
.ov-hot-pop {
    display: none; position: absolute; z-index: 12;
}
/* Transparent travel-gap padding on the gap side of the wrapper — lets the
   mouse move from the pin to the card without a dead gap closing the popover
   (the wrapper itself, padding included, stays hoverable). */
/* Direction transform lives on the class (not inline on the wrapper) so
   pill_accordion.js can flip the popover's anchor direction by swapping the class
   (Refinement 5b): -above anchors the popover's BOTTOM 9px above the pin, -below
   anchors its TOP 9px below the pin. */
.ov-hot-pop-above { padding-bottom: 12px; transform: translate(-50%, calc(-100% - 9px)); }
.ov-hot-pop-below { padding-top: 12px;    transform: translate(-50%, 9px); }
/* The wrapper positions itself against the overlay; the card inside goes back
   to normal flow. position:relative (not static) so the arrow's ::after —
   which anchors absolute against the card — still has something to anchor to. */
.ov-hot-pop > .ov-pin-popover { position: relative; left: auto; top: auto; }

/* Promoted-modal ✕ (shown only when the desktop popover is promoted on
   species-open — see the .ov-hot-pop-promoted rules below) and its backdrop scrim
   (created by pill_accordion.js and appended to <body>). Both hidden by default. */
.ov-promo-close {
    display: none; position: absolute; top: 8px; right: 8px;
    width: 26px; height: 26px; border-radius: 50%; border: none;
    background-color: var(--forest-tint); color: var(--text);
    font-size: 12px; line-height: 1; cursor: pointer;
    align-items: center; justify-content: center; z-index: 2;
}
.ov-promo-scrim {
    position: fixed; inset: 0; z-index: 1390;
    background-color: rgba(0, 0, 0, 0.5); display: none;
}
.ov-promo-scrim.ov-promo-scrim-on { display: block; }
@media (max-width: 768px) {
    /* Promotion is a desktop-only mechanism; belt-and-braces so a stray class on a
       phone can never dim the screen or reveal the anchored popover. */
    .ov-promo-scrim { display: none !important; }
    .ov-hot-pop.ov-hot-pop-promoted { display: none !important; }
}

@media (min-width: 769px) {
    /* Desktop only: a touch tap on mobile must not reveal this anchored
       variant — mobile shows the callback-driven scrim popover instead. */
    .ov-hot:hover + .ov-hot-pop, .ov-hot-pop:hover { display: block; }
    /* Refinement 5a — fixed width both states: pin a species-carrying popover to
       the species-open width (336px) so opening the dropdown toggles HEIGHT only,
       never width (the card no longer snaps wider). Pins with no species bar keep
       the base max-content width. */
    .ov-hot-pop .ov-pin-popover.ov-pop-has-species { width: 336px; }
    /* Refinement 5c — count/species divider: a hairline above the species block
       separating the count line from the breakdown. */
    .ov-hot-pop .ov-pop-species-block {
        border-top: 1px solid var(--border); margin-top: var(--sp-1);
        padding-top: var(--sp-3);
    }
    /* Round 5d — desktop toggle spacing. The shared summary.species-pill carries
       its own margin-top (var(--sp-2)), meant for the Reports-tab card. Inside
       this popover it lands ON TOP of the divider block's padding-top
       (var(--sp-3)), so the gap ABOVE the "Species breakdown" toggle came out
       ~2 units while the gap BELOW it (toggle -> revealed content) was 0. Zero
       the pill's margin here so the divider -> toggle gap is one clean unit (the
       block's padding-top), then give the OPEN body a matching one-unit
       margin-top so toggle -> content reads the same (mirrors the mobile rule
       above). Desktop popover only — the Reports tab and mobile are untouched. */
    .ov-hot-pop .ov-pop-species > summary.species-pill { margin-top: 0; }
    .ov-hot-pop .ov-pop-species[open] .ov-pop-species-body { margin-top: var(--sp-3); }
    /* Round 5d — kill the "blink down then recenter". The native <details>
       toggle event fires ASYNC, so on species-open the body would paint IN PLACE
       at the pin (growing down, clipping off the overflow-hidden map card) for a
       frame or two before pill_accordion.js adds the promoted class. Keep the
       body hidden until that class lands, so instead of expanding at the pin the
       popover simply fades in already centered (see the promotion animation
       below). This also serves the old no-JS clip guard — a hidden body can't
       clip the card. */
    .ov-hot-pop:not(.ov-hot-pop-promoted) .ov-pop-species-body { display: none; }

    /* Species-open PROMOTION: detach from the pin and present the popover as a
       CENTERED MODAL — full content, NO inner scroll — dismissed by its ✕ or the
       backdrop (pill_accordion.js). Supersedes the resting cap while promoted.
       The short fade-in (ov-promote-in) masks the reposition so it settles
       into center rather than blinking down first. */
    .ov-hot-pop.ov-hot-pop-promoted {
        display: block !important;          /* stay open independent of :hover */
        position: fixed !important;
        left: 50% !important; top: 50% !important;
        transform: translate(-50%, -50%) !important;
        padding: 0 !important;              /* drop the pin travel-gap padding */
        z-index: 1400;
        max-height: calc(100vh - 32px);     /* whole-modal scroll only if truly huge */
        overflow-y: auto;
        animation: ov-promote-in 0.15s ease-out;
    }
    .ov-hot-pop.ov-hot-pop-promoted > .ov-pin-popover {
        width: 336px !important;            /* full content width, fixed */
    }
    .ov-hot-pop.ov-hot-pop-promoted .ov-pop-species-body {
        max-height: none !important;        /* NO inner scroll — the whole point */
        overflow: visible !important;
    }
    /* No pin arrow when centered on the scrim. */
    .ov-hot-pop.ov-hot-pop-promoted .ov-pin-popover::after { display: none; }
    /* Reveal the ✕ only while promoted. */
    .ov-hot-pop.ov-hot-pop-promoted .ov-promo-close { display: flex; }

    /* The desktop pin-popover "Species breakdown" is open-only: pill_accordion.js
       blocks the collapse (so the promoted centered popover can't vanish off the pin),
       so once open the pill must stop advertising a toggle. Hide the caret + drop the
       pointer affordance; the ✕ / backdrop / outside-click still dismiss. Mobile
       (.ov-pop-mobile, no .ov-hot-pop ancestor) and the Reports pill keep their
       honest, working collapse. */
    .ov-hot-pop .ov-pop-species[open] > summary.species-pill {
        pointer-events: none;
        cursor: default;
    }
    .ov-hot-pop .ov-pop-species[open] > summary.species-pill .species-caret {
        display: none;
    }
}

/* Promotion appear animation (desktop): a quick, subtle opacity fade so the
   centered modal eases in rather than blinking down at the pin then jumping to
   center. Opacity only — the promoted rule owns the centering transform with
   !important (which outranks any transform an animation could set), so the modal
   is centered from the first painted frame and nothing shifts as it fades in. */
@keyframes ov-promote-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}
/* Respect reduced-motion: instant appearance, no fade (the body is still
   held hidden until promoted, so there's no blink either way). */
@media (prefers-reduced-motion: reduce) {
    .ov-hot-pop.ov-hot-pop-promoted { animation: none; }
}

.h-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
}
.h-scroll::-webkit-scrollbar { display: none; }

/* Push calendar (Trends tab) — responsive month grid.
   Desktop: up to 3 month-blocks per row, the whole grid centered in the card,
   partial rows left-aligned to the leftmost column; cells stay small and fixed
   (never stretched to 1fr). Mobile: 2 months per row, cells auto-size to the
   half-width column so two months always fit. Each cell's color + the "Now"
   ring are set inline (per-cell, dynamic); sizing lives here so it can flex per
   breakpoint. Month width = 7 cells + 6 gaps. */
.push-months {
    --cal-cell: 14px;
    --cal-gap: 6px;
    --cal-month-w: calc(var(--cal-cell) * 7 + var(--cal-gap) * 6);
    display: grid;
    grid-template-columns: repeat(2, 1fr);   /* mobile: 2-up, columns fill width */
    gap: var(--sp-4);
}
.push-month { min-width: 0; }
/* The 7-day grid, shared by each month's weekday header and its day cells */
.push-week-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);   /* mobile: cells fill the half-column */
    gap: var(--cal-gap);
}
/* One calendar cell. Mobile: fills its column as a square (auto-sized by the
   2-up constraint). Blank leading/trailing slots use the same class, untinted. */
.push-cell {
    width: 100%;
    aspect-ratio: 1 / 1;
    border-radius: 5px;
    box-sizing: border-box;
}
@media (min-width: 768px) {
    /* Left-aligned grid of fixed-width month tracks, capped at 3-up. auto-fit
       drops to 2-up then 1-up as the card narrows; the grid anchors to the
       card's left content edge (same inset as the heading), so months fill
       left->right/top->bottom and partial rows align to column 1 automatically.
       Leftover room falls to the right of the last month as clean margin. */
    .push-months {
        grid-template-columns: repeat(auto-fit, var(--cal-month-w));
        justify-content: start;
        max-width: calc(var(--cal-month-w) * 3 + var(--sp-5) * 2);
        gap: var(--sp-5);
    }
    .push-week-grid { grid-template-columns: repeat(7, var(--cal-cell)); }
    .push-cell { width: var(--cal-cell); height: var(--cal-cell); aspect-ratio: auto; }
}


/* ---- Date panel -----------------------------------------------------------
   Desktop: docks beside the rail. Mobile: slides up as a bottom sheet so it's
   easy to reach (not a right-side box). Visibility is toggled inline. */
.date-panel-overlay {
    position: absolute; top: 0; left: 184px; width: 300px; bottom: 0;
    background-color: #ffffff; box-shadow: 1px 0 6px rgba(0,0,0,0.12);
    z-index: 750; overflow-y: auto; padding: 12px 14px;
}

@media (max-width: 768px) {
    .date-panel-overlay {
        top: auto; left: 0; right: 0; width: auto; height: 85vh; bottom: 0;
        box-shadow: 0 -2px 8px rgba(0,0,0,0.18);
        border-top-left-radius: 12px; border-top-right-radius: 12px;
    }
}

/* Tame stray tap-highlight / focus boxes on the over-map buttons */
button { -webkit-tap-highlight-color: transparent; }
#sheet-open:focus, #date-panel-close:focus, #return-to-present:focus { outline: none; }

/* ---- Unified control card (species + Additional Data + legend) ------------
   ONE component with the SAME visual design on both platforms; only PLACEMENT
   differs. The card's chrome (white fill, border, radius, shadow, the two rows,
   the centered hairline) all comes inline from controls.build_control_card();
   these rules own placement only.

   The dock wraps the card + the map attribution "i". On DESKTOP the dock is
   display:contents so it generates no box — the card and the "i" each float
   independently against the map (card bottom-left, "i" bottom-right, see
   .map-attribution above). The card sits at left 200px (184px rail + 16px
   gutter), bottom 14px, a fixed 362px wide. */
.control-card-dock { display: contents; }
.control-card {
    position: absolute;
    z-index: 470;    /* match .map-attribution: below the drawer scrim (480) + panel (495) so an open rail drawer covers it, above the map + 450 ad layer (same trade-off the old bottom-left legend used) */
    left: 200px;
    bottom: 14px;
    width: 362px;
}

/* Info "i" tooltip (e.g. next to "Additional Data"): the bubble reveals on hover
   (desktop) / tap-focus (mobile) and opens UPWARD out of row 1 (the card sits
   near the map's bottom), right-aligned so it doesn't overflow the map edge. */
.info-icon .info-tip {
    display: none;
    position: absolute;
    bottom: calc(100% + 6px);   /* open upward (the card sits near the map's bottom) */
    right: 0;
    z-index: 650;
    white-space: nowrap;        /* single line, sizes to the text */
}
@media (max-width: 768px) {
    /* mobile: the controls card sits near the BOTTOM of the map, so the tip opens
       upward (room above, peek below) and is allowed to wrap + width-capped so the
       longer copy can't run off a narrow phone. The Additional Data pill can WRAP
       to line 2 (left side) on ≤~384px phones, so anchor the tip to the CARD's
       top-right — NOT the icon — otherwise a left-positioned pill's tip runs off
       the left edge. .info-icon goes static (dropping it as the containing block);
       .control-card (relative, above) is then the containing block. */
    .add-toggle .info-icon { position: static !important; }
    .info-icon .info-tip {
        white-space: normal; width: max-content;
        right: 14px;                     /* right-aligned to the card */
        bottom: calc(100% + 6px);        /* above the card's top edge */
        /* Cap leaves a comfortable left margin at 320px (clear of the 30px
           attribution disc at left:14), so the copy can't clip off-screen. */
        max-width: calc(100vw - 92px);
    }
}
.info-icon:hover .info-tip,
.info-icon:focus .info-tip,
.info-icon:focus-within .info-tip { display: block; }

@media (max-width: 768px) {
    /* Mobile home layout (measured on 390x844): the dock is a bottom-center flex
       column holding the attribution "i" (first) 15px above the control card
       (last). The founder's hand-tuned placement is KEPT: left/right 14px, bottom
       53px (just above the ~46.5px "Explore regions" peek), and a true 15px gap
       between the "i" and the card. The card fills the dock's width (362px at a
       390px phone), so it needs no width or bottom of its own here — the dock
       positions it, with NO hardcoded card height. */
    .control-card-dock {
        position: absolute;
        left: 14px; right: 14px; bottom: 53px;
        z-index: 600;
        display: flex;
        flex-direction: column;
        gap: 15px;
    }
    .control-card {
        /* relative (not static) so the card is the containing block for the
           Additional Data tooltip — see the .info-tip mobile rule below. No
           offsets, so the dock still positions it via flex exactly as before. */
        position: relative;
        left: auto; bottom: auto; width: auto;
    }
}

/* ============================================================================
   Auth & onboarding journey (WP6) — Surface B (log in / create / check email /
   forgot) + Surface C (choose your plan). Mobile-first: full-screen on phones,
   a centered card over the dimmed live map on desktop. Everything is scoped
   under the auth modal / .auth-surface so it can't touch other layouts. Tokens
   come from the :root block above (mirror of theme.py).
   ============================================================================ */

/* ---- Backdrop: dim + blur whatever is behind the account modal ------------
   The account modal can open on TOP of the region modal (the save-region
   prompt) as well as over the bare map (sign in / sign up / account home).
   Bootstrap gives .modal z-index 1055 and .modal-backdrop 1050, so the default
   backdrop sits BELOW the region modal (1055) and leaves it crisp behind the
   save card. We raise the account backdrop to 1085 so it dims the region modal
   (or the map) behind it, and blur it so the background reads as pushed back.
   Same precedent as the photo lightbox above. The account-modal WRAPPER
   (.auth-modal-shell) goes to 1090 so the card itself sits ABOVE the raised
   backdrop and stays crisp (only what is BEHIND the backdrop is blurred).
   The blur lives ONLY here, on the backdrop, which dbc mounts/unmounts with the
   modal — so on ✕ / backdrop tap / ESC / post-signup close the element (and its
   blur) is removed and nothing lingers over the region modal or map. */
.auth-modal-backdrop { z-index: 1085 !important; }
.auth-modal-shell { z-index: 1090 !important; }
/* Blur the region modal / map behind the account modal. Desktop only: on mobile
   the dialog is fullscreen (md-down), so it fully covers the backdrop and the blur
   would just be wasted compositing. Gate the blur on .show and transition it so it
   fades in AND out together with Bootstrap's opacity dim — backdrop-filter is NOT
   attenuated by the element's own opacity (Filter Effects L2), so without this the
   blur would snap on and, on close, briefly hang at full blur while the dim fades
   out (a flash of "blurred but no longer dimmed" over the region modal). */
@media (min-width: 769px) {
    .auth-modal-backdrop {
        backdrop-filter: blur(0);
        -webkit-backdrop-filter: blur(0);
        transition: opacity .15s linear, backdrop-filter .15s linear,
                    -webkit-backdrop-filter .15s linear;
    }
    .auth-modal-backdrop.show {
        backdrop-filter: blur(5px);
        -webkit-backdrop-filter: blur(5px);
    }
}

/* ---- Modal shell ---------------------------------------------------------- */
.auth-modal-content {
    background-color: var(--page-bg);
    border: none;
    border-radius: var(--radius-lg);
    overflow: hidden;
}
/* Desktop: a centered ~420px card floating over the dimmed map (the Bootstrap
   backdrop is the scrim; the map + rail stay visible behind it). GUARDED to
   desktop: unscoped, this rule (specificity 0,2,0) beat Bootstrap's
   .modal-fullscreen-md-down (0,1,0) on phones too, capping the mobile fullscreen
   dialog at 420px — which left a strip of map showing on the right edge of phones
   wider than 420px. On mobile, dbc fullscreen="md-down" makes the dialog
   full-viewport, which is what we want. */
@media (min-width: 769px) {
    .auth-modal-shell .modal-dialog { max-width: 420px; }
}

/* Mobile: dbc fullscreen="md-down" already makes the dialog full-viewport; drop
   the corners and let the surface fill the screen. */
@media (max-width: 768px) {
    .auth-modal-content { border-radius: 0; min-height: 100vh; }
    .auth-surface { min-height: 100vh; }
    /* Pin the fullscreen dialog's content to the TOP, not the vertical center.
       The dialog is .modal-dialog-centered (align-items: center) AND fullscreen,
       and the content above is min-height: 100vh. On mobile 100vh is the LARGE
       viewport (browser toolbars retracted), so while the toolbar is showing the
       content is taller than the visible dialog; centering then splits that
       overflow top and bottom and the top half — the X and the first content
       (e.g. the account-home title) — lands above the fold where the modal scroll
       can't reach it. Top-aligning sends the overflow downward instead, where it
       scrolls normally, so the top is always reachable. Short views still center
       via their own auto-margins inside the surface (below). */
    .auth-modal-shell .modal-dialog { align-items: flex-start; }
    /* Vertically CENTER the active auth view inside the fullscreen surface so the
       short views (forgot / check-email / save-prompt) sit centered like the
       login / create cards instead of clinging to the top. Auto margins center
       the view when there's spare room and collapse to 0 when the view is taller
       than the screen (create account), so a tall view still scrolls from the top
       and is never clipped. Desktop (the centered 420px card) is untouched. */
    .auth-surface > .auth-view:not([hidden]) {
        margin-top: auto;
        margin-bottom: auto;
    }
    /* When a status notice is showing, center the NOTICE + view together as one
       block (the notice carries the top auto-margin, the view the bottom) so the
       banner sits directly above the centered form — where the user is looking —
       instead of marooned at the very top. */
    .auth-surface > .auth-msg:not(:empty) {
        margin-top: auto;
        margin-bottom: var(--sp-3);
    }
    .auth-surface > .auth-msg:not(:empty) ~ .auth-view:not([hidden]) {
        margin-top: 0;
    }
    /* Same, for the signed-in account home (it's .acct-home, not .auth-view) so a
       notice shown above it doesn't double up the top auto-margin. */
    .auth-surface > .auth-msg:not(:empty) ~ .acct-home:not([hidden]) {
        margin-top: 0;
    }
}

/* ---- Surface + shared chrome --------------------------------------------- */
.auth-surface {
    position: relative;
    padding: 24px 28px 28px;
    font-family: var(--font-body);
    background-color: var(--page-bg);
    color: var(--text);
    display: flex;
    flex-direction: column;
}

/* Auth status notice — a prominent filled callout (NOT a faint top line) so a
   confirmation ("reset link on its way", "check your email", resend sent) can't
   be missed. INFO (house blue) reads as a neutral notice; it also carries the
   occasional soft, non-credential sign-in message. True credential errors stay
   inline in red (.auth-inline-error) and never route through here. */
.auth-msg {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--info-tint);
    border: 1px solid var(--info);
    border-left: 4px solid var(--info);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    /* Extra right padding keeps the text clear of the floating ✕ close button
       (top-right) when the notice sits at the top of the surface. */
    padding: 12px 56px 12px 14px;
    margin-bottom: var(--sp-3);
    font-size: var(--fs-small);
    font-weight: 500;
    line-height: 1.4;
    color: var(--info);
}
.auth-msg:empty { display: none; }

/* Warn variant — a gate / sign-in / sign-up problem (NOT a credential error,
   which stays inline in red). Amber reads as "attention", mirroring the
   checkout-canceled banner, so it's clearly a heads-up without the alarm of red. */
.auth-msg.auth-msg-warn {
    background: var(--warning-tint);
    border-color: var(--warning);
    border-left-color: var(--warning);
    color: var(--warning);
}

.auth-close {
    position: absolute;
    top: 14px; right: 14px;
    width: 34px; height: 34px;
    border: none; border-radius: 50%;
    background: var(--panel-bg);
    box-shadow: var(--shadow-sm);
    color: var(--text-secondary);
    font-size: 15px; cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    z-index: 5;
}
/* Keep the 34px visual disc but extend the tap target to the 44px minimum. */
.auth-close::before { content: ""; position: absolute; inset: -5px; }
.auth-close:hover { color: var(--text); }

/* Each journey step. A class selector beats the bare [hidden] rule, so the
   hidden views MUST be re-hidden explicitly or they'd all show at once. */
/* --av-gap is the column gap; the Turnstile slot's collapse references it so its
   negative margin always matches the active gap (incl. the desktop override below). */
.auth-view { --av-gap: var(--sp-4); display: flex; flex-direction: column; gap: var(--av-gap); }
.auth-view[hidden] { display: none; }

/* ---- Logo / titles / subs ------------------------------------------------ */
.auth-logo { display: flex; justify-content: center; }
.auth-logo-img { height: 120px; width: auto; display: block; }

.auth-title {
    font-family: var(--font-display);
    font-weight: 600;
    font-size: var(--fs-display);
    letter-spacing: -0.015em;
    line-height: 1.1;
    color: var(--text);
    margin: 0;
    text-align: center;
}
.auth-title-left { text-align: left; }

.auth-sub {
    font-size: var(--fs-body);
    color: var(--text-secondary);
    line-height: 1.5;
    text-align: center;
    margin: 6px 0 0;
}
.auth-sub-narrow { max-width: 260px; }

/* ---- Fields -------------------------------------------------------------- */
.auth-fields { display: flex; flex-direction: column; gap: var(--sp-3); }
.auth-label {
    font-size: var(--fs-micro);
    color: var(--text-secondary);
    font-weight: 500;
    margin-bottom: 6px;
}

.onb-input {
    width: 100%;
    box-sizing: border-box;
    min-height: var(--touch-min);
    padding: 12px;                 /* was 14px — tighter pill (min-height keeps the mobile tap target) */
    font-size: 16px;               /* 16px prevents iOS zoom-on-focus; desktop steps to 15px below */
    font-family: var(--font-body);
    color: var(--text);
    background: var(--panel-bg);
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
}
.onb-input::placeholder { color: var(--text-tertiary); }
.onb-input:focus { outline: none; border-color: var(--forest); }
/* Sign-in credentials error (red is reserved for true errors). */
.onb-input-error, .onb-input-error:focus { border-color: var(--danger); }

/* Password field: a bordered wrapper holding a borderless input + eye toggle */
.onb-field-pw {
    display: flex;
    align-items: center;
    min-height: var(--touch-min);
    padding: 0 12px 0 14px;
    background: var(--panel-bg);
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
}
.onb-field-pw:focus-within { border-color: var(--forest); }
.onb-input-bare {
    flex: 1;
    border: none;
    background: transparent;
    outline: none;
    padding: 12px 0;               /* was 14px — matches .onb-input's tighter pill */
    font-size: 16px;               /* 16px prevents iOS zoom-on-focus; desktop steps to 15px below */
    font-family: var(--font-body);
    color: var(--text);
}
.onb-input-bare::placeholder { color: var(--text-tertiary); }

/* Desktop steps the auth input font down to 15px; MOBILE stays at 16px on purpose
   — a focused input under 16px makes iOS Safari zoom the page in, which the founder
   does NOT want. (So mobile fields sit ~1px taller than desktop; that's the cost of
   no auto-zoom.) The auth modal is a centered card on desktop / fullscreen on mobile. */
@media (min-width: 769px) {
    .onb-input, .onb-input-bare { font-size: 15px; }
}

/* Desktop: the auth card is a centered modal, not a full screen, so it felt too
   tall — most of all the create-account view (name + email + password + opt-in +
   turnstile + CTA + Google + footer). Tighten the vertical rhythm on desktop ONLY
   (mobile is fullscreen and stays as-is): trim the surface padding, the per-step
   column gap, and the inter-field gap. The input pills / fonts / tap targets are
   unchanged, so nothing crowds. */
@media (min-width: 769px) {
    .auth-surface { padding: 18px 28px 22px; }
    .auth-view { --av-gap: var(--sp-3); }
    .auth-fields { gap: var(--sp-2); }
}
.onb-eye {
    border: none; background: none; cursor: pointer;
    color: var(--text-tertiary);
    padding: 8px; margin-left: 6px;
    display: flex; align-items: center;
}
.onb-eye:hover { color: var(--text-secondary); }

.auth-inline-error {
    color: var(--danger);
    font-size: var(--fs-caption);
    margin-top: -2px;
}
.auth-inline-error:empty { display: none; }

.auth-forgot-row { font-size: var(--fs-caption); }

/* ---- Turnstile bot-check slot (Log in / Create account) -------------------
   An empty div the Cloudflare widget draws into (assets/turnstile.js). It sits
   between the fields and the CTA inside the .auth-view flex column. Turnstile runs
   in "interaction-only" mode, so for almost every user it stays INVISIBLE — so the
   slot stays COLLAPSED (height 0) and a negative top margin cancels the column's
   gap, leaving NO empty band in the form (the common case). The slot is only in
   the DOM when the check is ON (onboarding.py's _turnstile_slot returns []
   otherwise). On the rare occasion Cloudflare needs a click, turnstile.js adds
   .ms-turnstile-show and the box opens to the 65px checkbox — but it SLIDES open
   over ~0.22s (height + margin transition) instead of hard-jumping the form, so
   the reveal is smooth, not jarring. --av-gap (set on .auth-view) keeps the
   collapse's negative margin matched to the active column gap at every breakpoint. */
.ms-turnstile {
    height: 0;
    overflow: hidden;
    margin-top: calc(-1 * var(--av-gap));
    display: flex;
    align-items: center;
    justify-content: center;
    transition: height 0.22s ease, margin-top 0.22s ease;
}
.ms-turnstile.ms-turnstile-show {
    height: 65px;
    margin-top: 0;
    overflow: visible;
}
/* Honor reduced-motion: open instantly (no slide), still no permanent gap. */
@media (prefers-reduced-motion: reduce) {
    .ms-turnstile { transition: none; }
}

/* ---- Buttons / CTAs ------------------------------------------------------ */
.auth-cta {
    width: 100%;
    box-sizing: border-box;
    min-height: var(--touch-min);
    padding: 15px;
    border: none;
    border-radius: var(--radius-md);
    font-family: var(--font-body);
    font-size: var(--fs-body-lg);
    font-weight: 600;
    text-align: center;
    text-decoration: none;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
}
.auth-cta.forest { background: var(--forest); color: #fff; }
.auth-cta.forest:hover { background: var(--forest-dark); color: #fff; }
.auth-cta.rust { background: var(--rust); color: #fff; }
.auth-cta.rust:hover { background: var(--rust-dark); color: #fff; }
.auth-cta.freeout {
    background: var(--forest-tint);
    color: var(--forest);
    border: 1.5px solid var(--forest);
}
.auth-cta.disabled {
    background: var(--border);
    color: var(--text-tertiary);
    cursor: not-allowed;
}

.auth-linkbtn {
    background: none; border: none; padding: 0;
    color: var(--forest);
    font-family: var(--font-body);
    font-size: var(--fs-small);
    font-weight: 600;
    cursor: pointer;
}
.auth-linkbtn:hover { color: var(--forest-dark); }
.auth-linkbtn.rust { color: var(--rust); }
.auth-linkbtn.rust:hover { color: var(--rust-dark); }

.auth-switch {
    text-align: center;
    font-size: var(--fs-small);
    color: var(--text-secondary);
}
.auth-switch .auth-linkbtn { display: inline; }

/* ---- OR divider + Google ------------------------------------------------- */
/* The OR divider separates two METHODS of one path (email vs Google), so it sits
   slightly tighter than the plain divider: the negative margin trims the
   auth-view flex gap (16px) down to ~11px above/below. */
.auth-or {
    display: flex; align-items: center; gap: 12px;
    color: var(--text-tertiary);
    font-size: var(--fs-micro);
    margin: -5px 0;
}
.auth-or span { flex: 1; height: 1px; background: var(--border); }

/* Plain hairline divider (no "OR"). Separates two DIFFERENT paths (sign-in vs
   create-account), so it keeps the normal auth-view rhythm (the 16px flex gap). */
.auth-divider { height: 1px; background: var(--border); }

.auth-google {
    display: flex; align-items: center; justify-content: center; gap: 10px;
    width: 100%; box-sizing: border-box;
    min-height: var(--touch-min);
    padding: 13px;
    background: var(--panel-bg);
    color: var(--text);
    border: 1.5px solid var(--border-strong);
    border-radius: var(--radius-md);
    font-family: var(--font-body);
    font-size: var(--fs-body);
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}
.auth-google:hover { background: var(--page-bg); color: var(--text); }

/* ---- Opt-in + fine print ------------------------------------------------- */
.auth-optin { font-size: 12.5px; color: var(--text-secondary); }
.auth-optin .form-check-label { color: var(--text-secondary); line-height: 1.4; }
.auth-optin input { accent-color: var(--forest); }

.auth-fine {
    font-size: var(--fs-micro);
    color: var(--text-tertiary);
    line-height: 1.45;
    text-align: center;
}
.auth-fine-bottom { margin-top: auto; padding: 10px 0 4px; }
.auth-fine-lnk { color: var(--forest); font-weight: 600; }
.auth-fine-lnk-underline { text-decoration: underline; text-underline-offset: 2px; }
/* The subscribe acknowledgment must stay legible (handoff rule) — a step up
   from the tertiary grey used for the purely informational fine print. */
.onb-plan-cta .auth-fine { color: var(--text-secondary); }

/* ---- Check-your-email ---------------------------------------------------- */
.auth-centered {
    flex: 1;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    text-align: center; gap: var(--sp-4);
    padding: 24px 0 32px;
}
.auth-icore {
    width: 72px; height: 72px; border-radius: 50%;
    background: var(--forest-tint); color: var(--forest);
    font-size: 30px;
    display: flex; align-items: center; justify-content: center;
}
.auth-resend { font-size: var(--fs-small); color: var(--text-tertiary); }
.auth-resend .auth-linkbtn { display: inline; }
.auth-back-row { text-align: center; padding: 8px 0 0; }

/* ---- Progress bar -------------------------------------------------------- */
.onb-progress { display: flex; gap: 5px; width: 100%; }
.onb-progress i {
    flex: 1; height: 5px; border-radius: 3px;
    background: var(--border);
}
.onb-progress i.on { background: var(--forest); }

/* ---- Choose-your-plan comparison ----------------------------------------- */
.onb-plan-table { position: relative; margin-top: 16px; }

.onb-thead {
    position: relative; z-index: 1;
    display: flex; align-items: flex-end;
    padding: 0 0 8px;
}
.onb-thead-b { flex: 1; font-size: 13px; font-weight: 700; color: var(--text); }
.onb-thead-f {
    width: 60px; font-size: 12px; font-weight: 600; color: var(--text-tertiary);
    display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.onb-thead-p {
    width: 76px; font-size: 12px; font-weight: 700; color: var(--rust);
    display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.onb-free-lab-on { color: var(--forest); }

.onb-radio {
    width: 14px; height: 14px; border-radius: 50%;
    border: 1.5px solid var(--border-strong);
    display: inline-block; box-sizing: border-box;
}
.onb-radio.on-free { border-color: var(--forest); box-shadow: inset 0 0 0 3px var(--forest); }
.onb-radio.on-prem { border-color: var(--rust); box-shadow: inset 0 0 0 3px var(--rust); }

.onb-trow {
    position: relative; z-index: 1;
    display: flex; align-items: center;
    padding: 7.5px 0;
    border-top: 1px solid var(--border);
}
.onb-trow-lab { flex: 1; }
.onb-trow-lab b { display: block; font-size: 13px; font-weight: 600; color: var(--text); }
.onb-trow-lab span { display: block; font-size: 11px; color: var(--text-tertiary); margin-top: 1px; }
.onb-trow-lab.onb-dim { opacity: 0.35; }
.onb-fc { width: 60px; text-align: center; }
.onb-pc { width: 76px; text-align: center; }
.onb-fc.yes-f { color: var(--forest); }
.onb-pc.yes-p { color: var(--rust); }
.onb-fc.no, .onb-pc.no { color: var(--border-strong); }

/* Soft tint behind the chosen column (rows sit above it via z-index) */
.onb-tint { position: absolute; z-index: 0; border-radius: 14px; }
.onb-tint-premium {
    top: 26px; right: -6px; width: 82px; bottom: -6px;
    background: var(--rust-tint);
    border: 1.5px solid var(--rust);
}
.onb-tint-free {
    top: 26px; right: 76px; width: 62px; bottom: -6px;
    background: var(--forest-tint);
    border: 1.5px solid var(--forest);
}

/* Transparent tap targets over each column (whole-column selection) */
.onb-col-hit {
    position: absolute; top: 0; bottom: 0;
    background: transparent; border: none; padding: 0;
    cursor: pointer; z-index: 2;
}
.onb-col-hit-free { right: 76px; width: 64px; }
.onb-col-hit-premium { right: 0; width: 82px; }

.onb-plan-cta { margin-top: 16px; display: flex; flex-direction: column; gap: var(--sp-2); }

/* ============================================================================
   Account cluster (WP6) — Surface A entry point (mobile disc + desktop rail cap
   + flyout) and the Account home / settings / deletion screens. Mobile-first;
   everything scoped under .acct-* so it can't touch other layouts. Tokens come
   from the :root block above (mirror of theme.py). COLOR DISCIPLINE: rust is
   reserved for the subscribe path — nothing here is rust; the delete action is
   the one true-danger surface (--danger).
   ============================================================================ */

/* ---- Surface A: mobile disc (top-right island) --------------------------- */
.acct-entry-island {
    position: absolute; top: 12px; right: 12px; z-index: 600;
}
/* Mobile date entry: mirrors the account disc, top-LEFT. Reuses .acct-entry-btn +
   .acct-disc-glyph for an identical 44x44 disc; only the corner differs. */
.calendar-entry-island {
    position: absolute; top: 12px; left: 12px; z-index: 600;
}
.acct-entry-btn {
    width: 46px; height: 46px; padding: 0; border: none; background: none;
    cursor: pointer; display: flex; align-items: center; justify-content: center;
}
/* Signed-out: neutral white utility disc with a forest person glyph */
.acct-disc-glyph {
    width: 46px; height: 46px; border-radius: 50%;
    background: var(--panel-bg); color: var(--forest);
    display: flex; align-items: center; justify-content: center;
    font-size: 20px; box-shadow: 0 3px 10px rgba(45,40,32,.24);
}
/* Signed-in: first-initial avatar with a forest presence dot + white ring */
.acct-avatar { position: relative; display: inline-flex; }
.acct-avatar-initial {
    width: 46px; height: 46px; border-radius: 50%;
    background: var(--forest-tint); color: var(--forest-dark);
    font-family: var(--font-display); font-weight: 600; font-size: 19px;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 0 0 3px var(--panel-bg), 0 3px 10px rgba(45,40,32,.24);
}
.acct-presence-dot {
    position: absolute; bottom: 0; right: 0;
    width: 12px; height: 12px; border-radius: 50%;
    background: var(--forest); box-shadow: 0 0 0 2px var(--panel-bg);
}

@media (max-width: 768px) {
    /* Mobile home layout (measured on 390x844): the account button's TOP edge sits
       15px from the top of the map — the same top line as the ad island (it is no
       longer vertically centered on the ad card) — inset from the right at 14px.
       The .ad-slot.mobile-only max-width guard keeps the centered ad clear of it
       sideways. The signed-out disc becomes a 44x44 white
       circle sharing the ad
       card's 1px border + soft two-layer shadow, with a muted-grey person glyph
       (the previous forest glyph + heavier drop shadow are replaced). The
       signed-in avatar matches the 44px footprint; its own tinted look is kept. */
    .acct-entry-island { top: 15px; right: 14px; }   /* was 61px — top-aligned with the ad island at 15px */
    .calendar-entry-island { top: 15px; left: 14px; }   /* mirrors the account disc on the left */
    .acct-entry-btn { width: 44px; height: 44px; }
    .acct-entry-island .acct-avatar-initial { width: 44px; height: 44px; }
    .acct-disc-glyph {
        width: 44px; height: 44px; box-sizing: border-box;   /* border-box so the 1px border keeps the disc at 44x44 */
        color: var(--text-secondary); font-size: 19px;
        border: 1px solid var(--border);
        box-shadow: 0 1px 2px rgba(45,45,45,.06), 0 2px 6px rgba(45,45,45,.08);
    }
}

/* ---- Surface A: desktop rail cap + flyout --------------------------------
   The account row is pinned flush to the BOTTOM of the left rail (mirrors the
   brand cap on top), spanning the rail width with a hairline border-top. It's a
   rail sibling in app.py, positioned absolute over the rail's bottom. The
   "Account" label is injected as a ::after on the button (its DOM text lives in
   account_home.py, out of this item's scope). */
.acct-cap-desktop {
    /* z-index clears the legend + attribution (now 470) so the signed-out cap's
       hover tooltip isn't covered; stays under scrim/flyout/sheet (690/700) + date
       panel (750), still above the rail (500). */
    position: absolute; left: 0; bottom: 0; width: 184px; z-index: 560;
    border-top: 1px solid var(--border); background: var(--panel-bg);
}
.acct-cap-btn {
    width: 100%; padding: 13px 16px; border: none; background: none; cursor: pointer;
    display: flex; align-items: center; gap: 10px;
    font-family: var(--font-body);
}
.acct-cap-btn:hover { background: #f3f2ee; }
/* The "Account" label (text node lives in account_home.py; shown in both the
   signed-out and signed-in states). */
.acct-cap-btn::after {
    content: "Account";
    font-family: var(--font-body); font-size: 14px; font-weight: 500;
    color: var(--text); white-space: nowrap;
}
.acct-cap-glyph { position: relative; display: inline-flex; align-items: center; flex: none; }
/* Signed-out: a 30px warm-grey circle holding the person glyph */
.acct-cap-glyph > i {
    width: 30px; height: 30px; border-radius: 50%;
    background: #f2f0ec; color: var(--text-secondary);
    display: flex; align-items: center; justify-content: center;
    font-size: 15px; border: 1px solid var(--border); flex: none;
}
/* Hover tooltip for the signed-out cap — floated above the row so it clears the
   now-visible "Account" label. */
.acct-cap-tip {
    position: absolute; left: 0; bottom: calc(100% + 10px); top: auto; transform: none;
    background: var(--text); color: var(--page-bg); font-size: 12px; white-space: nowrap;
    padding: 7px 11px; border-radius: var(--radius-sm);
    box-shadow: var(--shadow-md); opacity: 0; pointer-events: none;
    transition: opacity .12s ease; z-index: 702;
}
.acct-cap-btn:hover .acct-cap-tip { opacity: 1; }
/* Signed-in cap avatar (presence dot beside the initial, sitting in the row) */
.acct-cap-avatar { display: flex; flex-direction: row; align-items: center; gap: 6px; flex: none; }
.acct-presence-dot-cap {
    position: static; width: 7px; height: 7px;
    box-shadow: 0 0 0 3px rgba(61,76,65,.22);
}
.acct-cap-avatar .acct-avatar-initial {
    width: 30px; height: 30px; font-size: 15px;
    box-shadow: 0 0 0 3px rgba(255,255,255,.9), var(--shadow-sm);
}

.acct-flyout-scrim { position: absolute; inset: 0; z-index: 690; background: transparent; }
.acct-flyout {
    position: absolute; left: 188px; bottom: 14px; width: 214px; z-index: 700;
    background: var(--panel-bg); border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg); padding: 8px; font-family: var(--font-body);
}
.acct-flyout[hidden], .acct-flyout-scrim[hidden] { display: none; }
.acct-flyout-idrow {
    display: flex; align-items: center; gap: 11px;
    padding: 10px 10px 12px; border-bottom: 1px solid var(--border);
    margin-bottom: 6px;
}
.acct-flyout-initial {
    width: 40px; height: 40px; flex: none; border-radius: 50%;
    background: var(--forest-tint); color: var(--forest-dark);
    font-family: var(--font-display); font-weight: 600; font-size: 17px;
    display: flex; align-items: center; justify-content: center;
}
.acct-flyout-idtext { min-width: 0; }
.acct-flyout-name { font-size: var(--fs-small); font-weight: 600; color: var(--text); }
.acct-flyout-email {
    font-size: var(--fs-caption); color: var(--text-tertiary);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acct-flyout-item {
    width: 100%; display: flex; align-items: center; gap: 11px;
    padding: 11px 12px; border: none; background: none; cursor: pointer;
    border-radius: var(--radius-sm); font-size: var(--fs-small);
    color: var(--text); text-align: left; font-family: var(--font-body);
}
.acct-flyout-item i { color: var(--forest); font-size: 16px; }
.acct-flyout-item:hover { background: var(--forest-tint); }
.acct-flyout-divider { height: 1px; background: var(--border); margin: 6px 8px; }
.acct-flyout-signout { color: var(--forest); font-weight: 500; }

/* ---- Account home: screens + titles -------------------------------------- */
.acct-home { display: flex; flex-direction: column; }
.acct-home[hidden] { display: none; }   /* a class selector otherwise beats bare [hidden] */
.acct-screen { display: flex; flex-direction: column; gap: var(--sp-4); }
.acct-screen[hidden] { display: none; }
.acct-settings-msg {
    font-size: var(--fs-small); color: var(--text-secondary);
    min-height: 0; margin-bottom: var(--sp-1);
}
.acct-settings-msg:empty { display: none; }
.acct-title {
    font-family: var(--font-display); font-weight: 600; font-size: 30px;
    letter-spacing: -.015em; line-height: 1.05; color: var(--text); margin: 2px 0 2px;
}
.acct-title-danger { color: var(--danger); }
.acct-title-count { font-family: var(--font-body); font-weight: 400; font-size: 18px; color: var(--text-tertiary); }

/* ---- Account home: cards + rows ------------------------------------------ */
.acct-card {
    background: var(--panel-bg); border-radius: var(--radius-md);
    box-shadow: 0 1px 2px rgba(45,40,32,.05), 0 6px 18px -10px rgba(45,40,32,.18);
}
.acct-group { overflow: hidden; }
.acct-identity { display: flex; align-items: center; gap: 14px; padding: 16px; }
.acct-id-avatar {
    width: 52px; height: 52px; flex: none; border-radius: 50%;
    background: var(--forest-tint); color: var(--forest);
    font-family: var(--font-display); font-weight: 600; font-size: 22px;
    display: flex; align-items: center; justify-content: center;
}
.acct-id-text { flex: 1; min-width: 0; }
.acct-id-name { font-size: 17px; font-weight: 600; color: var(--text); }
.acct-id-email {
    font-size: var(--fs-small); color: var(--text-tertiary); margin-top: 2px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.acct-badge {
    flex: none; font-size: 9px; font-weight: 700; letter-spacing: .07em;
    border-radius: var(--radius-pill); padding: 5px 9px;
}
.acct-badge-premium { color: var(--forest); background: var(--forest-tint); }
.acct-badge-free { color: var(--text-tertiary); background: var(--border); }

.acct-membership {
    display: flex; align-items: center; gap: 10px; margin: -4px 4px 0;
    font-size: var(--fs-caption); color: var(--text-secondary);
}
.acct-presence-dot-strip { position: static; width: 7px; height: 7px; box-shadow: none; flex: none; }

/* List rows (buttons + the Manage-billing anchor) share one look */
.acct-row {
    width: 100%; display: flex; align-items: center; gap: 15px;
    min-height: var(--touch-min); padding: 13px 16px;
    border: none; background: none; cursor: pointer; text-align: left;
    font-family: var(--font-body); font-size: 15px; color: var(--text);
    text-decoration: none;
}
.acct-row:hover { background: var(--forest-tint); }
.acct-row-ico { color: var(--forest); font-size: 20px; display: flex; flex: none; width: 22px; }
.acct-row-lab { flex: 1; min-width: 0; }
.acct-row-chev { color: var(--border-strong); font-size: 18px; }
.acct-row-count { font-size: var(--fs-small); color: var(--text-tertiary); }
.acct-row-ext { color: var(--text-tertiary); font-size: 14px; display: flex; }
.acct-row-div { height: 1px; background: var(--border); margin-left: 52px; }
.acct-row-danger, .acct-row-danger .acct-row-ico { color: var(--danger); }
/* MS-007 — the lifetime "no billing to manage" note (dbc.Popover). A small,
   neutral card on brand tokens; never rust (rust is the subscribe path only). */
.acct-lifetime-pop { max-width: 260px; border-color: var(--border); }
.acct-lifetime-pop .popover-body {
    font-family: var(--font-body); font-size: var(--fs-caption);
    color: var(--text-secondary); padding: 10px 12px;
}
.acct-hint { font-size: var(--fs-caption); color: var(--text-tertiary); margin: 4px 6px 0; }
/* Inline field-validation error (UX-007) — danger red, under the field, hidden
   when empty. Mirrors the .auth-inline-error convention (red = true errors). */
.acct-field-err { font-size: var(--fs-caption); color: var(--danger); font-weight: 500; margin: 4px 6px 0; }
.acct-field-err:empty { display: none; }
.acct-hint-tight { margin: 6px 0 0; }
.acct-hint-row { display: flex; align-items: flex-start; gap: 10px; line-height: 1.45; }
.acct-hint-ico { color: var(--forest); font-size: 15px; margin-top: 1px; }
.acct-empty { font-size: var(--fs-small); color: var(--text-secondary); padding: 4px 2px; }

/* ---- Account home: back control + drill-in fields ------------------------ */
.acct-back {
    align-self: flex-start; display: inline-flex; align-items: center; gap: 4px;
    border: none; background: none; cursor: pointer; padding: 4px 0;
    min-height: var(--touch-min);
    font-family: var(--font-body); font-size: 15px; font-weight: 600; color: var(--forest);
}
.acct-back-caret { font-size: 19px; line-height: 0; margin-top: -1px; }
.acct-fields { display: flex; flex-direction: column; gap: 18px; }
.acct-field { display: flex; flex-direction: column; }
.acct-field-topdiv { border-top: 1px solid var(--border); padding-top: 16px; }
.acct-field-lab { font-size: var(--fs-caption); color: var(--text-tertiary); font-weight: 500; margin-bottom: 7px; }
.acct-input {
    width: 100%; border: 1.5px solid var(--border-strong); border-radius: var(--radius-md);
    padding: 13px 14px; font-size: 16px; color: var(--text);
    background: var(--panel-bg); font-family: var(--font-body); box-sizing: border-box;
}
.acct-input::placeholder { color: var(--text-tertiary); }
.acct-input:focus { outline: none; border-color: var(--forest); }
.acct-input-lock {
    display: flex; justify-content: space-between; align-items: center;
    border-color: var(--border); color: var(--text-tertiary);
}
.acct-input-lock i { color: var(--text-tertiary); }
.acct-toggle-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; }
.acct-toggle-text { flex: 1; }
.acct-toggle-lab { font-size: 15px; font-weight: 500; color: var(--text); }
.acct-toggle-switch { margin: 0; padding: 0; min-height: 0; }
.acct-toggle-switch .form-check-input {
    width: 44px; height: 26px; margin: 0; cursor: pointer;
}
.acct-mkt-switch:checked { background-color: var(--forest) !important; border-color: var(--forest) !important; }
.acct-note { font-size: var(--fs-body); color: var(--text-secondary); line-height: 1.5; }

/* ---- Account home: My regions rows --------------------------------------- */
.acct-region-row { display: flex; align-items: center; gap: 13px; padding: 13px 16px; }
.acct-region-ico {
    width: 40px; height: 40px; flex: none; border-radius: var(--radius-md);
    background: var(--forest-tint); color: var(--forest);
    display: flex; align-items: center; justify-content: center; font-size: 18px;
}
.acct-region-name { flex: 1; font-size: 15px; font-weight: 500; color: var(--text); }

/* ---- Account home: CTAs + delete flow ------------------------------------ */
.acct-cta {
    width: 100%; min-height: var(--touch-min); border: none;
    border-radius: var(--radius-md); font-family: var(--font-body);
    font-size: 15px; font-weight: 600; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
}
.acct-cta.forest { background: var(--forest); color: #fff; }
.acct-cta.forest:hover { background: var(--forest-dark); color: #fff; }
.acct-acks { display: flex; flex-direction: column; gap: 14px; margin: 4px 0; }
/* Bootstrap 5 .form-check-input is appearance:none, so accent-color is ignored —
   style the box directly so the checked state reads danger, not Bootstrap blue. */
.acct-ack .form-check-input {
    width: 24px; height: 24px; margin-top: 0;
    border: 1.6px solid var(--danger); border-radius: 7px;
}
.acct-ack .form-check-input:checked {
    background-color: var(--danger); border-color: var(--danger);
}
.acct-ack .form-check-input:focus { box-shadow: 0 0 0 3px rgba(124, 39, 35, .2); }
.acct-ack .form-check-label { font-size: var(--fs-body); color: var(--text); line-height: 1.4; }
/* Delete button: grey/disabled until all three acks are ticked, then brick red */
.acct-del-btn { background: var(--danger-tint); color: var(--danger); opacity: .55; cursor: not-allowed; }
.acct-del-btn.acct-del-armed { background: var(--danger); color: #fff; cursor: pointer; }
.acct-keep-btn {
    width: 100%; border: none; background: none; cursor: pointer; padding: 6px;
    min-height: var(--touch-min);
    font-family: var(--font-body); font-size: 15px; font-weight: 600; color: var(--forest);
}
.acct-version { text-align: center; font-size: var(--fs-micro); color: var(--text-tertiary); margin: 22px 0 6px; }

/* On phones the account home fills the full-screen modal like the auth views. */
@media (max-width: 768px) {
    .acct-cap-desktop, .acct-flyout, .acct-flyout-scrim { display: none !important; }
}

/* ============================================================================
   Transactional cluster (WP6) — post-checkout Stripe returns, the anonymous
   save-region prompt, and the save-confirmation toast. Mobile-first; everything
   scoped under .co-* / .save-prompt-* / .save-toast so it can't touch other
   layouts. Tokens come from the :root block above (mirror of theme.py). COLOR
   DISCIPLINE: the success moment is FOREST (nothing left to sell); the canceled
   banner is AMBER (--warning, attention not error); the save prompt is FOREST
   (saving is a free action) — no rust anywhere in this file.
   ============================================================================ */

/* ---- Canceled-checkout amber banner (sits atop the plan step) ------------- */
.co-banner {
    display: flex; align-items: center; gap: 10px;
    background: var(--warning-tint);
    border: 1px solid var(--warning);
    border-radius: var(--radius-md);
    padding: 12px 14px;
    margin-top: var(--sp-3);
}
.co-banner-ic {
    width: 22px; height: 22px; flex: none; border-radius: 50%;
    background: var(--warning); color: #fff;
    font-size: 13px; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
}
.co-banner-tx { font-size: 12.5px; color: var(--warning); line-height: 1.35; }
.co-banner-tx b { font-weight: 700; }
/* An empty banner slot must collapse (the ordinary onboarding path). */
#onb-plan-banner:empty { display: none; }

/* ---- Checkout success: processing beat ----------------------------------- */
.co-processing {
    flex: 1;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    text-align: center; gap: var(--sp-5);
    padding: 40px 8px 48px;
}
.co-proc-copy { display: flex; flex-direction: column; gap: var(--sp-2); }
.co-spinner {
    width: 56px; height: 56px; flex: none; border-radius: 50%;
    border: 4px solid var(--border);
    border-top-color: var(--forest);
    animation: co-spin 0.9s linear infinite;
}
@keyframes co-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .co-spinner { animation-duration: 2.4s; }
}

/* ---- Checkout success: confirmed (premium active, forest, no rust CTA) ---- */
.co-confirmed {
    display: flex; flex-direction: column;
    align-items: center; text-align: center; gap: var(--sp-4);
    padding: 4px 0 8px;
}
.co-confirmed .auth-logo-img { height: 150px; }
.co-feat-card {
    align-self: stretch;
    background: var(--panel-bg);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
    padding: 4px 16px;
    text-align: left;
}
.co-feat-row {
    display: flex; align-items: center; gap: 12px;
    padding: 11px 0;
}
.co-feat-div { border-bottom: 1px solid var(--forest-tint); }
.co-check {
    width: 26px; height: 26px; flex: none; border-radius: 50%;
    background: var(--forest-tint); color: var(--forest);
    display: flex; align-items: center; justify-content: center;
    font-size: 15px;
}
.co-feat-tx { font-size: 12.5px; color: var(--text); }
.co-confirmed .auth-cta { align-self: stretch; margin-top: 2px; }
.co-receipt { font-size: 11.5px; color: var(--text-tertiary); }
.co-receipt b { color: var(--text-secondary); font-weight: 600; }

/* ---- Anonymous save-region prompt (free hook, forest throughout) ----------
   Reworked to the house card system (§6.3): the icon + copy sit in a lifted
   white card on the warm surface; the actions below keep the house button
   hierarchy (forest primary / Google secondary / Log in tertiary). */
.save-prompt-view { align-items: center; text-align: center; }
.save-prompt-card {
    align-self: stretch;
    display: flex; flex-direction: column; align-items: center;
    gap: var(--sp-3);
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
    padding: var(--sp-6) var(--sp-5);
}
.save-prompt-ico {
    width: 64px; height: 64px; border-radius: 50%;
    background: var(--forest-tint); color: var(--forest);
    display: flex; align-items: center; justify-content: center;
    font-size: 28px;
    box-shadow: inset 0 0 0 1px rgba(61, 76, 65, 0.10);
    margin-bottom: 2px;
}
.save-prompt-title {
    font-family: var(--font-display); font-weight: 600;
    font-size: var(--fs-h1); color: var(--text);
    letter-spacing: -0.015em; line-height: 1.15;
}
.save-prompt-sub {
    font-size: var(--fs-body); color: var(--text-secondary); line-height: 1.5;
    max-width: 300px; margin: 0 auto;
}
.save-prompt-view .auth-cta,
.save-prompt-view .auth-google { align-self: stretch; }

/* ---- Save-confirmation toast (auto-dismissed by its duration) ------------- */
.save-toast {
    position: fixed;
    top: 96px; left: 50%; transform: translateX(-50%);
    z-index: 2000;
    width: auto; max-width: calc(100vw - 32px);
    border: none; border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
}
/* dbc.Toast wraps its children in .toast-body; we have no header. */
.save-toast .toast-header { display: none; }
.save-toast .toast-body {
    display: flex; align-items: center; gap: 10px;
    padding: 12px 16px;
    font-size: 13px; white-space: nowrap;
}
.save-toast-ico { display: flex; align-items: center; font-size: 16px; }
.save-toast-tx { font-weight: 500; }
/* Saved = forest fill, light text, filled green star. */
.save-toast-saved { background: var(--forest-dark); }
.save-toast-saved .toast-body { color: #f3efe7; }
.save-toast-saved .save-toast-ico { color: #8fb47e; }
.save-toast-saved .save-toast-tx { font-weight: 500; }
/* Removed = white fill, forest text, outline star. */
.save-toast-removed { background: var(--panel-bg); }
.save-toast-removed .toast-body { color: var(--forest); }
.save-toast-removed .save-toast-ico { color: var(--forest); }
.save-toast-removed .save-toast-tx { font-weight: 600; }

/* ============================================================================
   Weather Outlook card (Overview tab, below the map) — see weather_card.py.
   Token-driven flex layout. The icon box is a FIXED size so an na/fallback icon
   can never reflow the 4-column row.
   ============================================================================ */
.wx-strip {
    display: flex;
    gap: var(--sp-2);
    align-items: flex-start;
    /* Cap + center so on desktop the 4 cells don't spread across the whole
       Overview column; still fits 4 columns at ~366px on mobile. */
    max-width: 420px;
    margin: 0 auto;
}
.wx-day {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    text-align: center;
}
.wx-day-icon {
    width: 36px;
    height: 36px;
    object-fit: contain;
    display: block;
}
.wx-day-dow,
.wx-day-date,
.wx-day-temp,
.wx-day-wind { line-height: 1.2; }
.wx-day-date,
.wx-day-temp,
.wx-day-wind { white-space: nowrap; }

/* ============================================================================
   Town search ("Find a town") — drawer / bottom-sheet location search.
   The permanent panel CLONES the Date panel: docked beside the rail on desktop,
   a tall bottom sheet on mobile so the top search box clears the keyboard. The
   mobile entry disc sits just below the calendar disc. Token-driven throughout.
   ============================================================================ */

/* Permanent panel overlay — clones .date-panel-overlay show/hide/position. */
.search-panel-overlay {
    position: absolute; top: 0; left: 184px; width: 300px; bottom: 0;
    background-color: var(--panel-bg); box-shadow: 1px 0 6px rgba(45,45,45,0.12);
    z-index: 750; overflow-y: auto; padding: var(--sp-3) var(--sp-4);
}
@media (max-width: 768px) {
    .search-panel-overlay {
        top: auto; left: 0; right: 0; width: auto; height: 85vh; bottom: 0;
        box-shadow: var(--shadow-sheet);
        border-top-left-radius: var(--radius-lg);
        border-top-right-radius: var(--radius-lg);
    }
}

/* Close ✕ — a real 44x44 tap target, flex-centered, pulled back with a negative
   margin so the glyph still sits at the header's top-right edge. */
#search-panel-close {
    width: var(--touch-min); height: var(--touch-min);
    display: flex; align-items: center; justify-content: center;
    margin: calc(-1 * var(--sp-2)) calc(-1 * var(--sp-2)) calc(-1 * var(--sp-2)) 0;
}

/* Dropdown chrome (react-select) — forest focus, touch height, house tokens.
   Scoped to the "Find a town" Dropdown (dcc.Dropdown className="search-city-dd").
   The extra .Select qualifiers lift specificity above react-select's own blue
   focus rule. */
.search-city-dd .Select-control {
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    min-height: var(--touch-min);
    font-family: var(--font-body);
}
.search-city-dd.Select.is-focused:not(.is-open) > .Select-control,
.search-city-dd.Select.is-open > .Select-control {
    border-color: var(--forest);
    box-shadow: 0 0 0 3px var(--forest-tint);
}
.search-city-dd .Select-placeholder,
.search-city-dd .Select-arrow { color: var(--text-tertiary); }
.search-city-dd .Select-menu-outer {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
}
.search-city-dd .VirtualizedSelectFocusedOption,
.search-city-dd .Select-option.is-focused {
    background: var(--forest-tint);
    color: var(--forest);
}
@media (max-width: 768px) {
    /* Keep the input at 16px so iOS doesn't zoom the page in on focus. */
    .search-city-dd .Select-input > input { font-size: 16px; }
}

/* Mobile search entry disc: a second 44x44 disc BELOW the calendar disc
   (top-left of the map). Mirrors .calendar-entry-island; reuses .acct-entry-btn
   + .acct-disc-glyph for an identical disc — only the position differs. */
.search-entry-island {
    position: absolute; top: 66px; left: 12px; z-index: 600;
}
@media (max-width: 768px) {
    .search-entry-island { top: 67px; left: 14px; }   /* SP_2 (8px) gap below the calendar disc: 15+44=59, +8=67 */
}

/* Result rows: three-line clickable row — Location header / Region sub-header
   (smaller + muted) / count body. Card + dividers mirror .drawer-region-card. */
/* No card chrome — the white card on the white panel read as a seam. Rows run
   edge-to-edge within the panel padding; the hairline dividers stay (mobile-sheet
   precedent). No shadow. */
.search-result-card {
    background: var(--panel-bg); border: none; border-radius: 0;
}
.search-result-row {
    display: block; width: 100%; text-align: left; cursor: pointer;
    border: none; background: none; font-family: var(--font-body);
    padding: var(--sp-3);
    border-bottom: 1px solid var(--border);
    border-left: 3px solid transparent;
}
.search-result-row:last-child { border-bottom: none; }
.search-result-row:hover {
    background: var(--forest-tint); border-left-color: var(--forest);
}
/* Touch press feedback (no hover on touch). */
.search-result-row:active {
    background: var(--forest-tint); border-left-color: var(--forest);
}
.search-result-name {
    font-size: var(--fs-small); font-weight: 600; color: var(--text);
}
.search-result-region {
    font-size: var(--fs-caption); color: var(--text-tertiary); margin-top: 1px;
}
.search-result-body {
    font-size: var(--fs-caption); color: var(--text-secondary); margin-top: 2px;
}

/* Find-a-town sort toggle (UX-016): the two RadioItems options read as quiet
   inline labels; the active one goes forest + semibold (the app's active
   convention), the small radio dots pick up the forest accent. */
.search-sort { display: inline-flex; gap: var(--sp-3); font-size: var(--fs-caption); }
.search-sort label {
    display: inline-flex; align-items: center; gap: 4px; cursor: pointer;
    color: var(--text-secondary);
}
.search-sort label:has(input:checked) { color: var(--forest); font-weight: 600; }
.search-sort input[type="radio"] {
    accent-color: var(--forest); width: 13px; height: 13px; cursor: pointer;
    margin: 0 4px 0 0;   /* dcc renders label as inline-block, so gap:4px is dead;
                            a right margin is what actually separates dot from text */
}
