/* custom.css — hand-rolled styles. All rules read against Tabler's own CSS
   variables so they track the active theme.

   ⚠ READ THIS BEFORE ADDING A RULE. This header used to claim custom.css held
   "the ONLY hand-rolled styles in the app" and that everything in it was for a
   component "with no native Tabler equivalent". Both were false, and the false
   confidence had a real cost: because the file advertised itself as audited and
   unavoidable, nobody re-checked it — which is exactly how `.stat-hint` got in
   as a hand-rolled duplicate of Tabler's shipped `.form-help` (removed v0.13.0,
   see statHint() in app.js). A guardrail that lies is worse than none.

   The truths it was hiding:
     - static/css/theme.css is ~7.8 KB MORE hand-rolled app CSS.
     - There are two KINDS of group here, not one.

   Groups 1-3 are genuinely bespoke — verified to have no Tabler equivalent:
     1. multi-select dropdown   (static/js/custom.js) — the app's OWN component;
        Tom Select was removed in v0.12.3, see UI-STANDARDS.md
     2. messages / chat bubbles (static/js/comms.js, nodepage.js)
     3. map: MapLibre GL JS (static/js/map.js) — third-party chrome themed by
        targeting its .maplibregl-* classes.

   Groups 4-8 are OVERRIDES of Tabler/Bootstrap components, each justified inline.
   An override is legitimate; silently reimplementing a component is not. So:
   before adding anything, grep tabler.min.css for the component first. */

/* =========================================================================
   1. multi-select dropdown
   ========================================================================= */
.ms-wrap { position: relative; }
.ms-trigger {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  width: 100%; padding: .4375rem .75rem;
  background: var(--tblr-bg-forms, var(--tblr-bg-surface));
  border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius);
  color: var(--tblr-body-color); font-size: .875rem; line-height: 1.4286;
  cursor: pointer; text-align: left;
}
.ms-trigger:hover { border-color: var(--tblr-border-color-active, var(--tblr-primary)); }
.ms-trigger:focus {
  border-color: var(--tblr-primary); outline: 0;
  box-shadow: 0 0 0 .25rem rgba(var(--tblr-primary-rgb), .25);
}
.ms-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ms-arrow { font-size: .875rem; color: var(--tblr-secondary); flex-shrink: 0; }
.ms-panel {
  position: absolute; z-index: 1000; top: calc(100% + 4px); left: 0; right: 0; min-width: 200px;
  background: var(--tblr-bg-surface);
  border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius);
  box-shadow: var(--tblr-box-shadow-dropdown, 0 .5rem 1.5rem rgba(0, 0, 0, .3));
  max-height: 240px; display: flex; flex-direction: column;
}
.ms-panel[hidden] { display: none; }
.ms-search-row {
  display: flex; align-items: center; gap: 6px; padding: .5rem .75rem;
  border-bottom: var(--tblr-border-width) solid var(--tblr-border-color); flex-shrink: 0;
}
.ms-search-row .ti { color: var(--tblr-secondary); font-size: .875rem; flex-shrink: 0; }
.ms-search { flex: 1; background: none; border: 0; color: var(--tblr-body-color); font-size: .875rem; outline: none; }
.ms-search::placeholder { color: var(--tblr-secondary); }
.ms-list { overflow-y: auto; padding: .25rem; display: flex; flex-direction: column; gap: 1px; }
.ms-opt {
  display: flex; align-items: center; gap: 8px; color: var(--tblr-body-color);
  font-size: .875rem; cursor: pointer; padding: .375rem .5rem;
  border-radius: var(--tblr-border-radius); user-select: none;
}
.ms-opt:hover { background: rgba(var(--tblr-body-color-rgb), .06); }
.ms-opt input[type=checkbox] {
  width: 15px; height: 15px; accent-color: var(--tblr-primary);
  cursor: pointer; flex-shrink: 0; margin: 0;
}

/* =========================================================================
   2. messages / chat — Tabler has no chat-bubble component. Bubbles carry the
      sender's node-identity color (via --node-color, set inline) as a tint;
      the avatar carries it as a ring. Text stays on the theme body color.
   ========================================================================= */
.msg-layout {
  display: grid; grid-template-columns: 240px 1fr; gap: 14px;
  height: calc(100dvh - 260px); min-height: 320px; align-items: stretch;
}
.msg-layout > .card { height: 100%; min-height: 0; overflow: hidden; margin-top: 0; }
/* Channels card hugs its content (channels + any DMs) rather than stretching to
   the full message-pane height; it caps at the layout height and the list
   scrolls only once it grows long. */
.msg-layout > .m-chans-card {
  height: auto; max-height: 100%; align-self: start;
  display: flex; flex-direction: column;
}
/* Below md the channel list is the #m-chan-sel dropdown instead of the card
   (the card carries d-none d-md-flex). An explicit 240px track would survive
   the hidden item, so collapse the grid to the message pane alone. */
@media (max-width: 767.98px) {
  .msg-layout { grid-template-columns: 1fr; }
}
.chanlist { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.msg-pane { display: flex; flex-direction: column; min-width: 0; }
.msg-scroll { flex: 1; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }

.msg-row { display: flex; gap: 9px; align-items: flex-end; }
.msg-row.tx { justify-content: flex-end; }
.msg-row .avatar { margin-bottom: 18px; cursor: pointer; font-weight: 700; font-size: .8rem; border: 2px solid var(--tblr-border-color); }

/* Node cards: enlarge the avatar a touch so 4-character short names sit inside
   the box, and keep the card-stamp role icon (node-colored bg) legible. */
.node-card .avatar { width: 3rem; height: 3rem; font-size: .75rem; font-weight: 600; }
/* White role icon on the node-colored stamp — node-list cards and the node
   detail ID card share the same treatment for uniformity. */
.node-card .card-stamp-icon,
#view-node .card-stamp-icon { color: #fff; }

.bubble { max-width: 76%; align-self: flex-start; }
.bubble.tx { align-self: flex-end; }
.bubble .meta { font-size: .7188rem; color: var(--tblr-secondary-color); margin: 0 4px 3px; display: flex; gap: 8px; }
.bubble.tx .meta { justify-content: flex-end; }
.bubble .meta .who { color: var(--node-color, var(--tblr-secondary-color)); font-weight: 700; cursor: pointer; }
.bubble .body {
  position: relative;
  background: var(--tblr-bg-surface-secondary); border: 1px solid var(--tblr-border-color);
  border-radius: 12px 12px 12px 4px; padding: 8px 12px; font-size: .9375rem; word-wrap: break-word;
}
.bubble.tx .body {
  background: rgba(var(--tblr-primary-rgb), .13); border-color: rgba(var(--tblr-primary-rgb), .25);
  border-radius: 12px 12px 4px 12px;
}
.bubble.rx .body {
  background: color-mix(in srgb, var(--node-color, var(--tblr-bg-surface-secondary)) 14%, var(--tblr-bg-surface));
  border-color: color-mix(in srgb, var(--node-color, var(--tblr-border-color)) 35%, var(--tblr-border-color));
}
/* Unknown sender (no NodeInfo yet) — dark grey bubble + avatar. */
.bubble.rx.unknown .body {
  background: var(--tblr-bg-surface-secondary);
  border-color: var(--tblr-border-color);
}
.msg-row .avatar.avatar-unknown {
  color: var(--tblr-secondary-color);
  border-color: var(--tblr-border-color);
  background: var(--tblr-bg-surface-secondary);
}
.bubble .body .msgtext { white-space: pre-wrap; }
.bubble .body .msgtext a { color: var(--tblr-primary); text-decoration: underline; }

/* Reply visualization — the quoted message stacked above the reply, dimmed,
   with a reply arrow. Click to jump to the original. Emoji reactions are
   handled separately (.reactions) and never rendered as quotes. */
.reply-quote {
  display: flex; align-items: center; gap: 6px;
  margin: 0 4px 3px; padding: 3px 8px;
  border-left: 3px solid var(--node-color, var(--tblr-primary));
  border-radius: 5px; background: var(--tblr-bg-surface-secondary);
  opacity: .6; font-size: .75rem; color: var(--tblr-secondary-color);
  cursor: pointer; max-width: 100%;
}
.bubble.tx .reply-quote { border-left-color: var(--tblr-primary); }
.reply-quote:hover { opacity: .85; }
.reply-quote-icon { flex-shrink: 0; }
.reply-quote-who { color: var(--node-color, var(--tblr-primary)); font-weight: 600; }
.reply-quote-body { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Brief highlight when jumping to a quoted message */
.bubble-flash .body { animation: bubble-flash 1.2s ease-out; }
@keyframes bubble-flash {
  0%, 30% { box-shadow: 0 0 0 2px var(--tblr-primary); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* @mentions ----------------------------------------------------------------
   A mention chip is an inline accent, NOT decoration on the message text: per
   the Meshtastic design rules message text stays --text, so the chip carries a
   tinted background and keeps normal-weight body text rather than recolouring
   the sentence. Deliberately NOT per-node coloured — a busy channel would then
   render dozens of different hues inside running prose. */
.mention {
  background: var(--tblr-primary-lt);
  color: var(--tblr-primary);
  border-radius: 4px;
  padding: 0 .25em;
  cursor: pointer;
  white-space: nowrap;          /* a name must never wrap mid-chip */
  text-decoration: none;
}
.mention:hover, .mention:focus-visible {
  filter: brightness(1.15);
  outline: none;
}
.mention:focus-visible { box-shadow: 0 0 0 2px var(--tblr-primary); }
/* A mention OF US reads as a direct address, so it gets the accent proper. */
.mention-self {
  background: var(--tblr-primary);
  color: var(--tblr-dark);
  font-weight: 600;
}
/* An inbound message naming us — a left edge marker, not a full-bubble wash,
   so it survives alongside the node-colour bubble treatment. */
.bubble-mentioned .body { box-shadow: inset 3px 0 0 0 var(--tblr-primary); }

/* Compose suggestions. ABOVE the field on purpose: below it is where a phone
   keyboard appears, which would cover the list exactly when it is needed. */
.mention-pop {
  position: absolute;
  bottom: 100%;
  left: 0;
  right: 0;
  margin-bottom: 4px;
  max-height: min(40vh, 260px);
  overflow-y: auto;
  z-index: 1030;
}
.mention-pop .list-group-item { cursor: pointer; padding: .4rem .75rem; }
.mention-pop .list-group-item.active { background: var(--tblr-primary-lt); color: inherit; }
.mention-pop .mention-id { font-family: var(--tblr-font-monospace); font-size: .8em; opacity: .7; }
.bubble .sig { font-size: .6875rem; color: var(--tblr-secondary-color); margin: 3px 4px 0; }
/* Reception badges (SNR / RSSI / hops / packet ID) wrap instead of stretching a
   narrow bubble, and sit a size below the meta-row badges so the measurement
   never outweighs the message. */
.bubble .sig { display: flex; flex-wrap: wrap; gap: 3px; }
.bubble .sig .badge { font-size: .625rem; padding: 1px 5px; font-weight: 500; }
/* Hover action group (reply + react) anchored to the bubble's top-right.
   Icons are sized up from the old 11px pill so they're legible on touch. */
.bubble .msg-actions {
  position: absolute; top: -13px; right: -6px; display: none; gap: 4px;
}
.bubble:hover .msg-actions { display: flex; }
.msg-act-btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--tblr-bg-surface); border: 1px solid var(--tblr-border-color);
  border-radius: 8px; padding: 3px 7px; cursor: pointer; line-height: 1;
  color: var(--tblr-secondary-color); font-size: 1rem;
}
.msg-act-btn:hover { color: var(--tblr-primary); border-color: var(--tblr-primary); }

/* Composer reply banner — sits above the input while a reply is being composed. */
.m-reply-banner {
  display: flex; align-items: center; gap: 8px;
  padding: 4px 10px; margin-bottom: 6px;
  border-left: 3px solid var(--tblr-primary); border-radius: 5px;
  background: var(--tblr-bg-surface-secondary);
  font-size: .7813rem; color: var(--tblr-secondary-color);
}
/* Packets table: cap the Summary column with an ellipsis so a long string
   doesn't keep resizing the columns on each refresh (full text is in the modal
   and the title tooltip). */
.pk-summary { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ack.pending { color: var(--tblr-warning); }
.ack.delivered { color: var(--tblr-success); }
.ack.failed { color: var(--tblr-danger); }
/* Received-message encryption lock (HL11 §L). Its own state rather than reusing
   .delivered — a received message has no delivery state of ours to report. */
.ack.encrypted { color: var(--tblr-success); }

/* Composer send button — extreme tones beyond Tabler's contextual classes:
   exactly at the 200-byte limit (dark red) and over it (black w/ red text). */
.btn-send-max { background: #7a1212; border-color: #7a1212; color: #fff; }
.btn-send-max:hover { background: #5e0d0d; border-color: #5e0d0d; color: #fff; }
.btn-send-over { background: #000; border-color: #000; color: var(--tblr-red); }
.btn-send-over:hover { background: #000; border-color: #000; color: var(--tblr-red); }

.reactions { display: flex; gap: 4px; margin: 3px 4px 0; flex-wrap: wrap; }
.reaction { background: var(--tblr-bg-surface); border: 1px solid var(--tblr-border-color); border-radius: 10px; font-size: .8125rem; padding: 1px 7px; cursor: default; }
.reaction .cnt { font-family: var(--tblr-font-monospace); font-size: .6875rem; color: var(--tblr-secondary-color); margin-left: 3px; }
/* A reaction our connected node sent (e.g. an auto-ack tapback) — accent
   border + faint tint so we can see at a glance that WE responded, even when
   other nodes used the same emoji and the count > 1. */
.reaction-mine { border-color: var(--tblr-primary); box-shadow: inset 0 0 0 1px var(--tblr-primary); }

.emoji-pick {
  display: flex; gap: 4px; flex-wrap: wrap; position: fixed; z-index: 1070;
  background: var(--tblr-bg-surface); border: 1px solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius); padding: 6px;
  box-shadow: var(--tblr-box-shadow-dropdown, 0 8px 24px rgba(0, 0, 0, .55));
}
.emoji-pick button { background: none; border: 0; font-size: 1.125rem; padding: 4px 6px; cursor: pointer; border-radius: 5px; }
.emoji-pick button:hover { background: rgba(var(--tblr-primary-rgb), .13); }

/* Node-page DM card: stretches to match the identity card's row height (the
   card carries h-100), with 480px as a floor so it never collapses too short
   when the identity card is sparse. The msg-scroll pane (flex:1) takes the slack.
   The max-height cap is what makes long chat history SCROLL inside msg-scroll
   instead of growing the card (and the whole row) without bound — without it
   the card is content-sized and flex:1 never engages as a scroll region. */
.dm-pane { display: flex; flex-direction: column; min-height: 480px; max-height: 75vh; }

/* Messages: collapse the channel sidebar into a select on narrow screens */
/* =========================================================================
   3. map — MapLibre GL JS (map-rework). MapLibre's own CSS is third-party and
      can only be themed by targeting its classes (.maplibregl-*). Markers/
      clusters are canvas images now, not DOM/CSS. Legend/panel/overlays below.
   ========================================================================= */
#view-map { height: 100%; position: relative; }
#map { height: 100%; background: var(--tblr-bg-surface); }
.map-missing { padding: 40px; text-align: center; color: var(--tblr-secondary-color); }

/* Placement mode (waypoint / node position / ops drop / marker / coordinate pin):
   crosshair cursor + a banner across the top of the map prompting the operator to
   click where the pin should drop. The class-based cursor beats the per-layer hover
   handlers, which would otherwise flip it back to a pointer over a node marker —
   while armed, the click places, so the cursor must say so everywhere. */
#map.map-placing .maplibregl-canvas { cursor: crosshair !important; }
.wp-place-banner {
  /* Sits BELOW the top-center coordinate readout (.map-coordbox, top:10px) so the
     two don't stack on top of each other during placement. */
  position: absolute; top: 52px; left: 50%; transform: translateX(-50%);
  z-index: 1000; display: flex; align-items: center;
  background: var(--tblr-primary); color: var(--tblr-primary-fg); font-weight: 600;
  padding: 6px 12px; border-radius: var(--tblr-border-radius);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .5);
}
.wp-icon-btn { font-size: 1rem; line-height: 1; padding: 4px 7px; }
.wp-icon-btn.active { border-color: var(--tblr-primary); background: var(--tblr-primary-lt); }

/* Follow-node chip — same look as .wp-place-banner, stacked below it (top: 96px
   vs 52px) so an armed placement and an active follow can never overlap. */
.map-follow-chip {
  position: absolute; top: 96px; left: 50%; transform: translateX(-50%);
  z-index: 1000; display: flex; align-items: center;
  background: var(--tblr-primary); color: var(--tblr-primary-fg); font-weight: 600;
  padding: 6px 12px; border-radius: var(--tblr-border-radius);
  box-shadow: 0 4px 16px rgba(0, 0, 0, .5);
}

/* MapLibre GL controls (map-rework) — themed to match the controls overlay/
   legend/panel: dark surface, themed border/radius/shadow. MapLibre's own CSS
   loads AFTER custom.css, so the few visual overrides need !important to win on
   load order. */
.maplibregl-map { font-family: inherit; }
.maplibregl-ctrl-group {
  background: var(--tblr-bg-surface) !important;
  border: var(--tblr-border-width) solid var(--tblr-border-color) !important;
  border-radius: var(--tblr-border-radius) !important;
  box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5)) !important;
}
/* Match the right-side tool rail's 38px button footprint so the zoom/compass
   stack lines up with the rail instead of looking narrower. */
.maplibregl-ctrl-group button { background: var(--tblr-bg-surface) !important; width: 38px !important; height: 38px !important; }
.maplibregl-ctrl-group button:hover { background: var(--tblr-bg-surface-secondary) !important; }
.maplibregl-ctrl-group button + button { border-top: 1px solid var(--tblr-border-color) !important; }
/* The built-in zoom/compass glyphs are dark SVGs baked into the CSS; invert them
   so they read on the dark surface. */
.maplibregl-ctrl-group button .maplibregl-ctrl-icon { filter: invert(1) brightness(1.4); }
.maplibregl-ctrl-attrib {
  background: rgba(15, 16, 23, .7) !important; color: var(--tblr-secondary-color) !important;
}
.maplibregl-ctrl-attrib a { color: var(--tblr-body-color) !important; }
.maplibregl-ctrl-attrib-button { filter: invert(1) brightness(1.4); }

/* MapLibre node/estimate hover tooltip (popup with class "nodetip") — themed to
   match the old Leaflet .nodetip: dark surface, themed border, small data rows. */
.maplibregl-popup.nodetip .maplibregl-popup-content {
  background: var(--tblr-bg-surface); color: var(--tblr-body-color);
  border: 1px solid var(--tblr-border-color); border-radius: var(--tblr-border-radius);
  box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5));
  font-family: inherit; font-size: .8438rem; padding: 7px 10px;
}
.maplibregl-popup.nodetip .ti { width: 1.1em; margin-right: 5px; color: var(--tblr-secondary-color); vertical-align: -1px; }
.maplibregl-popup.nodetip .cb-id { font-size: .6563rem; color: var(--tblr-secondary-color); }
/* The popup's little pointer triangle inherits the surface fill. */
.maplibregl-popup.nodetip .maplibregl-popup-tip { border-top-color: var(--tblr-bg-surface); border-bottom-color: var(--tblr-bg-surface); }

/* Waypoint click-popup (Edit/Delete) — dark surface + themed close button. */
.maplibregl-popup.wp-popup .maplibregl-popup-content {
  background: var(--tblr-bg-surface); color: var(--tblr-body-color);
  border: 1px solid var(--tblr-border-color); border-radius: var(--tblr-border-radius);
  box-shadow: var(--tblr-box-shadow-dropdown, 0 6px 22px rgba(0, 0, 0, .5));
  font-family: inherit; padding: 10px 12px;
}
.maplibregl-popup.wp-popup .maplibregl-popup-tip { border-top-color: var(--tblr-bg-surface); border-bottom-color: var(--tblr-bg-surface); }
.maplibregl-popup.wp-popup .maplibregl-popup-close-button { color: var(--tblr-secondary-color); font-size: 1.125rem; padding: 0 6px; }
.maplibregl-popup.wp-popup .maplibregl-popup-close-button:hover { color: var(--tblr-body-color); background: none; }

/* Node pills, cluster bubbles, and estimate pins are all rasterized to canvas
   images (map.js) and drawn by MapLibre symbol layers — no DOM/CSS markers, so
   the old .nodepin / .marker-cluster rules are gone. (Activity-pulse on RX is
   parked for Batch 5; a canvas/opacity pulse will replace the old .nodepin-rx.) */

/* Operator drawing tools (terra-draw) — a wrap grid of icon buttons inside the
   Draw fly-out of the right-side tool rail (see .map-rail below). */
.map-draw { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px 10px; max-width: 184px; }
.map-draw button {
  width: 38px; height: 38px; display: flex; align-items: center; justify-content: center;
  background: var(--tblr-bg-surface-secondary); border: 1px solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius); color: var(--tblr-body-color); cursor: pointer;
  font-size: 1.0625rem; line-height: 1;
}
.map-draw button:hover:not(:disabled) { border-color: var(--tblr-primary); color: var(--tblr-primary); }
.map-draw button.active { background: var(--tblr-primary); color: var(--tblr-primary-fg); border-color: var(--tblr-primary); }
.map-draw button:disabled { opacity: .4; cursor: default; }

/* Hop-count legend — a popup anchored above the go-to bar's hops button (its
   containing block is the absolutely-positioned .map-gotobar). It used to be a
   standalone bottom-right overlay, which partly covered the go-to box on phone. */
.map-legend {
  position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 1000;
  background: var(--tblr-bg-surface); border: 1px solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius); padding: 9px 12px;
  font-family: var(--tblr-font-monospace); font-size: .7188rem; color: var(--tblr-secondary-color);
  box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5)); line-height: 1.7;
}
.map-legend b { display: block; margin-bottom: 2px; color: var(--tblr-body-color); font-size: .6875rem; letter-spacing: .08em; }
.map-legend span { display: block; }
.map-legend i { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; }

/* Empty-state overlay shown when there are no nodes to plot and no link.
   Sits above the (still-mounted) MapLibre map but below the node panel. */
.map-empty {
  position: absolute; inset: 0; z-index: 900;
  display: flex; align-items: center; justify-content: center;
  background: var(--tblr-body-bg); pointer-events: none;
}

/* Node-list overlay panel */
.map-panel {
  position: absolute; top: 10px; left: 10px; z-index: 1000;
  background: var(--tblr-bg-surface); border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius); box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5));
  max-height: 70%; display: flex; flex-direction: column;
}
.map-panel-toggle {
  background: none; border: 0; color: var(--tblr-body-color); cursor: pointer;
  padding: 7px 10px; font-size: 1rem; line-height: 1; border-radius: var(--tblr-border-radius); align-self: flex-start;
}
.map-panel-toggle:hover { color: var(--tblr-primary); }
.map-panel-body { display: flex; flex-direction: column; min-height: 0; padding: 0 0 8px; }
.map-panel-head {
  font-family: var(--tblr-font-monospace); font-size: .6563rem; font-weight: 700;
  color: var(--tblr-secondary-color); text-transform: uppercase; letter-spacing: .1em;
  padding: 0 10px 6px; border-bottom: 1px solid var(--tblr-border-color);
}
.map-panel-head span { color: var(--tblr-body-color); font-weight: 400; margin-left: 6px; }
.map-panel-list { overflow-y: auto; min-height: 0; max-height: calc(70vh - 80px); }

/* Coordinate probe — top-center live cursor readout + a single temp pin.
   Live: tracks the cursor in the operator's coord format. Frozen: shows the
   pinned point with Copy + clear. (Map batch 2C.) */
.map-coordbox {
  position: absolute; top: 10px; left: 50%; transform: translateX(-50%); z-index: 1000;
  background: var(--tblr-bg-surface); border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius); box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5));
  padding: 4px 8px; font-size: .75rem; line-height: 1.25; pointer-events: auto;
  display: flex; align-items: center; gap: 8px; max-width: min(92vw, 440px);
}
.map-coordbox .cb-ico { color: var(--tblr-primary); font-size: .9375rem; flex: none; }
.map-coordbox .cb-main { font-family: var(--tblr-font-monospace); font-weight: 600; white-space: nowrap; }
.map-coordbox .cb-dec { font-family: var(--tblr-font-monospace); color: var(--tblr-secondary-color); font-size: .6563rem; white-space: nowrap; }
.map-coordbox button { flex: none; }
/* Probe pin = MapLibre's built-in color-tinted SVG Marker (no custom CSS). */

/* Selected symbol in the marker picker grid */
.sym-pick.active { outline: 2px solid var(--tblr-primary); outline-offset: -1px; background: var(--tblr-primary-lt); }

/* Right-side TOOL RAIL — a vertical strip of icon buttons (Layers, Draw, …),
   top-right below the zoom control. Each button opens a fly-out panel to the
   LEFT so the rail stays a compact strip on the right edge; only one panel open
   at a time. Extensible: drop in another .map-rail-item for the next tool group. */
.map-rail {
  /* Sit directly below the zoom + compass NavigationControl, lined up with it.
     The control = 3 buttons × 38px + borders, starting at the 10px top margin
     (≈128px tall), so top:136px leaves the same 8px gap the rail uses between its
     own buttons. right:10px matches the control's margin so both right edges align. */
  position: absolute; top: 136px; right: 10px; z-index: 1000;
  display: flex; flex-direction: column; gap: 8px;
}
.map-rail-item { position: relative; }
.map-rail-item[hidden] { display: none; }
.map-rail-btn {
  width: 38px; height: 38px; display: flex; align-items: center; justify-content: center;
  background: var(--tblr-bg-surface); border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius); box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5));
  color: var(--tblr-body-color); cursor: pointer; font-size: 1.125rem; line-height: 1;
}
.map-rail-btn:hover { background: var(--tblr-bg-surface-secondary); color: var(--tblr-primary); }
.map-rail-btn.active { background: var(--tblr-primary); color: var(--tblr-primary-fg); border-color: var(--tblr-primary); }
.map-flyout {
  position: absolute; top: 0; right: calc(100% + 8px); z-index: 1001;
  min-width: 188px; max-width: min(78vw, 280px);
  max-height: calc(100vh - 110px); overflow-y: auto;
  background: var(--tblr-bg-surface); border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius); box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5));
  padding: 0 0 8px;
}
.map-flyout[hidden] { display: none; }
/* Full-width action buttons in the controls fly-out (Add waypoint / Refresh map /
   Export / Download area) — block-level + side margins so they sit inset from the
   card edges like the section heads/rows, not flush against them. The [hidden]
   guard keeps the non-admin Download-area button hidden (block would override it). */
.map-flyout > .btn,
.map-sec-body > .btn { display: block; width: auto; margin: 6px 10px; }
.map-flyout > .btn[hidden],
.map-sec-body > .btn[hidden] { display: none; }
.map-ctrl-opt {
  display: flex; align-items: center; gap: 8px; padding: 6px 12px;
  cursor: pointer; font-size: .8125rem; color: var(--tblr-body-color);
}
.map-ctrl-opt:hover { background: var(--tblr-bg-surface-secondary); }
.map-ctrl-opt input { cursor: pointer; flex: none; }
/* Collapsible section header (BASEMAP, OVERLAYS) — acts like .map-panel-head but toggles. */
.map-sec-toggle {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%;
  background: none; border: 0; border-bottom: 1px solid var(--tblr-border-color); cursor: pointer;
  font-family: var(--tblr-font-monospace); font-size: .6563rem; font-weight: 700;
  color: var(--tblr-secondary-color); text-transform: uppercase; letter-spacing: .1em;
  padding: 9px 10px 6px;
}
.map-sec-toggle:hover { color: var(--tblr-primary); }
.map-sec-count { color: var(--tblr-body-color); font-weight: 400; }
.map-sec-chev { transition: transform .15s ease; font-size: .8125rem; }
.map-sec-toggle.collapsed .map-sec-chev { transform: rotate(-90deg); }
.map-overlays-list { max-height: 168px; overflow-y: auto; }
.map-overlays-list[hidden] { display: none; }
/* Collapsible layer-group bodies (MESH / TERRAIN / REFERENCE / DISPLAY). */
.map-sec-body[hidden] { display: none; }
/* Small note under a fly-out (e.g. Weather "needs internet"). */
.map-panel-hint { padding: 4px 12px 2px; font-size: .6875rem; color: var(--tblr-secondary-color); }
#map-basemaps[hidden] { display: none; }
/* Filters live at the top of the node list now (out of the controls flyout). */
.map-panel-filters { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--tblr-border-color); }
.map-panel-filters .form-select, .map-panel-filters .form-control { font-size: .8125rem; }
/* GO TO is its own bottom-center search bar (broken out of the controls flyout). */
/* Tile-download progress, pinned directly above the GO TO bar (which is
   bottom:14px and ~38px tall, hence bottom:60px). Persistent while a download
   runs, so dismissing the progress modal no longer leaves an invisible,
   uncancellable job — clicking this reopens the modal. Shares the GO TO bar's
   width + centering so the two read as one bottom stack. */
.map-dlbar {
  position: absolute; left: 50%; bottom: 60px; transform: translateX(-50%);
  z-index: 1000; width: min(92vw, 340px);
  display: flex; flex-direction: column; gap: 4px; text-align: left;
  background: var(--tblr-bg-surface); border: var(--tblr-border-width) solid var(--tblr-border-color);
  border-radius: var(--tblr-border-radius); box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5));
  padding: 6px 10px 8px; font-size: .75rem; line-height: 1.25; color: var(--tblr-body-color);
}
/* Author [hidden] rule is REQUIRED: the display:flex above outranks the UA
   [hidden] rule, so without this the bar would never hide. Same trap as
   .map-flyout / .map-rail-item / .map-sec-body above. */
.map-dlbar[hidden] { display: none; }
.map-dlbar:hover { border-color: var(--tblr-primary); }
.map-dlbar .map-dlbar-head { display: flex; align-items: center; gap: 6px; }
.map-dlbar .ti { color: var(--tblr-primary); font-size: .875rem; flex: none; }
.map-dlbar .map-dlbar-label { font-weight: 600; }
.map-dlbar .map-dlbar-pct { margin-left: auto; font-family: var(--tblr-font-monospace); color: var(--tblr-secondary-color); }
.map-dlbar .progress { height: 4px; width: 100%; }
.map-gotobar { position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); z-index: 1000; width: min(92vw, 340px); }
.map-gotobar .input-group { box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5)); }
.map-gotobar .form-control, .map-gotobar .input-group-text, .map-gotobar .btn { background: var(--tblr-bg-surface); }
/* "You are here" pin from browser geolocation — a blue dot with a pulsing ring,
   distinct from the green node pins and the primary-colored probe marker. */
.map-me-pin {
  width: 16px; height: 16px; border-radius: 50%;
  background: #2196f3; border: 2px solid #fff;
  box-shadow: 0 0 0 2px rgba(33,150,243,.5), 0 1px 4px rgba(0,0,0,.5);
  position: relative;
}
.map-me-pin::after {
  content: ""; position: absolute; inset: -6px; border-radius: 50%;
  border: 2px solid rgba(33,150,243,.6); animation: map-me-pulse 2s ease-out infinite;
}
@keyframes map-me-pulse {
  0% { transform: scale(.6); opacity: .9; }
  100% { transform: scale(1.8); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) { .map-me-pin::after { animation: none; } }
.mp-entry { display: flex; align-items: center; gap: 8px; padding: 6px 10px; cursor: pointer; font-size: .8125rem; border-bottom: 1px solid var(--tblr-border-color); }
.mp-entry:last-child { border-bottom: 0; }
.mp-entry:hover { background: var(--tblr-bg-surface-secondary); }
.mp-dot { width: 9px; height: 9px; border-radius: 3px; flex: none; }
.mp-names { min-width: 0; flex: 1; }
.mp-names b { font-family: var(--tblr-font-monospace); font-size: .75rem; font-weight: 700; display: block; }
.mp-names span { font-size: .75rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; color: var(--tblr-secondary-color); }
.mp-time { font-family: var(--tblr-font-monospace); font-size: .6875rem; color: var(--tblr-secondary-color); white-space: nowrap; }
.mp-stats { font-family: var(--tblr-font-monospace); font-size: .6563rem; color: var(--tblr-secondary-color); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Channel-share QR fills its bordered box */
#share-qr svg { width: 100%; height: 100%; display: block; }

/* =========================================================================
   4. hand-rolled modals (confirmModal / pickers / reboot-watch)
   =========================================================================
   These modals inject a <div class="modal"> plus a sibling
   <div class="modal-backdrop"> into #modal-root. Despite the modal's higher
   z-index (1055 > backdrop 1050), the sibling backdrop can still paint on top
   and intercept EVERY click — the modal looks open but nothing is clickable and
   it can't be dismissed without a page refresh (the frozen "Get Telemetry"
   modal). The backdrop here is purely a visual dim: click-outside-to-close is
   handled by the .modal element's own click handler, not the backdrop. So make
   the injected backdrop click-transparent — clicks always reach the modal. */
#modal-root .modal-backdrop { pointer-events: none; }

/* =========================================================================
   5. Messages — active/selected channel
   =========================================================================
   The selected channel/DM in the list used Bootstrap's default flat blue
   .active fill, which ignores the operator's chosen theme color and reads
   weakly. Tie it to the theme accent and make it unmistakably selected: a
   solid primary fill plus a left accent bar.

   Text is --tblr-primary-fg, NOT white. Since v0.13.102 the accent is derived
   per mode, and a dark-mode accent is a LIGHT tone — white on it is unreadable
   (mesh green measured 1.17:1). --tblr-primary-fg is the accent's paired
   foreground and is correct for every seed in both modes. The muted index span
   (CHx) is the same color at 80%, mixed rather than a fixed rgba() so it tracks
   the accent instead of assuming a dark fill. */
.chan-item.active {
  background-color: var(--tblr-primary);
  border-color: var(--tblr-primary);
  color: var(--tblr-primary-fg);
  box-shadow: inset 3px 0 0 rgba(0, 0, 0, .3);
}
.chan-item.active .text-secondary {
  color: color-mix(in oklab, var(--tblr-primary-fg) 80%, transparent) !important;
}
/* Mission channel — ONLY while a mission is active (the channel picked at mission
   start). Orange left bar + tint so the operator can spot the mission channel
   (the default TX target) even while reading another conversation. When it's also
   the selected conversation the green .active fill shows, keeping the orange bar. */
.chan-item.chan-mission { box-shadow: inset 3px 0 0 var(--tblr-orange); }
.chan-item.chan-mission:not(.active) {
  background-color: rgba(var(--tblr-orange-rgb), .12);
}

/* =========================================================================
   6. Stat-hint help badge — dark-theme fix for Tabler's .form-help
   =========================================================================
   OVERRIDE, not a reimplementation: the component is Tabler's (statHint() in
   app.js), we only recolor it.

   Tabler hardcodes `.form-help{background:var(--tblr-gray-100)}` and NEVER
   redefines --tblr-gray-100 for dark mode — it stays #f3f4f6 in every theme. On
   our dark surfaces that renders as a solid white disc, which is how it was
   reported. The glyph is a passive affordance beside a stat label, so it should
   read as a muted outline and only assert itself on interaction.

   Deliberately keyed to Tabler's own tokens rather than fixed colors, so this
   tracks the theme picker (and light mode, where a subtle tint is still right). */
.form-help {
  background: transparent;
  border: 1px solid var(--tblr-border-color);
  color: var(--tblr-secondary-color);
  font-weight: 500;
}
.form-help:hover,
.form-help[aria-describedby] {
  background: var(--tblr-primary);
  border-color: var(--tblr-primary);
  color: var(--tblr-primary-fg);
}

/* =========================================================================
   7. Badge icon spacing
   =========================================================================
   Badges that carry a leading Tabler glyph (FAV / IGN / MUTE / CACHED, the key
   and telemetry badges) were written with the `me-1` utility, which is 0.25rem
   — sized for body text, not for a .75rem badge. At badge scale it reads as a
   stray space before the label, which is exactly how it was reported ("why does
   the FAV badge have a space before the letter F?"). SELF has no icon, so it
   looked correctly tight beside them and made the gap obvious.

   Fixed HERE rather than at the ~8 call sites so every current and future
   icon-bearing badge matches — a duplicated UI mechanic is the thing the
   define-once discipline exists to prevent (OMS DESIGN §4).

   `em` not `rem`: the gap then scales with whatever font-size the badge is
   rendered at. `!important` IS required — Bootstrap's spacing utilities are
   themselves `!important`, so a plain rule loses to `me-1` regardless of
   specificity. */
.badge > i.ti { margin-inline-end: .2em !important; }

/* =========================================================================
   8. Empty-state illustrations
   =========================================================================
   OVERRIDE/extension: Tabler ships `.empty-img` but only sets its margin — it
   assumes an <img> child that carries its own dimensions. We inline the SVG
   instead (static/js/illustrations.js; an <img> would sandbox the illustration's
   embedded theme styles and freeze it in its light palette), and the vendor step
   strips width/height from the root <svg> so it can scale to its wrapper. That
   leaves nothing sizing it, so an unstyled inline SVG would render at the UA
   default rather than the intended size.

   max-width matches Tabler's own empty-state art so illustrated and icon-based
   empty states sit at the same visual weight. */
.empty-img svg {
  width: 100%;
  max-width: 232px;
  height: auto;
}

/* Tighter inside a card or a table row, where a full-size illustration would
   dominate content that is only incidentally empty. */
.card .empty-img svg { max-width: 180px; }
td .empty-img svg    { max-width: 140px; }

/* =========================================================================
   9. Tooltips — theme-following recolor of Bootstrap/Tabler's tooltip
   =========================================================================
   OVERRIDE, not a reimplementation: the component and all its behaviour are
   Bootstrap's (one delegated instance, see initTooltips in app.js); only the
   palette changes.

   Tabler ships `--tblr-tooltip-bg: var(--tblr-bg-surface-inverted)` and
   `--tblr-tooltip-color: var(--tblr-text-inverted)` — a tooltip is INVERTED
   against the surface by design, so in our dark theme it renders as a white
   bubble with dark text. That is Tabler working as intended, not a bug, but it
   reads as a foreign element next to the .form-help popovers, which use surface
   colors. Operator asked for the two to match.

   Set the CUSTOM PROPERTIES rather than the resolved colors: Bootstrap derives
   both `.tooltip-inner`'s background AND the arrow's border-color from
   --tblr-tooltip-bg, so overriding the token keeps the arrow in step for free.
   Hardcoding .tooltip-inner instead would leave a white arrow behind. Opacity
   goes to 1 — Tabler's 0.9 lets the map show through a surface-colored bubble.

   custom.css loads after tabler.min.css and the selector matches Tabler's own,
   so this wins on order alone; no !important needed. */
.tooltip {
  --tblr-tooltip-bg: var(--tblr-bg-surface);
  --tblr-tooltip-color: var(--tblr-body-color);
  --tblr-tooltip-opacity: 1;
}
/* Surface-on-surface needs a lift to separate from the map / cards underneath.
   The arrow inherits the fill but cannot carry the shadow, which is why the
   border lives on the bubble only — a bordered arrow would need a second
   rotated element and buys nothing at this size. */
.tooltip .tooltip-inner {
  border: var(--tblr-border-width) solid var(--tblr-border-color);
  box-shadow: var(--tblr-box-shadow-dropdown, 0 4px 14px rgba(0, 0, 0, .5));
}
