:root {
  color-scheme: dark;
  --bg: #0d1117;
  --panel: #161b22;
  --border: #263041;
  --text: #e6edf3;
  --muted: #8b949e;
  --accent: #2f81f7;
  --ok: #3fb950;
  --warn: #d29922;
  --error: #f85149;
  --tx: #ff8c42;
  --echo: #58a6ff;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-text-size-adjust: 100%;
}

body {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  /* The map runs edge to edge; only the notches get padding. */
  padding: env(safe-area-inset-top) 0 0 0;
  overflow: hidden;
  overscroll-behavior: none;
}

/* Two rows. The name and the live state share the first one, so the status is
   readable without scanning a second line; the second row is the app menu
   (which screen is open) plus the radio controls. */
.top {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 9px 12px 8px;
  border-bottom: 1px solid var(--border);
  background: rgba(13, 17, 23, 0.96);
  backdrop-filter: blur(8px);
  z-index: 3;
}

.top-row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

/* The name is the identity of the screen, not a toolbar label: a little larger
   than body text, and lighter than the controls around it, so it reads as a
   title instead of competing with the status pill beside it. */
.top h1 {
  display: flex;
  align-items: baseline;
  gap: 7px;
  font-size: 19px;
  font-weight: 400;
  margin: 0;
  letter-spacing: 0.01em;
  line-height: 1.15;
  white-space: nowrap;
}

/* The version is a build marker, not content: small, quiet, and next to the
   name so "is the phone running the new build" is answerable at a glance. */
.version {
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.04em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The second row: screens on the left, radio state on the right. */
.top-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.app-nav {
  display: flex;
  gap: 4px;
  padding: 3px;
  border-radius: 11px;
  background: rgba(16, 21, 28, 0.92);
  border: 1px solid var(--border);
}

/* A screen button is a place, not an action: the pressed one keeps the accent
   so the header always answers "where am I" without a label. */
.nav-btn {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 40px;
  height: 34px;
  min-height: 0;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
}

.nav-btn[aria-pressed="true"] {
  background: #1f2631;
  border-color: var(--border);
  color: var(--accent);
}

/* The zone chip and the slot count are one fact — "here, this many may
   transmit" — so they share a right edge and never separate when the chip is
   hidden. `margin-left: auto` on the wrapper pushes the pair right. */
.header-pills {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-left: auto;
  min-width: 0;
}
.top-bar .slots-pill { margin-left: 0; }

/* Update prompt. It only appears when the server reports a newer build, so it
   does not take map height on a normal launch: the page is usually already
   current after a restart. */
.update {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px 5px 10px;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: 10px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  font-size: 12px;
  color: var(--text);
}

/* `display: flex` above beats `hidden`, the same trap as the error box. */
.update[hidden] { display: none; }

.update span {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.update-btn {
  flex: 0 0 auto;
  min-height: 32px;
  padding: 4px 12px;
  border-radius: 8px;
  border: 1px solid transparent;
  background: var(--accent);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
}

/* The reload button is removed from the layout while a drive is running, so the
   strip reads as a notice, not as a disabled control the driver would poke. */
.update-btn[hidden] { display: none; }

/* Tap targets are 40px even though the glyph is 20px: the icon sits above the
   map, so it gets hit with a thumb while moving. */
.icon-btn {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  padding: 0;
  min-height: 0;
  border-radius: 10px;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--muted);
}

.icon-btn[aria-expanded="true"] {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 50%, transparent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* With the setup card closed there is no visible Connect button, so while no
   radio is attached the gear is the only route to one. Give it the accent so it
   reads as "there is something to do here", not as decoration. */
.icon-btn[data-attention="yes"] {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}

.slots-pill {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-size: 12px;
  padding: 5px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--muted);
  white-space: nowrap;
}
.slots-pill b { color: var(--text); font-variant-numeric: tabular-nums; font-size: 13px; }
.slots-pill .slots-label { font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; }
.slots-pill[data-kind="warn"] {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 50%, transparent);
}
.slots-pill[data-kind="warn"] b { color: var(--warn); }

/* The region the cap belongs to. Same silhouette as the slot pill so the pair
   reads as one sentence ("EKB · 3/10"), but quieter: the code is context, not a
   number to act on. Hidden outright when the fix is outside every boundary,
   because a code the map does not draw would be a lie. */
.zone-pill {
  flex: 0 0 auto;
  padding: 5px 8px;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--muted);
  white-space: nowrap;
}
.zone-pill[data-kind="warn"] {
  color: var(--warn);
  border-color: color-mix(in srgb, var(--warn) 50%, transparent);
}

/* The status pill sits at the right edge of the title row and gives ground
   first when a long message needs the space: the name and the menu are fixed. */
.status {
  flex: 0 1 auto;
  margin-left: auto;
  min-width: 0;
  max-width: 62%;
  font-size: 11px;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.status[data-kind="ok"] { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 45%, transparent); }
.status[data-kind="connecting"] { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); }
.status[data-kind="tx"] { color: var(--tx); border-color: color-mix(in srgb, var(--tx) 45%, transparent); }
.status[data-kind="error"] { color: var(--error); border-color: color-mix(in srgb, var(--error) 45%, transparent); }
.status[data-kind="warn"] { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 45%, transparent); }

/* The map fills whatever the header leaves. Everything else is positioned
   against this box, not against the page, so the bottom controls stay put when
   the keyboard or the URL bar resizes the viewport. */
main {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

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

/* MapLibre's own chrome is desktop-sized; shrink it so the phone keeps the
   screen for the map. */
.maplibregl-ctrl-bottom-right { margin-bottom: 4px; }
.maplibregl-ctrl-attrib { font-size: 9px; }

.map-fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  gap: 6px;
  padding: 24px;
  text-align: center;
}
.map-fallback[hidden] { display: none; }

/* Top-left corner: what the last ping or discovery heard. No panel and no
   outline — the map is the screen, and the list is read at a glance between
   glances at the road. A translucent plate sits behind it because the basemap
   is bright in places and coloured town names used to win the contrast fight
   against green repeater ids. */
.heard {
  /* Wide enough for a full repeater name at the largest text size the settings
     offer; it still leaves the right third of the map clear. */
  max-width: min(86%, 420px);
  padding: 4px 7px;
  border-radius: 8px;
  /* Set from Settings → background opacity. Light plate, not a dark card: the
     rows are read over a map, and a white wash keeps them legible in daylight
     while hiding the streets underneath. */
  background: rgba(255, 255, 255, var(--heard-plate, 0.55));
  /* Ink defaults to the light palette, for a weak or absent plate. A strong
     plate flips these to dark in one place — see the `data-heard-plate` rule. */
  --heard-ink: #eef3f8;
  --heard-ink-strong: #fff;
  --heard-ink-muted: #cfd8e3;
  --heard-ink-id: #8ed0ff;
  /* Green and blue keep their meaning, so they do not join the light/dark
     switch: the same two dark shades are used on the map and on the white
     plate. Flipping them with the plate made the list look like it changed
     subject whenever the slider crossed the threshold. */
  --heard-ink-echo: #27610f;
  --heard-ink-disc: #2323b8;
  pointer-events: auto;
}

/* Only the neutral ink flips with the plate. Above ~40% the white wash is light
   enough that white text stops working, so names, counts and the muted SNR turn
   dark; green and blue above keep their shade either way. */
[data-heard-plate="light"] .heard {
  --heard-ink: #1b2430;
  --heard-ink-strong: #0b0f14;
  --heard-ink-muted: #3f4a57;
  --heard-ink-id: #0b5ea8;
}

.heard-counts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0 0 3px;
  color: var(--heard-ink);
  font-size: 12px;
  letter-spacing: 0.02em;
}
.heard-counts b {
  color: var(--heard-ink-strong);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}

/* One node per line: the list answers "who repeated that ping", not "what is out
   there". Large type on purpose — read at arm's length in a windscreen mount. */
.heard-nodes {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  /* Driver-adjustable: the list is read at arm's length, and what is legible
     from one windscreen mount is not from another. Set from Settings. */
  font-size: var(--heard-font, 16px);
  line-height: 1.35;
  color: var(--heard-ink);
}

.heard-nodes:empty { display: none; }

.heard-nodes li {
  display: flex;
  gap: 6px;
  align-items: baseline;
  white-space: nowrap;
  overflow: hidden;
}

/* Colour carries the kind of answer: a repeater that repeated the ping is
   green, one that only answered discovery is blue, matching the DISC colour on
   the map so the two views agree. The name is the part a driver reads, so the
   colour goes on the whole row. Both hues are darkened on a light plate, where
   the bright versions would sit at about 2:1 contrast. */
.heard-nodes .id { color: var(--heard-ink-id); letter-spacing: 0.04em; font-weight: 700; }
.heard-nodes li.echoed .id,
.heard-nodes li.echoed .nm { color: var(--heard-ink-echo); }
.heard-nodes li.discovered .id,
.heard-nodes li.discovered .nm { color: var(--heard-ink-disc); }
.heard-nodes .nm {
  color: var(--heard-ink);
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
}
.heard-nodes .snr { margin-left: auto; color: var(--heard-ink-muted); }

.heard { position: relative; }

/* Bottom: the drive controls, thumb height, over the map. */
.map-controls {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(8px + env(safe-area-inset-bottom));
  /* Phone-first: cap the strip so a desktop window does not stretch one Ping
     button across the screen. */
  width: min(calc(100% - 16px), 560px);
  display: grid;
  gap: 6px;
  z-index: 2;
}

/* `display: grid` above beats the `hidden` attribute, so the dock would stay on
   screen while a History route is open. */
.map-controls[hidden] { display: none; }

.ctrl-row {
  display: flex;
  align-items: stretch;
  gap: 6px;
}

/* `display: flex` above beats the `hidden` attribute, so the interval row would
   otherwise stay on screen in Manual mode. */
.ctrl-row[hidden] { display: none; }

/* Top overlay: the heard list and the error banner share the corner, stacked so
   an error never lands on top of the list. */
.overlay-top {
  position: absolute;
  top: 8px;
  left: 8px;
  right: 8px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  pointer-events: none;
  z-index: 2;
}

.ctrl-row .seg { flex: 1 1 auto; margin-bottom: 0; }

/* The interval row is the one above the action row. It is narrower than the full
   dock so it reads as a setting for the row below rather than a second set of
   actions, and it keeps its height out of the way of the Ping button. */
#interval-row { justify-content: center; }
#interval-row .seg-time { flex: 0 1 auto; }

/* The mode switch is the *small* control the driver asked for: it stays at its
   natural width and the primary action takes the rest of the row. */
.ctrl-row .seg-mode { flex: 0 0 auto; }
.ctrl-row .seg-mode button { flex: 0 0 auto; padding: 5px 14px; }

/* Ping is the action; Find is the occasional one. Sizes say so. */
#ping { flex: 1 1 auto; min-width: 70px; }
#discover { min-width: 58px; }

/* The Ping button carries the countdown as a background that drains to the
   right, not as digits: the driver reads the shape at a glance, and a number
   ticking every second is unreadable in peripheral vision. The fill sits under
   the label, so the text stays legible over it. */
#ping {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
#ping .ping-fill {
  position: absolute;
  inset: 0;
  z-index: -1;
  transform-origin: left center;
  transform: scaleX(0);
  background: rgba(255, 255, 255, 0.28);
  /* Driven by client.js; the transition only smooths the per-frame updates. */
  transition: transform 0.2s linear;
  pointer-events: none;
}
/* Discovery rides the same button: its fill is the dark blue of the DISC
   colour, so the next automatic action is identifiable without reading. */
#ping[data-next="discovery"] .ping-fill { background: rgba(59, 130, 246, 0.55); }
#ping .ping-label { position: relative; }

/* In Auto the button is a countdown, not a control: it is disabled to stop a
   manual ping fighting the loop, but must not look switched off. */
#ping[data-next] { opacity: 1; cursor: default; }

/* The dock is a control strip, not a form: match the segmented controls' type
   size so the row reads as one unit rather than two big buttons bolted on. */
.ctrl-row > button {
  flex: 0 0 auto;
  min-height: 40px;
  padding: 6px 10px;
  font-size: 13px;
  font-weight: 600;
}

/* A screen is a page inside the app: it covers the map completely rather than
   floating over it, so a list is read without roads moving behind it. The map
   keeps running underneath and is simply out of sight, which is what makes
   switching back instant. */
.screen {
  position: absolute;
  inset: 0;
  z-index: 7;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  background: var(--bg);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* `display: flex` above beats the `hidden` attribute. */
.screen[hidden] { display: none; }

.screen-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.screen-head h2 {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.screen .hint {
  margin: 0;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.4;
}

.empty {
  margin: 8px 0 0;
  color: var(--muted);
  font-size: 13px;
}
.empty[hidden] { display: none; }

/* One drive per row: the date is the handle, the numbers are the summary. */
.session-list,
.event-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 6px;
}

.session {
  position: relative;
  display: grid;
  gap: 3px;
  width: 100%;
  padding: 10px 30px 10px 12px;
  text-align: left;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  font-weight: 400;
}

.session[aria-current="true"] {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 10%, var(--panel));
}

.session .when {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}

.session .facts-line {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* The session list can be long, and the whole row is the tap target: the right
   chevron says it opens something without adding a second control. */
.session::after {
  content: "›";
  position: absolute;
  right: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
}

/* The Log is chronological, newest last, like a paper logbook. */
.event-list {
  gap: 4px;
  font-size: 13px;
}

.event-list li {
  display: flex;
  gap: 8px;
  align-items: baseline;
  padding: 8px 10px;
  border-radius: 8px;
  border-left: 3px solid var(--border);
  background: #10151c;
}

.event-list li.tx { border-left-color: var(--tx); }
.event-list li.disc { border-left-color: #3a3afa; }
.event-list .time {
  flex: 0 0 auto;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
}
.event-list .body {
  display: grid;
  gap: 4px;
  min-width: 0;
  flex: 1 1 auto;
}
.event-list .summary { overflow-wrap: anywhere; }

/* Who answered a discovery, one repeater per line. Monospace because the ids are
   hashes read a character at a time, and the SNR is pushed right so a column of
   answers can be compared without reading the names again. */
.event-list .answers {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
}
.event-list .answers li {
  display: flex;
  gap: 6px;
  align-items: baseline;
  padding: 0;
  border: 0;
  background: none;
}
.event-list .answers .id {
  flex: 0 0 auto;
  color: #8ed0ff;
  letter-spacing: 0.04em;
  font-weight: 700;
}
.event-list .answers .nm {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: #eef3f8;
}
.event-list .answers .snr {
  flex: 0 0 auto;
  margin-left: auto;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* While a route from History is open the drive controls are not the subject:
   the bar names the drive and closes it, and the map shows the line. */
.route-bar {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(8px + env(safe-area-inset-bottom));
  width: min(calc(100% - 16px), 560px);
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px 8px 12px;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: rgba(22, 27, 34, 0.97);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
  z-index: 3;
}
.route-bar[hidden] { display: none; }

.route-text {
  display: grid;
  gap: 1px;
  min-width: 0;
  flex: 1 1 auto;
}
.route-text b { font-size: 13px; font-weight: 600; }
.route-text .muted {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Map tools sit in one column at the top right, under the header and clear of
   the bottom dock. Fixed order: zoom, layers, crosshair, arrow. */
.map-tools {
  position: absolute;
  right: 8px;
  top: 8px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  z-index: 3;
}

/* The two zoom buttons read as one control, so they share a plate. */
.map-tools .tool-group {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 10px;
}

.map-tools .tool-group .icon-btn {
  border-radius: 0;
  border-bottom-width: 0;
}
.map-tools .tool-group .icon-btn:first-child { border-radius: 10px 10px 0 0; }
.map-tools .tool-group .icon-btn:last-child {
  border-radius: 0 0 10px 10px;
  border-bottom-width: 1px;
}

/* These float over a busy basemap, so unlike the header gear they need an
   opaque plate: a transparent icon disappears over streets and water. */
.map-tools .icon-btn {
  background: rgba(16, 21, 28, 0.92);
  border-color: var(--border);
  color: var(--text);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}

/* The bearing and crosshair toggles are modes, so they keep the accent while
   active; without that the icon alone cannot say which way the map will turn. */
#map-bearing[aria-pressed="true"],
#map-center[aria-pressed="true"],
#layers-toggle[aria-expanded="true"] {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 22%, rgba(16, 21, 28, 0.92));
}

/* A small panel under the layers button. Anchored to the icon column so it
   never covers the map centre. */
.layers-panel {
  position: absolute;
  right: 56px;
  /* Level with the layers button: 8px inset + two 40px zoom buttons + 6px gap,
     less its own padding so the first row lines up with the icon. */
  top: 88px;
  display: grid;
  gap: 6px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: rgba(16, 21, 28, 0.96);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  font-size: 13px;
  z-index: 4;
}
.layers-panel[hidden] { display: none; }

.layers-panel .check {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  color: var(--text);
  text-align: left;
  white-space: nowrap;
}

/* The status filter is a nested list, so it is indented and visually part of
   "Coverage" rather than a sibling layer. */
.layer-types {
  margin: 2px 0 6px;
  padding: 5px 0 3px 9px;
  border-left: 2px solid var(--border);
}

.layer-types[hidden] { display: none; }

.layer-types-title {
  margin: 0 0 4px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.layer-types-list {
  display: grid;
  gap: 2px;
}

.layer-types .check {
  font-size: 12px;
  padding: 1px 0;
}

/* The same 10 px chip the legend uses: a colour alone would not name a status,
   and the name alone would not tie it to a square on the map. */
.layer-types .swatch {
  flex: 0 0 auto;
  width: 10px;
  height: 10px;
  border-radius: 2px;
}

/* The global `input { flex: 1 }` makes a bare checkbox grow and push its label
   to the right edge, which read as "centred". The box keeps its 16px and the
   label sits immediately after it. */
.layers-panel input {
  flex: 0 0 auto;
  min-height: 0;
  padding: 0;
  width: 16px;
  height: 16px;
  margin: 0;
}

/* The heatmap is a gradient, so a checkbox alone would not say which end is
   good. The legend is shown only while the layer is on: a permanent scale for a
   hidden layer is just clutter over the map. */
.layers-legend {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 2px 0 0;
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
}
.layers-legend[hidden] { display: none; }
.layers-legend .ramp {
  flex: 1 1 auto;
  height: 8px;
  border-radius: 4px;
  /* The same ramp the map layer uses, so the legend cannot drift from the
     colours on screen. Five colours here are the one copy that cannot be
     imported from `signal-layer.js`: change both when tuning the ramp. */
  background: linear-gradient(to right, #d7191c, #f46d43, #fdae61, #a6d96a, #1a9641);
}

/* The OSM credit: a quiet line under the buttons in the dock, rather than the
   map-info control the drive screen used to carry. */
.map-credit {
  margin: 0;
  text-align: center;
  font-size: 9px;
  line-height: 1.2;
  color: rgba(255, 255, 255, 0.5);
  pointer-events: none;
}

/* Setup and diagnostics are panels, not part of the drive screen: they float
   above the map and scroll on their own. */
.card {
  background: rgba(22, 27, 34, 0.97);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 12px;
}

.radio-card,
.settings-card,
.rx-card {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 8px;
  width: min(calc(100% - 16px), 560px);
  max-height: calc(100% - 16px);
  overflow-y: auto;
  z-index: 4;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
}

/* A panel is opened on purpose, so it wins over the diagnostics furniture
   rather than growing out of its shadow. The radio card and the settings card
   are never open at the same time (each closes the other), so they share a
   level.

   That level sits above `.screen` (z-index 7) on purpose. The gear and the
   Bluetooth icon stay in the header on every screen, so they have to open
   something: at 6 the card appeared underneath an open History or Log list and
   the icons looked dead. A card is transient and always closable, so covering
   the list is the correct trade against a tap that does nothing. */
.radio-card,
.settings-card { z-index: 8; }

/* The diagnostics panel is debug furniture: closed by default, anchored at the
   top so it never competes with the drive controls at the bottom. When it is on
   screen the heard list steps down below it — measured from the collapsed row,
   which is the only state that shares the screen with the map. */
.rx-card {
  top: 8px;
  bottom: auto;
  z-index: 5;
  max-height: calc(100% - 16px);
}

body.debug-rx .overlay-top { top: calc(8px + var(--rx-collapsed-h, 54px)); }

.rx-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.rx-head h2 { margin: 0; }

.ghost-btn {
  flex: 0 0 auto;
  min-height: 28px;
  padding: 3px 10px;
  font-size: 12px;
  background: transparent;
  color: var(--muted);
}

.radio-card[hidden],
.settings-card[hidden],
.rx-card[hidden] { display: none; }

.card h2 {
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin: 0 0 10px;
}

.row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.row:last-child { margin-bottom: 0; }

label { color: var(--muted); font-size: 13px; }

button, select, input {
  font: inherit;
  color: var(--text);
  background: #1f2631;
  border: 1px solid var(--border);
  border-radius: 9px;
  padding: 8px 12px;
  min-height: 40px;
  cursor: pointer;
}

input {
  flex: 1;
  min-width: 0;
  cursor: text;
  letter-spacing: 0.06em;
}

input:focus, select:focus, button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

button { flex: 1; }

button.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}

button:disabled { opacity: 0.45; cursor: not-allowed; }

/* Segmented control: the two choices are a state, not a menu, so showing both
   at once beats a dropdown that hides half the options behind a tap. */
.seg {
  display: flex;
  gap: 3px;
  padding: 3px;
  background: rgba(16, 21, 28, 0.92);
  border: 1px solid var(--border);
  border-radius: 11px;
}

/* `display: flex` above beats the `hidden` attribute, so the interval control
   would otherwise stay on screen in Manual mode. */
.seg[hidden] { display: none; }

.seg button {
  flex: 1;
  min-height: 34px;
  padding: 5px 8px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
}

.seg button.on {
  background: #1f2631;
  border-color: var(--border);
  color: var(--text);
}

.seg button.on[data-value="tx"] {
  color: var(--tx);
  border-color: color-mix(in srgb, var(--tx) 45%, transparent);
}

.seg button:disabled { opacity: 0.55; }
.seg button.on:disabled { opacity: 0.85; }

.seg-time button { min-height: 30px; font-size: 11px; }

/* Text size setting: the labels are the sizes, so the control is the preview. */
.seg-font { margin-bottom: 6px; }
.seg-font button { min-height: 38px; font-size: 14px; }
.seg-font button[data-value="14"] { font-size: 12px; }
.seg-font button[data-value="19"] { font-size: 16px; }
.seg-font button[data-value="23"] { font-size: 19px; }

/* Language switch: the labels are the languages themselves, so the control is
   readable in either one. "Русский" is longer than "English" and gets the room. */
.seg-lang { margin-bottom: 10px; }
.seg-lang button { min-height: 36px; font-size: 13px; }

.setting-label {
  margin: 0 0 6px;
  font-size: 12px;
  color: var(--text);
}

/* A slider needs a little more room around it than a segmented control: the
   thumb is the touch target, and on a phone it must be grabbable while the car
   is moving. The value is shown numerically because "a bit more" is not a
   setting a driver can reproduce later. */
.range-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}

.range {
  flex: 1 1 auto;
  min-width: 0;
  height: 28px;
  margin: 0;
  accent-color: var(--accent);
  background: transparent;
}

.range-value {
  flex: 0 0 auto;
  min-width: 40px;
  text-align: right;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* Collapsed deployment config (API key). */
.more { margin-top: 2px; }
.more summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 12px;
  padding: 4px 0;
  list-style: none;
}
.more summary::-webkit-details-marker { display: none; }
.more summary::before { content: "▸ "; }
.more[open] summary::before { content: "▾ "; }
.more input { width: 100%; margin-top: 6px; letter-spacing: 0.06em; }

.facts { margin: 10px 0 0; display: grid; gap: 5px; }
/* `display: grid` above beats the `hidden` attribute, so the LoRa block would
   otherwise show a column of dashes before a radio is connected. */
.facts[hidden] { display: none; }
.facts div { display: flex; justify-content: space-between; gap: 12px; }

/* A section heading inside a facts list: the rule says where the new block
   starts without adding another card. */
.facts .facts-head {
  margin-top: 4px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
.facts .facts-head dt {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.facts dt { color: var(--muted); font-size: 13px; }
.facts dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.facts dd[data-kind="warn"] { color: var(--warn); }
.facts dd[data-kind="ok"] { color: var(--ok); }

#error {
  display: grid;
  gap: 8px;
  max-width: 100%;
  pointer-events: auto;
}

/* `display: grid` above beats the `hidden` attribute, so an empty error box
   would otherwise darken a corner of the map. */
#error[hidden] { display: none; }

#error ol {
  margin: 0;
  padding-left: 18px;
  display: grid;
  gap: 4px;
  color: var(--text);
}

#error p { margin: 0; }

#error button {
  background: transparent;
  border-color: currentColor;
  color: inherit;
  font-weight: 600;
}

.counts {
  display: flex;
  justify-content: space-around;
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: 12px;
}
.counts b { color: var(--text); font-size: 16px; font-variant-numeric: tabular-nums; }

.error {
  margin: 0;
  padding: 9px 11px;
  border-radius: 10px;
  background: color-mix(in srgb, var(--error) 16%, #0d1117);
  border: 1px solid color-mix(in srgb, var(--error) 40%, transparent);
  color: var(--error);
  font-size: 12px;
}

.muted { color: var(--muted); }

.log {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 2px;
  max-height: 42vh;
  overflow-y: auto;
  font-size: 13px;
}

.log li {
  padding: 7px 9px;
  border-radius: 8px;
  border-left: 3px solid var(--border);
  background: #10151c;
  display: flex;
  gap: 8px;
  align-items: baseline;
  flex-wrap: wrap;
}

.log li.tx { border-left-color: var(--tx); }
.log li.echo { border-left-color: var(--echo); }
.log li.drop { border-left-color: var(--muted); }

.log .muted { color: var(--muted); font-variant-numeric: tabular-nums; }

#rx-debug .hint,
.settings-card .hint {
  margin: 0 0 10px;
  color: var(--muted);
  font-size: 12px;
  line-height: 1.4;
}

/* The settings panel has no diagnostics table under the hint, so the note does
   not need the table's bottom margin. */
.settings-card .hint { margin: 0; }

.rx-log {
  margin-top: 12px;
  max-height: 34vh;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
}

.rx-log li { border-left-color: var(--muted); }
.rx-log li.echo { border-left-color: var(--echo); }
.rx-log li.own { border-left-color: var(--tx); }
.rx-log li.other { border-left-color: var(--warn); }
.rx-log li .verdict { color: var(--text); font-weight: 600; }
.rx-log li.echo .verdict { color: var(--echo); }
.rx-log li.other .verdict { color: var(--warn); }

/* Short screens: the header is the thing that can shrink; the map must not. */
@media (max-height: 620px) {
  .top { padding: 6px 10px 7px; gap: 4px; }
  .top h1 { font-size: 17px; }
}

/* Narrow phones (320-360 px): the header row has four controls and a pill on
   it, and at 320 px their natural widths overflow by ~26 px. Shrinking the
   screen buttons and the gaps keeps every one of them on one row. */
@media (max-width: 380px) {
  .top { padding-left: 10px; padding-right: 10px; }
  .top-bar { gap: 5px; }
  .app-nav { gap: 2px; padding: 2px; }
  .nav-btn { width: 36px; }
  .top-bar .slots-pill { padding: 4px 7px; }
  .top-bar .zone-pill { padding: 4px 6px; font-size: 10px; }
  .icon-btn { width: 38px; }
}
