/* ============================================================
   VISUAL REDESIGN PASS ("instrument panel") — checkpoint 1: base +
   typography foundation. tokens.css (loaded before this file — see
   base.html) is the single source of truth for every colour/font/
   geometry value referenced below via var(--...); nothing in THIS pass
   hardcodes a new one-off colour or font stack.
   Applied app-wide to shared navigation, controls and workspaces.
   Phase 25 recoloured the base palette dark -> "daylight" (cool paper,
   ink text, periwinkle accent) purely by changing tokens.css values;
   this file only followed with the handful of hardcoded tints that
   could not read from a token (see the "Phase 25" comments inline).
   ============================================================ */
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: var(--font-body);
  background: var(--color-base);
  color: var(--color-text);
}
/* One global link colour (accent) rather than styling every individual
   popup/popover's own anchors separately — covers popup body links
   (school website, GP contact, info-popover citation) uniformly; the
   topnav's own links (above) intentionally override this with a more
   muted resting colour, only turning accent on hover. */
a { color: var(--color-accent); }
code { font-family: var(--font-mono); }

.topnav {
  display: flex;
  gap: 1.25rem;
  align-items: center;
  padding: 0.6rem 1rem;
  background: var(--color-panel);
  border-bottom: var(--border-width) solid var(--color-border);
  color: var(--color-text);
}
/* Brand/nav links stay in the BODY face, not the display face — the
   brief scopes the display face to "section/group headers and sidebar
   category labels only"; the topnav's own links are navigation, not a
   category label, so this is a deliberate literal reading rather than
   an inconsistency. Weight/spacing carries the "wordmark" feel instead
   of a font swap. */
.topnav a { color: var(--color-text-muted); text-decoration: none; }
.topnav a:hover { color: var(--color-accent); text-decoration: underline; }
.topnav .brand { display: inline-flex; align-items: center; margin-right: 0.5rem; color: var(--color-text); flex-shrink: 0; }
.topnav .brand picture { display: contents; }
.topnav .brand img { display: block; width: 98px; height: 34px; object-fit: contain; }
.topnav .about-link { white-space: nowrap; }

.about-page { max-width: 1120px; margin: 0 auto; padding: 2rem 1.25rem 4rem; line-height: 1.55; }
.about-logo { display: block; width: 172px; height: 60px; object-fit: contain; margin-bottom: 1.25rem; }
.about-page h1, .about-page h2 { font-family: var(--font-display); }
.source-table-wrap { overflow-x: auto; }
.source-table { width: 100%; border-collapse: collapse; font-size: 0.84rem; }
.source-table th, .source-table td { padding: 0.7rem; border: 1px solid var(--color-border); text-align: left; vertical-align: top; }
.source-table details p { max-width: 65ch; }
/* The signature live status readout (app/static/js/status-readout.js) —
   pushed to the far right of the nav row via margin-left:auto, quiet and
   small so it reads as ambient telemetry, not a competing UI element.
   Hidden on Home; the map journeys retain their live readout. */
#status-readout {
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--color-text-muted);
  white-space: nowrap;
  letter-spacing: 0.02em;
}

main { padding: 0; }

/* --- Shared control chrome (visual redesign pass) ---
   Every dropdown, text/number/date/search input in the app gets the
   EXACT same border/corner/background/focus treatment from one place,
   rather than each control type styling itself independently — the
   brief's own "consistency across every control type" ask. Type
   selectors, not a shared class, since every matching element in this
   app genuinely IS one of this project's own controls (no embedded
   third-party forms to accidentally catch). Layout-only properties
   (width, margin, font-size) stay on each control's own specific
   selector, defined elsewhere in this file. <input type=range> (the
   opacity slider) is the one deliberate exception, styled separately
   further down — a filled track communicates state differently than a
   bordered box. */
select,
input[type="text"],
input[type="number"],
input[type="date"],
input[type="search"] {
  background-color: var(--color-input-bg);
  color: var(--color-text);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  font-family: var(--font-body);
}
select:hover,
input[type="text"]:hover,
input[type="number"]:hover,
input[type="date"]:hover,
input[type="search"]:hover {
  border-color: var(--color-accent);
}
select:focus,
input[type="text"]:focus,
input[type="number"]:focus,
input[type="date"]:focus,
input[type="search"]:focus {
  outline: 1px solid var(--color-accent);
  outline-offset: -1px;
  border-color: var(--color-accent);
}
/* Date inputs render their own native calendar-picker glyph — inverted
   so it's visible against a dark field (the browser draws it in black
   by default, which disappears against --color-input-bg). */
input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(0.8); }

/* --- Checkboxes: a crisp, mechanical state change (interaction/motion
   direction) — a custom flat box + tick rather than the browser's own
   default control (which can't be recoloured to fit a dark palette
   consistently across browsers), with NO transition on the checked
   state — the instant flip IS the "mechanical" feel, not an animated
   approximation of one. */
.layer-toggle, .tier-toggle {
  appearance: none;
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  margin: 0;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-sm);
  background-color: var(--color-input-bg);
  cursor: pointer;
  position: relative;
  flex-shrink: 0;
  display: inline-block;
  vertical-align: middle;
}
.layer-toggle:hover, .tier-toggle:hover:hover { border-color: var(--color-accent); }
.layer-toggle:checked, .tier-toggle:checked:checked {
  background-color: var(--color-accent);
  border-color: var(--color-accent);
}
.layer-toggle:checked::after, .tier-toggle:checked::after:checked::after {
  content: "";
  position: absolute;
  left: 4px;
  top: 1px;
  width: 3px;
  height: 7px;
  border: solid var(--color-base);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
.layer-toggle:focus-visible, .tier-toggle:focus-visible {
  outline: 1px solid var(--color-accent);
  outline-offset: 2px;
}

/* --- Motion, extended (seven-checkpoint layout & visual pass, item 7) —
   the visual redesign pass's own motion section (above: instant/no-
   transition checkboxes and carets, a "crisp, mechanical" deliberate
   choice) is EXTENDED here with real motion for four specific triggers
   the brief named, not replaced or duplicated with a second system:
   layer fade in/out (app/static/js/layers.js's fadeLayerIn/
   fadeLayerOutThenRemove add/remove this class on each sublayer's own
   real DOM element — L.circleMarker/L.marker/L.Path all expose
   getElement() in the vendored Leaflet 1.9.4, confirmed directly before
   relying on it), and the active-layers chip enter/exit further below.
   Sidebar category expand/collapse and popup fade-in are covered too —
   see their own comments at each one's actual rule for why neither
   needed new CSS here. All of this already inherits the EXISTING
   blanket prefers-reduced-motion rule just below (a universal selector,
   so it already overrides these new rules' own durations too — checked,
   not assumed, since it's a real correctness question for new motion
   added after that blanket rule was written). */
.oll-layer-fade { transition: opacity 180ms ease; }

/* (Phase 17 removed the separate active-layers strip and its chip
   enter/exit animation — the legend rebuilds its whole body on every
   state change, so there is no per-pill diff to animate. The legend
   pill styles live in the Legend block further down.) */

/* --- Motion: respected throughout, not just where it happens to be
   convenient — kills every transition/animation in the app for anyone
   who has asked their OS/browser for reduced motion, including the
   Recently Sold loading spinner (its own "Loading…" text already
   conveys the same state without the animation, so nothing is lost). --- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

/* --- Map page ---
   Bug fix: the sidebar (#layer-panel, inside #map-controls) previously
   had no height bound of its own — with enough sidebar content expanded
   at once (now six top-level categories, some with nested
   groups), it grew taller than the viewport. Since it was
   `position: absolute` with no positioned+clipping ANCESTOR, that
   overflow grew the whole PAGE's own scrollable height (a real CSS
   gotcha: an absolutely-positioned element with no containing/clipping
   ancestor still expands the document's scrollHeight), so scrolling the
   page to reach the rest of the sidebar's content scrolled #map — a
   separate, normal-flow element earlier in the DOM — off-screen
   entirely, leaving a floating sidebar over blank page background
   (exactly what the reported screenshots showed).
   Fix: #map-page is a real bounding container, sized to EXACTLY the
   viewport height minus the top nav, with `overflow: hidden` — no
   absolutely-positioned child inside it (map-controls, legend,
   shortlist) can ever grow the PAGE's own
   scrollable area, regardless of how much content it holds. #map fills
   100% of that fixed box, so it can never move or shrink no matter what
   the sidebar does. #map-controls itself is separately bounded to the
   box's own height (`bottom: 12px`) with #layer-panel — the one part
   whose content genuinely varies a lot — as the flexed, independently
   `overflow-y: auto` scrolling column; #office-search-panel keeps its
   own natural (small, bounded) height above it.
   (position:fixed elements — the mobile toolbar/sheets, the first-visit
   tour — are unaffected by this: `overflow: hidden` on an ancestor does
   NOT clip a `position: fixed` descendant unless that ancestor also has
   a transform/filter/perspective set, none of which apply here.) --- */
:root {
  /* Fallback defaults for the custom properties app/static/js/map-init.js
     sets from real rendered measurements (see that file's own
     updateSafeAreaInsets comment) — used only for the one frame before
     that JS runs; nothing depends on them staying accurate afterwards.
     --leaflet-attribution-safe: the attribution control's own height —
     every bottom-anchored panel/control now shares this ONE baseline
     (layout pass, item 1) since the base-layer switcher was pulled out
     of Leaflet's own bottom-right flow (see .leaflet-control-layers
     below), leaving attribution as the only thing left to clear there.
     --shortlist-safe-bottom: #shortlist-panel's own current bottom edge
     — the zoom control (below) is positioned from this instead of the
     other way around, now that it moves BELOW shortlist, not above it. */
  --leaflet-attribution-safe: 34px;
  --shortlist-safe-bottom: 160px;
}
#map-page {
  position: relative;
  height: calc(100vh - var(--topnav-height, 46px));
  overflow: hidden;
  margin: 0;
}
#map {
  height: 100%;
  width: 100%;
}
/* Positions both the office-search box and the layer panel as one
   stacked column in the same top-right corner — office search sits
   directly above the layer panel, not scattered separately. Bounded to
   #map-page's own height rather than growing past it — see the bug-fix
   comment above.
   top: 12px — tightened from an earlier 60px (bug-fix pass, item 2):
   there's no Leaflet control at top-right to clear (the zoom control is
   top-LEFT, the base-layer switcher is bottom-right), so the old 60px gap
   was just unused space; freeing it is what makes room for the bottom
   inset below without the panel's own visible area shrinking as much as
   it otherwise would.
   bottom: --leaflet-attribution-safe — this panel's own bottom edge now
   shares the EXACT SAME baseline as #legend-panel (layout pass, item 1 —
   previously it used a
   separate, taller "clear the whole bottom-right stack" inset, which
   left this panel stopping noticeably short of Legend's own bottom edge
   and left the base-layer switcher crowding this panel's corner). The
   switcher has since moved out of Leaflet's bottom-right flow entirely
   (see .leaflet-control-layers below), so attribution's own height is
   now the only thing any bottom-anchored panel needs to clear. */
#map-controls {
  position: absolute;
  z-index: 1000;
  top: 12px;
  right: 12px;
  bottom: calc(var(--leaflet-attribution-safe, 34px) + 12px);
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-width: 260px;
}

/* Leaflet's base-map (light/dark/standard/satellite) switcher, moved
   clear of #map-controls' own corner (layout pass, item 1) — it used to
   sit in Leaflet's default bottom-right slot, which put it directly
   beneath/beside this panel's bottom-right corner with only a small
   safe-area gap between them, easy to reintroduce a collision if either
   panel's size changed. `position: absolute` on the SPECIFIC control
   (same technique as .leaflet-control-zoom above) pulls it out of
   Leaflet's own float-based layout so it can sit CLEAR to the left of
   the visible desktop workspace instead, using its measured left edge
   plus a 12px gap. The fallback retains Advanced Search's old 284px.
   Bottom-aligned to the same
   --leaflet-attribution-safe baseline every other bottom-anchored panel
   now shares — with this control pulled out of the bottom-right flow,
   attribution is the only thing left in it, so that one measurement is
   sufficient (see map-init.js's updateSafeAreaInsets comment). */
.leaflet-control-layers {
  position: absolute !important;
  right: var(--map-right-workspace-safe, 284px) !important;
  bottom: calc(var(--leaflet-attribution-safe, 34px) + 12px) !important;
  margin: 0 !important;
}

/* Every floating panel/module in this app shares one "instrument panel"
   surface treatment: dark panel colour + a barely-visible blueprint grid
   texture, a hairline border, and sharp (zero-radius) corners — hairline
   dividers do the separating work a drop-shadow used to, per the brief's
   own "sharpen corners... hairline dividers instead of drop-shadow
   separation" direction. Defined once here (office-search + layer-panel)
   and repeated verbatim for every other panel below (shortlist, legend,
   info popover, popups) rather than a shared class,
   since several of these already need their OWN id-scoped rule for
   position/sizing anyway — duplicating four declarations is simpler than
   threading a shared class through markup that already has a lot going
   on. */
#office-search-panel, #layer-panel {
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  box-shadow: none;
  color: var(--color-text);
}
/* office-search-panel keeps its natural (small) height; layer-panel is
   the one that flexes to fill whatever's left. Phase 27 split the
   scrolling that used to live on #layer-panel itself onto the new
   #layer-panel-body instead, so the header stays put (and stays
   clickable) while only the tree beneath it scrolls — same reason
   #legend-panel/#legend-body are already split this way. min-height: 0
   is required on both — without it, a flex child won't shrink below its
   own content size, which would silently defeat overflow-y: auto and
   bring back the exact bug the original split fixed. */
#office-search-panel { flex: 0 0 auto; }
#layer-panel {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
/* Panel titles ("Layers", "Office address", "Shortlist", "Legend") are
   the ONE other spot besides sidebar category labels that reads as a
   genuine module label rather than body prose — display face, same
   treatment as .oll-label (tokens.css), not a new one-off rule.
   #layer-panel-header replaces the old plain <h2>Layers</h2> (Phase
   27) — a real button in both modes, styled identically to that old
   heading by default; only body.oll-mode-explorer (below) gives it a
   pointer cursor and a visible caret, since only there does clicking it
   do anything. */
#layer-panel-header {
  flex: 0 0 auto;
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: none;
  border: none;
  padding: 0;
  margin: 0 0 0.5rem;
  font: inherit;
  cursor: default;
  text-align: left;
  color: var(--color-text);
}
.layer-panel-title {
  font-size: 0.85rem;
  font-family: var(--font-display);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.layer-panel-caret {
  display: none; /* shown only in explorer mode, below */
  margin-left: auto;
  font-size: 0.7rem;
  color: var(--color-text-muted);
  /* No transition — same crisp, instant flip as .layer-group-caret. */
}
#layer-panel-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

/* Expanded categories keep their content height inside the scrolling
   column. Flex shrinking otherwise clips rows when several are open. */
#layer-panel-body > .layer-group,
#layer-panel-body > .layer-search-input {
  flex-shrink: 0;
}

/* --- Sidebar search (UX pass, item 2) — same input styling as
   #office-search-input for visual consistency. app/static/js/
   layer-search.js expands a match's containing group(s) and highlights
   the matched row(s) via .layer-search-match; never hides anything. --- */
.layer-search-input {
  display: block;
  width: 100%;
  font-size: 0.85rem;
  padding: 0.35rem 0.5rem;
  margin-bottom: 0.6rem;
}
/* A found/highlighted row reads as an accent-drawn-attention state (the
   same family as a focus ring), not a new warm-amber "found!" colour —
   restrained to the one accent colour rather than adding a second
   attention hue. */
.layer-control.layer-search-match {
  background: rgba(76, 91, 212, 0.1); /* --color-accent (#4C5BD4) at ~10% — Phase 25 */
  outline: 1px solid var(--color-accent);
  outline-offset: 1px;
  border-radius: var(--radius);
}

/* --- Shortlist / notes (UX pass, item 5): top-left — the one map
   corner not already claimed by #map-controls (top-right), the legend
   (bottom-left), or the base-map switcher (bottom-right).
   app/static/js/shortlist.js owns all behaviour; always visible (no
   "empty" hidden state — the button needs to stay reachable). --- */
/* top: 12px — back to a plain fixed inset (layout pass, item 1). The
   previous bug-fix pass pushed this panel DOWN to clear Leaflet's zoom
   control, which used to sit above it at a higher effective stacking
   order; this pass instead moves the ZOOM CONTROL itself below
   #shortlist-panel (see .leaflet-control-zoom further down), so this
   panel no longer needs to make room for anything above it — it can go
   back to the same tight top inset every other top-anchored element
   here uses. */
#shortlist-panel {
  position: absolute;
  z-index: 999;
  top: 12px;
  left: 12px;
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: none;
  /* Widened from 220px (feedback round Phase 2, item 3) — a real,
     worth-fixing-now issue found while adding Export/Import alongside
     the existing Compare/+Add pin buttons: the header was ALREADY
     tight enough with just two buttons to clip the panel's own title
     text ("HORTLIST"), confirmed by direct measurement, not left for
     the later mobile sweep (checkpoint 7) since this is a desktop
     layout issue, not a mobile-specific one. */
  max-width: 260px;
  max-height: 55vh;
  overflow-y: auto;
  font-size: 0.78rem;
  color: var(--color-text);
}
#shortlist-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.4rem 0.5rem;
  padding: 0.5rem 0.6rem;
  border-bottom: var(--border-width) solid var(--color-border);
}
.shortlist-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  flex-shrink: 0;
}
/* The "+ Add pin" button is this panel's one primary action — the
   accent colour is deliberately spent here (a meaningful action
   indicator, not decoration) rather than left as a neutral grey button.
   NOTE: the placed PIN's own marker colour (violet, shortlist.js) is
   UNCHANGED by this pass — CLAUDE.md already frames that as a deliberate
   "different silhouette" marker-identification decision, the same
   category as icon-config.js's own marker colours, so it's treated as
   data-adjacent, not chrome. Only this button's OWN chrome (independent
   of the marker it creates) moves to the token palette. */
#shortlist-add-btn {
  font-size: 0.72rem;
  padding: 0.25rem 0.5rem;
  border: var(--border-width) solid var(--color-accent);
  border-radius: var(--radius);
  background: var(--color-accent);
  color: var(--color-base);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
#shortlist-add-btn:hover { opacity: 0.85; }
#shortlist-add-btn:disabled { background: var(--color-border); border-color: var(--color-border); color: var(--color-text-dim); cursor: default; opacity: 1; }
/* Visual cue while a pin placement is armed — see shortlist.js's
   beginPlacing/endPlacing. */
#map.shortlist-placing { cursor: crosshair; }

/* Comparison view (feedback round Phase 2, item 2) — the header now
   holds two buttons, not one; Compare is the secondary/neutral action
   (the accent colour stays spent on "+ Add pin" as this panel's one
   primary action, per that button's own existing comment above). */
#shortlist-header-actions { display: flex; align-items: center; gap: 0.4rem; }
#shortlist-compare-btn {
  font-size: 0.72rem;
  padding: 0.25rem 0.5rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-text);
  cursor: pointer;
  white-space: nowrap;
}
#shortlist-compare-btn:hover:not(:disabled) { border-color: var(--color-accent); color: var(--color-accent); }
#shortlist-compare-btn:disabled { color: var(--color-text-dim); cursor: default; }

/* Share (three-pin-type pass, requirement 5 — replaces the earlier
   Export/Import pair) — same neutral button treatment as Compare
   above. */
#shortlist-share-btn {
  font-size: 0.72rem;
  padding: 0.25rem 0.5rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-text);
  cursor: pointer;
  white-space: nowrap;
}
#shortlist-share-btn:hover:not(:disabled) { border-color: var(--color-accent); color: var(--color-accent); }
#shortlist-share-btn:disabled { color: var(--color-text-dim); cursor: default; }
/* A plain, honest one-line outcome for the Share action (link copied,
   or a fallback selectable box when clipboard access is denied) —
   mirrors .recently-sold-hint's own existing "small status line under
   a control" treatment rather than inventing a new one. */
#shortlist-share-status {
  font-size: 0.72rem;
  padding: 0.35rem 0.6rem 0;
  color: var(--color-text-muted);
}
.shortlist-share-fallback-input {
  display: block;
  width: 100%;
  font-size: 0.72rem;
  margin-top: 0.2rem;
  padding: 0.2rem 0.4rem;
}
/* Phase 34d — the one property-centred "Distance from home" ring
   control, replacing the old per-layer radius dropdowns. Sits in the
   Shortlist panel (where candidate selection already lives), hidden
   entirely until a candidate is selected — see
   app/static/js/property-distance-ring.js. */
#distance-ring-control[hidden] { display: none; }
#distance-ring-control {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.72rem;
  color: var(--color-text-muted);
  padding: 0.35rem 0.6rem;
  border-top: var(--border-width) solid var(--color-border);
  border-bottom: var(--border-width) solid var(--color-border);
}
#distance-ring-select { font-size: 0.72rem; }
/* A pin loaded from someone else's Share link — view-only, never
   written to this browser's own localStorage (see shortlist.js's
   `ephemeral` flag) — reuses the ~approx badge's own visual treatment,
   distinct wording. */
.shortlist-shared-badge {
  font-size: 0.62rem;
  font-weight: 600;
  color: var(--color-accent);
  border: var(--border-width) solid var(--color-accent);
  border-radius: 999px;
  padding: 0.02rem 0.35rem;
  margin-left: 0.2rem;
}

/* Comparison modal — same fixed-overlay/backdrop/dialog shell as
   #first-visit-tour, sized for a table rather than a short paragraph. */
#shortlist-compare-modal[hidden] { display: none; }
#shortlist-compare-modal {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}
#shortlist-compare-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
}
#shortlist-compare-dialog {
  position: relative;
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  padding: 1rem 1.25rem 1.25rem;
  max-width: min(1000px, 100%);
  max-height: 85vh;
  width: 100%;
  display: flex;
  flex-direction: column;
}
#shortlist-compare-dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 0.6rem;
}
#shortlist-compare-title {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text);
  margin: 0;
}
#shortlist-compare-close {
  background: none;
  border: none;
  color: var(--color-text-muted);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  padding: 0.1rem 0.3rem;
}
#shortlist-compare-close:hover { color: var(--color-accent); }
/* Wide tables scroll inside their own container rather than the dialog
   (or the page) growing horizontally. */
#shortlist-compare-table-wrap { overflow: auto; }
#shortlist-compare-table { border-collapse: collapse; font-size: 0.82rem; width: 100%; }
#shortlist-compare-table th, #shortlist-compare-table td {
  border: var(--border-width) solid var(--color-border);
  padding: 0.4rem 0.6rem;
  text-align: left;
  vertical-align: top;
}
#shortlist-compare-table thead th { background: var(--color-input-bg); white-space: nowrap; }
#shortlist-compare-table tbody th { white-space: nowrap; color: var(--color-text-muted); font-weight: 600; }
.shortlist-compare-pin-name { font-weight: 600; color: var(--color-text); }
.shortlist-compare-pin-address { font-weight: 400; font-size: 0.72rem; color: var(--color-text-muted); margin-top: 0.15rem; }
.shortlist-compare-row-label,
.shortlist-summary-label {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}
.shortlist-summary-symbol {
  display: inline-flex;
  width: 1rem;
  height: 1rem;
  flex: 0 0 1rem;
  color: var(--color-accent);
}
.shortlist-summary-symbol svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.45;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* Phase 6: comparison columns carry the same three pin kinds as the
   Shortlist. Candidate stays on the base table surface; neighbourhood
   reuses its established teal wash; POI gets a restrained neutral wash. */
#shortlist-compare-table .shortlist-compare-col-neighbourhood {
  background-color: rgba(79, 209, 197, 0.09);
}
#shortlist-compare-table .shortlist-compare-col-poi {
  background-color: rgba(127, 140, 141, 0.08);
}

/* Leaflet's own zoom control (+/-), moved below #shortlist-panel instead
   of above it (layout pass, item 1) — `position: absolute` on the
   SPECIFIC control (not its `.leaflet-top.leaflet-left` container)
   pulls it out of Leaflet's own float-based control layout entirely, so
   it can be positioned from a real measurement instead: map-init.js's
   updateSafeAreaInsets keeps --shortlist-safe-bottom in sync with
   #shortlist-panel's own actual rendered height (which changes as pins
   are added/removed) via ResizeObserver, the same live-measurement
   pattern the bottom-anchored panels below already use for the
   attribution control. `!important` is needed specifically for the
   margin: leaflet.css's own `.leaflet-top .leaflet-control { margin-top:
   10px; }` (a descendant-combinator rule, specificity 0,2,0) outranks a
   plain `.leaflet-control-zoom` class rule (0,1,0) regardless of source
   order — `position` doesn't strictly need it (a same-specificity tie
   already goes to this file, which loads after leaflet.css), but it's
   kept alongside for one clear, consistent override rather than two
   different override mechanisms in one rule. */
.leaflet-control-zoom {
  position: absolute !important;
  top: calc(var(--shortlist-safe-bottom, 160px) + 12px) !important;
  left: 12px !important;
  margin: 0 !important;
}

#shortlist-list {
  list-style: none;
  margin: 0;
  padding: 0 0.5rem 0.5rem;
}
.shortlist-list-item {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
  padding: 0.2rem 0;
  border-top: var(--border-width) solid var(--color-border);
}
.shortlist-list-item:first-child { border-top: none; }
.shortlist-list-link-wrap {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
}
.shortlist-list-link {
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  background: none;
  border: none;
  padding: 0.1rem 0;
  font-size: 0.78rem;
  font-family: var(--font-body);
  color: var(--color-text);
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shortlist-list-link:hover { color: var(--color-accent); text-decoration: underline; }
/* Phase 36, requirement 8 — the candidate list row's inline rename
   input, swapped in for the name link on double-click. Sized/spaced to
   sit in exactly the same flex slot as .shortlist-list-link so the row
   doesn't jump when renaming starts. A visible focus ring (the browser
   default outline, just not suppressed) is the "visible focus state"
   the brief asks for — deliberately not styled away. */
.shortlist-rename-input {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.78rem;
  font-family: var(--font-body);
  padding: 0.05rem 0.2rem;
  border: var(--border-width) solid var(--color-accent);
  border-radius: var(--radius-sm);
  background: var(--color-panel);
  color: var(--color-text);
}
/* Phase 36, requirement 9/10 — the small persistent-colour dot used on a
   candidate row, its popup title and its Compare column header. A
   border in the STRONGER stroke tone around the pastel FILL keeps it
   legible even against a similarly-pale panel background. */
.shortlist-candidate-dot {
  flex: 0 0 auto;
  display: inline-block;
  width: 0.62rem;
  height: 0.62rem;
  border-radius: 50%;
  border: 1.5px solid;
  margin-right: 0.35rem;
  vertical-align: middle;
}
.shortlist-list-remove {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: var(--color-text-muted);
  font-size: 0.85rem;
  line-height: 1;
  padding: 0 0.15rem;
  cursor: pointer;
}
.shortlist-list-remove:hover { color: var(--color-warning); }
.shortlist-list-empty-hint { color: var(--color-text-muted); font-style: italic; padding: 0.2rem 0; }

/* Three-pin-type pass (requirement 3) — a candidate row's own controls
   live in this nested "row" div rather than directly in the <li>, so
   the <li> can also hold the nested POI <ul> (.shortlist-poi-sublist)
   as a normal block sibling underneath. .shortlist-list-item-candidate
   itself carries no padding of its own — the row div does, exactly
   matching the padding every other .shortlist-list-item already used
   before this pass, so a candidate WITHOUT any nested POI still looks
   identical to a plain row. */
.shortlist-list-item-candidate { display: block; padding: 0; }
.shortlist-list-item-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.3rem;
  padding: 0.2rem 0;
}
.shortlist-parent-toggle {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: var(--color-text-muted);
  font-size: 0.65rem;
  line-height: 1;
  padding: 0 0.1rem;
  cursor: pointer;
}
.shortlist-parent-toggle:hover { color: var(--color-accent); }
.shortlist-parent-count {
  flex: 0 0 auto;
  font-size: 0.68rem;
  color: var(--color-text-muted);
}
/* Phase 36 fix, requirement 5 — the resolved address as secondary text
   under a candidate's editable name, same row; wraps onto its own line
   via flex-basis:100% rather than truncating or squeezing the name. */
.shortlist-candidate-address {
  flex: 1 0 100%;
  font-size: 0.68rem;
  color: var(--color-text-muted);
  margin-left: 1.6rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Nested POI rows: an indented plain <ul>, no bullet/marker of its own
   — each row already reads as a list item via its own border-top. */
.shortlist-poi-sublist { list-style: none; margin: 0; padding: 0; }
.shortlist-poi-sublist .shortlist-list-item-poi { padding-left: 1.1rem; }
.shortlist-poi-symbol {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 0; /* the inline SVG itself carries the real size */
}
/* An unassigned POI's own top-level bucket heading (requirement 3's
   fallback for a POI whose tagged candidate no longer exists, or never
   existed) — plain muted label, not a row of its own controls. */
.shortlist-list-section-heading {
  color: var(--color-text-dim);
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding-top: 0.4rem;
}
/* Neighbourhood row: a translucent wash of the marker's own discovery-
   teal colour (requirement 3's "visually distinct row background from
   candidate pins"). This stays the legacy teal #4FD1C5 at 12% — it is the
   neighbourhood-PIN distinction rendered as a row, not generic chrome, so
   Phase 25's base recolour left it alone (same call as the neighbourhood
   pin colour in shortlist.js and the Compare neighbourhood column). It is
   no longer equal to --color-accent, which moved to periwinkle. */
.shortlist-list-item-neighbourhood {
  background: rgba(79, 209, 197, 0.12);
  padding: 0.2rem 0.3rem;
  border-radius: var(--radius-sm);
}

/* Mode-scoped emphasis (Candidate Lookup rename + persona polish pass):
   BOTH pin types stay visible in either mode, but the pins belonging to
   the mode you're NOT in are de-emphasised — candidate pins recede in
   explorer mode, neighbourhood pins recede in Candidate Lookup mode.
   Saved-POI pins are never dimmed: a POI is an accessory to either
   journey. The list side is pure CSS (rows already carry a per-kind
   class); the marker side needs shortlist.js's refreshPinDimming() to
   put .oll-pin-dimmed on the Leaflet icon element, since a POI pin's
   marker is icon-config.js's own divIcon, not one this file controls. */
body.oll-mode-explorer #shortlist-list > .shortlist-list-item-candidate,
body.oll-mode-decided  #shortlist-list > .shortlist-list-item-neighbourhood {
  opacity: 0.5;
}
.leaflet-marker-icon.oll-pin-dimmed { opacity: 0.38; }

/* Phase 36 — the "Distance ring" button that replaced the per-pin
   isochrone lookup (buildIsochroneControls, removed from shortlist.js),
   sitting in the same row slot as the old 🧭 icon. A plain icon button;
   its glyph is an inline SVG using currentColor, so this is the only
   place its visual size/colour is set. */
.shortlist-distance-ring-btn {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: var(--color-text-muted);
}
.shortlist-distance-ring-btn:hover { color: var(--color-accent); }
.shortlist-distance-ring-btn svg { width: 100%; height: 100%; }

/* Popup content (view + edit modes) — bound via L.divIcon/bindPopup in
   shortlist.js, not server-rendered. Leaflet's own .leaflet-popup-
   content-wrapper/.leaflet-popup-tip carry the panel surface treatment
   (see the "Leaflet popup chrome" block further down); this only styles
   what's INSIDE that wrapper. */
.shortlist-popup { font-size: 0.85rem; min-width: 160px; font-family: var(--font-body); }
.shortlist-popup-note { margin: 0.3rem 0; color: var(--color-text-muted); white-space: pre-wrap; }
/* A saved POI's own source layer (six-checkpoint pass, item 6) — small
   muted subtitle, same treatment as .borough-popup-subtitle elsewhere. */
.shortlist-popup-source { font-size: 0.72rem; color: var(--color-text-muted); margin-top: 0.1rem; }
/* Phase 36 fix, requirement 5 — the resolved address as secondary text
   under a candidate's editable name in its popup, same muted treatment. */
.shortlist-summary-address { font-size: 0.72rem; color: var(--color-text-muted); margin-top: 0.1rem; }
.shortlist-popup-actions { display: flex; gap: 0.4rem; margin-top: 0.4rem; }
.shortlist-popup-actions button {
  font-size: 0.75rem;
  padding: 0.2rem 0.5rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-text);
  cursor: pointer;
}
.shortlist-popup-actions button:hover { border-color: var(--color-accent); color: var(--color-accent); }
.shortlist-save-btn { border-color: var(--color-accent) !important; color: var(--color-accent) !important; font-weight: 600; }
.shortlist-name-input, .shortlist-note-input {
  display: block;
  width: 100%;
  font-size: 0.82rem;
  padding: 0.3rem 0.4rem;
  margin-bottom: 0.35rem;
  font-family: inherit;
}
/* Phase 36 — the lightweight candidate-creation popup's single "Name the
   home" field label; .shortlist-name-input above already carries its
   own full input styling, so this only needs the label text itself. */
.shortlist-create-label {
  display: block;
  font-size: 0.72rem;
  color: var(--color-text-muted);
  margin-bottom: 0.2rem;
}

/* Per-pin summary card (feedback round Phase 2, item 1) — a plain
   label/value checklist, deliberately not a score/ranking (see
   shortlist.js's own comment). Wider than the ordinary view/edit popup
   since it has more to show. */
.shortlist-popup-summary {
  min-width: 260px;
  max-width: 320px;
  max-height: calc(100vh - var(--topnav-height, 46px) - var(--mobile-toolbar-height, 0px) - var(--leaflet-attribution-safe, 34px) - 100px);
  overflow-y: auto;
}
.shortlist-summary-body { margin-top: 0.4rem; }
.shortlist-summary-row {
  display: flex;
  flex-direction: column;
  gap: 0.05rem;
  padding: 0.3rem 0;
  border-top: var(--border-width) solid var(--color-border);
}
.shortlist-summary-row:first-child { border-top: none; }
/* label + optional "Show on map" button on one line, value below —
   mirrors the guided-discovery hex card's .discovery-check-head. */
.shortlist-summary-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
}
.shortlist-summary-label {
  font-size: 0.68rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
}
.shortlist-summary-value { font-size: 0.82rem; color: var(--color-text); }
.shortlist-summary-back {
  margin-top: 0.5rem;
  font-size: 0.75rem;
  padding: 0.2rem 0.5rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-text);
  cursor: pointer;
}
.shortlist-summary-back:hover { border-color: var(--color-accent); color: var(--color-accent); }

/* --- Legend (UX pass, item 1): persistent, collapsible colour key for
   the CURRENTLY ACTIVE layers only — app/static/js/legend.js rebuilds
   #legend-body's content on every active-layers.js change. Bottom-left,
   deliberately apart from the office-search/layer-panel stack in the
   top-right (#map-controls) so the two persistent UI stacks don't
   compete for the same corner. .legend-empty (no active layers) hides
   the whole panel rather than showing an empty box. --- */
/* bottom: --leaflet-attribution-safe (bug-fix pass, item 1) — this panel
   is bottom-LEFT, nowhere near the bottom-right base-layer switcher
   horizontally, but the attribution control's own text is now long
   enough (OSM + Esri + Geoapify + HM Land Registry + TfL, all
   concatenated) that it reaches most of the way across the map at its
   own z-index:1000 (see map-init.js's updateSafeAreaInsets comment) —
   confirmed by screenshot: the old flat `bottom: 12px` let this panel's
   own lower entries render behind the attribution strip's vertical band,
   regardless of the strip's horizontal reach. Only the attribution
   control's OWN height is needed here (not the full bottom-right stack
   #map-controls needs) since the base-layer switcher itself stays
   compact and right-anchored. */
#legend-panel {
  position: absolute;
  z-index: 999;
  bottom: calc(var(--leaflet-attribution-safe, 34px) + 12px);
  left: 12px;
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: none;
  max-width: 240px;
  max-height: 45vh;
  overflow-y: auto;
  font-size: 0.78rem;
  color: var(--color-text);
}
#legend-panel.legend-empty { display: none; }
#legend-header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem;
  background: none;
  border: none;
  padding: 0.5rem 0.7rem;
  font: inherit;
  cursor: pointer;
  color: var(--color-text);
}
.legend-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.legend-caret { font-size: 0.7rem; color: var(--color-text-muted); }
#legend-panel.collapsed .legend-caret { transform: rotate(-90deg); }
#legend-panel.collapsed #legend-body { display: none; }
#legend-body { padding: 0 0.7rem 0.6rem; }
.legend-entry {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.15rem 0;
  line-height: 1.25;
}
/* Swatches read a LAYER's own data colour (icon-config.js/legendData —
   untouched by this pass) — only the neutral surrounding chrome (border,
   corner) moves to the token system. */
.legend-swatch {
  flex: 0 0 auto;
  width: 0.8rem;
  height: 0.8rem;
  border-radius: var(--radius-sm);
  border: 1px solid rgba(0, 0, 0, 0.35);
}
.legend-line-swatch {
  flex: 0 0 auto;
  width: 1.1rem;
  height: 0;
  border-top-style: solid;
}
.legend-icon-swatch {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 1.1rem;
  width: 1.1rem;
  height: 1.1rem;
}
.legend-cased-line-swatch {
  position: relative;
  flex: 0 0 1.1rem;
  width: 1.1rem;
  height: 0.4rem;
}
.legend-cased-line-swatch > span {
  position: absolute;
  left: 0;
  top: 50%;
  width: 100%;
  height: 0;
  transform: translateY(-50%);
  border-top-style: solid;
}
.legend-scale-group { margin-bottom: 0.4rem; }
.legend-scale-group:last-child { margin-bottom: 0; }
/* (Phase 17 removed .legend-scale-title — every section is now headed by
   a .legend-pill instead. Pill styles are below the noise cue.) */
/* Zoom-gating cue (UX pass, item 10) — e.g. crime's "Simplified view"
   note when the coarse hex grid is currently showing. Amber — a genuine
   "heads up, you're seeing a simplified view" notice, the exact "warning/
   attention" job --color-warning exists for. */
.legend-scale-note {
  font-size: 0.68rem;
  font-style: italic;
  color: var(--color-warning);
  background: rgba(178, 107, 0, 0.12); /* --color-warning (#B26B00) tint — Phase 25 */
  border: var(--border-width) solid rgba(178, 107, 0, 0.35);
  border-radius: var(--radius);
  padding: 0.1rem 0.35rem;
  margin-bottom: 0.25rem;
  display: inline-block;
}

/* --- Legend pills (Phase 17): the separate bottom active-layers strip
   was removed; each active layer's legend section is now headed by a
   removable coloured pill (replacing the old .legend-scale-title text
   heading), and a "Clear all" pill sits at the top of #layer-legend-body
   whenever ≥1 layer is active. legend.js builds both; the pill's × and
   "Clear all" reuse the same "set checkbox.checked = false, dispatch
   change" path the sidebar checkbox itself runs. A pill (999px radius)
   is the one deliberate exception to "sharp corners throughout" — a
   small removable token reads as a different kind of element than a
   panel, and the pill shape is what makes that legible at a glance.
   The label can ellipsis-truncate so a long row name (e.g. "Requires
   improvement") never forces the 240px-max panel to scroll sideways or
   wrap the pill onto two lines. --- */
.legend-clear-all {
  display: inline-flex;
  align-items: center;
  font-size: 0.72rem;
  padding: 0.2rem 0.6rem;
  margin-bottom: 0.45rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: 999px;
  background: var(--color-input-bg);
  color: var(--color-text);
  cursor: pointer;
  white-space: nowrap;
}
.legend-clear-all:hover { border-color: var(--color-accent); color: var(--color-accent); }
.legend-pill {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  max-width: 100%;
  margin: 0.35rem 0 0.15rem;
  padding: 0.15rem 0.3rem 0.15rem 0.45rem;
  background-color: var(--color-base);
  border: var(--border-width) solid var(--color-border);
  border-radius: 999px;
  font-size: 0.72rem;
  line-height: 1.4;
}
/* "subtle highlight only" (Phase 17 spec item 6) — no tooltip, no
   affordance text, no whole-pill click target; the × is the only
   control. */
.legend-pill:hover { background-color: var(--color-input-bg); }
/* Swatch reads a LAYER's own data colour (icon-config.js/legendData,
   untouched) — only the pill's neutral chrome uses tokens. */
.legend-pill-swatch {
  flex: 0 0 auto;
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.35);
}
/* This phase: a pill showing a real marker badge (GP/Pubs/Cafés/
   Underground/Overground/Places of worship/Gyms/etc — see legend.js's
   iconPreviewBadge) needs its own real proportions instead of the plain
   colour-dot's fixed round 0.6rem — the badge SVG (iconPreviewHtml) already
   draws its own shape/border, so this modifier only clears the dot's
   circular clipping and sizes the swatch to fit it. */
.legend-pill-swatch-icon {
  width: 1rem;
  height: 1rem;
  border-radius: 0;
  border: none;
}
.legend-pill-swatch-icon svg { display: block; }
.legend-pill-swatch-boundary {
  width: 22px;
  height: 0;
  border-radius: 0;
  border-top: 3px dashed currentColor;
}
.legend-pill-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--color-text);
}
.legend-pill-remove {
  flex: 0 0 auto;
  background: none;
  border: none;
  color: var(--color-text-muted);
  font-size: 0.85rem;
  line-height: 1;
  padding: 0 0.15rem;
  cursor: pointer;
}
.legend-pill-remove:hover { color: var(--color-warning); }
/* Context that isn't part of the layer name (borough_boundaries' current
   council-tax band) — a small muted line under the pill. */
.legend-heading-suffix {
  font-size: 0.66rem;
  color: var(--color-text-muted);
  margin: 0 0 0.15rem 0.15rem;
}

/* --- Office address search (type-ahead against /api/geocode) --- */
.office-search-label {
  display: block;
  font-family: var(--font-display);
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-text-muted);
  margin-bottom: 0.3rem;
}
#office-search-panel { position: relative; } /* anchors the results dropdown below */
#office-search-input {
  width: 100%;
  font-size: 0.85rem;
  padding: 0.35rem 0.5rem;
}
/* Candidate Lookup empty-state orientation line — office-search.js
   toggles [hidden] on it once an address is picked / the box cleared. */
.office-search-hint {
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--color-text-muted);
  margin: 0.4rem 0 0;
}
.office-search-results {
  position: absolute;
  left: 1rem;
  right: 1rem;
  top: 100%;
  margin-top: -2px;
  z-index: 1001;
  list-style: none;
  padding: 0.25rem 0;
  margin-block-end: 0;
  background-color: var(--color-panel);
  border: var(--border-width) solid var(--color-border);
  border-top: none;
  border-radius: 0;
  max-height: 220px;
  overflow-y: auto;
  box-shadow: none;
  color: var(--color-text);
}
.office-search-result {
  padding: 0;
  font-size: 0.82rem;
  cursor: pointer;
}
.office-search-result:hover { background: rgba(76, 91, 212, 0.1); color: var(--color-accent); } /* --color-accent tint — Phase 25 */
.office-search-empty {
  padding: 0.35rem 0.6rem;
  font-size: 0.8rem;
  color: var(--color-text-muted);
  font-style: italic;
}

/* --- Commute isochrone controls (checkbox + duration <select>, shown
   once an office address is selected — see isochrone.js) --- */
#isochrone-controls {
  margin-top: 0.6rem;
  padding-top: 0.5rem;
  border-top: var(--border-width) solid var(--color-border);
  font-size: 0.85rem;
}
#isochrone-controls label { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; }
.commute-display-switch { display:flex; padding:2px; margin:0 0 .55rem; border:1px solid var(--color-border); border-radius:999px; }
.commute-display-switch label { flex:1; justify-content:center; padding:.3rem .5rem; border-radius:999px; font-size:.78rem; }
.commute-display-switch input { accent-color:var(--color-accent); }
.commute-display-switch label:has(input:checked) { background:var(--color-accent); color:#fff; }
.commute-display-switch label:has(input:disabled) { opacity:.48; cursor:not-allowed; }
#isochrone-duration-select { margin-top: 0.35rem; width: 100%; font-size: 0.8rem; }
/* .layer-control-row's own `display: flex` (below) otherwise beats the
   `hidden` attribute's default `display: none` at equal specificity,
   since it's the later rule — this keeps Route to work's hidden
   duration group (route-to-work.js's syncDurationVisibility) actually
   hidden rather than merely inert. */
#isochrone-duration-group[hidden] { display: none; }

.layer-control { margin-bottom: 0.5rem; font-size: 0.85rem; }
.layer-control label { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; }
.layer-control select { margin-top: 0.25rem; width: 100%; font-size: 0.8rem; }
.layer-control-row > label { min-width: 0; }
.sidebar-row-label { min-width: 0; }
.sidebar-row-symbol,
.layer-group-icon {
  display: inline-flex;
  width: 1rem;
  height: 1rem;
  flex: 0 0 1rem;
  color: var(--color-accent);
}
.sidebar-row-symbol svg,
.layer-group-icon svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* This phase: a row showing its REAL marker badge (icon-config.js's
   iconPreviewHtml — see sidebar-symbols.js's rowIconBadge) draws its own
   fill/stroke colours already (the real marker's own palette), so the
   generic outline-glyph treatment above (stroke: currentColor, no fill)
   must not apply to it. */
.sidebar-row-symbol-icon {
  width: 1.15rem;
  height: 1.15rem;
  flex: 0 0 1.15rem;
}
.sidebar-row-symbol-icon svg {
  fill: revert;
  stroke: revert;
  stroke-width: revert;
}
.dependent-layer-control {
  margin-left: 1.35rem;
  padding-left: 0.55rem;
  /* Phase 26 depth rail, tier 4 (deepest): recolours the border this row
     already had. Deep muted indigo, a hair heavier than the layer-row
     rail — the one place in the tree a fourth level exists today
     (Serious cycling injuries under Cycle Lanes). */
  border-left: 3px solid var(--color-sidebar-depth-4);
  font-size: 0.78rem;
}
.dependent-layer-control input:disabled + * { opacity: 0.55; }
.dependent-layer-control label:has(input:disabled) { cursor: not-allowed; opacity: 0.55; }

/* Holds a row's checkbox label + its optional info icon side by side —
   used by layer_control, the tier_group rows, and the isochrone toggle,
   all of which want the same "label, then icon" layout without the icon
   living inside <label> itself (which would toggle the checkbox on
   click). */
.layer-control-row { display: flex; align-items: center; justify-content: space-between; gap: 0.4rem; }

/* --- Per-layer info popover ("i" icon -> title + 1-2 paragraphs) —
   content is entirely server-rendered (app/services/layer_info.py via
   map.html's info_icon macro); this only styles the icon/popover chrome
   and positions the popover relative to its own wrapper. Same floating-
   panel look as .office-search-results (white card, border, shadow) for
   visual consistency with the rest of this sidebar. */
.layer-info-wrap { position: relative; display: inline-flex; }
.layer-info-icon {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 1.5px solid var(--color-text-dim);
  background: transparent;
  color: var(--color-text-muted);
  font-size: 0.68rem;
  font-weight: 700;
  font-style: italic;
  font-family: Georgia, "Times New Roman", serif;
  line-height: 1;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.layer-info-icon:hover { border-color: var(--color-accent); color: var(--color-accent); }
.layer-info-popover {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 4px;
  z-index: 1002;
  width: 230px;
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  padding: 0.6rem 0.7rem;
  box-shadow: none;
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--color-text);
  font-family: var(--font-body);
}
.layer-info-popover-title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 0.3rem;
  color: var(--color-text);
}
.layer-info-popover p { margin: 0 0 0.4rem; }
.layer-info-popover p:last-of-type { margin-bottom: 0; }
.layer-info-popover-extra { margin-top: 0.4rem; }
/* Data freshness (feedback round Phase 2, item 5) — a small, muted line
   under the body text/extra link, pulled from manifest.yaml. "fine" is
   the common case and reads as plain muted text; "due_soon"/"overdue"
   reuse the existing warning token rather than a new colour, same as
   every other "something needs attention" state in this project. */
.layer-info-popover-freshness {
  margin-top: 0.5rem;
  padding-top: 0.4rem;
  border-top: var(--border-width) solid var(--color-border);
  font-size: 0.7rem;
  color: var(--color-text-muted);
}
.layer-info-popover-freshness-due_soon, .layer-info-popover-freshness-overdue { color: var(--color-warning); }
.layer-info-extra-link {
  display: inline-block;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--color-accent);
  text-decoration: none;
  border: var(--border-width) solid var(--color-accent);
  border-radius: var(--radius);
  padding: 0.2rem 0.5rem;
}
.layer-info-extra-link:hover { background: var(--color-accent); color: var(--color-base); }

/* Short "member brands" subtitle under a tier checkbox (e.g. "Waitrose,
   M&S Food" under Premium) — clearly secondary to the checkbox label:
   smaller, muted, indented to sit under the label text rather than the
   checkbox itself. */
.tier-brand-subtitle {
  padding-left: 1.3rem;
  font-size: 0.72rem;
  color: var(--color-text-muted);
  line-height: 1.3;
}

/* --- Per-group opacity slider (fill-type layers only — Noise, Crime,
   flood_risk); app/static/js/layers.js applies the multiplier. --- */
.layer-opacity-control {
  margin: 0.35rem 0 0.2rem;
  padding-top: 0.35rem;
  border-top: var(--border-width) dashed var(--color-border);
}
.layer-opacity-label {
  display: flex;
  justify-content: space-between;
  font-size: 0.72rem;
  color: var(--color-text-muted);
  margin-bottom: 0.15rem;
}
.opacity-value { font-weight: 600; color: var(--color-text); }
.opacity-slider { width: 100%; }

/* --- School click popup (app/static/js/school-popup.js) --- */
.school-popup { font-size: 0.85rem; line-height: 1.4; max-width: 260px; font-family: var(--font-body); }
.school-popup-subtitle { font-size: 0.75rem; color: var(--color-text-muted); margin: 0.1rem 0 0.3rem; }
.school-popup-website { font-size: 0.78rem; margin-bottom: 0.3rem; }
.school-popup-ofsted { margin: 0.3rem 0; padding: 0.3rem 0; border-top: var(--border-width) dashed var(--color-border); }
.school-popup-ofsted-label { font-size: 0.72rem; color: var(--color-text-muted); margin-bottom: 0.2rem; }
.school-popup-headline { margin-bottom: 0.25rem; }
.school-popup-safeguarding { font-size: 0.78rem; margin-bottom: 0.25rem; }
.school-popup-table { border-collapse: collapse; font-size: 0.76rem; width: 100%; }
.school-popup-table td { padding: 0.08rem 0.3rem 0.08rem 0; }
.school-popup-table td:last-child { text-align: right; }
/* Rating badges keep their own JS-set background colour (Ofsted/CQC
   grade — semantic data, untouched) with white text for contrast against
   any of those colours; shape/corner only moves to the token system. */
.school-rating-badge {
  display: inline-block;
  color: #fff;
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-sm);
}
.school-popup-recent-visit { font-size: 0.75rem; color: var(--color-text-muted); font-style: italic; margin: 0.2rem 0; }
.school-popup-extra { font-size: 0.78rem; margin: 0.25rem 0; }
.school-popup-address { font-size: 0.75rem; color: var(--color-text-muted); margin-top: 0.2rem; }

/* --- GP surgery click popup (app/static/js/gp-surgery-popup.js) --- */
.gp-popup { font-size: 0.85rem; line-height: 1.4; max-width: 240px; font-family: var(--font-body); }
.gp-popup-rating { margin: 0.3rem 0; }
.gp-popup-date { font-size: 0.75rem; color: var(--color-text-muted); }
.gp-rating-badge {
  display: inline-block;
  color: #fff;
  font-size: 0.75rem;
  font-weight: 600;
  padding: 0.06rem 0.45rem;
  border-radius: var(--radius-sm);
}
.gp-popup-note { font-size: 0.75rem; color: var(--color-text-muted); font-style: italic; margin-bottom: 0.3rem; }
.gp-popup-contact { font-size: 0.78rem; margin-bottom: 0.25rem; }
.gp-popup-link { font-size: 0.75rem; }

/* --- Cycleways route click popup (app/static/js/layers.js) --- */
.cycle-route-popup { font-size: 0.85rem; font-family: var(--font-body); }
.cycle-route-popup-label { color: var(--color-text-muted); font-size: 0.8rem; }
.cycle-route-popup-programme { font-size: 0.75rem; color: var(--color-text-muted); font-style: italic; margin-top: 0.15rem; }

/* --- Recently Sold: price/date filter controls (app/static/js/
   recently-sold.js) — occupies the same slot every other layer's radius
   <select> (or borough_boundaries' council-tax select) would. --- */
.recently-sold-filters {
  margin-top: 0.35rem;
}
.recently-sold-filter-row {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-bottom: 0.3rem;
}
.recently-sold-filter-label {
  flex: 0 0 32px;
  font-size: 0.72rem;
  color: var(--color-text-muted);
}
.recently-sold-filter-row input {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.78rem;
  font-family: var(--font-mono); /* price/date are numerals — the one
    input type in this app where the FIELD ITSELF, not just rendered
    output, should already read in the mono face */
  padding: 0.2rem 0.3rem;
}
.recently-sold-filter-sep { color: var(--color-text-muted); font-size: 0.75rem; }
/* Shared status-hint pattern (a small "in progress / succeeded /
   failed" line under a control) — originally built for Recently Sold,
   generalised (feedback round Phase 2, item 4 — CLAUDE.md/CHANGELOG.md)
   for the isochrone controls to reuse rather than inventing a second
   loading/error visual language. .recently-sold-hint is kept as an
   alias (both class names still work) purely so recently-sold.js's own
   existing selector didn't need touching for this — the new class is
   what's actually used by any NEW consumer. */
.oll-status-hint, .recently-sold-hint {
  font-size: 0.72rem;
  color: var(--color-text-muted);
  font-style: italic;
  margin-top: 0.1rem;
}
/* Loading state: a small spinner ahead of the text, so a request in
   flight is never visually identical to a settled result/empty/error
   state (all of which share the plain look above). Pure CSS, no image/
   icon dependency, matching this project's existing "no external asset
   for a small UI glyph" pattern (badges/symbols in icon-config.js are
   hand-drawn SVG for the same reason). */
.oll-status-hint.is-loading, .recently-sold-hint.is-loading {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-style: normal;
}
.oll-status-hint.is-loading::before, .recently-sold-hint.is-loading::before {
  content: "";
  width: 0.7rem;
  height: 0.7rem;
  border: 2px solid var(--color-border);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: oll-status-hint-spin 0.7s linear infinite;
  flex: 0 0 auto;
}
/* Error state: same italic-off treatment as loading (a settled state,
   not the muted-italic "nothing to report" look), coloured with the
   existing warning token rather than a new colour. */
.oll-status-hint.is-error { font-style: normal; color: var(--color-warning); }
@keyframes oll-status-hint-spin {
  to { transform: rotate(360deg); }
}

/* --- Recently Sold pin click popup — address, price, sale date, type,
   tenure. Same small-table layout as .borough-popup-table. --- */
.recently-sold-popup { font-size: 0.85rem; line-height: 1.4; font-family: var(--font-body); }
.recently-sold-popup-subtitle { font-size: 0.75rem; color: var(--color-text-muted); margin: 0.1rem 0 0.35rem; }
.recently-sold-popup-table { border-collapse: collapse; font-size: 0.82rem; }
.recently-sold-popup-table td { padding: 0.05rem 0.5rem 0.05rem 0; }
.recently-sold-popup-table td:last-child { text-align: right; padding-right: 0; }

/* --- Collapsible layer groups (top-level categories, Cycle Lanes,
   Noise, Crime, Supermarkets tier group, ...) — collapse state is a CSS
   class toggled by sidebar-accordion.js, not inline styles, so it stays
   easy to restyle. */
.layer-group {
  margin: 0.6rem 0;
  padding-top: 0.5rem;
  border-top: var(--border-width) solid var(--color-border);
}

/* --- Phase 26: sidebar depth-hierarchy rails -------------------------
   One hue (the --color-accent periwinkle), four steps (tokens.css's
   --color-sidebar-depth-1..4), applied as left-edge rails plus each
   group's own top hairline so a glance shows how deep a row sits.
   Rails only, no body fills. Encodes ROLE in the menu, not raw DOM
   depth — a group nested twice keeps the sub-section rail (depth-2) and
   the extra indent carries the nesting (Option A, confirmed at
   proposal). All selectors here are class-only (no `#layer-panel`), so
   the `.layer-group.layer-group-active` accent-border rule further down
   still wins on specificity + source order: "a layer is on in here"
   outranks "I'm N levels deep". `.layer-group` lives only inside the
   layer panel (map.html), so unscoped is safe. */

/* Group top hairline — recolour only, width left at the existing
   --border-width so this stays a hairline, not a heavier band. Base
   rule = top-level; the descendant rule = any nested group. */
.layer-group { border-top-color: var(--color-sidebar-depth-1); }
.layer-group-body-inner .layer-group { border-top-color: var(--color-sidebar-depth-2); }

/* Left rail down a group's body. Sits on .layer-group-body (which
   already carries the small left padding and the collapse overflow):
   when collapsed the body computes to 0 height so the rail is
   invisible, when open it runs the full block. */
.layer-group > .layer-group-body {
  border-left: 3px solid var(--color-sidebar-depth-1);
  padding-left: 0.55rem;
}
.layer-group-body-inner .layer-group > .layer-group-body {
  border-left: 2px solid var(--color-sidebar-depth-2);
  padding-left: 0.5rem;
}

/* Individual layer rows — every .layer-control checkbox row, at whatever
   nesting depth. Excludes the dependent sub-row (its own deeper rail
   below) and the tier group's trailing "Radius (checked tiers)" control,
   which is not a layer. */
.layer-control:not(.dependent-layer-control):not(.tier-radius-control) {
  border-left: 2px solid var(--color-sidebar-depth-3);
  padding-left: 0.5rem;
}
/* ------------------------------------------------------------------- */

/* Supermarkets' tier rows (Premium/Medium/Budget), rendered directly in
   Shopping & Groceries' own body — no collapsible wrapper, so Premium/
   Medium/Budget are visible as soon as the category itself expands (this
   phase). The header row reuses .layer-group-name's own text styling
   (same weight/size/case as every other group heading) but is plain
   text, not a button — there's nothing to collapse. */
.tier-group-inline-header {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.4rem;
}
.tier-group-inline .layer-control:not(.tier-radius-control) {
  border-left: 2px solid var(--color-sidebar-depth-2);
  padding-left: 0.5rem;
}

.layer-group-header {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  width: 100%;
  background: none;
  border: none;
  padding: 0;
  margin-bottom: 0.4rem;
  font: inherit;
  cursor: pointer;
  text-align: left;
}
.layer-group-name {
  font-family: var(--font-display);
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text);
}
.layer-group-count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--color-text-muted);
  background: var(--color-base);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0.05rem 0.4rem;
  white-space: nowrap;
  flex-shrink: 0;
}
.layer-group-caret {
  font-size: 0.7rem;
  color: var(--color-text-muted);
  /* No transition — a crisp, instant flip rather than an animated caret,
     same "mechanical state change" direction as the checkbox styling
     further down, so this needs no prefers-reduced-motion guard either
     (there's no motion to begin with). */
}
.layer-group.collapsed .layer-group-caret { transform: rotate(0deg); }
.layer-group:not(.collapsed) .layer-group-caret { transform: rotate(90deg); }
/* Smooth expand/collapse (seven-checkpoint layout & visual pass, item 7)
   — the `grid-template-rows: 0fr -> 1fr` technique: a single-row grid
   whose row track animates between "zero" and "fits content" without
   ever needing to measure the real pixel height in JS (impossible to
   `transition` a `height: auto`/`display: none` change directly).
   overflow:hidden on the same element clips the row's content while it's
   still collapsing. Sidebar-accordion.js's own toggle logic (just
   flipping the "collapsed" class) needed no changes — this is a pure
   CSS swap of what that class now does. */
.layer-group-body {
  padding-left: 0.2rem;
  display: grid;
  /* Six-checkpoint pass, item 1 — a REAL bug found by measuring the live
     layout, not assumed: this container used to put grid-auto-rows on
     the SEVERAL direct children themselves (.layer-control rows,
     .layer-group sub-groups), one implicit grid row each, all sized
     `minmax(0, 1fr)`. That's a genuine CSS Grid quirk, not a mistake in
     the values chosen — on an auto-height grid container, MULTIPLE `fr`
     row tracks are equalized to the single tallest row's own content
     size (confirmed by measuring: every row's track computed to an
     identical 82.58px, matching the tallest sibling, even though most
     rows' own real content was under 40px) — leaving a large empty gap
     under every shorter row. This is exactly the reported "empty space
     below Underground, above Overground" bug. Fix: only ONE grid row
     ever exists here now — every real child is wrapped in a single
     .layer-group-body-inner (below), so grid-template-rows only needs
     to size ONE track to ONE wrapper's own natural content height, with
     no grid-auto-rows/multi-row equalization involved at all. The
     `0fr <-> 1fr` collapse-animation technique itself (see the
     .layer-group.collapsed rule below) is unaffected — it still applies
     to that one row exactly as before. */
  /* minmax(0, Xfr), not a bare Xfr — a real bug found by MEASURING real
     collapsed heights, not assumed fixed: a plain `fr` track has an
     IMPLICIT automatic minimum of "auto" per the grid spec (won't shrink
     below its own content's min-content size) unless wrapped in
     minmax(0, ...) to override that minimum explicitly. Without this,
     several nested groups measured a genuinely non-zero height (e.g.
     80px) while "collapsed" — invisible-looking (clipped by
     overflow:hidden further up the tree in some cases) but still
     OCCUPYING LAYOUT SPACE and intercepting pointer events, confirmed by
     a real Playwright click failing with "element intercepts pointer
     events" on a collapsed group's own body. */
  grid-template-rows: minmax(0, 1fr);
  overflow: hidden;
  transition: grid-template-rows 200ms ease;
}
.layer-group-body-inner { min-height: 0; overflow: hidden; }
.layer-group.collapsed .layer-group-body {
  grid-template-rows: minmax(0, 0fr);
  /* Belt-and-braces: even a fully-collapsed 0-height row shouldn't be
     able to intercept clicks meant for whatever sits below/after it —
     this is what actually fixed the real click-interception bug above
     (the minmax fix alone brought the height to a true 0, but this stays
     as an explicit second guarantee, not reliance on 0px alone). */
  pointer-events: none;
}
/* A group's top border (the edge its own header/count badge sits right
   below) brightens to accent while it has ≥1 active row of its own —
   quiet otherwise, so an idle sidebar reads as calm instrument panel,
   not a wall of glowing edges. Toggled by sidebar-accordion.js's
   updateCount, not computed here — see that file's own comment. */
.layer-group.layer-group-active { border-top-color: var(--color-accent); }
.layer-group.layer-group-active > .layer-group-header .layer-group-count {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.poi-popup { font-size: 0.85rem; line-height: 1.4; font-family: var(--font-body); }
.poi-popup-detail-label { font-weight: 650; }

/* "Save to shortlist" (six-checkpoint pass, item 6) — appended by
   layers.js's wrapPopupWithSaveButton to every POI popup this file
   builds (generic/schools/gp_surgeries/green_spaces alike), so one rule
   here covers all of them regardless of which popup builder produced
   the content above it. Same small-button treatment as
   .shortlist-popup-actions button, kept as its own rule rather than
   sharing that class since this button sits in a DIFFERENT file's popup
   content, not the Shortlist panel's own. */
.poi-save-to-shortlist-btn {
  display: block;
  margin-top: 0.4rem;
  font-size: 0.75rem;
  padding: 0.2rem 0.5rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-text);
  cursor: pointer;
}
.poi-save-to-shortlist-btn:hover { border-color: var(--color-accent); color: var(--color-accent); }
.poi-save-to-shortlist-btn:disabled {
  color: var(--color-accent);
  border-color: var(--color-accent);
  cursor: default;
  opacity: 0.85;
}

/* --- Borough boundary click popup (app/static/js/borough-popup.js) —
   the one polygon layer in this project with per-feature click detail:
   a borough name plus its own Council Tax Band A-H table. */
.borough-popup { font-size: 0.85rem; line-height: 1.4; font-family: var(--font-body); }
.borough-popup-subtitle { font-size: 0.75rem; color: var(--color-text-muted); margin: 0.15rem 0 0.35rem; }
.borough-popup-table { border-collapse: collapse; font-size: 0.82rem; }
.borough-popup-table td { padding: 0.05rem 0.5rem 0.05rem 0; }
.borough-popup-table td:last-child { text-align: right; padding-right: 0; }

/* Overrides Leaflet's default divIcon box (white background + border) —
   icon-config.js's shapeDivHtml() supplies the actual marker shape/colour. */
.oll-poi-icon { background: transparent; border: none; }

/* --- Leaflet popup chrome (school/GP/borough/recently-sold/POI/
   shortlist popups all render INSIDE Leaflet's own popup wrapper) — one
   shared dark-panel treatment here rather than restyling each popup
   type's own outer box, since Leaflet owns that wrapper element, not any
   template in this app. The little downward-pointing tip is restyled to
   match, and Leaflet's own close "×" button (default dark-on-transparent)
   is recoloured for contrast against the now-dark background. */
.leaflet-popup-content-wrapper {
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: none;
  color: var(--color-text);
}
.leaflet-popup-tip { background-color: var(--color-panel); box-shadow: none; }
.leaflet-popup-close-button { color: var(--color-text-muted) !important; }
.leaflet-popup-close-button:hover { color: var(--color-accent) !important; }

main h1, main h2, main h3 { font-family: var(--font-display); }

/* --- First-visit tour (UX pass, item 9): a centred modal, shown once
   ever (app/static/js/first-visit-tour.js). z-index above everything
   else in this file (including the mobile toolbar/sheets, up to 1210)
   so it's never hidden behind any other panel. --- */
#first-visit-tour[hidden] { display: none; }
#first-visit-tour {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
}
#first-visit-tour-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
}
#first-visit-tour-dialog {
  position: relative;
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  box-shadow: none;
  padding: 1.25rem 1.5rem;
  max-width: 380px;
  width: calc(100% - 2rem);
  margin: 1rem;
}
#first-visit-tour-dialog h3 {
  margin: 0 0 0.6rem;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--color-text);
}
#first-visit-tour-dialog p { margin: 0 0 1rem; font-size: 0.88rem; line-height: 1.45; color: var(--color-text-muted); }
#first-visit-tour-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; }
#first-visit-tour-try {
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.45rem 0.8rem;
  border: var(--border-width) solid var(--color-accent);
  border-radius: var(--radius);
  background: var(--color-accent);
  color: var(--color-base);
  cursor: pointer;
}
#first-visit-tour-try:hover { opacity: 0.85; }
#first-visit-tour-dismiss {
  font-size: 0.85rem;
  padding: 0.45rem 0.8rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-text);
  cursor: pointer;
}
#first-visit-tour-dismiss:hover { border-color: var(--color-accent); color: var(--color-accent); }

/* Persistent peer-journey navigation remains accessible above overlays. */
body.oll-landing-open #mode-switch { display: flex; }
#mode-switch { display: flex; gap: 0.3rem; }
.mode-switch-btn {
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.25rem 0.55rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-text-muted);
  cursor: pointer;
}
.mode-switch-btn:hover { border-color: var(--color-accent); color: var(--color-accent); }
.mode-switch-btn[aria-pressed="true"] {
  border-color: var(--color-accent);
  background: var(--color-accent);
  color: var(--color-base);
}

/* Person A's ("explorer" mode) entry point — the guided-discovery
   screen (checkpoint 3). Positioned exactly where #map-controls sits,
   since it's shown INSTEAD of it, never alongside — but wider, and its
   own scrolling column (the tile list can get tall) rather than
   #map-controls' fixed two-panel split. */
#explorer-placeholder {
  display: none;
  position: absolute;
  z-index: 1000;
  top: 12px;
  right: 12px;
  bottom: calc(var(--leaflet-attribution-safe, 34px) + 12px);
  width: var(--workspace-right-panel-width);
  max-width: calc(100vw - 24px);
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  color: var(--color-text);
  overflow-y: auto;
}
#explorer-placeholder p { font-size: 0.82rem; line-height: 1.5; color: var(--color-text-muted); }
#gd-mobile-view-toggle { display: none; }

/* ---- Guided discovery screen ---------------------------------------- */
#guided-discovery { padding: 0.9rem 1rem 1rem; }
.gd-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.gd-switch-link {
  font-size: 0.75rem;
  color: var(--color-text-muted);
  text-decoration: underline;
}
.gd-switch-link:hover { color: var(--color-accent); }
.gd-intro { margin: 0.5rem 0 0; }

.gd-step { margin-top: 1rem; }
.gd-step-label {
  display: block;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  color: var(--color-text-muted);
  margin-bottom: 0.45rem;
}

.gd-transaction { display: flex; gap: 0.35rem; }
.gd-transaction-btn {
  flex: 1 1 0;
  padding: 0.5rem 0.6rem;
  background-color: var(--color-input-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  color: var(--color-text-muted);
  font-size: 0.85rem;
  cursor: pointer;
}
.gd-transaction-btn:hover { border-color: var(--color-accent); color: var(--color-accent); }
.gd-transaction-btn[aria-pressed="true"] {
  border-color: var(--color-accent);
  color: var(--color-accent);
  font-weight: 600;
}

.gd-tile-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem; }
.gd-tile {
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background-color: var(--color-input-bg);
}
/* A selected tile spans the whole width so its expanded body has room —
   the grid collapses back to two columns for the unselected ones. */
.gd-tile.gd-tile-selected { grid-column: 1 / -1; border-color: var(--color-accent); }

.gd-tile-toggle {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.5rem 0.55rem;
  background: none;
  border: 0;
  color: var(--color-text);
  font-size: 0.85rem;
  cursor: pointer;
  text-align: left;
}
.gd-tile-check {
  flex: 0 0 auto;
  width: 14px;
  height: 14px;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius-sm);
}
.gd-tile-selected .gd-tile-check {
  background-color: var(--color-accent);
  border-color: var(--color-accent);
}
.gd-tile-label { flex: 1 1 auto; }
.gd-tile-caret { flex: 0 0 auto; color: var(--color-text-dim); font-size: 0.7rem; }
.gd-tile-selected .gd-tile-caret { transform: rotate(90deg); }

.gd-tile-body {
  padding: 0.1rem 0.6rem 0.65rem;
  border-top: var(--border-width) solid var(--color-border);
}
.gd-tile-blurb {
  margin: 0.55rem 0 0.5rem;
  font-size: 0.8rem;
  color: var(--color-text-muted);
  line-height: 1.45;
}
.gd-tile-note {
  margin: 0.5rem 0 0;
  font-size: 0.75rem;
  color: var(--color-text-dim);
  line-height: 1.45;
}

.gd-presets { display: flex; flex-direction: column; gap: 0.35rem; }
.gd-preset {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.1rem 0.45rem;
  align-items: start;
  font-size: 0.8rem;
  cursor: pointer;
}
.gd-preset input { margin-top: 0.15rem; grid-row: 1 / span 2; }
.gd-preset-label { font-weight: 600; }
.gd-preset-desc { grid-column: 2; color: var(--color-text-muted); font-size: 0.75rem; line-height: 1.4; }

.gd-budget { display: flex; flex-direction: column; gap: 0.5rem; margin-top: 0.5rem; }
.gd-budget-row { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.8rem; }
.gd-budget[data-transaction="buy"] .gd-rent-only { display: none; }
#gd-budget-readout { margin-top: 0.15rem; }

/* The relocated #office-search-panel sheds its own panel chrome inside
   the advanced workplace disclosure so it reads as part of that choice,
   not a box inside a box. */
#gd-commute-slot #office-search-panel {
  background: none;
  border: 0;
  padding: 0.35rem 0 0;
}

/* The alternative travel path is offered plainly, below the primary one
   — never as an "Advanced" disclosure. Commute is the first question
   Area Finder asks, so neither route to answering it is buried. */
.gd-alt-prompt {
  margin: 0.55rem 0 0;
  padding-top: 0.45rem;
  border-top: var(--border-width) dashed var(--color-border);
  font-size: 0.75rem;
  color: var(--color-text-muted);
}
.gd-commute-note {
  margin: 0.45rem 0 0;
  font-size: 0.75rem;
  color: var(--color-text-muted);
}
.gd-section-note {
  margin: 0.45rem 0 0;
  font-size: 0.75rem;
  color: var(--color-text-muted);
}
.gd-workplace-link {
  appearance: none;
  background: none;
  border: 0;
  padding: 0.2rem 0;
  color: var(--color-text-muted);
  font: inherit;
  font-size: 0.75rem;
  text-decoration: underline;
  cursor: pointer;
}
.gd-workplace-link:hover,
.gd-workplace-link:focus-visible { color: var(--color-accent); }
#gd-workplace-panel,
#gd-station-panel {
  margin-top: 0.35rem;
  padding: 0.15rem 0.6rem 0.6rem;
  border: var(--border-width) solid var(--color-accent);
  border-radius: var(--radius);
  background: var(--color-input-bg);
}
/* The station alternative is one tile, so it fills the row rather than
   sitting in half of a two-column grid. */
#gd-station-panel .gd-tile-grid { grid-template-columns: 1fr; }

#gd-see-matches, #gd-explore-area {
  width: 100%;
  margin-top: 1rem;
  padding: 0.6rem;
  background-color: var(--color-accent);
  border: var(--border-width) solid var(--color-accent);
  border-radius: var(--radius);
  color: var(--color-base);
  font-weight: 600;
  font-size: 0.88rem;
  cursor: pointer;
}
#gd-see-matches:disabled {
  background-color: var(--color-input-bg);
  border-color: var(--color-border);
  color: var(--color-text-dim);
  cursor: not-allowed;
}
#gd-run-hint { margin-top: 0.6rem; }
/* Phase 27 — "Explore this area in detail": same visual weight as
   "See matching areas" (both are real calls to action) rather than a
   quieter secondary style, since burying it defeats the brief's own
   "prominent" requirement. Its own hint (which area it'll use) is the
   same plain italic .oll-status-hint look #gd-run-hint's settled state
   already has — nothing here is a loading/error state. */
#gd-explore-area-hint { margin-top: 0.5rem; }

/* ---- Matching-areas overlay legend (Phase 7) ------------------------
   Lives inside the shared left-hand Legend panel, after any ordinary
   active-layer entries. When it is the only legend, omit the separator
   that would otherwise divide the two sections. */
#discovery-legend {
  margin-top: 0.55rem;
  padding-top: 0.5rem;
  border-top: var(--border-width) solid var(--color-border);
  font-size: 0.78rem;
}
#layer-legend-body:empty + #discovery-legend {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}
.discovery-legend-title {
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 0.72rem;
  color: var(--color-text-muted);
  margin-bottom: 0.4rem;
}
.discovery-legend-row {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin: 0.2rem 0;
  color: var(--color-text-muted);
}
.discovery-legend-swatch {
  flex: 0 0 auto;
  width: 16px;
  height: 12px;
  border: var(--border-width) solid var(--color-border);
}
.discovery-legend-swatch.grad-flat { background: var(--color-discovery-unscored); opacity: 0.2; }
.discovery-score-scale { display: flex; flex-wrap: wrap; gap: 0.3rem; }
.discovery-score-band { display: flex; flex-direction: column; align-items: center; gap: 0.2rem; font-size: 0.6rem; }
.discovery-score-band .discovery-legend-swatch { width: 32px; border-color: var(--color-text-dim); }
.discovery-score-circle {
  display: inline-block;
  width: 10px;
  height: 10px;
  border: var(--border-width) solid var(--color-text-dim);
  border-radius: 50%;
  margin-right: 0.3rem;
  vertical-align: middle;
}
.discovery-legend-swatch.hatch {
  background-color: rgba(124, 134, 152, 0.06);
  background-image: repeating-linear-gradient(
    45deg,
    rgba(124, 134, 152, 0.38) 0,
    rgba(124, 134, 152, 0.38) 2px,
    transparent 2px,
    transparent 6px
  );
}
/* Over budget: the same crimson and 135deg counter-hatch the map uses,
   so the legend swatch is literally what's painted on the hexes and not
   a near-miss approximation of it. */
.discovery-legend-swatch.over-budget {
  background-color: var(--color-score-over-budget);
  background-image: repeating-linear-gradient(
    135deg,
    rgba(42, 4, 11, 0.5) 0,
    rgba(42, 4, 11, 0.5) 2.4px,
    transparent 2.4px,
    transparent 6px
  );
  border-color: var(--color-score-over-budget);
}
.discovery-legend-note {
  margin-top: 0.4rem;
  color: var(--color-text-dim);
  font-size: 0.72rem;
  line-height: 1.4;
}
.discovery-hex-popup-more { color: var(--color-text-dim); font-size: 0.72rem; }

/* ---- Area checklist card (checkpoint 5) --------------------------- */
.discovery-hex-card { min-width: 240px; }
.discovery-hex-title { display: block; }
.discovery-hex-sub {
  font-size: 0.78rem;
  color: var(--color-accent);
  margin: 0.15rem 0 0.5rem;
}
.discovery-hex-body { display: flex; flex-direction: column; gap: 0.5rem; }
.discovery-check-row {
  border-left: 2px solid var(--color-border);
  padding-left: 0.5rem;
}
.discovery-check-row.is-insufficient { border-left-color: #7c8698; }
.discovery-check-row.is-over-budget {
  border-left-color: var(--color-score-over-budget);
  border-left-width: 3px;
}
/* The card's affordability banner. Text states the status outright, so
   the colour is reinforcement rather than the only signal. */
.discovery-hex-budget-warning {
  font-family: var(--font-display);
  font-size: 0.76rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: #fff;
  background: var(--color-score-over-budget);
  padding: 0.25rem 0.4rem;
  margin: 0.25rem 0 0.35rem;
}
.discovery-hex-budget-warning.is-unknown {
  background: #7c8698;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 500;
}
.discovery-check-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
}
.discovery-check-label { font-weight: 600; font-size: 0.82rem; }
.discovery-check-show {
  flex: 0 0 auto;
  font-size: 0.7rem;
  padding: 0.1rem 0.35rem;
  background: var(--color-input-bg);
  border: var(--border-width) solid var(--color-border);
  color: var(--color-text-muted);
  cursor: pointer;
}
.discovery-check-show:hover { border-color: var(--color-accent); color: var(--color-accent); }
.discovery-check-threshold {
  font-size: 0.72rem;
  color: var(--color-text-dim);
  line-height: 1.35;
}
.discovery-check-value { font-size: 0.78rem; color: var(--color-text-muted); margin-top: 0.15rem; }
.discovery-check-state-symbol {
  display: inline-flex;
  width: 0.9rem;
  height: 0.9rem;
  margin-right: 0.28rem;
  vertical-align: -0.16rem;
}
.discovery-check-state-symbol svg { display: block; width: 100%; height: 100%; }
.discovery-check-state-symbol.is-meets { color: var(--color-accent); }
.discovery-check-state-symbol.is-partly { color: var(--color-warning); }
.discovery-check-state-symbol.is-none { color: var(--color-text-dim); }
.is-insufficient .discovery-check-value { color: #9aa4b6; font-style: italic; }
.discovery-hex-actions,
.shortlist-popup-refine .shortlist-popup-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.7rem; }
.discovery-hex-actions button {
  flex: 1 1 auto;
  padding: 0.4rem 0.5rem;
  font-size: 0.8rem;
  background: var(--color-input-bg);
  border: var(--border-width) solid var(--color-border);
  color: var(--color-text);
  cursor: pointer;
}
.discovery-hex-actions button:hover { border-color: var(--color-accent); color: var(--color-accent); }

.shortlist-approx-badge {
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted); /* was #7c8698 — tokenised in Phase 25 */
  border: var(--border-width) solid var(--color-border);
  padding: 0 0.25rem;
}
.shortlist-refine-input {
  width: 100%;
  margin-top: 0.3rem;
}
.shortlist-refine-results {
  list-style: none;
  margin: 0.3rem 0 0;
  padding: 0;
  max-height: 140px;
  overflow-y: auto;
  border: var(--border-width) solid var(--color-border);
}
.shortlist-refine-results li {
  padding: 0.35rem 0.5rem;
  font-size: 0.78rem;
  cursor: pointer;
  border-bottom: var(--border-width) solid var(--color-border);
}
.shortlist-refine-results li:last-child { border-bottom: 0; }
.shortlist-refine-results li:hover { background: var(--color-input-bg); color: var(--color-accent); }

/* Checkpoint 6 — approximate-area tag in a Compare column header */
.shortlist-compare-pin-approx {
  font-size: 0.68rem;
  color: var(--color-text-muted); /* was #7c8698 — tokenised in Phase 25 */
  margin-top: 0.15rem;
  line-height: 1.3;
}

/* Area Finder owns its workflow; the generic layer tree is hidden below. */
body.oll-mode-explorer #explorer-placeholder { display: block; }

/* ============================================================
   MOBILE RESPONSIVENESS PASS (UX pass, item 6)
   ============================================================
   Kept in ONE block at the end of the file, rather than scattered as
   a small media query per component, so "what's different on mobile"
   reads in one pass — every desktop rule above this point is
   completely unchanged; everything here is additive/overriding, and
   only takes effect at or below the breakpoint. Verified with a real
   Playwright pass at phone/tablet/desktop viewport widths, not just
   written and assumed — see CHANGELOG.md.

   What changes below the breakpoint:
   - The three floating desktop panels (Shortlist, office+Layers,
     Legend) — which would overlap and cover most of a
     phone screen if left simultaneously floating — become full-screen
     sheets, opened one at a time from a new bottom toolbar
     (#mobile-toolbar, app/static/js/mobile-nav.js). Each panel's own
     internal behaviour (collapse toggles, list rendering, popovers) is
     completely untouched — only ITS OWN POSITIONING changes here.
   - Touch targets (checkboxes, buttons, selects) are enlarged toward
     the ~44px minimum tap size both Apple's HIG and Google's Material
     guidelines recommend.
   - #map's own height leaves room for the fixed bottom toolbar.
   ============================================================ */

#mobile-toolbar, #mobile-backdrop, #mobile-panel-close { display: none; }
/* The real (non-generated-content) empty-state hint paragraph — see
   map.html and the mobile block's own rule below for when it shows. */
.mobile-empty-hint { display: none; }

@media (max-width: 700px) {
  :root { --mobile-toolbar-height: 54px; }
  .leaflet-control-layers { left: 12px !important; right: auto !important; }
  body.oll-mode-explorer:not(.oll-area-map-view) .leaflet-control-layers,
  body.oll-mode-explorer:not(.oll-area-map-view) .leaflet-control-zoom { visibility: hidden; }

  #map { height: calc(100vh - var(--topnav-height, 46px) - var(--mobile-toolbar-height)); }

  /* --- Bottom toolbar: the only always-visible custom UI on mobile
     besides the map itself and Leaflet's own native controls
     (zoom/base-layer switcher), which are left untouched. --- */
  #mobile-toolbar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1200;
    height: var(--mobile-toolbar-height);
    padding-bottom: env(safe-area-inset-bottom, 0);
    background: var(--color-panel);
    border-top: var(--border-width) solid var(--color-border);
    box-shadow: none;
  }
  .mobile-toolbar-btn {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.1rem;
    min-height: 44px;
    background: none;
    border: none;
    font-size: 1.15rem;
    color: var(--color-text-muted);
    cursor: pointer;
  }
  .mobile-toolbar-btn span { font-size: 0.62rem; font-weight: 600; }

  #mobile-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 1090;
    background: rgba(0, 0, 0, 0.35);
  }
  #mobile-backdrop[hidden] { display: none; }

  #mobile-panel-close {
    display: flex;
    position: fixed;
    top: 10px;
    right: 10px;
    z-index: 1210;
    width: 40px;
    height: 40px;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    border: var(--border-width) solid var(--color-border);
    background: var(--color-panel);
    color: var(--color-text);
    box-shadow: none;
    font-size: 1.3rem;
    line-height: 1;
    cursor: pointer;
  }
  #mobile-panel-close[hidden] { display: none; }

  /* --- The three floating desktop panels: hidden by default, a
     full-screen sheet when .mobile-open (mobile-nav.js). Equal
     specificity (single ID selector) to each panel's own desktop
     `position: absolute` rule earlier in this file — placed AFTER it
     in source order, which is what lets this win under the media
     query without needing !important. --- */
  #map-controls,
  #legend-panel,
  #shortlist-panel {
    display: none;
    position: fixed;
    /* Deliberately NOT inset:0 — a gap at the top leaves #mobile-backdrop
       genuinely visible and tappable above the sheet (a truly full-
       screen sheet would sit ABOVE the backdrop at every point,
       making "tap outside to dismiss" unreachable — caught by the
       real-browser check in CHANGELOG.md, not assumed). */
    top: 56px;
    left: 0;
    right: 0;
    bottom: var(--mobile-toolbar-height);
    z-index: 1100;
    max-width: none;
    max-height: none;
    width: auto;
    border-radius: 0;
    box-shadow: none;
    overflow-y: auto;
    padding: 1rem;
    background-color: var(--color-panel);
    background-image: var(--panel-grid-bg);
    color: var(--color-text);
  }
  #legend-panel.mobile-open,
  #shortlist-panel.mobile-open {
    display: block;
  }

  /* #map-controls is normally a flex column of two separately-styled
     boxes (office-search-panel, layer-panel) — on mobile those inner
     boxes lose their own background/border/shadow, since the OUTER
     sheet now provides it (avoids a "box inside a box" look). */
  #map-controls.mobile-open { display: flex; flex-direction: column; gap: 0.9rem; }
  #map-controls.mobile-open #office-search-panel,
  #map-controls.mobile-open #layer-panel {
    background: none;
    border: none;
    box-shadow: none;
    padding: 0;
  }

  /* The legend's own desktop "hide the whole panel when nothing is
     active" rule stays correct in spirit (there's nothing to show) —
     but on mobile the panel was just explicitly opened by a deliberate
     tap, so showing it BLANK with a one-line explanation (a real DOM
     element — see map.html/.mobile-empty-hint above, not CSS-generated
     content, so it's genuinely accessible and findable by a normal DOM
     text query) reads as "that worked, there's just nothing here yet"
     rather than a button that appears to do nothing. */
  #legend-panel.mobile-open.legend-empty {
    display: block;
  }
  #legend-panel.mobile-open.legend-empty .mobile-empty-hint {
    display: block;
    padding: 1rem 0;
    color: var(--color-text-muted);
    font-style: italic;
  }

  /* --- Touch targets: checkboxes, buttons, and selects sized up
     toward the ~44px recommended minimum tap size. --- */
  .layer-toggle, .tier-toggle { width: 1.15rem; height: 1.15rem; }
  .layer-control label { min-height: 32px; }
  .layer-group-header { min-height: 44px; padding: 0.6rem 0.5rem; }
  .layer-search-input,
  #office-search-input,
  #isochrone-duration-select,
  .opacity-slider,
  .recently-sold-filter-row input,
  .recently-sold-date-from,
  .recently-sold-date-to {
    min-height: 40px;
    font-size: 0.95rem;
  }
  .layer-info-icon { width: 26px; height: 26px; }
  #shortlist-add-btn,
  .shortlist-list-remove,
  .shortlist-distance-ring-btn,
  .legend-clear-all {
    min-height: 36px;
    min-width: 36px;
  }
  /* The pill's × grows toward a tap-friendly size on mobile without
     ballooning the compact pill heading it sits in. */
  .legend-pill-remove { min-width: 30px; min-height: 30px; padding: 0.25rem; }
  .legend-pill { padding-top: 0.2rem; padding-bottom: 0.2rem; }
  .shortlist-rename-input { min-height: 36px; font-size: 0.85rem; }
  .shortlist-popup-actions button,
  .poi-save-to-shortlist-btn { min-height: 38px; padding: 0.4rem 0.8rem; }

  /* --- Misc: the top nav stays legible/compact at phone width. --- */
  .topnav { padding: 0.5rem 0.75rem; gap: 0.75rem; font-size: 0.9rem; }
  /* The status readout's full telemetry string doesn't fit next to the
     nav links at phone width without truncating it into uselessness —
     hidden below the breakpoint rather than fought with; mobile already
     has its own simplified UI (the bottom toolbar/sheets), and this
     signature element is a desktop-instrument-panel piece specifically. */
  #status-readout { display: none; }

  /* Area Finder hides generic Layers on mobile too. */
  /* #explorer-placeholder's desktop workspace width would crowd
     a phone-width viewport — full-bleed to both edges here, keeping the
     top/bottom insets and its own overflow-y so the tile list stays
     scrollable rather than growing the page. */
  #explorer-placeholder {
    left: 12px;
    right: 12px;
    top: 56px; /* clear the Leaflet zoom control (top-left), same offset the mobile sheets use */
    bottom: calc(var(--leaflet-attribution-safe, 34px) + var(--mobile-toolbar-height) + 12px);
    width: auto;
    max-width: none;
  }
  .gd-tile-grid { grid-template-columns: 1fr; }
  .gd-tile { grid-column: 1 / -1 !important; }
  body.oll-mode-explorer #gd-mobile-view-toggle {
    display: block;
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 1001;
    padding: 0.4rem 0.65rem;
    border: var(--border-width) solid var(--color-accent);
    background: var(--color-panel);
    color: var(--color-accent);
    font: inherit;
    font-size: 0.8rem;
    cursor: pointer;
  }
  body.oll-mode-explorer.oll-area-map-view #explorer-placeholder { display: none; }
}

/* ============================================================
   Phase 29A — Sarah-first redesign: the landing hero + report view.
   See CHANGELOG.md's Phase 29A entry for the full proposal/decision
   record (this section implements it, doesn't re-argue it).
   ============================================================ */

/* Address-search overlay over the London-centred map. */
/* Phase 29B: RETIRED the Phase 29A "no backdrop, map stays clickable"
   design — review confirmed the map AND every floating panel behind it
   (#map-controls, #shortlist-panel, #legend-panel — all z-index < this
   one) should be genuinely unreachable until an address is entered, not
   just visually de-emphasised. #landing-hero-backdrop below is a real,
   pointer-events-capturing layer (fuzzed via backdrop-filter blur, not
   a flat dark scrim — the map should read as "not yet,
   but still there", not hidden outright). Direct-map-click-to-START-a-
   report (review decision 7, Phase 29A) is no longer reachable from a
   cold landing as a result — report-view.js's own map click handler is
   otherwise unchanged, so a click on the map WHILE a report is already
   open still works exactly as before. */
#landing-hero[hidden] { display: none; }
#landing-hero {
  position: fixed;
  inset: 0;
  z-index: 1400;
  display: flex;
  align-items: center;
  justify-content: center;
}
#landing-hero-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(28, 35, 51, 0.32);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
#landing-hero-dialog {
  position: relative;
  z-index: 1;
  width: min(560px, calc(100vw - 32px));
  padding: 2.25rem 2rem;
  background-color: var(--color-panel);
  background-image: var(--panel-grid-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  text-align: center;
}
#landing-hero-close { position: absolute; top: 0.6rem; right: 0.75rem; border: 0; background: transparent; color: var(--color-text-muted); font-size: 1.5rem; cursor: pointer; }

@media (max-width: 700px) {
  .leaflet-control-attribution {
    max-width: calc(100vw - 70px);
    padding: 2px 5px !important;
    font-size: 9px !important;
    line-height: 1.15;
    white-space: normal;
  }
}
#landing-hero-title {
  margin: 0 0 1.25rem;
  font-family: var(--font-display);
  font-size: 1.35rem;
  font-weight: 600;
  line-height: 1.35;
  color: var(--color-text);
}
#landing-hero-search {
  display: flex;
  gap: 0.5rem;
  padding: 0.35rem;
  background: var(--color-input-bg);
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
}
#landing-hero-input {
  flex: 1 1 auto;
  border: none;
  background: none;
  padding: 0.55rem 0.6rem;
  font-size: 0.95rem;
  color: var(--color-text);
}
#landing-hero-input:focus { outline: none; }
#landing-hero-search button[type="submit"] {
  border: var(--border-width) solid var(--color-accent);
  border-radius: var(--radius);
  background: var(--color-accent);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.85rem;
  padding: 0.55rem 1.1rem;
  cursor: pointer;
}
#landing-hero-search button[type="submit"]:hover { opacity: 0.9; }
#landing-hero-results {
  list-style: none;
  margin: 0.4rem 0 0;
  padding: 0;
  text-align: left;
  max-height: 240px;
  overflow-y: auto;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-panel);
}
#landing-hero-results li {
  padding: 0.5rem 0.7rem;
  font-size: 0.88rem;
  border-bottom: var(--border-width) solid var(--color-border);
  cursor: pointer;
}
#landing-hero-results li.landing-hero-result { padding: 0; }
#landing-hero-results li:last-child { border-bottom: none; }
#landing-hero-results li:hover { background: rgba(76, 91, 212, 0.08); }
#landing-hero-results li.landing-hero-empty { cursor: default; color: var(--color-text-muted); font-style: italic; }
#landing-hero-results li.landing-hero-empty:hover { background: none; }
#landing-hero-secondary {
  margin: 1rem 0 0;
  font-size: 0.82rem;
  color: var(--color-text-muted);
}
#landing-hero-secondary a { color: var(--color-accent); }

/* Landing hero hides #mode-switch, same as the old fork did (kept in
   sync via body.oll-landing-open — mode-state.js applies this class
   regardless of which of the two landing elements is actually open). */

/* --- Report view: shared desktop workspace width. body.oll-report-open turns
   <main> into a flex row of exactly two children — #map-page (unchanged
   internals) and #report-cards-panel. #map-page's own floating panels
   (#map-controls/#shortlist-panel/#legend-panel) are position:absolute
   scoped to #map-page itself, so shrinking #map-page's WIDTH here is
   the entire fix for keeping them pinned to the map's own edge instead
   of drifting toward the viewport's — no change needed to any of their
   own rules (review note B). --- */
@media (min-width: 701px) {
  body.oll-report-open main {
    display: flex;
    height: calc(100vh - var(--topnav-height, 46px));
    overflow: hidden;
  }
  body.oll-report-open #map-page {
    flex: 1 1 0;
    width: auto;
    min-width: 0;
  }
  #report-cards-panel {
    flex: 0 0 var(--workspace-right-panel-width);
    width: var(--workspace-right-panel-width);
    min-width: 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    background: var(--color-base);
    border-left: var(--border-width) solid var(--color-border);
  }
  #report-cards-panel[hidden] { display: none; }

  /* The Commute card opens the existing workplace editor. */
  body.oll-report-open #map-controls { display: none; }
  body.oll-report-open.oll-report-drawer-open #map-controls {
    display: flex;
    z-index: 1600;
  }

  #report-full-map-btn { display: none; } /* mobile-only affordance */
}

#report-explore-btn {
  border: var(--border-width) solid var(--color-accent);
  border-radius: var(--radius);
  background: var(--color-panel);
  color: var(--color-accent);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.72rem;
  padding: 0.35rem 0.7rem;
  cursor: pointer;
  white-space: nowrap;
}
#report-explore-btn:hover { background: var(--color-accent); color: #fff; }

/* --- Report card rail contents --- */
#report-topbar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.75rem 1rem;
  border-bottom: var(--border-width) solid var(--color-border);
  flex: 0 0 auto;
}
#report-back {
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-text);
  font-size: 1.1rem;
  line-height: 1;
  padding: 0.3rem 0.6rem;
  cursor: pointer;
  flex: 0 0 auto;
}
#report-address { flex: 1 1 auto; min-width: 0; }
#report-address-main {
  display: block;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
#report-address-sub {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.7rem;
  color: var(--color-text-dim);
}
#report-cards-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0.85rem;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

/* Report card: 0.5px hairline + a scoped rounded corner (--radius-card,
   Phase 29A review decision 5 — 6px, not the proposal's original 8px)
   — a deliberate, SCOPED fork from the zero-radius instrument-panel
   chrome everywhere else, the same "one deliberate exception" precedent
   --radius-sm already set (tokens.css). White surface; the expanded
   card is accented with the Phase 25 periwinkle border, nothing else
   about it changes shape. */
.rcard {
  border: 0.5px solid var(--color-border);
  border-radius: var(--radius-card);
  background: var(--color-panel);
}
.rcard-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem;
  width: 100%;
  border: none;
  background: none;
  padding: 0.7rem 0.85rem;
  cursor: pointer;
  font: inherit;
  text-align: left;
  color: var(--color-text);
}
.rcard-label { display: flex; align-items: center; gap: 0.5rem; font-family: var(--font-display); font-weight: 600; font-size: 0.82rem; }
.rcard-symbol { flex-shrink: 0; }
.rcard-symbol svg { width: 15px; height: 15px; display: block; color: var(--color-text-muted); }
.rcard-verdict {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--color-text-muted);
  text-align: right;
  white-space: normal; /* long verdicts (e.g. "1,509 reported · high for London") wrap rather than clip on narrow phones */
  flex-shrink: 1;
  min-width: 0;
}
.rcard-caret { color: var(--color-text-dim); font-size: 0.7rem; }
.rcard.expanded {
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 1px var(--color-accent);
}
.rcard.expanded .rcard-head { border-bottom: var(--border-width) solid var(--color-border); }
.rcard-detail {
  padding: 0.7rem 0.85rem 0.85rem;
  font-size: 0.82rem;
  line-height: 1.6;
  color: var(--color-text);
}
.rcard-detail[hidden] { display: none; }
.rcard-detail .shortlist-summary-row,
.rcard-detail .shortlist-summary-head,
.rcard-detail .shortlist-summary-value { all: unset; display: block; } /* the detail string is plain formatted HTML fragments (formatFoo's own output), not a full row build — this neutralises any accidental class collision rather than assuming there won't be one */

/* Phase 29B — the Commute card's own real action, fixing "Add workplace"
   being inert text. Same secondary-button treatment as #report-back,
   this is a
   navigation shortcut into the drawer, not the card's main action. */
.rcard-workplace-btn {
  display: block;
  margin-top: 0.6rem;
  border: var(--border-width) solid var(--color-border);
  border-radius: var(--radius);
  background: var(--color-input-bg);
  color: var(--color-accent);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.76rem;
  padding: 0.4rem 0.7rem;
  cursor: pointer;
}
.rcard-workplace-btn:hover { border-color: var(--color-accent); }

/* --- Mobile: sticky compact map strip + "Full map" toggle, cards flow
   beneath it in the normal page column instead of a 50/50 split (which
   isn't viable under this width). --- */
@media (max-width: 700px) {
  body.oll-report-open main {
    display: block;
    height: auto;
    overflow: visible;
  }
  body.oll-report-open #map-page {
    position: sticky;
    top: 0;
    z-index: 900;
    height: 35vh;
    width: auto;
  }
  body.oll-report-open.oll-report-map-expanded #map-page {
    position: fixed;
    inset: var(--topnav-height, 46px) 0 0 0;
    height: auto;
    z-index: 2000;
  }
  body.oll-report-open #report-cards-panel {
    position: relative;
    height: auto;
    border-left: none;
    border-top: var(--border-width) solid var(--color-border);
  }
  body.oll-report-open.oll-report-map-expanded #report-cards-panel { display: none; }
  #report-full-map-btn {
    display: block;
    position: absolute;
    z-index: 1000;
    bottom: calc(var(--leaflet-attribution-safe) + 8px);
    right: 8px;
    border: var(--border-width) solid var(--color-border);
    border-radius: 999px;
    background: var(--color-panel);
    color: var(--color-text);
    font-size: 0.72rem;
    font-weight: 600;
    padding: 0.35rem 0.7rem;
    cursor: pointer;
  }
}

/* Shared journey navigation on desktop and mobile. */
body.oll-mode-explorer #map-controls,
body.oll-mode-explorer .mobile-toolbar-btn[data-target="map-controls"] { display: none !important; }
/* Phase 36 — the Home shortlist is Advanced-Search-only (CLAUDE.md); it
   never had an explorer-mode visibility rule before this pass (unlike
   #map-controls just above), which is why it used to show through in
   Area Finder. CSS-only: nothing about stored shortlist data changes
   just because Area Finder happens to be the active journey. */
body.oll-mode-explorer #shortlist-panel,
body.oll-mode-explorer .mobile-toolbar-btn[data-target="shortlist-panel"] { display: none !important; }
/* The shared office-search hint points at the Home shortlist's "+ Add
   home", which Area Finder hides (rule directly above). Harmless when the
   workplace panel was a buried advanced option; misleading now that it is
   the first thing Area Finder asks for. The commute section supplies its
   own explanatory copy, so the shared hint is suppressed here only —
   Address Report and Advanced Search keep it unchanged. */
body.oll-mode-explorer #office-search-hint { display: none !important; }
body.oll-landing-open #report-cards-panel,
body.oll-landing-open #map-controls,
body.oll-landing-open #shortlist-panel,
body.oll-landing-open #legend-panel,
body.oll-landing-open #mobile-toolbar,
body.oll-landing-open #report-full-map-btn { display: none !important; }
.report-commute-example { display: none; }
body.oll-mode-report #office-search-hint { display: none; }
body.oll-mode-report #office-search-panel.report-compact-workplace {
  position: relative; margin-top: 0.65rem; padding: 0; background: none;
  background-image: none; border: 0; box-shadow: none;
}
body.oll-mode-report #office-search-panel.report-compact-workplace #isochrone-controls,
body.oll-mode-report #office-search-panel.report-compact-workplace .report-commute-example { display: none !important; }
body.oll-mode-report #office-search-panel.report-compact-workplace .office-search-results {
  left: 0; right: 0; z-index: 1300;
}
/* Address Report is one property and report-driven data, not a workspace. */
body.oll-mode-report #shortlist-panel,
body.oll-mode-report #mobile-toolbar,
body.oll-mode-report .poi-save-to-shortlist-btn,
body.oll-mode-report .legend-pill-remove,
body.oll-mode-report .legend-clear-all,
body.oll-mode-report #map-controls #layer-panel { display: none !important; }
body.oll-mode-report #map-controls { top: 12px; }
body.oll-mode-report #map-controls:not(.mobile-open) { display: none; }
body.oll-mode-report.oll-report-drawer-open #map-controls { display: block; }
body.oll-mode-report .rcard-options { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.8rem; }
/* Reported Crime card's subtle pointer at Advanced Search's wider crime
   breakdown (see report-view.js's appendSubOptions) — explanatory only,
   deliberately quieter than the card's own value/detail text. */
.rcard-note { margin: 0.5rem 0 0; font-size: 0.76rem; color: var(--color-text-muted); }
.rcard-advanced-link { padding: 0; border: 0; background: none; color: var(--color-accent); cursor: pointer; text-align: left; text-decoration: underline; }
body.oll-mode-report .rcard.map-active { border-color: var(--color-accent); }
.rcard-option {
  border: var(--border-width) solid var(--color-border); border-radius: 999px;
  padding: 0.4rem 0.75rem; background: var(--color-panel); color: var(--color-text);
  font: inherit; font-size: 0.8rem; cursor: pointer;
}
.rcard-option[aria-checked="true"], .rcard-option[aria-pressed="true"] {
  color: var(--color-accent); border-color: var(--color-accent); background: var(--color-base);
}
.rcard-option[aria-pressed="true"]::before { content: "✓ "; }
.rcard-option:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
.rcard-option:hover { border-color: var(--color-accent); }
@media (width <= 700px) {
  body.oll-mode-report #map { height: 100%; }
  body.oll-mode-report .rcard-option { min-height: 44px; }
}
#home-page {
  position: fixed;
  margin: 0;
  inset: var(--topnav-height, 46px) 0 0;
  overflow-y: auto;
  background: var(--color-base);
  padding: clamp(1.25rem, 3vh, 3rem) max(1.25rem, calc((100vw - 1120px) / 2));
}
#home-page[hidden] { display: none; }
/* Journey entry and orientation overlays leave global navigation accessible. */
#landing-hero, #first-visit-tour { inset: var(--topnav-height, 46px) 0 0; }
body.oll-mode-home #map-page,
body.oll-mode-home #report-cards-panel { visibility: hidden; pointer-events: none; }
body.oll-mode-home #mobile-toolbar,
body.oll-mode-home #mobile-backdrop,
body.oll-mode-home #mobile-panel-close,
body.oll-mode-home #status-readout { display: none; }
.home-intro { text-align: center; max-width: 760px; margin: 0 auto clamp(1rem, 2.5vh, 2rem); }
.home-logo { display: block; width: 193px; height: 67px; object-fit: contain; margin: 0 auto; }
.home-intro h1 { font-family: var(--font-display); font-size: clamp(1.8rem, 3.2vw, 2.8rem); line-height: 1.15; margin: 0.75rem 0 1rem; }
.home-intro h1:focus { outline: none; }
.home-intro > p:last-child { color: var(--color-text-muted); line-height: 1.7; }
.home-journeys { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; }
.home-card {
  display: flex; flex-direction: column; min-width: 0; padding: 1.25rem;
  background: var(--color-panel); color: var(--color-text); text-decoration: none;
  border: var(--border-width) solid var(--color-border); border-radius: var(--radius);
  cursor: pointer; transition: transform 150ms ease, border-color 150ms ease;
}
.home-card:hover { transform: translateY(-4px); border-color: var(--color-accent); }
.home-card:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; }
.topnav .brand:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; }
.home-card h2 { font-family: var(--font-display); font-size: 1.45rem; margin: 0.75rem 0 0.5rem; }
.home-card p { font-size: 0.9rem; line-height: 1.65; margin: 0.5rem 0; color: var(--color-text-muted); }
.home-card .home-card-prompt { color: var(--color-text); font-weight: 600; }
.home-card-cta { margin-top: auto; padding-top: 0.75rem; display: flex; justify-content: space-between; gap: 0.5rem; color: var(--color-accent); font-size: 0.9rem; font-weight: 600; }
.home-art { background: var(--color-base); padding: 0.5rem; }
.home-art svg { display: block; width: 100%; max-height: clamp(110px, 15vh, 180px); fill: none; stroke: var(--color-accent); stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; }
.home-art .home-art-grid { stroke: var(--color-border); stroke-width: 1; }
.home-art .home-art-paper { fill: var(--color-panel); }
.home-art .home-art-accent { fill: var(--color-accent); }
.home-art .home-art-area { fill: var(--color-accent-2); fill-opacity: 0.25; }
.home-art .home-art-route { stroke-dasharray: 4 5; }
@media (max-width: 850px) and (min-width: 701px) {
  .home-journeys { gap: 0.75rem; }
  .home-card { padding: 1rem; }
}
@media (min-width: 701px) and (max-height: 800px) {
  #home-page { padding-top: 1rem; padding-bottom: 1rem; }
  .home-logo { width: 164px; height: 57px; }
  .home-intro { margin-bottom: 0.75rem; }
  .home-intro h1 { margin-top: 0.35rem; margin-bottom: 0.6rem; }
  .home-intro > p:last-child { line-height: 1.5; margin: 0.5rem 0; }
}
@media (width <= 700px) {
  #home-page { padding-top: 2rem; padding-bottom: 2rem; }
  .home-journeys { grid-template-columns: 1fr; }
  .home-art svg { max-height: 150px; }
  .topnav .brand img { width: 32px; height: 32px; }
}
.mode-switch-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; }
.journey-icon { display: inline-block; width: 1.25em; line-height: 1; flex-shrink: 0; }
.mode-switch-btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .home-card { transition: none; }
  .home-card:hover { transform: none; }
}
#report-progression { padding: 1rem 1rem 2rem; color: var(--color-text-muted); flex-shrink: 0; }
.landing-hero-result button, .office-search-result button {
  display: block; width: 100%; padding: 0.55rem 0.75rem;
  border: 0; background: none; color: inherit; font: inherit;
  text-align: left; cursor: pointer;
}
.landing-hero-result button:focus-visible,
.office-search-result button:focus-visible,
.rcard-head:focus-visible, #report-back:focus-visible,
#report-explore-btn:focus-visible, #report-full-map-btn:focus-visible,
.rcard-workplace-btn:focus-visible, #gd-mobile-view-toggle:focus-visible {
  outline: 2px solid var(--color-accent); outline-offset: -2px;
}
@media (max-width: 700px) {
  .topnav { flex-wrap: wrap; }
  #mode-switch { width: 100%; }
  .mode-switch-btn { flex: 1; font-size: 0.8rem; padding: 0.45rem 0.25rem; }
  #status-readout { display: none; }
}
