:root {
  --accent: #0077b6;
  --nav-bg: #03045e;
  --nav-bg-2: #023e8a;
  --bg: #0a1929;
  --text: #e0e7ef;
  --text-muted: #8eafc4;
  --card-bg: #0d2137;
  --border: #1e3a5f;
  --status-active: #4ade80;
  --status-degraded: #facc15;
  --status-silent: #64748b;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}

#app {
  display: flex;
  flex-direction: column;
  height: 100%;
}

header {
  background: linear-gradient(90deg, var(--nav-bg), var(--nav-bg-2));
  padding: 0.75rem 1.25rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem;
  border-bottom: 1px solid var(--border);
  z-index: 1000;
}

header h1 {
  font-size: 1.25rem;
  margin: 0;
}

header .subtitle {
  color: var(--text-muted);
  font-size: 0.85rem;
  margin-left: 0.5rem;
}

#stats {
  display: flex;
  gap: 1rem;
  font-size: 0.85rem;
  color: var(--text-muted);
  align-items: center;
  flex-wrap: wrap;
}

#stats .stat { display: flex; align-items: center; gap: 0.35rem; }

.dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  display: inline-block;
}
.dot.active { background: var(--status-active); }
.dot.degraded { background: var(--status-degraded); }
.dot.silent { background: var(--status-silent); }

#map-wrap {
  position: relative;
  flex: 1;
  min-height: 0;
}

#map {
  position: absolute;
  inset: 0;
}

/* Both panels dock to the full right edge — shift Leaflet's own right-side
   controls (basemap/layers up top, legend down below) left by whichever
   panel's own width so they stay reachable instead of disappearing
   underneath it. The simulator used to be a centered overlay covering the
   whole map — reverted (see #sim-panel below): covering the map entirely
   defeated the point of a spatial tool, worse than the cramped-sidebar
   problem it was meant to fix. */
#map-wrap.plan-open .leaflet-top.leaflet-right,
#map-wrap.plan-open .leaflet-bottom.leaflet-right {
  right: 320px;
  transition: right 0.15s ease;
}
#map-wrap.sim-open .leaflet-top.leaflet-right,
#map-wrap.sim-open .leaflet-bottom.leaflet-right {
  right: 480px;
  transition: right 0.15s ease;
}
@media (max-width: 500px) {
  #map-wrap.plan-open .leaflet-top.leaflet-right,
  #map-wrap.plan-open .leaflet-bottom.leaflet-right,
  #map-wrap.sim-open .leaflet-top.leaflet-right,
  #map-wrap.sim-open .leaflet-bottom.leaflet-right {
    right: 90vw;
  }
}

/* A phone-width panel used to dock right at up to 92vw, which put its
   left edge ON TOP of the bottom-left #map-tools cluster — same z-index,
   later in DOM order, so the panel won the overlap and taps that looked
   like they hit the toolbar silently did whatever the panel had in that
   same spot instead (confirmed by hit-testing the visible "Declutter"
   button's own position and getting back a button from inside the panel).
   That whole class of overlap is gone now rather than worked around: at
   these widths the panels are bottom sheets and #map-tools is the
   full-width tab bar beneath them, so the two stack instead of overlapping
   and the nav stays reachable the entire time. See the MOBILE section at
   the end of this file. */

/* #map-tools (Plan / Simulate / Companion pin / Declutter) is absolutely
   positioned in the bottom-left corner at z-index 1100 — above Leaflet's
   own control corners — so anything Leaflet docks along the bottom edge
   sits underneath the buttons unless it clears them. Lifting the corners
   themselves rather than the individual controls means every bottom-docked
   control is covered, however many of them there are and in whatever order
   they were added (the simulator stacks two: the playback panel and the
   "Map key"). app.js measures the real rendered row into
   --map-tools-clearance so a button row that wraps to two lines on a
   narrow viewport pushes the controls up by the right amount instead of a
   hardcoded single-row guess; the value here is the single-row fallback
   used before that first measurement lands. */
#map-wrap { --map-tools-clearance: 3.25rem; --transport-clearance: 0px; }
#map-wrap .leaflet-bottom.leaflet-left,
#map-wrap .leaflet-bottom.leaflet-right {
  bottom: var(--map-tools-clearance);
}

/* Shared replay transport (see setTransportSource): the bottom-most element
   of the map's bottom stack, spanning the visible map width — it shifts
   left with the docked panels exactly like the Leaflet control corners do,
   so the scrubber never runs underneath the simulator panel. */
.sim-transport {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1100;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.85rem;
  background: var(--card-bg);
  border-top: 1px solid var(--border);
  box-shadow: 0 -4px 16px rgba(0,0,0,0.4);
  font-size: 0.78rem;
  transition: right 0.15s ease;
}
.sim-transport.hidden { display: none; }
#map-wrap.plan-open .sim-transport { right: 320px; }
#map-wrap.sim-open .sim-transport { right: 480px; }
@media (max-width: 500px) {
  #map-wrap.plan-open .sim-transport,
  #map-wrap.sim-open .sim-transport { right: 90vw; }
}
#sim-transport-play {
  flex-shrink: 0;
  width: 2.1rem;
  height: 2.1rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--accent);
  color: #fff;
  font-size: 0.85rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
#sim-transport-play:hover { filter: brightness(1.1); }
#sim-transport-seek {
  flex: 1;
  min-width: 80px;
  accent-color: var(--accent);
  cursor: pointer;
}
.sim-transport-time {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  min-width: 9ch;
  text-align: right;
}
.sim-transport-speed-label { flex-shrink: 0; display: flex; }
#sim-transport-speed {
  background: var(--surface1, #0a1929);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.15rem 0.3rem;
  font-size: 0.72rem;
  cursor: pointer;
}
.sim-transport-label {
  flex-shrink: 0;
  color: var(--text-muted);
  font-size: 0.72rem;
  white-space: nowrap;
}
/* Below the label's breakpoint the bar keeps only the controls that matter
   for driving playback. */
@media (max-width: 700px) {
  .sim-transport-label { display: none; }
}
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.leaflet-popup-content-wrapper {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
}
.leaflet-popup-tip { background: var(--card-bg); }

.popup-title {
  font-weight: 700;
  font-size: 1rem;
  margin-bottom: 0.25rem;
}
.popup-row {
  font-size: 0.8rem;
  color: var(--text-muted);
  display: flex;
  justify-content: space-between;
  gap: 1rem;
}
.popup-hint {
  margin-top: 0.35rem;
  font-size: 0.68rem;
  font-style: italic;
  color: var(--text-muted);
  opacity: 0.8;
}
.popup-neighbors {
  margin-top: 0.5rem;
  padding-top: 0.4rem;
  border-top: 1px solid var(--border);
}
.popup-neighbors-title {
  font-size: 0.7rem;
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}
.popup-neighbors ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  max-height: 160px;
  overflow-y: auto;
  font-size: 0.72rem;
}
.popup-neighbors li {
  display: flex;
  align-items: center;
  gap: 0.35rem;
}
.popup-status {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 600;
  text-transform: uppercase;
  margin-bottom: 0.4rem;
}
.popup-status.active { background: rgba(74,222,128,0.15); color: var(--status-active); }
.popup-status.degraded { background: rgba(250,204,21,0.15); color: var(--status-degraded); }
.popup-status.silent { background: rgba(100,116,139,0.2); color: var(--status-silent); }

.legend {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.6rem 0.75rem;
  font-size: 0.75rem;
  max-width: 220px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.legend-title { font-weight: 600; margin-bottom: 0.4rem; }
.legend-bar {
  height: 10px;
  border-radius: 5px;
  background: linear-gradient(90deg, #f97316, #22c55e);
  margin-bottom: 0.25rem;
}
.legend-labels {
  display: flex;
  justify-content: space-between;
  color: var(--text-muted);
  font-size: 0.68rem;
}
.legend-note {
  margin-top: 0.4rem;
  color: var(--text-muted);
  font-size: 0.65rem;
  line-height: 1.3;
}

.legend-opacity {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.45rem;
  font-size: 0.68rem;
  color: var(--text-muted);
}
.legend-opacity input[type="range"] {
  flex: 1;
  accent-color: var(--accent);
}

.scope-filter-control {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.65rem;
  margin-top: 0.5rem;
  font-size: 0.75rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.scope-filter-control label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  cursor: pointer;
}
.scope-filter-control input[type="checkbox"] {
  accent-color: var(--accent);
}

.position-mode-control {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.65rem;
  margin-top: 0.5rem;
  font-size: 0.75rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.position-mode-control select {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.2rem 0.3rem;
  font-size: 0.75rem;
}

.map-display-control {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.65rem;
  margin-top: 0.5rem;
  font-size: 0.75rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}
.map-display-control label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  cursor: pointer;
  white-space: nowrap;
}
.map-display-control input[type="checkbox"] {
  accent-color: var(--accent);
}

/* Shared collapsible-header pattern — see app.js's window.HopReachMapControls. */
.map-control-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.68rem;
  color: var(--text-muted);
  margin-bottom: 0.15rem;
  cursor: pointer;
  user-select: none;
}
.map-control-header:hover { color: var(--text); }
.map-control-chevron { font-size: 0.6rem; flex-shrink: 0; }
/* At its bare font-size-driven height this measured ~16px tall live — a
   real miss target on a touchscreen, not a near-miss (the ~44px
   guideline). Desktop density is fine as-is and stays untouched; this
   only adds padding below the same narrow-viewport breakpoint used
   elsewhere on this page. */
@media (max-width: 700px) {
  .map-control-header { padding: 0.65rem 0; margin-bottom: 0; min-height: 44px; }
}
.map-control-body.hidden { display: none; }

.sim-playback-control {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.65rem;
  margin-top: 0.5rem;
  font-size: 0.75rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  width: 240px;
  /* Clearance from #map-tools is handled once for the whole bottom-left
     corner (see --map-tools-clearance above) rather than per-control — a
     margin here only ever lifted whichever control happened to be last in
     the corner, leaving the other one (the "Map key") sitting under the
     buttons. */
}
/* A smaller sim-stat-strip for the map-docked corner — the modal's own
   full-size version (min-width: 4.5rem, larger padding) is comfortable in
   a 560px modal but cramped in a 240px map card; same shared component
   (renderStatStrip), just denser. */
.sim-stat-strip-compact { gap: 0.4rem; margin-bottom: 0; }
.sim-stat-strip-compact .sim-stat { min-width: 3.6rem; padding: 0.25rem 0.5rem; }
.sim-stat-strip-compact .sim-stat-value { font-size: 0.95rem; }

/* Still used by the real-packet replay's own map-docked transport controls
   (ensureBottleneckLegendControl) — NOT dead, unlike the simulated-flood
   playback buttons that used to share this class (see
   ensureSimPlaybackControl, now a live-stats card instead). */
.sim-playback-buttons { margin-bottom: 0.4rem; }
.sim-playback-buttons button {
  flex: 1;
  background: var(--surface1, #0a1929);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.3rem 0.4rem;
  font-size: 0.72rem;
  cursor: pointer;
}
.sim-playback-buttons button:hover { border-color: var(--accent); }

/* The real-packet replay's map-docked control (see
   ensureBottleneckLegendControl): transport buttons, live status, and the
   key for the line colours. Sized to match .sim-playback-control so the two
   stack tidily in the same corner. */
.sim-bottleneck-legend {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.65rem;
  font-size: 0.72rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  width: 240px;
}
.sim-real-replay-controls { margin-bottom: 0.5rem; }
.sim-real-replay-controls .plan-hint { margin: 0; font-size: 0.68rem; }
.sim-flood-reach-toggle {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0.35rem 0;
  font-size: 0.7rem;
  color: var(--text-muted);
  cursor: pointer;
}
.sim-flood-reach-toggle input[type="checkbox"] { accent-color: var(--accent); }
.sim-map-open-analysis {
  display: block;
  width: 100%;
  margin-top: 0.5rem;
  background: var(--surface1, #0a1929);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.3rem 0.4rem;
  font-size: 0.72rem;
  cursor: pointer;
}
.sim-map-open-analysis:hover { border-color: var(--accent); }
.map-control-header-static {
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--text-muted);
  margin-bottom: 0.3rem;
}
.sim-legend-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.1rem 0;
  white-space: nowrap;
}
.sim-legend-swatch {
  display: inline-block;
  width: 18px;
  height: 3px;
  border-radius: 2px;
  flex-shrink: 0;
}
.sim-legend-swatch.sim-legend-dashed {
  background: none;
  border-top: 3px dashed;
  height: 0;
}

.leaflet-control-layers {
  background: var(--card-bg) !important;
  color: var(--text) !important;
  border: 1px solid var(--border) !important;
}

/* Precision/Calibrated Precision coverage overlay: nearest-neighbour
   scaling instead of the browser's default smooth (bilinear) upscaling,
   so zooming in reveals the raster's real per-pixel detail as crisp
   squares rather than blurring it into a soft gradient. */
.coverage-crisp {
  image-rendering: pixelated;
  image-rendering: -moz-crisp-edges;
  image-rendering: crisp-edges;
}
.leaflet-control-layers-expanded { padding: 0.5rem 0.75rem !important; }

#error-banner {
  display: none;
  background: #7f1d1d;
  color: #fecaca;
  padding: 0.5rem 1.25rem;
  font-size: 0.85rem;
}

#progress-banner {
  background: var(--card-bg);
  border-bottom: 1px solid var(--border);
  padding: 0.5rem 1.25rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}
#progress-banner.hidden { display: none; }
#progress-text {
  font-size: 0.8rem;
  color: var(--text-muted);
  white-space: nowrap;
  flex-shrink: 0;
}
.progress-track {
  flex: 1;
  height: 8px;
  border-radius: 4px;
  background: rgba(255,255,255,0.08);
  overflow: hidden;
}
#progress-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #f97316, #22c55e);
  transition: width 0.4s ease;
}

/* --- planning tools --- */

#map-tools {
  position: absolute;
  /* Sits above the replay transport bar when one is showing (see
     --transport-clearance, measured in app.js) and drops back to the map's
     own bottom edge when there isn't one. */
  bottom: calc(0.75rem + var(--transport-clearance, 0px));
  left: 0.75rem;
  /* An absolutely-positioned element that overflows its own width doesn't
     just clip visually — it enlarges its containing block's SCROLLABLE
     area, which on a narrow phone forced the whole page into a wider
     layout viewport than the device actually has (~515px computed from a
     412px screen, confirmed live), and the browser then zoomed the entire
     page out to fit — not a rendering quirk, an actual horizontal-scroll
     bug affecting every control on the page, not just this one. `right`
     caps the box at the viewport edge same as `left` already did, and
     flex-wrap lets a fourth button drop to a second row instead of
     pushing the box past that cap. --map-tools-clearance (set in app.js)
     already measures the row's real rendered height via ResizeObserver, so
     a wrapped two-row layout pushes bottom-docked controls up correctly
     without any change there. */
  right: 0.75rem;
  flex-wrap: wrap;
  /* Leaflet's own control corners sit at z-index:1000, and the layers
     control (top-right, expanded) can overlap the panel's top edge since
     the panel spans the full right side — this must stay above that. */
  z-index: 1100;
  display: flex;
  gap: 0.5rem;
}
#map-tools button {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 0.5rem 0.9rem;
  font-weight: 600;
  font-size: 0.85rem;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
/* Each button carries both a full and an abbreviated label; the desktop
   row has room for the full one, the phone tab bar doesn't (four tabs on
   a 360px screen). The mobile block at the end of this file swaps them.
   #map-options-toggle opens the phone-only Map sheet, so on desktop —
   where those controls are still docked on the map itself — it has
   nothing to open.
   The "hide this button" rule is scoped #map-tools #id (2,0,0) on
   purpose: a bare #id is 1,0,0 and would LOSE to the #map-tools button
   (1,0,1) rule that sets display on every button in the row. */
#map-tools .tool-label-short { display: none; }
#map-tools #map-options-toggle { display: none; }
#map-tools button:hover { filter: brightness(1.1); }
#companion-pin-toggle.active {
  background: #f97316;
}
#map-declutter-toggle.active {
  background: #f97316;
}

.alpha-badge {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0.1rem 0.4rem;
  font-size: 0.6rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  border-radius: 999px;
  color: #fff;
  background: linear-gradient(120deg, #a855f7, #ec4899, #f97316, #a855f7);
  background-size: 300% 100%;
  animation: alpha-badge-shimmer 3s linear infinite;
  box-shadow: 0 0 6px rgba(236,72,153,0.6);
  vertical-align: middle;
}
@keyframes alpha-badge-shimmer {
  0% { background-position: 0% 50%; }
  100% { background-position: 300% 50%; }
}
#companion-pin-hint {
  position: absolute;
  bottom: calc(3.5rem + var(--transport-clearance, 0px));
  left: 0.75rem;
  z-index: 1100;
  max-width: 280px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}

#plan-panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 320px;
  max-width: 90vw;
  background: var(--card-bg);
  border-left: 1px solid var(--border);
  z-index: 1100;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0.75rem;
  box-shadow: -4px 0 16px rgba(0,0,0,0.4);
}
#plan-panel.hidden { display: none; }

/* Docked to the right edge, same as #plan-panel — a full-viewport overlay
   was tried and reverted: it left no map visible at all while the
   simulator was open, which defeats a tool that's fundamentally about
   node positions and links. Noticeably wider than #plan-panel's 320px
   (dozens of loaded repeaters, a full results log, and a per-repeater
   prediction breakdown need more room than that), while still leaving the
   majority of a normal-width screen as visible map. */
#sim-panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 480px;
  max-width: 92vw;
  background: var(--card-bg);
  border-left: 1px solid var(--border);
  z-index: 1100;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0.75rem;
  box-shadow: -4px 0 16px rgba(0,0,0,0.4);
}
#sim-panel.hidden { display: none; }

#sim-predict-progress {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
  font-size: 0.72rem;
  color: var(--text-muted);
}
#sim-predict-progress.hidden { display: none; }
#sim-predict-progress-text { white-space: nowrap; flex-shrink: 0; }
#sim-predict-progress .progress-track { flex: 1; }
#sim-predict-progress-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #f97316, #22c55e);
  transition: width 0.2s ease;
}
/* Item 15b's own progress bar — same shape as #sim-predict-progress above. */
#sim-stress-progress {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
  font-size: 0.72rem;
  color: var(--text-muted);
}
#sim-stress-progress.hidden { display: none; }
#sim-stress-progress-text { white-space: nowrap; flex-shrink: 0; }
#sim-stress-progress .progress-track { flex: 1; }
#sim-stress-progress-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, #f97316, #22c55e);
  transition: width 0.2s ease;
}
/* Item 15c's policy-search results section within #sim-predictions-modal —
   hidden until a policy search has actually run, so the existing
   single-rule "Predict settings" output isn't cluttered for anyone not
   using the newer search. */
#sim-policy-section.hidden { display: none; }
/* Phase 4 work item 4 — adaptive optimizer. Same "hidden until it has
   actually produced something" convention as #sim-policy-section above. */
#sim-optimize-section.hidden { display: none; }
#sim-optimize-progress.hidden { display: none; }
#sim-optimize-holdout-note.hidden { display: none; }
#sim-optimize-cancel.hidden { display: none; }
#sim-optimize-holdout-note.sim-holdout-warning { color: #f97316; }
#sim-optimize-node-detail.hidden { display: none; }
.sim-optimize-node-row { cursor: pointer; }
.sim-optimize-node-row:hover { background: rgba(255,255,255,0.08); }
/* An adjusted repeater is marked with a text glyph in its own cell, not a
   row colour — same word-labels-not-colour reasoning as the policy
   profile breakdown above. */
.sim-optimize-round-kept { color: #4ade80; }
/* Phase 8 — a before/after problem count that got WORSE after a change. */
.sim-episode-worse { color: #f87171; }
#sim-episode-modal { width: min(920px, 100%); }
/* Phase 6 work item H — an accepted flood.max reduction always carries a
   reachability warning (see internal/meshsim.OptimizeDeviation.Warning's
   own doc comment on why: the delivery gate can't see topology the
   simulator never knew about). Deliberately colour AND a ⚠ glyph/prefix
   already in the text — this is a real risk flag, not a cosmetic label,
   so it gets more than the plain word-labels convention above. */
.sim-optimize-deviation-warning { color: #f97316; }

/* The four toolbar buttons that open a results modal (Results, Predicted
   settings, Bottleneck analysis, Rankings) start with class="hidden" and
   only lose it once there's real data to open (see
   renderResults/renderSuggestions/renderBottleneckAnalysis/renderRankings)
   — this is what makes that actually take effect. Without a matching
   rule, class="hidden" alone does nothing and every one of them would sit
   there clickable-but-empty from the moment Simulate mode opens, the same
   bug this project already hit once with the old docked sections these
   buttons replaced. */
#sim-open-results-modal.hidden,
#sim-open-predictions-modal.hidden,
#sim-open-bottleneck-modal.hidden,
#sim-rankings-expand.hidden,
/* These two were never added when the optimizer and episode-analysis
   modals were introduced — found while reviewing the redesigned panel:
   both sat visible-but-empty from the moment Simulate mode opened,
   exactly the bug this comment already warns about. Pre-existing, not
   introduced by the panel restructure (confirmed against git history). */
#sim-open-optimize-modal.hidden,
#sim-open-episode-modal.hidden {
  display: none;
}

.sim-workspace {
  display: flex;
  flex-direction: column;
}

.plan-panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 700;
  margin-bottom: 0.75rem;
}
.plan-panel-header button {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 1.2rem;
  cursor: pointer;
  line-height: 1;
}
.panel-header-actions { display: flex; align-items: center; gap: 0.15rem; }
/* Minimising is a bottom-sheet idea — on desktop the panel is a full-height
   sidebar with nothing to shrink towards. */
.panel-minimise { display: none; }

.plan-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}
.plan-row select, .plan-row input[type="text"] {
  flex: 1;
  min-width: 0;
}

.neighbor-window-control {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.65rem;
  margin-top: 0.5rem;
  font-size: 0.72rem;
  box-shadow: 0 2px 8px rgba(0,0,0,0.4);
  /* Vertical stack (header row, then body) now it goes through the shared
     collapsible pattern — was a single flex row when this was the one
     floating control with no collapse mechanism of its own. */
  display: flex;
  flex-direction: column;
}
/* :not(.hidden), not a bare .map-control-body selector — that would tie
   .map-control-body.hidden's own specificity exactly and win on source
   order alone, silently cancelling the collapse (found live: classList
   correctly had "hidden", computed display was "flex" anyway). This
   selector is more specific than the hidden one and simply doesn't match
   a hidden element at all, so there's nothing left to fight it. */
.neighbor-window-control .map-control-body:not(.hidden) {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}
.neighbor-window-control label {
  color: var(--text-muted);
  flex: 1 1 auto;
}
.neighbor-window-control select {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.2rem 0.4rem;
  font-size: 0.72rem;
}

#plan-panel button, #plan-panel select, #plan-panel input[type="text"],
#sim-panel button, #sim-panel select, #sim-panel input[type="text"], #sim-panel input[type="number"] {
  background: var(--surface1, #0a1929);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.35rem 0.5rem;
  font-size: 0.78rem;
  cursor: pointer;
}
#plan-panel input[type="text"], #sim-panel input[type="text"], #sim-panel input[type="number"] { cursor: text; }
#plan-panel button:hover, #sim-panel button:hover { border-color: var(--accent); }
#sim-panel button:disabled { opacity: 0.5; cursor: not-allowed; }

.plan-mode-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin: 0.75rem 0;
}
.plan-mode-btn { flex: 1 1 30%; }
.plan-mode-btn.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.plan-full-width-btn {
  width: 100%;
  margin-top: 0.5rem;
}
.plan-full-width-btn.active {
  background: #a855f7;
  border-color: #a855f7;
  color: #fff;
}

/* The one action that gets full-weight primary treatment on Plan's own
   file-operations row — see PLAN-05 in the UX review. Export/Import/KML
   sit underneath, deliberately quieter (.plan-secondary-row), since
   they're aimed at round-tripping through another tool rather than at
   the everyday "send this to someone" case. */
.plan-promoted-btn {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
  padding: 0.55rem 0.6rem;
}
.plan-promoted-btn:hover { filter: brightness(1.08); border-color: var(--accent); }
.plan-secondary-row button {
  background: transparent;
  color: var(--text-muted);
  font-size: 0.72rem;
  padding: 0.3rem 0.45rem;
  border-color: transparent;
}
.plan-secondary-row button:hover { color: var(--text); border-color: var(--border); }

.plan-checkbox-row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.5rem;
  font-size: 0.78rem;
  color: var(--text);
  cursor: pointer;
}
.plan-checkbox-row input[type="checkbox"] {
  width: auto;
  cursor: pointer;
}
/* Nothing to preview until at least one repeater is planned — greyed
   rather than silently inert when clicked. */
.plan-checkbox-row-disabled {
  opacity: 0.5;
  cursor: default;
}
.plan-checkbox-row-disabled input[type="checkbox"] { cursor: default; }

.plan-hint {
  font-size: 0.72rem;
  color: var(--text-muted);
  background: rgba(255,255,255,0.05);
  border-radius: 6px;
  padding: 0.4rem 0.5rem;
  margin-bottom: 0.6rem;
}
.plan-hint.hidden { display: none; }

/* --- Simulate panel: workflow rail, Basic/Advanced tiering, accordions -
   The panel used to be one long scroll through ~9 workflows at equal
   visual weight. This groups them into named, collapsible sections; a
   rail above tracks real progress through the four that make a run
   possible; a tier toggle defers the rest (policy search, the optimizer,
   stress test) until asked for. Nothing here removes a control or
   changes what it does — see simulator.js's updateWorkflowState/setSimTier. */

.sim-acc {
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-bottom: 0.5rem;
  overflow: hidden;
  background: rgba(255,255,255,0.02);
}
.sim-acc-head {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  background: none;
  border: none;
  text-align: left;
  font: inherit;
  color: var(--text);
  padding: 0.55rem 0.7rem;
  cursor: pointer;
}
/* Measured live at ~32px — under the ~44px touch-target guideline.
   Desktop stays as-is. */
@media (max-width: 700px) {
  .sim-acc-head { min-height: 44px; }
}
.sim-acc-title { flex: 1; font-size: 0.85rem; font-weight: 600; }
.sim-acc-head:hover .sim-acc-title { color: var(--accent); }
.sim-acc-badge { font-size: 0.68rem; color: var(--text-muted); white-space: nowrap; }
.sim-acc-chevron { font-size: 0.65rem; color: var(--text-muted); transition: transform 0.15s; flex-shrink: 0; }
.sim-acc.open .sim-acc-chevron { transform: rotate(90deg); }
.sim-acc-body { padding: 0 0.7rem 0.65rem; display: none; }
.sim-acc.open .sim-acc-body { display: block; }
/* An accordion body's own contents (plan-row/plan-field-row/buttons)
   inherit the existing #sim-panel-scoped rules further up this file —
   nothing about being inside .sim-acc-body changes their own styling. */

.sim-rail {
  display: flex;
  align-items: center;
  background: rgba(255,255,255,0.03);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.6rem;
  margin: 0.75rem 0;
  overflow-x: auto;
}
.sim-rail-step {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  background: none;
  border: none;
  font: inherit;
  font-size: 0.68rem;
  color: var(--text-muted);
  white-space: nowrap;
  cursor: pointer;
  padding: 0.1rem 0.15rem;
}
/* Measured live at ~30px — under the ~44px touch-target guideline.
   Desktop stays as-is. */
@media (max-width: 700px) {
  .sim-rail-step { min-height: 44px; }
}
.sim-rail-step:hover { color: var(--text); }
.sim-rail-dot { width: 9px; height: 9px; border-radius: 50%; border: 1.5px solid var(--text-muted); flex-shrink: 0; }
.sim-rail-step.done { color: var(--status-active); }
.sim-rail-step.done .sim-rail-dot { background: var(--status-active); border-color: var(--status-active); }
.sim-rail-step.now { color: var(--text); font-weight: 700; }
.sim-rail-step.now .sim-rail-dot { background: var(--accent); border-color: var(--accent); }
.sim-rail-link { width: 0.9rem; height: 1px; background: var(--border); margin: 0 0.3rem; flex-shrink: 0; }

.sim-tier-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 0 0 0.75rem;
}
.sim-tier-label { font-size: 0.72rem; color: var(--text-muted); }
.sim-tier-seg {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.15rem;
  background: rgba(255,255,255,0.03);
}
.sim-tier-seg button {
  background: none;
  border: none;
  font: inherit;
  color: var(--text-muted);
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.25rem 0.7rem;
  border-radius: 999px;
  cursor: pointer;
}
.sim-tier-seg button.on { background: var(--accent); color: #fff; }

/* Hidden, not removed — see setSimTier in simulator.js. */
.sim-advanced-only { display: none; }
.sim-workspace.tier-advanced .sim-advanced-only { display: block; }

/* The tool's own strongest credibility signal (replaying its model
   against real production traffic) gets a visually distinct treatment at
   every tier, rather than a plain section heading at the very bottom of
   the panel — cyan, the same colour the replay's own "observed" traffic
   already uses on the map, not a new accent. */
.sim-promoted {
  border: 1px solid #22d3ee;
  background: linear-gradient(180deg, rgba(34,211,238,0.08), rgba(34,211,238,0) 60%);
  border-radius: 8px;
  padding: 0.75rem 0.8rem;
  margin: 1rem 0;
}
.sim-promoted-eyebrow {
  font-size: 0.62rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #22d3ee;
  font-weight: 700;
  margin-bottom: 0.3rem;
}
.sim-promoted-title { margin: 0 0 0.35rem; font-size: 0.95rem; color: var(--text); }
.sim-promoted-body { margin: 0 0 0.6rem; font-size: 0.76rem; color: var(--text-muted); line-height: 1.5; }
/* Default link blue barely read against this card's own dark background —
   white/bold/underlined stands out the way an inline link inside a muted
   paragraph needs to, without competing with the cyan eyebrow/border. */
.sim-promoted-body a {
  color: var(--text);
  font-weight: 700;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.sim-promoted-body a:hover { color: #22d3ee; }

/* Phase 6 Tier 2/3 — the "Advanced (experimental)" optimizer toggles are a
   plain native <details>, deliberately: it's a genuinely optional, rarely-
   needed section (the plan's own advice is to try at most one of these at
   a time), and a native disclosure widget needs no JS open/close wiring. */
#sim-optimize-advanced {
  margin: 0.5rem 0;
  font-size: 0.78rem;
}
#sim-optimize-advanced summary {
  cursor: pointer;
  color: var(--text-muted);
  padding: 0.3rem 0;
}
#sim-optimize-advanced[open] summary {
  color: var(--text);
}

#companion-pin-height-label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.35rem;
}
#companion-pin-height {
  width: 4rem;
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.1rem 0.3rem;
  font-size: 0.72rem;
}

.plan-section-title {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 0.6rem 0 0.3rem;
}

.sim-subsection-title {
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--text);
  margin: 0.6rem 0 0.15rem;
}

.plan-list { display: flex; flex-direction: column; gap: 0.3rem; }
.plan-empty { font-size: 0.75rem; color: var(--text-muted); font-style: italic; }

.plan-list-item {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: rgba(255,255,255,0.04);
  border-radius: 6px;
  padding: 0.3rem 0.5rem;
  font-size: 0.75rem;
}
.plan-item-label { font-weight: 600; }
.plan-item-sub { color: var(--text-muted); font-size: 0.68rem; flex: 1; }

/* Shown instead of rendering every row of a long list up front (item 10E —
   a dense run can produce thousands of receptions) — see
   LONG_LIST_ROW_CAP/appendShowAllButton in simulator.js. */
.sim-show-all-btn {
  align-self: center;
  margin-top: 0.3rem;
  font-size: 0.72rem;
  padding: 0.3rem 0.8rem;
}

/* item 10C — discrete labelled figures replacing a run-on summary sentence
   (see renderStatStrip in simulator.js), so the number that matters reads
   at a glance instead of competing with the rest of the sentence. */
.sim-stat-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.6rem;
}
.sim-stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  background: rgba(255,255,255,0.04);
  border-radius: 6px;
  padding: 0.3rem 0.6rem;
  min-width: 4.5rem;
}
.sim-stat-value { font-size: 1.05rem; font-weight: 700; line-height: 1.1; }
.sim-stat-label { font-size: 0.65rem; color: var(--text-muted); }
.sim-stat.sim-stat-bad .sim-stat-value { color: #f87171; }
.sim-stat.sim-stat-bad { background: rgba(248,113,113,0.12); }
.plan-item-actions { display: flex; gap: 0.2rem; }
.plan-item-actions button {
  padding: 0.1rem 0.35rem;
  font-size: 0.7rem;
}

.plan-field-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.75rem;
  margin-bottom: 0.5rem;
}
.plan-field-row input[type="number"] {
  width: 4rem;
  padding: 0.2rem 0.35rem;
  font-size: 0.75rem;
}

.plan-connect-option {
  cursor: pointer;
  border: 1px solid transparent;
}
.plan-connect-option:hover {
  background: rgba(255,255,255,0.08);
}
.plan-connect-option.selected {
  border-color: #4ade80;
  background: rgba(74,222,128,0.12);
}
.plan-connect-option .plan-item-actions button {
  background: #4ade80;
  border-color: #4ade80;
  color: #0b1220;
  font-weight: 600;
}

.plan-hop-result {
  font-size: 0.7rem;
  color: var(--text-muted);
  padding: 0.15rem 0.6rem;
}

#plan-share-result {
  font-size: 0.72rem;
  background: rgba(255,255,255,0.05);
  border-radius: 6px;
  padding: 0.4rem 0.5rem;
  margin-bottom: 0.6rem;
  word-break: break-all;
}
#plan-share-result a { color: var(--accent); }
#plan-share-result.hidden { display: none; }

.planned-marker-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #a855f7;
  border: 2px solid #fff;
  box-shadow: 0 0 4px rgba(0,0,0,0.6);
}

.companion-pin-dot {
  width: 18px;
  height: 18px;
  border-radius: 50% 50% 50% 0;
  transform: rotate(-45deg);
  background: #f97316;
  border: 2px solid #fff;
  box-shadow: 0 0 4px rgba(0,0,0,0.6);
}

/* A personal, local-only repositioning of a real repeater — distinct from
   both the purple "new planned site" dot and the orange companion pin, so
   all three read as different things at a glance. Never replaces the real
   repeater's own marker; see the dashed connector below. */
.override-marker-dot {
  width: 14px;
  height: 14px;
  border-radius: 3px;
  background: #f59e0b;
  border: 2px solid #fff;
  box-shadow: 0 0 4px rgba(0,0,0,0.6);
}

/* --- neighbours --- */

.plan-item-neighbors {
  cursor: default;
}

.leaflet-tooltip.neighbor-tooltip-wrap {
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
}
.leaflet-tooltip.neighbor-tooltip-wrap::before {
  display: none;
}

.neighbor-tooltip {
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.65rem;
  font-size: 0.72rem;
  max-width: 240px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
.neighbor-tooltip-title { font-weight: 700; margin-bottom: 0.1rem; }
.neighbor-tooltip-sub { color: var(--text-muted); font-size: 0.68rem; margin-bottom: 0.35rem; }
.neighbor-tooltip ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  max-height: 180px;
  overflow-y: auto;
}
.neighbor-tooltip li {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nb-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.nb-dot.nb-strong { background: #4ade80; }
.nb-dot.nb-marginal { background: #facc15; }
.nb-dot.nb-weak { background: #f87171; }

/* --- LoRa flood simulator (sim-panel / simulator.js) ------------------ */

/* These lists can grow large (loading real repeaters alone can mean
   dozens of rows) — each still gets its own capped scroll region so one
   long list can't push every section after it out of view within its
   column, but the cap itself is generous now that setup/results each have
   a full workspace column (not a 320px sidebar) to work with. */
#sim-node-list, #sim-message-list, #sim-results-log, #sim-per-node-list, #sim-suggestions-list, #sim-bottleneck-list, #sim-unmodeled-list {
  max-height: 320px;
  overflow-y: auto;
}

.sim-node-badge {
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 0.1rem 0.35rem;
  border-radius: 4px;
  flex-shrink: 0;
}
.sim-node-badge.sim-badge-planned { background: rgba(74,222,128,0.18); color: #4ade80; }
.sim-node-badge.sim-badge-real { background: rgba(56,189,248,0.18); color: #38bdf8; }
.sim-node-badge.sim-badge-companion { background: rgba(251,146,60,0.18); color: #fb923c; }
.sim-node-badge.sim-badge-region { background: rgba(168,85,247,0.18); color: #c084fc; }
/* Marks a row as a real CoreScope measurement rather than a model output —
   cyan, matching the observed-traffic line colour on the map. */
.sim-node-badge.sim-badge-observed { background: rgba(34,211,238,0.18); color: #22d3ee; }
.sim-node-badge.sim-badge-direct { background: rgba(250,204,21,0.18); color: #facc15; }
/* Item 17 — a sender's own configured path-hash size (1-3 bytes). */
.sim-node-badge.sim-badge-hashsize { background: rgba(148,163,184,0.18); color: #94a3b8; }
/* Phase 8 — a fixed background transmission of reconstructed real traffic. */
.sim-node-badge.sim-badge-background { background: rgba(100,116,139,0.22); color: #94a3b8; }

#sim-message-cancel-edit.hidden { display: none; }
#sim-setup-import-file.hidden { display: none; }
#sim-packet-modal-back.hidden { display: none; }
#sim-packet-modal-back {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 0.72rem;
  cursor: pointer;
  padding: 0 0.5rem 0 0;
  margin-right: 0.3rem;
}
#sim-packet-modal-back:hover { color: var(--text); }

#sim-packet-modal-sent-section.hidden { display: none; }
#sim-packet-modal-checklist-section.hidden { display: none; }
#sim-bottleneck-replay-section.hidden { display: none; }

.sim-checklist-row { cursor: pointer; }
.sim-checklist-row:hover { background: rgba(255,255,255,0.08); }
.sim-checklist-row.sim-checklist-yes { border-left: 3px solid #4ade80; }
.sim-checklist-row.sim-checklist-no { border-left: 3px solid #f87171; }
.sim-checklist-row.sim-checklist-origin { border-left: 3px solid #a855f7; }
.sim-checklist-status {
  margin-left: auto;
  font-size: 0.68rem;
  font-weight: 700;
  white-space: nowrap;
  cursor: help;
}

.sim-message-row { cursor: pointer; }
.sim-message-row:hover { background: rgba(255,255,255,0.08); }
.sim-message-row-selected {
  background: rgba(74,222,128,0.12) !important;
  border-left: 3px solid #4ade80;
}

/* Phase 4 work item 6 — policy profile breakdown. Deliberately no colour
   coding for profile identity (word labels only, see
   renderPolicyProfileSummary's own doc comment): the community guides
   this feature is built from DO assign a colour per profile, but a colour
   needs a legend to decode and doesn't survive being read aloud or pasted
   into a message. */
.sim-policy-profile-row { cursor: pointer; }
.sim-policy-profile-row:hover { background: rgba(255,255,255,0.08); }
.sim-policy-profile-chevron { margin-left: auto; opacity: 0.6; }
.sim-policy-profile-detail-approx {
  font-style: italic;
  color: var(--text-muted);
}
/* No global `.hidden { display: none; }` rule exists in this file — every
   hideable element needs its own `#id.hidden` rule (a bug this project
   has already hit once for exactly this reason). */
#sim-policy-profile-detail.hidden { display: none; }
#sim-policy-source-note.hidden { display: none; }

.sim-message-details-btn {
  margin-left: 0.5rem;
  font-size: 0.62rem;
  padding: 0.05rem 0.35rem;
}

.sim-packet-path {
  font-weight: 400;
  font-size: 0.65rem;
  color: var(--text-muted);
}

#sim-add-companion.active {
  background: #fb923c;
  border-color: #fb923c;
  color: #1c1206;
}

.sim-list-item.sim-collided { border-left: 3px solid #f87171; }
.sim-list-item.sim-clean { border-left: 3px solid #4ade80; }
.sim-list-item.sim-dropped { border-left: 3px solid #facc15; }

/* Packet inspector rows (see renderPacketReceptionRows) — a wider modal
   than the shared 560px default (see #sim-nodes-modal above for the same
   pattern) so the path, time/hop, and reason columns have room without
   wrapping into each other. */
#sim-packet-modal { width: min(920px, 100%); }

.sim-packet-row { flex-direction: column; align-items: stretch; gap: 0.3rem; }
.sim-packet-row-top { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
.sim-packet-row-bottom {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  font-size: 0.68rem;
  color: var(--text-muted);
}
/* .sim-packet-context deliberately wraps (unlike time/hop, always short) —
   item 12's relay-link suffix ("⤴ relaying what arrived at 1,240ms") can
   push it past a single line at narrower widths. */
.sim-packet-context { white-space: normal; }
.sim-packet-time, .sim-packet-hop { white-space: nowrap; }

.sim-packet-reason {
  margin-left: auto;
  font-size: 0.65rem;
  font-weight: 700;
  padding: 0.1rem 0.45rem;
  border-radius: 4px;
  white-space: nowrap;
  cursor: help;
}
.sim-packet-reason.sim-reason-relayed { background: rgba(74,222,128,0.18); color: #4ade80; }
.sim-packet-reason.sim-reason-received { background: rgba(56,189,248,0.18); color: #38bdf8; }
.sim-packet-reason.sim-reason-dropped { background: rgba(250,204,21,0.18); color: #facc15; }
.sim-packet-reason.sim-reason-collided { background: rgba(248,113,113,0.18); color: #f87171; }
.sim-packet-reason.sim-reason-captured { background: rgba(167,139,250,0.18); color: #a78bfa; }
/* tx_busy (item 13) — a miss, not a collision (nothing was ever heard) and
   not an active drop decision — deliberately distinct from both the red
   sim-reason-collided and the yellow sim-reason-dropped. */
.sim-packet-reason.sim-reason-missed { background: rgba(148,163,184,0.20); color: #94a3b8; }

.sim-txrx-badge {
  font-size: 0.6rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  padding: 0.1rem 0.3rem;
  border-radius: 4px;
  flex-shrink: 0;
}
.sim-txrx-badge.sim-txrx-tx { background: rgba(168,85,247,0.22); color: #c084fc; }
.sim-txrx-badge.sim-txrx-rx { background: rgba(56,189,248,0.18); color: #38bdf8; }
/* Distinct from plain TX (the origin's own first send) — a repeater's own
   re-transmission of someone else's packet (item 12). */
.sim-txrx-badge.sim-txrx-relay { background: rgba(74,222,128,0.20); color: #4ade80; }

/* item 12 — linking an RX row to the relay TX it caused (or vice versa).
   sim-jump-link is the small clickable "⤴/⤵ ..." text; sim-row-linked is
   the hover state applied to BOTH halves of a pair via their shared
   data-link-key, so hovering either highlights the other. */
.sim-packet-relay-link { color: var(--text-muted); font-size: 0.65rem; }
.sim-jump-link { cursor: pointer; text-decoration: underline dotted; }
.sim-jump-link:hover { color: #4ade80; }
.sim-linkable-row { cursor: default; }
.sim-list-item.sim-row-linked { background: rgba(74,222,128,0.10); }
/* Reuses the same flash-on-jump treatment #sim-nodes-modal's own row
   highlight uses (see openNodesModal), scoped here to the plain-div
   activity rows rather than table rows. */
.sim-list-item.sim-row-highlight {
  background: rgba(74,222,128,0.18);
  transition: background 1.5s ease;
}

.sim-suggestion-rank {
  font-weight: 700;
  color: var(--accent);
  flex-shrink: 0;
  width: 1.4rem;
}
.sim-suggestion-rate { color: var(--text-muted); font-size: 0.68rem; }
.sim-suggestion-rate.sim-rate-better { color: #4ade80; }

.sim-marker-icon {
  background: #a855f7;
  border: 2px solid #fff;
  border-radius: 50%;
  width: 12px;
  height: 12px;
  box-shadow: 0 0 0 2px rgba(168,85,247,0.4);
}

/* Companion locations are draggable, user-placed points, distinct from
   fixed repeater sites — a diamond (rotated square) instead of a dot
   reads as "a portable device" at a glance, plus its own colour. */
.sim-marker-companion {
  background: #fb923c;
  border: 2px solid #fff;
  width: 11px;
  height: 11px;
  transform: rotate(45deg);
  box-shadow: 0 0 0 2px rgba(251,146,60,0.45);
  cursor: grab;
}

/* --- repeater rankings table (docked + full-window "pop out") --------- */

.sim-rankings-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.75rem;
}
.sim-rankings-table th, .sim-rankings-table td {
  text-align: right;
  padding: 0.3rem 0.5rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.sim-rankings-table th:first-child, .sim-rankings-table td:first-child {
  text-align: left;
}
.sim-rankings-table th {
  color: var(--text-muted);
  font-weight: 600;
  cursor: pointer;
  user-select: none;
  position: sticky;
  top: 0;
  background: var(--bg);
}
.sim-rankings-table th.sim-rank-sorted { color: var(--accent); }
.sim-rankings-table tbody tr:hover { background: rgba(255,255,255,0.05); }
.sim-rankings-table td.sim-rank-good { color: #4ade80; }
.sim-rankings-table td.sim-rank-bad { color: #f87171; }

#sim-rankings-section .plan-section-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
#sim-rankings-expand {
  font-size: 0.68rem;
  padding: 0.15rem 0.4rem;
}
#sim-rankings-table-container {
  max-height: 260px;
  overflow-y: auto;
}

#sim-rankings-fullwindow {
  position: fixed;
  inset: 0;
  z-index: 5000;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  padding: 1rem 1.5rem;
}
#sim-rankings-fullwindow.hidden { display: none; }
.sim-rankings-fullwindow-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 700;
  font-size: 1rem;
  margin-bottom: 0.75rem;
}
#sim-rankings-fullwindow-body {
  flex: 1;
  overflow-y: auto;
}
#sim-rankings-fullwindow-body .sim-rankings-table { font-size: 0.85rem; }
#sim-rankings-fullwindow-body .sim-rankings-table th, #sim-rankings-fullwindow-body .sim-rankings-table td {
  padding: 0.5rem 0.75rem;
}

/* --- sim modal system ---------------------------------------------------
   Every heavier chunk of simulator output (repeater config, message
   senders, results, bottleneck analysis, predicted settings) lives in one
   of these instead of a permanently-docked panel section — keeps the side
   panel itself a short, fixed list of controls. Centered dialogs, not a
   full-viewport takeover (that's reserved for the rankings "Expand" view,
   which explicitly wants the whole window — see #sim-rankings-fullwindow
   above). One backdrop, several possible .sim-modal children; openModal
   hides every other one first, so only one is ever visible at a time. */

#sim-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 4000;
  background: rgba(0,0,0,0.55);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}
#sim-modal-backdrop.hidden { display: none; }

.sim-modal {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  width: min(560px, 100%);
  max-height: 85vh;
  display: flex;
  flex-direction: column;
}
.sim-modal.hidden { display: none; }

/* Repeaters & settings has a wide table (label + 11 setting columns +
   actions) that doesn't fit the shared 560px width — give it more room
   specifically, rather than widening every modal. The table itself still
   scrolls horizontally within sim-config-table-scroll below for whatever
   doesn't fit even at this width. */
#sim-nodes-modal { width: min(1040px, 100%); }
/* Same reasoning as #sim-nodes-modal above — the adaptive optimizer's own
   modal carries two full-width tables (every repeater, and every round),
   which is exactly why it's a separate modal rather than another section
   stacked inside #sim-predictions-modal. */
#sim-optimize-modal { width: min(1040px, 100%); }

.sim-modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-weight: 700;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
}
.sim-modal-header button {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 1rem;
  cursor: pointer;
}
.sim-modal-body {
  padding: 0.75rem 1rem;
  overflow-y: auto;
}
.sim-modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 0.4rem;
  padding: 0.6rem 1rem;
  border-top: 1px solid var(--border);
}

/* --- "Repeaters & settings" table (inside #sim-nodes-modal) ------------ */

.sim-bulk-apply-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin-bottom: 0.6rem;
  padding: 0.4rem 0.5rem;
  background: rgba(255,255,255,0.04);
  border-radius: 6px;
}
.sim-bulk-apply-label { font-size: 0.7rem; color: var(--text-muted); flex-shrink: 0; }
.sim-bulk-apply-row input[type="number"] { width: 4.5rem; }
.sim-bulk-apply-row input, .sim-bulk-apply-row select {
  font-size: 0.72rem;
  padding: 0.2rem 0.3rem;
}
.sim-bulk-apply-row button { font-size: 0.72rem; padding: 0.25rem 0.5rem; }

.sim-config-table-scroll { max-height: 50vh; overflow-y: auto; overflow-x: auto; }

.sim-config-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.75rem;
}
.sim-config-table th, .sim-config-table td {
  text-align: left;
  padding: 0.3rem 0.4rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.sim-config-table th { color: var(--text-muted); font-weight: 600; }
/* Sticky header + sticky first column (item 10G) — a long/wide node table
   otherwise scrolls the node's own label out of view in both directions at
   once, the one column you actually need to keep visible while scanning. */
.sim-config-table thead th { position: sticky; top: 0; background: var(--bg); z-index: 1; }
.sim-config-table th.sim-col-sticky, .sim-config-table td.sim-col-sticky {
  position: sticky;
  left: 0;
  background: var(--bg);
  z-index: 2;
  box-shadow: 1px 0 0 var(--border);
}
.sim-config-table thead th.sim-col-sticky { z-index: 3; }
/* Item 16 — Duty cycle/Received result columns, shown only once a run
   exists (see renderNodesModalTable's own hidden-class toggle). */
.sim-config-table .sim-results-col.hidden { display: none; }
.sim-config-table td.sim-results-col { color: var(--text-muted); white-space: nowrap; }
.sim-config-table input[type="number"], .sim-config-table input[type="text"] {
  width: 3.6rem;
  font-size: 0.72rem;
  padding: 0.15rem 0.25rem;
}
.sim-config-table input[type="text"][data-field="regions"] { width: 6.5rem; }
.sim-config-table select {
  font-size: 0.72rem;
  padding: 0.15rem 0.25rem;
  background: var(--card-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
}
.sim-config-table td.sim-checkbox-cell { text-align: center; }
.sim-radio-cell { min-width: 11rem; }
.sim-radio-preset-select { display: block; width: 100%; margin-bottom: 0.2rem; }
.sim-radio-fields { display: flex; gap: 0.2rem; }
.sim-radio-fields input { width: 2.5rem !important; }
.sim-config-table tr.sim-row-highlight {
  background: rgba(74,222,128,0.18);
  transition: background 1.5s ease;
}
/* Item 15b — the knee row in the stress-test capacity curve table, a
   persistent marker rather than sim-row-highlight's fade-out flash. */
.sim-config-table tr.sim-knee-row { background: rgba(74,222,128,0.14); font-weight: 700; }

/* ======================================================================
   Leaflet's own controls, themed
   ----------------------------------------------------------------------
   Leaflet ships these white, which reads as three bright rectangles
   pasted onto a dark map — most obviously the attribution bar, which is
   full-width and the brightest thing on the page. Leaflet's own rules are
   `.leaflet-container .leaflet-control-*` (specificity 0,2,0), so these
   have to be scoped the same way to win rather than relying on load
   order alone.
   ====================================================================== */
.leaflet-container .leaflet-control-attribution {
  background: rgba(13, 33, 55, 0.85);
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-right: none;
  border-bottom: none;
  border-top-left-radius: 8px;
  padding: 2px 8px;
  font-size: 0.65rem;
  backdrop-filter: blur(6px);
}
.leaflet-container .leaflet-control-attribution a { color: var(--text-muted); }
.leaflet-container .leaflet-control-attribution a:hover { color: var(--accent); }

@media (min-width: 701px) {
  /* The credits belong on the page's bottom edge, not floating partway up
     the map. Leaflet keeps them in the bottom-right control corner, and
     that whole corner is lifted by --map-tools-clearance so the coverage
     legend clears the floating #map-tools cluster — which lifted the
     credits along with it. Pinning them to the viewport corner decouples
     the two: the legend still sits above the buttons, the credits sit at
     the bottom. (The phone layout folds them into the tab bar instead —
     see the mobile section.) */
  .leaflet-container .leaflet-control-attribution {
    position: fixed;
    bottom: 0;
    right: 0;
    margin: 0;
    z-index: 1200;
  }

  /* #map-tools spans the full width (it needs a `right` bound so a wrapped
     row can't widen the page — see its own comment), but only the buttons
     at the left actually draw anything. The rest was still a hit target:
     an invisible full-width strip along the map's bottom edge that
     swallowed map drags and, now, sat over the credits. Only the buttons
     should take pointer events. The phone tab bar is a real opaque bar and
     deliberately keeps capturing everything, so this is desktop-only. */
  #map-tools { pointer-events: none; }
  #map-tools button { pointer-events: auto; }
}

.leaflet-container .leaflet-control-layers,
.leaflet-container .leaflet-bar {
  background: var(--card-bg);
  border: 1px solid var(--border);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}
.leaflet-container .leaflet-bar a,
.leaflet-container .leaflet-bar a:hover {
  background: var(--card-bg);
  color: var(--text);
  border-bottom-color: var(--border);
}
.leaflet-container .leaflet-bar a:hover { background: var(--nav-bg-2); }
.leaflet-container .leaflet-bar a.leaflet-disabled {
  background: var(--card-bg);
  color: var(--status-silent);
}
.leaflet-container .leaflet-control-layers-toggle {
  /* Leaflet's is a dark glyph on white; invert it for a dark tile. */
  filter: invert(1) hue-rotate(180deg);
}
.leaflet-container .leaflet-control-layers-expanded {
  color: var(--text);
  padding: 0.5rem 0.65rem;
}
.leaflet-container .leaflet-control-layers-separator { border-top-color: var(--border); }

/* Verdict from running a proposed connect-repeaters route through the
   flood simulator (see planner-worker.js's checkRoute). Colour carries the
   same three states the text does — delivered every trial, delivered
   sometimes, never got through — so a glance is enough. */
.plan-route-check {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin: 0.4rem 0 0.5rem;
  padding: 0.5rem 0.6rem;
  border-radius: 8px;
  border: 1px solid var(--border);
  border-left-width: 3px;
  background: rgba(255, 255, 255, 0.03);
  font-size: 0.72rem;
}
.plan-route-check.hidden { display: none; }
.plan-route-check.good { border-left-color: var(--status-active); }
.plan-route-check.warn { border-left-color: var(--status-degraded); }
.plan-route-check.bad { border-left-color: #f87171; }
.plan-route-check-head { font-weight: 700; color: var(--text); }
.plan-route-check.good .plan-route-check-head { color: var(--status-active); }
.plan-route-check.warn .plan-route-check-head { color: var(--status-degraded); }
.plan-route-check.bad .plan-route-check-head { color: #f87171; }
.plan-route-check-detail { color: var(--text-muted); line-height: 1.35; }

/* The phone-only furniture: a bottom sheet and the grabber that drags it.
   Both are in the markup at every width, so they need switching off
   explicitly above the breakpoint — otherwise the Map options sheet
   renders as a stray "Map options ✕" block in the middle of the desktop
   map. The mobile block below turns them back on. */
.mobile-sheet { display: none; }
.panel-grab { display: none; }

/* ======================================================================
   MOBILE (<= 700px)
   ----------------------------------------------------------------------
   Below this width the app stops being "the desktop layout, narrower" and
   switches to a phone layout: a compact header, a full-width bottom tab
   bar, and bottom SHEETS instead of right-edge sidebars. The old model
   docked #plan-panel/#sim-panel to the right at up to 92vw, which left a
   33px slice of visible map on a 412px phone — a sidebar in name only,
   and useless for a tool whose whole point is looking at the map while
   you change things. A sheet leaves the top half of the map live and
   visible, and can be dragged between two heights or flicked away.
   ====================================================================== */
@media (max-width: 700px) {
  /* --- header: 136px of title/subtitle/stats down to a single row ---- */
  header { padding: 0.4rem 0.75rem; flex-wrap: nowrap; gap: 0.5rem; }
  /* min-width:0 lets the title block actually shrink inside the flex row
     (a flex item's default min-width:auto refuses to go below its content
     and forces the wrap instead). */
  header > div:first-child { min-width: 0; }
  header h1 {
    font-size: 0.95rem;
    line-height: 1.2;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  header .subtitle { display: none; }
  #stats { gap: 0.55rem; font-size: 0.72rem; flex-wrap: nowrap; }
  #stats .stat { gap: 0.25rem; }
  /* Keep the counts (the at-a-glance value) but drop the words and the
     timestamp — the dot colours already say which is which, and the
     words are still there for screen readers. */
  #stats .stat-word {
    position: absolute;
    width: 1px; height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }
  /* Scoped #stats #id (2,0,0): a bare #last-updated is 1,0,0 and loses to
     the #stats .stat (1,1,0) rule that makes every stat a flex row. */
  #stats #last-updated { display: none; }

  /* --- bottom stack ------------------------------------------------- */
  /* Bottom to top: tab bar (pinned to the very bottom), then the replay
     transport, then any sheet, then Leaflet's own bottom controls. Each
     layer clears the one below it using heights app.js measures live, so
     nothing here assumes a fixed row height. */
  #map-tools {
    left: 0;
    right: 0;
    bottom: 0;
    gap: 0.2rem;
    padding: 0.25rem 0.3rem;
    flex-wrap: nowrap;
    background: rgba(10, 25, 41, 0.94);
    backdrop-filter: blur(10px);
    border-top: 1px solid var(--border);
    z-index: 1250;
  }
  #map-tools button {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.12rem;
    min-height: 50px;
    padding: 0.3rem 0.1rem;
    background: transparent;
    color: var(--text-muted);
    box-shadow: none;
    border-radius: 10px;
    font-size: 0.62rem;
    font-weight: 600;
  }
  #map-tools .tool-icon { font-size: 1.1rem; line-height: 1; }
  #map-tools .tool-label { display: none; }
  #map-tools .tool-label-short { display: inline; }
  #map-tools .alpha-badge { display: none; }
  /* Decluttering the map is exactly what the Map sheet does here, so the
     button has nothing left to do; the sheet toggle takes its slot.
     Both scoped #map-tools #id (2,0,0) to beat the #map-tools button
     (1,0,1) display rule above. */
  #map-tools #map-declutter-toggle { display: none; }
  #map-tools #map-options-toggle { display: flex; }
  #map-tools button.active,
  #map-wrap.plan-open #plan-toggle,
  #map-wrap.sim-open #sim-toggle {
    background: rgba(0, 119, 182, 0.3);
    color: var(--text);
  }
  #companion-pin-toggle.active { background: rgba(249, 115, 22, 0.3); color: var(--text); }

  .sim-transport { bottom: var(--tools-h, 0px); }
  #map-wrap .leaflet-bottom.leaflet-left {
    bottom: calc(var(--tools-h, 0px) + var(--transport-clearance, 0px) + 6px);
  }

  /* The credits used to float as their own strip in the gap just above the
     tab bar, which read as a second, half-height bar and cost the map a
     line of height for something nobody looks at. On a phone the only
     thing left in this corner is the attribution (the legend moved into
     the Map options sheet), so the corner becomes the bar's own bottom
     line instead: one solid piece of bottom chrome rather than two
     stacked ones. #map-tools reserves the room for it below the buttons,
     and it draws on top of the bar's background (z-index above the bar,
     transparent, no border of its own). */
  #map-wrap .leaflet-bottom.leaflet-right {
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1260;
    /* Only the credits themselves should take taps; the rest of this strip
       is the tab bar's background showing through. */
    pointer-events: none;
  }
  #map-tools { padding-bottom: 15px; }
  .leaflet-container .leaflet-control-attribution {
    pointer-events: auto;
    width: 100%;
    max-width: 100%;
    background: transparent;
    border: none;
    border-radius: 0;
    backdrop-filter: none;
    padding: 0 0.6rem 2px;
    text-align: center;
    font-size: 0.55rem;
    line-height: 1.1;
    white-space: nowrap;
    overflow-x: auto;
  }

  /* A sheet is bottom-docked and full-width, so the side-panel offsets
     that shift Leaflet's corners and the transport bar out from under a
     right-edge sidebar no longer apply — and at <=500px the old rules
     pushed them 90vw off-screen. Later in the file than those rules, so
     this wins at equal specificity. */
  #map-wrap.plan-open .leaflet-top.leaflet-right,
  #map-wrap.plan-open .leaflet-bottom.leaflet-right,
  #map-wrap.sim-open .leaflet-top.leaflet-right,
  #map-wrap.sim-open .leaflet-bottom.leaflet-right,
  #map-wrap.plan-open .sim-transport,
  #map-wrap.sim-open .sim-transport {
    right: 0;
  }

  /* --- panels as bottom sheets -------------------------------------- */
  #plan-panel,
  #sim-panel {
    top: auto;
    left: 0;
    right: 0;
    bottom: calc(var(--tools-h, 0px) + var(--transport-clearance, 0px));
    width: auto;
    max-width: none;
    height: var(--sheet-h, 58vh);
    max-height: calc(100% - var(--tools-h, 0px) - var(--transport-clearance, 0px));
    border-left: none;
    border-top: 1px solid var(--border);
    border-radius: 18px 18px 0 0;
    box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.6);
    padding: 0 0.85rem 1rem;
    z-index: 1200;
    overscroll-behavior: contain;
  }

  .panel-grab {
    display: block;
    position: sticky;
    top: 0;
    z-index: 3;
    height: 22px;
    margin: 0 -0.85rem;
    background: var(--card-bg);
    border-radius: 18px 18px 0 0;
    cursor: grab;
    /* The drag is handled by pointer events in app.js; without this the
       browser claims the gesture for scrolling first. */
    touch-action: none;
  }
  .panel-grab::after {
    content: "";
    display: block;
    width: 42px;
    height: 4px;
    margin: 9px auto 0;
    border-radius: 999px;
    background: var(--text-muted);
    opacity: 0.5;
  }
  .panel-grab.dragging { cursor: grabbing; }

  /* The close × has to stay reachable however far the body is scrolled. */
  #plan-panel .plan-panel-header,
  #sim-panel .plan-panel-header {
    position: sticky;
    top: 22px;
    z-index: 2;
    margin: 0 -0.85rem 0.5rem;
    padding: 0.1rem 0.85rem 0.5rem;
    background: var(--card-bg);
    border-bottom: 1px solid var(--border);
  }
  #plan-panel-close,
  #sim-panel-close,
  .panel-minimise {
    min-width: 44px;
    min-height: 44px;
  }
  .panel-minimise { display: block; }

  /* Minimised to the title strip: the body is clipped rather than
     scrollable, so the strip can't be scrolled out of view leaving an
     unlabelled stub with no way back up. */
  #plan-panel.sheet-minimised,
  #sim-panel.sheet-minimised {
    overflow: hidden;
  }
  #plan-panel.sheet-minimised .plan-panel-header,
  #sim-panel.sheet-minimised .plan-panel-header {
    border-bottom: none;
    margin-bottom: 0;
  }

  /* --- the Map options sheet ---------------------------------------- */
  .mobile-sheet {
    display: flex;
    flex-direction: column;
    position: absolute;
    left: 0;
    right: 0;
    bottom: calc(var(--tools-h, 0px) + var(--transport-clearance, 0px));
    max-height: 78%;
    background: var(--card-bg);
    border-top: 1px solid var(--border);
    border-radius: 18px 18px 0 0;
    box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.6);
    z-index: 1200;
  }
  .mobile-sheet.hidden { display: none; }
  .mobile-sheet-grab { height: 22px; flex-shrink: 0; }
  .mobile-sheet-grab::after {
    content: "";
    display: block;
    width: 42px;
    height: 4px;
    margin: 9px auto 0;
    border-radius: 999px;
    background: var(--text-muted);
    opacity: 0.5;
  }
  .mobile-sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.1rem 0.85rem 0.5rem;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
  }
  .mobile-sheet-title { font-weight: 700; font-size: 0.9rem; }
  .mobile-sheet-close {
    background: none;
    border: none;
    color: var(--text-muted);
    font-size: 1rem;
    min-width: 44px;
    min-height: 44px;
    cursor: pointer;
  }
  .mobile-sheet-body {
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0.7rem 0.85rem 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    /* These controls normally sit inside .leaflet-container, which is
       where they inherit Leaflet's own 12px type from. The sheet is a
       sibling of the map, not a descendant, so without this they'd render
       at the page's 16px base and tower over everything around them. */
    font-size: 0.78rem;
    line-height: 1.4;
  }

  /* Controls moved in here from Leaflet's corners (see app.js's
     syncMapOptionsSheet) are no longer inside .leaflet-right, so Leaflet's
     own float/margin rules stop applying on their own — what's left is to
     let them fill the sheet's width instead of hugging a map edge. */
  #map-options-body > * {
    width: 100% !important;
    max-width: none !important;
    margin: 0 !important;
    float: none !important;
    box-shadow: none !important;
  }
  #map-options-body .leaflet-control-layers-expanded { width: 100%; }
  /* The basemap list is collapsed-to-an-icon on the map, which makes no
     sense in a sheet — show it expanded. */
  #map-options-body .leaflet-control-layers-toggle { display: none; }
  #map-options-body .leaflet-control-layers-list { display: block; }
  #map-options-body label { display: flex; align-items: center; gap: 0.4rem; min-height: 34px; }
  /* These controls are sized for a cramped corner of the map on desktop
     (down to 0.68rem); in a sheet there's room to read them, and 11px
     labels next to Leaflet's own 13.5px ones just looked inconsistent. */
  #map-options-body .map-control-header,
  #map-options-body .plan-checkbox-row,
  #map-options-body .legend { font-size: 0.8rem; }
  #map-options-body .map-control-header { min-height: 44px; }

  /* --- panel content at phone width --------------------------------- */
  /* .plan-row lets its select flex down to min-width:0, which on a 412px
     screen crushed the region filter to 48px — "All regions" rendered as
     "All re…". The row already wraps, so giving the field a floor makes it
     drop to its own line instead of being squeezed to illegibility. */
  .plan-row select,
  .plan-row input[type="text"] { min-width: 9rem; }

  /* The four workflow steps overflow a phone by ~20px, which left "Run"
     sliced in half at the edge. It still scrolls if it needs to, but at
     these paddings it fits outright. */
  .sim-rail { padding: 0.4rem 0.35rem; margin: 0.5rem 0; }
  .sim-rail-step { gap: 0.25rem; font-size: 0.68rem; }
  .sim-rail-link { width: 0.5rem; margin: 0 0.15rem; }

  /* The hint is advisory and bottom-left docked; with a sheet open there's
     nowhere for it to go that isn't underneath one. */
  #map-wrap.plan-open #companion-pin-hint,
  #map-wrap.sim-open #companion-pin-hint {
    display: none;
  }
}
