:root {
  color-scheme: light;
  --page: #f9f9f7;
  --surface: #fcfcfb;
  --text-primary: #0b0b0b;
  --text-secondary: #52514e;
  --text-muted: #898781;
  --gridline: #e1e0d9;
  --border: rgba(11, 11, 11, 0.10);
  --status-good: #0ca30c;
  --status-bad: #c0392b;

  --slot-blue: #2a78d6;
  --slot-green: #008300;
  --slot-magenta: #e87ba4;
  --slot-yellow: #eda100;
  --slot-gold: #b8860b;

  /* Transit layer colors (metro/rail/tram). All 5 --slot-* colors were
     already claimed by the six existing map layers, so these are their
     own tokens rather than reusing one and making two layers ambiguous. */
  --transit-metro: #7b3fb8;
  --transit-rail: #0d6e70;
  --transit-tram: #c65416;
  --transit-bus: #4a6fa5;
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) {
    color-scheme: dark;
    --page: #0d0d0d;
    --surface: #1a1a19;
    --text-primary: #ffffff;
    --text-secondary: #c3c2b7;
    --text-muted: #898781;
    --gridline: #2c2c2a;
    --border: rgba(255, 255, 255, 0.10);
    --status-good: #0ca30c;
    --status-bad: #e05a48;

    --slot-blue: #3987e5;
    --slot-green: #008300;
    --slot-magenta: #d55181;
    --slot-yellow: #c98500;
    --slot-gold: #e0b84a;

    --transit-metro: #a878e0;
    --transit-rail: #34a6a8;
    --transit-tram: #e8834d;
    --transit-bus: #6d94cc;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --page: #0d0d0d;
  --surface: #1a1a19;
  --text-primary: #ffffff;
  --text-secondary: #c3c2b7;
  --text-muted: #898781;
  --gridline: #2c2c2a;
  --border: rgba(255, 255, 255, 0.10);
  --status-good: #0ca30c;
  --status-bad: #e05a48;

  --slot-blue: #3987e5;
  --slot-green: #008300;
  --slot-magenta: #d55181;
  --slot-yellow: #c98500;
  --slot-gold: #e0b84a;

  --transit-metro: #a878e0;
  --transit-rail: #34a6a8;
  --transit-tram: #e8834d;
  --transit-bus: #6d94cc;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--page);
  color: var(--text-primary);
}

.topbar {
  position: relative;
  padding: 20px 24px 12px;
}
.topbar h1 { margin: 0; font-size: 1.4rem; }
.topbar .subtitle { margin: 4px 0 0; color: var(--text-secondary); font-size: 0.9rem; }

.theme-toggle {
  position: absolute;
  top: 20px;
  right: 24px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  width: 36px;
  height: 36px;
  cursor: pointer;
  color: var(--text-primary);
  font-size: 1rem;
}

/* Phase 23-G follow-up: a distinct button, not a Map/Data sub-tab —
   admin is a different kind of view (operational, not gym data). */
.admin-toggle {
  position: absolute;
  top: 20px;
  right: 68px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  height: 36px;
  padding: 0 14px;
  cursor: pointer;
  color: var(--text-primary);
  font-size: 0.85rem;
}
.admin-toggle.active { background: var(--slot-blue); color: #fff; border-color: var(--slot-blue); }

.admin-view { padding: 0 24px 24px; }
.admin-header { display: flex; align-items: center; gap: 14px; margin-bottom: 8px; }
.admin-header h2 { margin: 0; }
.admin-header .revenue-note { flex: 1; }
.admin-view h3 { margin: 24px 0 8px; font-size: 1rem; }
.admin-table th { text-align: left; white-space: nowrap; }
.admin-table td { white-space: nowrap; }

.admin-drift-ok, .admin-drift-warn {
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 0.85rem;
  margin-bottom: 8px;
}
.admin-drift-ok { background: color-mix(in srgb, var(--status-good) 12%, transparent); color: var(--status-good); }
.admin-drift-warn { background: color-mix(in srgb, var(--status-bad) 12%, transparent); color: var(--status-bad); }

/* Admin sub-tabs. Deliberately NOT .view-tab styling — those are the
   Data-mode sub-tabs and must keep outranking anything nested inside a
   single view. Built on the same card recipe as everything else. */
.admin-tabs { display: flex; gap: 6px; margin: 14px 0 4px; border-bottom: 1px solid var(--border); }
.admin-tab {
  background: none; border: none; border-bottom: 2px solid transparent;
  padding: 8px 14px; cursor: pointer; color: var(--text-muted);
  font: inherit; font-size: 0.92em;
}
.admin-tab:hover { color: var(--text-primary); }
.admin-tab.active { color: var(--text-primary); border-bottom-color: var(--slot-blue); font-weight: 600; }

/* The provides/description column is prose, so it is the one cell in an
   admin table allowed to wrap rather than force horizontal scrolling. */
#admin-sources-tbody td:nth-child(3) { white-space: normal; max-width: 22rem; }
#admin-sources-tbody td:first-child { white-space: normal; max-width: 24rem; }

/* Now only used inside #view-admin's Dashboard section (moved there from
   the top-level Table view) — .admin-view already supplies its own 24px
   horizontal padding, so this doesn't repeat it. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  padding: 0 0 16px;
}

.stat-tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 14px 16px;
  border-left: 3px solid var(--accent, var(--gridline));
}
.stat-tile .value {
  font-size: 1.6rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.stat-tile .label {
  color: var(--text-secondary);
  font-size: 0.82rem;
  margin-top: 2px;
}

.filters {
  padding: 0 24px 16px;
}

.filter-row { margin-bottom: 10px; }

.search-input {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-primary);
  font-size: 0.95rem;
}

.filter-selects {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: flex-end;
}

.filter-field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 0.78rem;
  color: var(--text-secondary);
}

.filter-field-checkbox {
  flex-direction: row;
  align-items: center;
  gap: 6px;
}

/* "All" / "None" links next to a multi-select filter's own label —
   selects/deselects every option in that one field and dispatches the
   select's existing native "change" event, so it reloads through the
   exact same code path a manual click already does; no per-filter JS. */
.filter-field-label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.sel-links { display: flex; gap: 6px; }
.sel-link {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  font-size: 0.72rem;
  color: var(--slot-blue);
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: transparent;
}
.sel-link:hover { text-decoration-color: currentColor; }

/* Searchable multi-select combo (department dropdowns — ~100 options,
   painful to scroll natively). The wrapped <select multiple> stays the
   real state; this is a view on top of it. */
.ms-native-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}
.ms-combo { position: relative; min-width: 140px; max-width: 220px; }
.ms-combo-toggle {
  width: 100%;
  text-align: left;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-primary);
  font-size: 0.85rem;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ms-combo-toggle:hover { border-color: var(--slot-blue); }
.ms-combo-panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 4px);
  left: 0;
  width: max(100%, 220px);
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 6px 18px color-mix(in srgb, #000 18%, transparent);
  padding: 8px;
}
.ms-combo-search {
  width: 100%;
  padding: 6px 8px;
  margin-bottom: 6px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-primary);
  font-size: 0.85rem;
}
.ms-combo-options { max-height: 240px; overflow-y: auto; }
.ms-combo-option {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 4px;
  border-radius: 5px;
  font-size: 0.82rem;
  color: var(--text-primary);
  cursor: pointer;
}
.ms-combo-option:hover { background: color-mix(in srgb, var(--slot-blue) 10%, transparent); }
.ms-combo-option-hidden { display: none; }

.detail-actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 10px;
}

.scouting-row-unavailable { opacity: 0.6; }

.filter-field select {
  min-width: 140px;
  max-width: 220px;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-primary);
  font-size: 0.85rem;
}

/* Dual-handle range slider filter — replaces a plain "Min"/"Max" number-
   input pair. Two overlapping native <input type="range"> stacked via
   the classic dual-slider trick: each input's own track is made
   invisible/non-interactive and only its thumb (pointer-events: auto)
   is draggable, so both handles stay independently grabbable on one
   visual track. A single-handle variant (e.g. "Max rent") just omits
   the min thumb — see initRangeSlider(), app.js. */
.range-filter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 170px;
  font-size: 0.78rem;
  color: var(--text-secondary);
}
.range-slider {
  position: relative;
  height: 20px;
  margin: 4px 0 2px;
}
.range-slider::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 4px;
  transform: translateY(-50%);
  background: var(--border);
  border-radius: 2px;
}
.range-slider-fill {
  position: absolute;
  top: 50%;
  height: 4px;
  transform: translateY(-50%);
  background: var(--slot-blue);
  border-radius: 2px;
  pointer-events: none;
}
.range-thumb {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 20px;
  margin: 0;
  background: transparent;
  -webkit-appearance: none;
  appearance: none;
  pointer-events: none;
}
.range-thumb:focus { outline: none; }
.range-thumb::-webkit-slider-runnable-track { background: transparent; -webkit-appearance: none; }
.range-thumb::-moz-range-track { background: transparent; border: none; }
.range-thumb::-webkit-slider-thumb {
  -webkit-appearance: none;
  pointer-events: auto;
  width: 14px;
  height: 14px;
  margin-top: 3px;
  border-radius: 50%;
  background: var(--slot-blue);
  border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--border);
  cursor: pointer;
}
.range-thumb::-moz-range-thumb {
  pointer-events: auto;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--slot-blue);
  border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--border);
  cursor: pointer;
}
.range-filter-values {
  display: flex;
  align-items: center;
  gap: 6px;
}
.range-number {
  width: 64px;
  padding: 4px 6px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  color: var(--text-primary);
  font-size: 0.82rem;
}
.range-filter-sep { color: var(--text-muted); }

/* Phase 21: Map/Data mode switch — deliberately heavier than .view-tab
   below it so the two navigation levels are never confusable at a
   glance (segmented-pill look, not another folder tab). */
.mode-switch {
  margin: 0 24px 14px;
  display: inline-flex;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 3px;
  gap: 2px;
}
.mode-btn {
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: 0.85rem;
  font-weight: 600;
  padding: 8px 22px;
  border-radius: 7px;
  cursor: pointer;
}
.mode-btn.active { background: var(--slot-blue); color: #fff; }

/* Shared toolbar: search + one "Filters" affordance, used identically in
   both modes — one filter panel, one shared state, never a second
   stacked filter bar. */
.toolbar-row {
  display: flex;
  gap: 10px;
  align-items: center;
  padding: 0 24px 12px;
}
.toolbar-row .search-input { flex: 1; width: auto; }

.filters-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 10px 16px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-primary);
  font-size: 0.85rem;
  cursor: pointer;
  white-space: nowrap;
}
.filters-btn.open { border-color: var(--slot-blue); color: var(--slot-blue); }
.filter-count {
  background: var(--slot-blue);
  color: #fff;
  border-radius: 999px;
  font-size: 0.68rem;
  font-weight: 700;
  padding: 1px 7px;
  font-variant-numeric: tabular-nums;
}

.filter-panel {
  margin: 0 24px 16px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
}
.filter-panel-actions {
  display: flex;
  gap: 10px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--gridline);
}

.map-count-chip {
  color: var(--text-secondary);
  font-size: 0.8rem;
  margin-left: auto;
}

.btn-ghost, .btn-primary {
  padding: 8px 14px;
  border-radius: 8px;
  font-size: 0.85rem;
  cursor: pointer;
  border: 1px solid var(--border);
  text-decoration: none;
  display: inline-block;
}
.btn-ghost {
  background: transparent;
  color: var(--text-secondary);
}
.btn-primary {
  background: var(--slot-blue);
  color: #fff;
  border-color: transparent;
}
.btn-primary.active {
  background: var(--status-good);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--status-good) 30%, transparent);
}

.results { padding: 0 24px 40px; }

.results-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--text-secondary);
  font-size: 0.85rem;
  margin-bottom: 8px;
}

.last-reconciled {
  color: var(--text-muted);
  font-size: 0.78rem;
  margin-bottom: 6px;
}

.pager { display: flex; align-items: center; gap: 10px; }
.pager button { padding: 6px 10px; font-size: 0.8rem; }

.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
}

table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }

thead th {
  text-align: left;
  padding: 10px 12px;
  color: var(--text-muted);
  font-weight: 600;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  border-bottom: 1px solid var(--gridline);
  white-space: nowrap;
}
thead th[data-sort] { cursor: pointer; }
thead th.sorted::after { content: " \25BE"; color: var(--text-secondary); }
thead th.sorted.desc::after { content: " \25B4"; }

tbody td {
  padding: 9px 12px;
  border-bottom: 1px solid var(--gridline);
  vertical-align: top;
}
tbody tr.gym-row:last-child td { border-bottom: none; }
tbody tr.gym-row { cursor: pointer; }
tbody tr.gym-row:hover { background: color-mix(in srgb, var(--text-primary) 4%, transparent); }
tbody tr.gym-row.expanded { background: color-mix(in srgb, var(--slot-blue) 6%, transparent); }
/* Shared across every table (Rentals, For Sale, ...) that shows a
   "saved to scouting list" badge — not rentals-specific despite the
   name once suggesting otherwise. */
tbody tr.gym-row.row-saved { background: color-mix(in srgb, var(--status-good) 12%, transparent); }
tbody tr.gym-row.row-saved:hover { background: color-mix(in srgb, var(--status-good) 18%, transparent); }

/* A whole-row accent for what a reconciliation run just did — the "new"/
   "changed" badges already in the row are easy to miss scanning down a
   50-row page; a left-edge stripe reads at a glance without needing a
   full background tint (reserved for .rental-row-saved, so the two
   states stay visually distinct rather than competing for the same
   full-row color). Changed rows aren't also new, so border-left alone
   (no fill) keeps both legible if a future row is ever both. */
tbody tr.gym-row.rental-row-new { border-left: 3px solid var(--status-good); }
tbody tr.gym-row.rental-row-changed { border-left: 3px solid var(--slot-yellow); }

.gym-name { font-weight: 500; }
.gym-address { color: var(--text-muted); font-size: 0.78rem; margin-top: 1px; }

/* Rentals' Catchment and Competition cells are line-per-figure lists
   ("500m: 22 586", "Age 1km: 43.1", ...) — without nowrap the narrow
   default column width wraps each of those short lines onto two, which
   is what made the block unreadable. Sized to the widest real line
   ("Visibility (150m): 188 stores") so every figure stays on one line. */
.rental-catchment-cell,
.rental-competition-cell {
  white-space: nowrap;
}
.rental-catchment-cell { min-width: 190px; }
.rental-competition-cell { min-width: 110px; }

/* Widened so the street/city/source/map-links block below isn't squeezed
   into an unreadable single column by the fixed-width catchment/
   competition cells next to it — sized for up to 4 real lines rather
   than clamping/truncating text at some arbitrary character count. */
.rental-address-cell { min-width: 240px; max-width: 340px; white-space: normal; }
.mono { font-variant-numeric: tabular-nums; font-family: ui-monospace, monospace; font-size: 0.78rem; }
.revenue-note { color: var(--text-muted); font-size: 0.72rem; margin-top: 1px; }

.expand-toggle {
  color: var(--text-muted);
  transition: transform 0.15s ease;
  /* Was width:1em — a ~14px tap target, effectively unhittable on a
     phone. Widened with padding so the whole cell is tappable while the
     glyph stays the same size. */
  width: 2.2em;
  padding-left: 10px;
  padding-right: 10px;
  cursor: pointer;
  user-select: none;
}
.expand-toggle:hover { color: var(--text-primary); }
/* Rotation feedback for every table that has an expander, not just gyms —
   scouting/rentals/listings previously got no visual confirmation that a
   row had opened. */
tr.gym-row.expanded .expand-toggle,
tr.scouting-row.expanded .expand-toggle,
tr.rental-row.expanded .expand-toggle,
tr.listing-row.expanded .expand-toggle { transform: rotate(90deg); }

.detail-row td {
  background: color-mix(in srgb, var(--text-primary) 3%, var(--surface));
  padding: 0;
}
/* The detail panel is as wide as the TABLE, not the screen. Rentals'
   own min-widths (240 + 190x3 + 110 + ...) force the table past 1600px
   inside an overflow-x:auto .table-wrap, so a td[colspan] stretched to
   match and pushed the right side of every expansion off-screen —
   scrolling to reach it then hid the left side. Sticky at left:0 pins
   the panel to the viewport instead, which is what makes any
   multi-column layout inside it viable at all. */
.detail-panel {
  position: sticky;
  left: 0;
  width: min(100%, calc(100vw - 48px));
  padding: 14px 20px;
  box-sizing: border-box;
}
.detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 10px 24px;
}
/* Catchment and Competition sit side by side rather than stacked — both
   are tall line-per-figure lists, and stacking them made the Rentals
   expansion very long. auto-fit (not auto-fill) so they collapse to one
   column on a narrow screen instead of leaving an empty track. */
.detail-columns {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 10px 24px;
  margin-top: 10px;
}
/* ---- Expanded-row detail cards -------------------------------------
   Same recipe as .filter-panel / #profile-content — 1px --border, 10px
   radius, --surface — so an expansion frames its groups the way every
   other panel in the app already does. Because .detail-panel sits on a
   3% color-mix ground, a --surface card reads as raised against it with
   no shadow and NO NEW COLOUR TOKEN, which is what makes it hold in both
   themes for free. */
.detail-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 11px 13px 12px;
  min-width: 0;
}

/* This rule did not exist. .detail-column-title was used 12 times across
   app.js/membership.js/scouting.js with nothing defined, so every section
   heading — Transit, Station footfall, Household income, Bus service,
   Street traffic, Member forecast, Surroundings — rendered at body weight
   and body size, indistinguishable from its own values. That single
   omission is most of why the expansions read as an undifferentiated
   pile. */
.detail-column-title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  padding-bottom: 5px;
  margin-bottom: 9px;
  border-bottom: 1px solid var(--gridline);
}
/* The parenthetical source notes inside those headings ("(straight-line,
   GTFS)", "(counted, not estimated)") are .revenue-note and would
   otherwise inherit the uppercase/tracking and stop reading as asides. */
.detail-column-title .revenue-note {
  margin-top: 0;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}
/* Sub-headings within a card. One step quieter and no rule, so the card
   border stays the strongest line: card > title > subhead. */
.detail-subhead {
  font-size: 0.68rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-muted);
  margin: 12px 0 6px;
}
.detail-card > .detail-subhead:first-child { margin-top: 0; }
.detail-column { min-width: 0; }

/* 12-column grid with span utilities. auto-fit (what .detail-columns
   uses) cannot express "the map is worth 7 columns and the forecast
   working 5", which is exactly the control needed here.
   minmax(0, 1fr) rather than 1fr matters: several cards contain
   white-space:nowrap content, and a bare 1fr track has min-width:auto,
   which would let that content force the grid wider than its container
   and re-introduce the horizontal scroll .detail-panel just removed. */
.detail-panel-grid {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  gap: 12px;
  align-items: start;
  margin-top: 12px;
}
.dc-3  { grid-column: span 3; }
.dc-4  { grid-column: span 4; }
.dc-5  { grid-column: span 5; }
.dc-6  { grid-column: span 6; }
.dc-7  { grid-column: span 7; }
.dc-12 { grid-column: 1 / -1; }
@media (max-width: 1180px) {
  .detail-panel-grid { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .dc-3, .dc-4, .dc-5 { grid-column: span 3; }
  .dc-6, .dc-7 { grid-column: span 6; }
}
@media (max-width: 760px) {
  .detail-panel-grid { grid-template-columns: minmax(0, 1fr); }
  .dc-3, .dc-4, .dc-5, .dc-6, .dc-7 { grid-column: span 1; }
}

/* Provenance strip: facts already visible in the collapsed row, kept for
   reference but demoted from three full-width rows to one quiet line. */
.detail-strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  font-size: 0.78rem;
  color: var(--text-secondary);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--gridline);
}
.detail-strip .sep { color: var(--text-muted); }

/* Progressive disclosure. Native <details> — no JS, and safe here because
   the row-expand handler matches closest("tr"), while these live inside
   the sibling detail row. Every summary states WHAT is inside rather than
   "show more": so many fields are legitimately absent in this dataset
   that a closed toggle must never be confusable with "not measured". */
.detail-more { margin-top: 8px; }
.detail-more > summary {
  cursor: pointer;
  font-size: 0.72rem;
  color: var(--text-muted);
  list-style: none;
  padding: 3px 0;
}
.detail-more > summary::-webkit-details-marker { display: none; }
.detail-more > summary::before { content: "\25B8 "; }
.detail-more[open] > summary::before { content: "\25BE "; }
.detail-more > summary:hover { color: var(--text-primary); }

.detail-item { display: flex; flex-direction: column; gap: 2px; }
.detail-label {
  color: var(--text-muted);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.detail-value { font-size: 0.85rem; word-break: break-word; }

.market-research {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--gridline);
}
.market-research-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 10px;
}
.market-research-header h4 {
  margin: 0;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--text-muted);
}
/* Phase 16: one section per audit group (entity_summary is rendered
   directly under the header instead, via the plain .detail-grid already
   used everywhere else — only the 8 grouped_audit_data sections get their
   own sub-heading). */
.audit-group { margin-top: 14px; }
.audit-group h5 {
  margin: 0 0 8px;
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  color: var(--text-muted);
}

/* Phase 20: Entity Profile tab — canonical fields the system tracks but
   has no value for read exactly like a populated .detail-item, just
   visually dimmed, never hidden (the whole point of this tab). */
.detail-item-empty { opacity: 0.6; }
.detail-value-empty { color: var(--text-muted); font-style: italic; }

.profile-link {
  margin-left: auto;
  font-size: 0.78rem;
  color: var(--slot-blue);
  text-decoration: none;
  white-space: nowrap;
}
.profile-link:hover { text-decoration: underline; }

#profile-content {
  margin-top: 14px;
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
}
#profile-content .market-research-header:first-child { margin-top: 0; }

.profile-search-field { position: relative; }
.profile-search-field input {
  min-width: 260px;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text-primary);
  font-size: 0.85rem;
}
.profile-search-results {
  position: absolute;
  top: 100%;
  left: 0;
  z-index: 20;
  min-width: 280px;
  margin-top: 2px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--surface);
  box-shadow: 0 6px 18px color-mix(in srgb, var(--text-primary) 12%, transparent);
  max-height: 260px;
  overflow-y: auto;
}
.profile-search-result {
  padding: 8px 10px;
  font-size: 0.82rem;
  cursor: pointer;
  border-bottom: 1px solid var(--gridline);
}
.profile-search-result:last-child { border-bottom: none; }
.profile-search-result:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); }

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.75rem;
  border: 1px solid var(--border);
  white-space: nowrap;
}
.badge-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }

.badge-both .badge-dot { background: var(--slot-blue); }
.badge-sirene .badge-dot { background: var(--slot-green); }
.badge-data_es .badge-dot { background: var(--slot-magenta); }

.badge-active .badge-dot { background: var(--status-good); }
.badge-closed .badge-dot { background: var(--text-muted); }
.badge-genuine .badge-dot { background: var(--status-bad); }
.badge-admin-only .badge-dot { background: var(--text-muted); }
.badge-new .badge-dot { background: var(--status-good); }
.badge-changed .badge-dot { background: var(--slot-yellow); }
.badge-removed .badge-dot { background: var(--text-muted); }
.badge-low-confidence .badge-dot { background: var(--slot-magenta); }
.badge-saved .badge-dot { background: var(--status-good); }

.badge-gym .badge-dot { background: var(--slot-blue); }
.badge-for_sale_listing .badge-dot { background: var(--slot-green); }
.badge-rental_listing .badge-dot { background: var(--slot-magenta); }
.badge-hypothetical .badge-dot { background: var(--slot-yellow); }

/* Phase 22 — Opportunities recommendation labels. */
.badge-strong_candidate .badge-dot { background: var(--status-good); }
.badge-worth_a_look .badge-dot { background: var(--slot-yellow); }
.badge-pass .badge-dot { background: var(--text-muted); }
.badge-unscored .badge-dot { background: var(--text-muted); }

.empty-state {
  text-align: center;
  padding: 40px;
  color: var(--text-muted);
}

@media (max-width: 640px) {
  .filter-field select { max-width: 100%; flex: 1 1 45%; }

  .mode-switch { display: flex; margin: 0 16px 12px; }
  .mode-btn { flex: 1; padding: 10px 8px; }

  .toolbar-row { padding: 0 16px 10px; }
  .filter-panel { margin-left: 16px; margin-right: 16px; padding-left: 16px; padding-right: 16px; }

  /* Sub-tabs become a horizontal scroll strip with an edge-fade hinting
     there's more — a phone-width dropdown would cost an extra tap for
     something switched often mid-session. */
  #data-subtabs {
    padding: 0 16px 10px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    mask-image: linear-gradient(to right, black 90%, transparent 100%);
  }
  #data-subtabs::-webkit-scrollbar { display: none; }
  .view-tab { flex: none; white-space: nowrap; }
  .view-tabs-note { display: none; }

  /* Filter panel becomes a fixed bottom sheet on narrow screens — an
     11-field inline form doesn't fit a phone width no matter the column
     count, so this is a real layout change, not just reflow. */
  .filter-panel {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    max-height: 80vh;
    margin: 0;
    border-radius: 16px 16px 0 0;
    overflow-y: auto;
    z-index: 30;
    box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.25);
  }
  .filter-selects { flex-direction: column; align-items: stretch; }
  .filter-field select { width: 100%; max-width: 100%; }

  .results { padding: 0 16px 40px; }
  .topbar { padding: 16px 16px 10px; }
}

.view-tabs {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 24px 10px;
}

.view-tab {
  padding: 8px 16px;
  border-radius: 8px 8px 0 0;
  border: 1px solid var(--border);
  border-bottom: none;
  background: var(--page);
  color: var(--text-secondary);
  font-size: 0.85rem;
  cursor: pointer;
}
.view-tab.active {
  background: var(--surface);
  color: var(--text-primary);
  font-weight: 600;
}

.view-tabs-note {
  color: var(--text-muted);
  font-size: 0.78rem;
  margin-left: 8px;
}

.hidden { display: none !important; }

.map-toolbar {
  margin-bottom: 10px;
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

.map-paste-link {
  display: flex;
  gap: 6px;
  align-items: center;
}
.map-paste-link .search-input {
  width: 260px;
  padding: 7px 10px;
  font-size: 0.82rem;
}

.map-layers-panel {
  margin-bottom: 10px;
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.map-layers-mode {
  display: flex;
  align-items: center;
  gap: 16px;
  font-size: 0.82rem;
  color: var(--text-secondary);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--gridline);
}
.map-layers-mode-label {
  color: var(--text-muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.map-layers-mode label { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; }
.map-layers-list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 18px;
}
.map-layer-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.85rem;
  color: var(--text-primary);
  cursor: pointer;
}
.map-layer-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
}
.map-layer-dot-genuine { background: var(--status-bad); }
.map-layer-dot-other { background: var(--slot-blue); }
.map-layer-dot-rentals { background: var(--slot-magenta); }
.map-layer-dot-listings { background: var(--slot-green); }
.map-layer-dot-hypothetical { background: var(--slot-yellow); }
.map-layer-dot-scouting {
  background: var(--slot-gold);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--slot-gold) 35%, transparent);
}
.map-layer-count {
  color: var(--text-muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* --- Transit layers ------------------------------------------------ */
/* Square markers, deliberately a different SHAPE from every existing
   map marker (teardrop pins for gyms/rentals/listings, circles for
   hypothetical/jump targets) so a transit station is never mistaken for
   one of the app's own record types, in either theme. */
.map-layer-dot-transit-metro { background: var(--transit-metro); border-radius: 2px; }
.map-layer-dot-transit-rail  { background: var(--transit-rail);  border-radius: 2px; }
.map-layer-dot-transit-tram  { background: var(--transit-tram);  border-radius: 2px; }
.map-layer-dot-transit-bus   { background: var(--transit-bus);   border-radius: 2px; }

.map-layers-group-label {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  color: var(--text-muted);
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
}
.map-layers-note {
  margin-top: 6px;
  color: var(--text-muted);
  font-size: 0.75rem;
  line-height: 1.4;
}

.transit-station-marker {
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 2px;
  border: 1.5px solid var(--surface);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.transit-station-metro { background: var(--transit-metro); }
.transit-station-rail  { background: var(--transit-rail); }
.transit-station-tram  { background: var(--transit-tram); }
.transit-station-bus   { background: var(--transit-bus); }

.transit-cluster {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 6px;
  color: #fff;
  font-size: 0.75rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  border: 2px solid var(--surface);
}
.transit-cluster-metro { background: var(--transit-metro); }
.transit-cluster-rail  { background: var(--transit-rail); }
.transit-cluster-tram  { background: var(--transit-tram); }
.transit-cluster-bus   { background: var(--transit-bus); }

/* Proximity markers on the catchment rows. These reflect the ONLY
   distance figures this project could source to a real French standard
   (Article L151-36 du code de l'urbanisme: 800m current, 500m prior
   wording) — shown as a factual annotation, never a score or a
   pass/fail judgement. */
.transit-prox {
  display: inline-block;
  padding: 0 5px;
  border-radius: 3px;
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
  vertical-align: baseline;
}
.transit-prox-500 {
  background: color-mix(in srgb, var(--status-good) 18%, transparent);
  color: var(--status-good);
}
.transit-prox-800 {
  background: color-mix(in srgb, var(--slot-blue) 18%, transparent);
  color: var(--slot-blue);
}

.map-popup-subhead {
  margin-top: 8px;
  padding-top: 6px;
  border-top: 1px solid var(--border);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-primary);
}
.map-popup-muted {
  color: var(--text-muted);
  font-weight: 400;
}
/* Continuation lines under a footfall station heading — indented so the
   counts read as belonging to the station named above them. */
.map-popup-indent {
  padding-left: 12px;
}

#map {
  height: 70vh;
  min-height: 420px;
  border-radius: 10px;
  border: 1px solid var(--border);
}

.map-popup { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; font-size: 0.85rem; }
.map-popup .gym-name {
  font-weight: 600;
  margin-bottom: 4px;
  display: flex;
  align-items: baseline;
  gap: 6px;
  flex-wrap: wrap;
}
.popup-brand-tag {
  font-weight: 400;
  font-size: 0.72rem;
  color: var(--text-secondary);
  background: color-mix(in srgb, var(--slot-blue) 12%, transparent);
  padding: 1px 7px;
  border-radius: 999px;
}
.map-popup .map-popup-row { color: var(--text-secondary); margin-top: 2px; }
.map-popup .map-popup-loading { font-style: italic; color: var(--text-muted); }
.map-popup .map-popup-hint {
  color: var(--text-muted);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin-top: 8px;
}
.map-popup-neighbor-link {
  cursor: pointer;
  color: var(--slot-blue);
}
.map-popup-neighbor-link:hover { text-decoration: underline; }

.btn-save-scouting {
  display: block;
  width: 100%;
  margin-top: 10px;
  padding: 6px 10px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--slot-blue);
  color: #fff;
  font-size: 0.8rem;
  cursor: pointer;
}
.btn-save-scouting:disabled { cursor: default; opacity: 0.7; }
.btn-save-scouting.saved { background: var(--status-good); }

.map-popup-detail-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 6px 16px;
  margin-top: 8px;
  max-height: 260px;
  overflow-y: auto;
  padding-right: 4px;
}
.map-popup-detail-grid .detail-item { gap: 1px; }
.map-popup-detail-grid .detail-label { font-size: 0.62rem; }
.map-popup-detail-grid .detail-value { font-size: 0.78rem; }

.leaflet-div-icon.distance-label {
  background: transparent;
  border: none;
}
.distance-label span {
  display: inline-block;
  transform: translate(-50%, -50%);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px 6px;
  font-size: 0.7rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
  white-space: nowrap;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}

.neighbor-name-label {
  background: var(--surface);
  border: 1px solid var(--border);
  color: var(--text-primary);
  font-size: 0.72rem;
  font-weight: 500;
  padding: 2px 7px;
  border-radius: 6px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}
.neighbor-name-label::before {
  border-top-color: var(--border);
}

/* --- Expanded-row site context: signals column + mini-map ----------- */
/* The mini-map is its own small Leaflet instance per expanded row (see
   app.js's renderMiniMap) — a fixed height is required, since Leaflet
   cannot size a container that has none. */
.detail-minimap-section { margin-top: 14px; }
.detail-minimap {
  height: 280px;
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.minimap-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  margin: 6px 0 8px;
  font-size: 0.75rem;
  color: var(--text-secondary);
}
.minimap-key { display: inline-flex; align-items: center; gap: 5px; }
.minimap-key i {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
}
/* Leaflet injects its own link colours/backgrounds; keep the mini-map's
   popups on the app's own tokens in both themes. */
.detail-minimap .leaflet-popup-content-wrapper,
.detail-minimap .leaflet-popup-tip {
  background: var(--surface);
  color: var(--text-primary);
}
