: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);
}

.top-bar .slots-pill { margin-left: auto; }

/* 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 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;
  --heard-ink-echo: #67b437;
  --heard-ink-disc: #3a3afa;
  pointer-events: auto;
}

/* A white wash behind white text is unreadable, and the two other colours in the
   list (green echo, blue discovery) have to keep their meaning either way. So the
   palette switches on the plate's strength rather than the text being tinted:
   above ~40% the plate is light enough to carry dark ink. */
[data-heard-plate="light"] .heard {
  --heard-ink: #1b2430;
  --heard-ink-strong: #0b0f14;
  --heard-ink-muted: #3f4a57;
  --heard-ink-id: #0b5ea8;
  /* The bright map colours sit at about 2:1 on a white wash; these keep the
     green/blue distinction while clearing 4.5:1 on the default plate. */
  --heard-ink-echo: #27610f;
  --heard-ink-disc: #2323b8;
}

.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 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 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 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. */
.radio-card,
.settings-card { z-index: 6; }

/* 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; }

.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; }
  .icon-btn { width: 38px; }
}
