/*
 * Copyright © 2025-26 l5yth & contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * The app is dark-only. These tokens are the (formerly ``body.dark``) dark
 * palette, promoted to ``:root`` so every element — including ``html`` itself —
 * resolves them; the previous light palette and its always-overridden
 * ``body.dark`` token block were removed with the light theme.
 */
:root {
  --bg: #0e1418;
  --bg2: #0e141b;
  --fg: #e6ebf0;
  --muted: #9aa7b4;
  --card: rgba(255, 255, 255, 0.04);
  --line: rgba(255, 255, 255, 0.1);
  --accent: #5fa8ff;
  --row-alt: rgba(255, 255, 255, 0.05);
  /* Opaque: the former rgba(255, 255, 255, 0.06) over --bg, so the sticky
     header rows hide the rows scrolled beneath them (SPEC GF4). */
  --table-head-bg: #1c2226;
  --table-head-fg: var(--fg);
  --input-bg: #1e262c;
  --input-fg: var(--fg);
  --input-border: rgba(230, 235, 240, 0.24);
  --input-placeholder: rgba(230, 235, 240, 0.55);
  --control-accent: var(--accent);
  --announcement-bg: #3b2500;
  --announcement-fg: #ffd184;
  --announcement-border: #a56a00;
  --danger: #ff6b6b;
  /* One grey family (SPEC GF1): the raised surface of chat, footer, dialogs
     and popovers, the hairline of surfaces and controls, secondary text
     between --fg and --muted, and the faint text below --muted (the chat's
     radio code, SPEC LA5; 4.56:1 on --panel, UX2). */
  --panel: #151c22;
  --line-strong: rgba(255, 255, 255, 0.14);
  --fg-faint: #788592;
  --fg-soft: #c9d1d9;
  /* Map overlay chrome over the tiles: the panel at 92% and its hairline. */
  --overlay-bg: color-mix(in srgb, var(--panel) 92%, transparent);
  --overlay-line: var(--line-strong);
  /* Alias layer (SPEC UX3): names consumed by the federation styles resolve
     to the canonical tokens instead of silently failing (audit D-009). */
  --border: var(--line);
  --surface: var(--bg2);
  --table-header-bg: var(--table-head-bg);
  --hover-bg: var(--row-alt);
  --pad: 16px;
}

/* --overlay-bg for engines without color-mix() (SPEC SR3): there the mix above
   is invalid at computed-value time and every overlay paints transparent. A
   literal declared first in :root cannot help, since both values are valid
   custom properties and the later one wins. This is --panel at 92%. */
@supports not (color: color-mix(in srgb, #000 50%, transparent)) {
  :root {
    --overlay-bg: rgba(21, 28, 34, 0.92);
  }
}

html {
  color-scheme: dark;
}

html,
body {
  background-color: var(--bg);
  color: var(--fg);
  transition: background-color 160ms ease, color 160ms ease;
}

a {
  color: var(--accent);
}

hr {
  border-color: var(--line);
}

.card,
.panel,
.box {
  background: var(--card);
  backdrop-filter: blur(2px);
  border: 1px solid var(--line);
  border-radius: 10px;
}

table {
  border-collapse: collapse;
  width: 100%;
  border: 1px solid var(--line);
}

thead th {
  background: var(--table-head-bg);
  color: var(--table-head-fg);
  text-align: left;
  border-bottom: 1px solid var(--line);
  padding: 8px;
}

tbody td {
  padding: 8px;
  border-bottom: 1px solid var(--line);
}

tbody tr:nth-child(even) td {
  background: var(--row-alt);
}

.leaflet-container {
  background: transparent !important;
  color: var(--fg);
}

.neighbor-connection-line {
  cursor: pointer;
}

.trace-connection-line {
  stroke-dasharray: 6 6;
}

/*
 * Spider leader line drawn from each co-located marker back to its shared
 * coordinate.  Stroke uses the foreground token so the line stays visible
 * against both light and dark basemaps; opacity keeps it subdued enough to
 * read as a hint rather than a primary connection.  `pointer-events: none`
 * matches the `interactive: false` Leaflet option so the line never
 * intercepts clicks intended for the marker beneath it.
 */
.colocated-spider-line {
  stroke: var(--fg);
  stroke-width: 1;
  stroke-opacity: 0.5;
  stroke-dasharray: 1 1;
  fill: none;
  pointer-events: none;
}

/*
 * Asterisk + count badge that represents a collapsed (or expanded-but-still-
 * present) co-located node group.  The Leaflet ``L.divIcon`` host element
 * receives the ``.colocated-spider-hub`` class; the inner ``__glyph`` span
 * carries the visible circle so the cursor can still flip back to the
 * default state outside the badge.  Theming is delegated to the existing
 * ``--fg`` / ``--bg`` tokens so the badge blends with both light and dark
 * basemaps.
 */
.colocated-spider-hub {
  cursor: pointer;
  background: transparent;
  border: 0;
}

.colocated-spider-hub__glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--fg);
  font-size: 10px;
  font-weight: 600;
  line-height: 1;
  user-select: none;
}

.leaflet-tooltip.trace-tooltip {
  background: var(--bg2);
  color: var(--fg);
  border: 1px solid var(--line);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
  padding: 6px 8px;
  font-size: 13px;
}

.trace-tooltip__content {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.trace-tooltip__arrow {
  opacity: 0.7;
}

.neighbor-snr {
  margin-left: 4px;
  color: var(--muted);
  font-size: 12px;
}

body {
  font-family: system-ui, Segoe UI, Roboto, Ubuntu, Arial, sans-serif;
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.page-shell {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  padding: var(--pad);
  padding-bottom: calc(96px + var(--pad));
  gap: var(--pad);
}

/* A full-screen shell keeps the dashboard's top gutter, inside the 100vh it
   fills (border-box), so the banner and the header start where the dashboard's
   do (SPEC SR1); the map, the chat panel and the nodes table stay full-bleed. */
.page-shell--full-screen {
  box-sizing: border-box;
  padding: var(--pad) 0 0;
  gap: var(--pad);
}

.page-shell--full-screen .site-header,
.page-shell--full-screen .app-footer {
  padding-left: var(--pad);
  padding-right: var(--pad);
}

.page-shell--full-screen .app-footer {
  padding-top: var(--pad);
  padding-bottom: var(--pad);
}

/* The banner and the controls row take the side gutter (SPEC SR1). */
.page-shell--full-screen .announcement-banner {
  margin: 0 var(--pad);
}

.page-shell--full-screen .meta-controls {
  padding-left: var(--pad);
  padding-right: var(--pad);
}

.page-main {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: var(--pad);
  min-height: 0;
}

.page-main--dashboard {
  padding: 0;
}

.page-main--full-screen {
  padding: 0;
  gap: var(--pad);
}

h1 {
  margin: 0;
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  gap: 16px;
  min-height: 56px;
  padding: 4px 0;
  margin-bottom: 8px;
}

.site-header__left,
.site-header__right {
  display: flex;
  align-items: center;
  gap: 12px;
}

/* The title and the region toggle share one row (SH6). When the open region
   select does not fit beside the title, it wraps onto its own line instead of
   squeezing the title. */
.site-header__left {
  flex: 1 1 auto;
  min-width: 0;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.site-header__right {
  flex: 0 0 auto;
  margin-left: auto;
}

/* Header glyphs (SH5): inline SVG at their drawn size. As flex items they
   would otherwise shrink into the generic button reset's padding. */
.icon-button > svg {
  flex: none;
}

.announcement-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  height: 1.6em;
  padding: 0 var(--pad);
  border-radius: 999px;
  background: var(--announcement-bg);
  color: var(--announcement-fg);
  border: 1px solid var(--announcement-border);
  box-sizing: border-box;
  overflow: hidden;
}

.announcement-banner__content {
  margin: 0;
  line-height: 1.6;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Banner links keep the banner's amber, underlined (SH4): the page accent
   blue read as a second colour on the amber pill. */
.announcement-banner a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.site-title {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
}

.site-title__link {
  display: inline-flex;
  align-items: center;
  gap: inherit;
  color: inherit;
  text-decoration: none;
  min-width: 0;
}

.site-title-text {
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.site-title img {
  width: 36px;
  height: 36px;
  display: block;
  border-radius: 12px;
}

.site-nav {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.site-nav__link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 12px;
  border-radius: 999px;
  color: var(--fg);
  text-decoration: none;
  border: 1px solid transparent;
  font-size: 14px;
}

.site-nav__link:hover {
  background: var(--card);
}

.site-nav__link.is-active {
  border-color: var(--accent);
  color: var(--accent);
  background: transparent;
  font-weight: 600;
}

.site-nav__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Compound selector required: .icon-button { display: inline-flex } (below) shares
   the same specificity and appears later in the source, so it would otherwise win
   and keep the toggle permanently visible above the 900px breakpoint. */
.icon-button.menu-toggle {
  display: none;
}

.menu-toggle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.mobile-menu {
  position: fixed;
  inset: 0;
  /* Above the footer (z-index 4100, which a flex item keeps even where it is
     static): the footer painted over the open menu and hid its Pages group
     (SH2). */
  z-index: 4200;
  display: flex;
  justify-content: flex-end;
  pointer-events: none;
}

.mobile-menu[hidden] {
  display: none;
}

.mobile-menu__backdrop {
  flex: 1 1 auto;
  background: rgba(0, 0, 0, 0.4);
  opacity: 0;
  transition: opacity 200ms ease;
}

.mobile-menu__panel {
  width: min(320px, 86vw);
  background: var(--bg2);
  color: var(--fg);
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  /* The panel fills the viewport height by stretching, padding included
     (SH2): `height: 100%` added the padding below the viewport, so a panel
     that scrolls hid the end of its last link. */
  overflow-y: auto;
  transform: translateX(100%);
  transition: transform 220ms ease;
  box-shadow: -12px 0 32px rgba(0, 0, 0, 0.3);
}

.mobile-menu__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

/* 44 px close target (SH7). Compound selector: `.icon-button` (36 px) has the
   same specificity and comes later in the file. */
.icon-button.mobile-menu__close {
  width: 44px;
  height: 44px;
}

.mobile-menu__title {
  margin: 0;
  font-size: 16px;
}

.mobile-menu__close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.mobile-nav {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* The Pages group (SH2): static pages, GitHub and the chat link, which the
   phone footer no longer shows, under a hairline below the product views. */
.mobile-nav--pages {
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

/* 10 px padding over the 22 px line and the 2 px border make a 44 px target
   (SH7); no min-height, which would add to the padding without border-box. */
.mobile-nav__link {
  display: inline-flex;
  align-items: center;
  padding: 10px 12px;
  border-radius: 10px;
  color: var(--fg);
  text-decoration: none;
  border: 1px solid transparent;
}

.mobile-nav__link.is-active {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.mobile-nav__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.mobile-menu.is-open {
  pointer-events: auto;
}

.mobile-menu.is-open .mobile-menu__backdrop {
  opacity: 1;
}

.mobile-menu.is-open .mobile-menu__panel {
  transform: translateX(0);
}

.menu-open {
  overflow: hidden;
}

.section-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--fg);
  text-decoration: none;
  font-size: 14px;
}

.section-link:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.section-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.meta {
  color: var(--muted);
  margin-bottom: 12px;
}

.instance-selector {
  display: flex;
  align-items: center;
}

.instance-select {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-color: var(--input-bg);
  color: var(--input-fg);
  border: 1px solid var(--input-border);
  border-radius: 8px;
  padding: 6px 32px 6px 12px;
  font-size: 14px;
  line-height: 1.4;
  min-width: 220px;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 18px) calc(50% - 4px), calc(100% - 12px) calc(50% - 4px);
  background-size: 6px 6px, 6px 6px;
  background-repeat: no-repeat;
}

.instance-select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 1100px) {
  .site-header {
    margin-bottom: 4px;
  }

  .site-nav {
    display: none;
  }

  .icon-button.menu-toggle {
    display: inline-flex;
  }

  .instance-selector {
    flex: 0 1 auto;
  }

  .instance-select {
    width: 100%;
  }

  .instance-select {
    min-width: 0;
  }
}


.pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--line-strong);
  font-size: 12px;
}

#map {
  position: relative;
  /* The border stays inside the 100% width: /map scrolled sideways 2px (SPEC GF6). */
  box-sizing: border-box;
  width: 100%;
  height: 60vh;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  overflow: hidden;
  display: block;
}

.map-panel.is-fullscreen,
.map-panel:fullscreen,
.map-panel:-webkit-full-screen,
.map-panel:-moz-full-screen,
.map-panel:-ms-fullscreen,
#map.is-fullscreen,
#map:fullscreen,
#map:-webkit-full-screen,
#map:-moz-full-screen,
#map:-ms-fullscreen {
  width: 100vw;
  height: 100vh;
  min-width: 100vw;
  min-height: 100vh;
  max-width: 100vw;
  max-height: 100vh;
  margin: 0;
  border: none;
  border-radius: 0;
  background: #000;
}

.map-panel.is-fullscreen,
.map-panel:fullscreen,
.map-panel:-webkit-full-screen,
.map-panel:-moz-full-screen,
.map-panel:-ms-fullscreen {
  display: block;
}

.map-panel.is-fullscreen #map,
.map-panel:fullscreen #map,
.map-panel:-webkit-full-screen #map,
.map-panel:-moz-full-screen #map,
.map-panel:-ms-fullscreen #map,
#map.is-fullscreen,
#map:fullscreen,
#map:-webkit-full-screen,
#map:-moz-full-screen,
#map:-ms-fullscreen {
  height: 100vh !important;
  width: 100vw !important;
  min-height: 100vh;
  min-width: 100vw;
  max-height: 100vh;
  max-width: 100vw;
  border: none;
  border-radius: 0;
  flex: none;
}

.map-panel.is-fullscreen .map-toolbar,
.map-panel:fullscreen .map-toolbar,
.map-panel:-webkit-full-screen .map-toolbar,
.map-panel:-moz-full-screen .map-toolbar,
.map-panel:-ms-fullscreen .map-toolbar {
  top: 16px;
  right: 16px;
}

#map[data-map-status="placeholder"] {
  background: repeating-linear-gradient(
    135deg,
    rgba(255, 255, 255, 0.08),
    rgba(255, 255, 255, 0.08) 12px,
    rgba(255, 255, 255, 0.12) 12px,
    rgba(255, 255, 255, 0.12) 24px
  );
}

#map .map-placeholder-message {
  text-align: center;
  color: var(--fg);
  font-size: 14px;
  line-height: 1.5;
  padding: 0 16px;
}

#map .map-placeholder-message strong {
  display: block;
  margin-bottom: 6px;
  font-size: 16px;
}

#map .map-placeholder-message span {
  display: block;
  margin-top: 4px;
}


#map .map-status-message {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 12px;
  border-radius: 999px;
  font-size: 12px;
  line-height: 1.2;
  backdrop-filter: blur(6px);
  pointer-events: none;
  border: 1px solid var(--overlay-line);
  background: var(--overlay-bg);
  color: var(--fg);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  z-index: 1200;
}


/* Toolbar pills in the map overlay chrome (SPEC ML5). The generic button
   reset skips them, so this rule carries the whole pill and its pointer. */
.map-toolbar button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid var(--overlay-line);
  background: var(--overlay-bg);
  color: var(--fg);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  font-size: 13px;
  line-height: 1.2;
  cursor: pointer;
  transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease,
    box-shadow 160ms ease;
  pointer-events: auto;
}

.map-toolbar button:hover {
  background: rgba(52, 52, 52, 0.9);
}

table {
  border-collapse: collapse;
  width: 100%;
  margin: 0;
}

th,
td {
  padding: 4px 6px;
  text-align: left;
}

th {
  position: sticky;
  top: 0;
  background: var(--panel);
}

.mono {
  font-family: ui-monospace, Menlo, Consolas, monospace;
}

/* Waypoint detail-card meta line (SPEC W6, design 1d-C): the muted
   "<expiry> · by" prefix ahead of the author badge on the minimal card. */
.waypoint-card-meta {
  color: var(--muted, #9aa7b4);
}

.row {
  display: flex;
  gap: var(--pad);
  align-items: center;
  justify-content: space-between;
}

.map-row {
  display: flex;
  gap: var(--pad);
  align-items: stretch;
}

.full-screen-section {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  width: 100%;
}

.map-panel--full {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: 100%;
}

.map-panel--full #map {
  flex: 1 1 auto;
  height: 100%;
  min-height: 0;
}

.full-screen-section--map {
  flex: 1 1 auto;
  min-height: 0;
  align-items: stretch;
}

.full-screen-section--map .map-panel {
  flex: 1 1 auto;
}

.view-map .page-main--full-screen {
  gap: 0;
}

.view-nodes .page-main--full-screen {
  gap: 0;
}

.view-map .full-screen-section--map {
  flex: 1 1 auto;
  display: flex;
}

.view-map .full-screen-section--map .map-panel {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.view-map .full-screen-section--map .map-panel #map {
  flex: 1 1 auto;
  min-height: 0;
}

.view-nodes .full-screen-section--nodes {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

body.view-map .map-panel--full #map {
  height: 100%;
  min-height: 100%;
}

.chat-panel--full {
  flex: 1 1 auto;
  max-width: 100%;
  width: 100%;
  height: 100%;
  min-height: 0;
  max-height: 100%;
  overflow: hidden;
  /* The 1px border inside the 100% width, or the page clips the right
     border (SPEC CD8). */
  box-sizing: border-box;
}

.full-screen-section--chat .chat-panel {
  height: 100%;
}

body.view-chat {
  height: 100vh;
  overflow: hidden;
}

body.view-chat .page-shell {
  height: 100vh;
  min-height: 0;
  overflow: hidden;
}

.view-chat .page-main--full-screen {
  gap: 0;
}

.view-chat .full-screen-section--chat {
  flex: 1 1 auto;
  display: flex;
  min-height: 0;
}

.view-chat .full-screen-section--chat .chat-panel {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* Full-screen /chat type (SPEC CD8, design review rc3 C6): where the tab
   strip shows, the page-wide panel sets 13px type with roomier padding and
   caps a line at 120ch. Phones keep the dashboard's 12px. */
@media (min-width: 901px) {
  body.view-chat .chat-panel {
    font-size: 13px;
  }

  body.view-chat .chat-tabpanel {
    padding: 10px 14px;
  }

  body.view-chat .chat-entry-msg,
  body.view-chat .chat-entry-node {
    max-width: 120ch;
  }
}

.chat-panel--disabled {
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: var(--pad) calc(var(--pad) * 2);
  min-height: 50vh;
}

.chat-disabled-message {
  font-size: 1rem;
  color: var(--muted);
}

.nodes-table-wrapper {
  width: 100%;
}

.map-panel {
  position: relative;
  flex: 1 1 0%;
  min-width: 0;
  display: block;
}

.map-toolbar {
  position: absolute;
  top: 12px;
  right: 12px;
  display: flex;
  gap: 8px;
  z-index: 1300;
  pointer-events: none;
}

.map-toolbar button svg {
  width: 18px;
  height: 18px;
  display: block;
  fill: currentColor;
}

#mapFullscreenToggle .icon-fullscreen-exit {
  display: none;
}

#mapFullscreenToggle[aria-pressed='true'] .icon-fullscreen-exit {
  display: block;
}

#mapFullscreenToggle[aria-pressed='true'] .icon-fullscreen-enter {
  display: none;
}

.chat-panel {
  flex: 0 0 33%;
  max-width: 33%;
  height: 60vh;
  box-sizing: border-box;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  font-size: 12px;
  background: var(--panel);
  color: var(--fg);
}

.chat-panel.chat-panel--full {
  flex: 1 1 auto;
  max-width: 100%;
  width: 100%;
  height: 100%;
  min-height: 0;
}

/* Variant used inside the node detail page Messages section: full width,
   bounded height so the surrounding section does not turn into a single
   massive scroll.  The 60vh default of ``.chat-panel`` works for the
   dashboard sidebar but is too tall when stacked under telemetry charts. */
.chat-panel.chat-panel--node-detail {
  flex: 1 1 auto;
  max-width: 100%;
  width: 100%;
  height: auto;
  max-height: 60vh;
  min-height: 0;
}

/* Wrapper spans the full width and owns the bottom border so the arrow
   buttons are visually part of the tab bar. */
.chat-tablist-wrapper {
  display: flex;
  align-items: stretch;
  overflow: hidden;
  border-bottom: 1px solid var(--line-strong);
}

.chat-tablist {
  display: flex;
  gap: 4px;
  padding: 6px 6px 0;
  flex: 1;
  overflow-x: auto;
  scroll-behavior: smooth;
  scrollbar-width: none; /* Firefox */
}

.chat-tablist::-webkit-scrollbar {
  display: none; /* Chrome / Safari */
}

.chat-tab {
  flex: none; /* natural content width — no equal-stretch */
  white-space: nowrap;
  border: none;
  background: transparent;
  color: var(--fg);
  padding: 6px 8px;
  border-radius: 6px 6px 0 0;
  cursor: pointer;
  font-size: 12px;
  transition: background-color 120ms ease, color 120ms ease;
  border-color: transparent;
}

/* Scroll arrow buttons flanking the tab list */
.chat-tab-scroll-btn {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  background: var(--bg2);
  border: none;
  cursor: pointer;
  color: var(--muted);
  font-size: 10px;
  padding: 0;
  z-index: 1;
  transition: background-color 120ms ease;
}

.chat-tab-scroll-btn:hover {
  background: rgba(255, 255, 255, 0.1);
}

.chat-tab-scroll-btn[hidden] {
  display: none;
}

/* Channel dropdown selector (SPEC LV8): a compact jump-to-channel control at the
   end of the tab bar. Mirrors the federation/region `.instance-select` chrome
   (theme tokens + a CSS-gradient triangle) so the two dropdowns match, sized down
   to sit beside the channel tabs. */
.chat-tab-select {
  flex: 0 0 auto;
  align-self: center;
  margin-left: 0.5rem;
  max-width: 11rem;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-color: var(--input-bg);
  color: var(--input-fg);
  border: 1px solid var(--input-border);
  border-radius: 8px;
  padding: 4px 26px 4px 10px;
  font-size: 13px;
  line-height: 1.4;
  background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position: calc(100% - 14px) calc(50% - 3px), calc(100% - 9px) calc(50% - 3px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.chat-tab-select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.chat-tab:is(:focus-visible, :hover) {
  background: rgba(255, 255, 255, 0.1);
}

.chat-tab.is-active {
  background: var(--panel);
  color: var(--accent);
  border-bottom: 2px solid var(--accent);
  border-bottom-color: var(--accent);
}

.chat-tabpanels {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
}

.chat-tabpanel {
  flex: 1 1 auto;
  padding: 6px;
  overflow-y: auto;
  min-height: 0;
}

.chat-empty {
  margin: 12px 0;
  color: var(--muted);
  font-style: italic;
}

.chat-entry-node {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  color: var(--muted);
}

.chat-entry-reply {
  /* Contains its absolutely positioned .visually-hidden label, which would
     otherwise escape the scrolling panel (SPEC CD4). */
  position: relative;
  color: var(--muted);
}

.chat-entry-msg {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  color: var(--fg-soft);
}

.short-name {
  display: inline-block;
  border-radius: 4px;
  padding: 0 2px;
}

/* The badge's corners carry the protocol, as the map marker's shape does
   (SPEC LA5, UX7): a MeshCore badge is near-square everywhere it shows. */
.short-name[data-protocol="meshcore"] {
  border-radius: 1px;
}

.short-name[data-node-info] {
  cursor: pointer;
}

/* Touch target (audit follow-up c): the badge opens the node overlay on tap
   but is only ~15 px tall in the 12 px chat panel — under the 44 px mobile
   floor. On coarse pointers a transparent, centred ::after lifts the hit box
   to 44 px without changing the badge's look; fine pointers (mouse) keep the
   tight target so dense chat lines don't cross-capture clicks. */
@media (pointer: coarse) {
  .short-name[data-node-info] {
    position: relative;
  }

  .short-name[data-node-info]::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100%;
    height: 100%;
    min-width: 44px;
    min-height: 44px;
    transform: translate(-50%, -50%);
  }
}

.short-info-overlay {
  position: absolute;
  background: var(--panel);
  color: var(--fg);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
  padding: 8px 10px 10px;
  font-size: 11px;
  line-height: 1.4;
  min-width: 200px;
  max-width: 240px;
  z-index: 20000;
}

.short-info-overlay[hidden] {
  display: none;
}

.short-info-overlay .short-info-close {
  position: absolute;
  top: 4px;
  right: 4px;
  border: none;
  background: transparent;
  font-size: 14px;
  line-height: 1;
  padding: 2px;
  border-radius: 4px;
  cursor: pointer;
  color: inherit;
}

.short-info-overlay .short-info-close:hover {
  background: rgba(255, 255, 255, 0.1);
}

.short-info-sats {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-weight: inherit;
  color: inherit;
  padding: 0;
  margin: 0;
  vertical-align: middle;
}

.short-info-sats__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: currentColor;
}

.short-info-sats__glyph {
  display: block;
  filter: invert(1) grayscale(1);
}


.short-info-sats__count {
  font-family: inherit;
  font-size: inherit;
  letter-spacing: 0.1px;
}

.node-detail-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.65);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  z-index: 15000;
}

.node-detail-overlay[hidden] {
  display: none;
}

.node-detail-overlay__dialog {
  /* 90vw x 90vh include the padding and border, so the dialog keeps a margin
     of scrim on every side (SPEC GF5). */
  box-sizing: border-box;
  width: 90vw;
  height: 90vh;
  max-width: 1400px;
  max-height: 960px;
  background: var(--panel);
  border: 1px solid var(--line-strong);
  color: var(--fg);
  border-radius: 16px;
  box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
  padding: 32px 24px 24px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  position: relative;
}

.node-detail-overlay__close {
  position: absolute;
  top: 12px;
  right: 12px;
  border: none;
  background: rgba(255, 255, 255, 0.08);
  color: inherit;
  font-size: 18px;
  width: 32px;
  height: 32px;
  border-radius: 999px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  line-height: 1;
}

.node-detail-overlay__close:hover {
  background: rgba(255, 255, 255, 0.18);
}

.node-detail-overlay__content {
  flex: 1;
  overflow-y: auto;
  padding-right: 6px;
}

.node-detail-overlay__status {
  margin: 0;
  font-size: 1rem;
  color: var(--muted);
}

.node-detail-overlay__status--error {
  color: var(--danger);
}




.charts-page {
  padding: 0 0 var(--pad);
  max-width: 1400px;
  margin: 0 auto;
  width: 100%;
}

.charts-page__intro {
  padding: 0 4px 12px;
}

.charts-page__intro h2 {
  margin: 0 0 6px;
  font-size: 1.6rem;
}

.charts-page__intro p {
  margin: 0;
  color: var(--muted);
}

.charts-page__content {
  min-height: 320px;
}

.charts-page__status {
  margin: 18px 0;
  font-size: 1rem;
  color: var(--muted);
}

.charts-page__status--error {
  color: var(--danger);
}

/* One gutter (SPEC DV4): the node page is a full-screen view without the
   shell's padding, so it pads itself with the shell's --pad, inside its width.
   No block inside it, here or in the overlay and on /charts, adds another. */
.node-detail {
  box-sizing: border-box;
  width: 100%;
  margin: 0;
  padding: 24px var(--pad) 40px;
}

.node-detail__header {
  margin-bottom: 12px;
  padding-inline: 0;
}

.node-detail__title {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  font-size: 1.4rem;
}

.node-detail__badge {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 1.2rem;
}

.node-detail__table {
  margin: 20px 0 32px;
  overflow-x: auto;
}

.node-detail__table table {
  width: 100%;
}

.node-detail__charts {
  padding-inline: 0;
  margin: 12px 0 24px;
}

.node-detail__charts-grid {
  display: grid;
  gap: 24px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 640px), 1fr));
}

.node-detail__chart {
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--card);
  padding: 18px 20px 20px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* A <figure>: drop the browser's 40px inline margin (SPEC DV4). */
  margin: 0;
}

.node-detail__chart-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 1rem;
  margin: 0;
}

/* Chart HTML text at the app's sizes (SPEC DV6). The SVG axis text below is
   in viewBox units and is left to RD8's deferred axis restyle. */
.node-detail__chart-header h4 {
  margin: 0;
  font-size: 15px;
}

.node-detail__chart-header span {
  color: var(--muted);
  font-size: 12px;
}

.node-detail__chart svg {
  width: 100%;
  height: auto;
  max-height: 420px;
}

.node-detail__chart-axis line {
  stroke: var(--line);
  stroke-width: 1;
}

.node-detail__chart-axis text,
.node-detail__chart-axis-label {
  fill: var(--muted);
  font-size: 0.95rem;
}

.node-detail__chart-grid-line {
  stroke: rgba(255, 255, 255, 0.15);
  stroke-width: 1;
}


.node-detail__chart-point {
  stroke: none;
}

.node-detail__chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 18px;
}

.node-detail__chart-legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
}

.node-detail__chart-legend-swatch {
  width: 14px;
  height: 14px;
  border-radius: 999px;
  flex: 0 0 auto;
}

.node-detail__chart-legend-text {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}

.node-detail__chart-legend-text small {
  color: var(--muted);
  font-size: 0.9rem;
}

.node-detail__identifier {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  color: var(--muted);
}

.node-long-link {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
}

.node-long-link:focus,
.node-long-link:hover {
  text-decoration-thickness: 2px;
}

/* One click affordance per table row (SPEC DV2): the row already shows hover
   and a pointer, so its long name underlines only while the row is hovered or
   the link has keyboard focus. Long names elsewhere (overlays, popups, chat)
   keep the underline. Longhands, so the 1px / 2px thickness above holds. */
#nodes .node-long-link {
  text-decoration-line: none;
}

#nodes tbody tr:hover .node-long-link,
#nodes .node-long-link:focus-visible {
  text-decoration-line: underline;
}

.node-detail__status,
.node-detail__error,
.node-detail__noscript {
  margin: 16px 0;
  padding-inline: 0;
}

.node-detail__error {
  color: var(--danger);
}

.node-detail__content {
  display: grid;
  gap: 24px;
  padding-inline: 0;
}

.node-detail__neighbors-grid {
  display: grid;
  gap: 16px;
}

@media (min-width: 720px) {
  .node-detail__neighbors-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.node-detail__neighbors-group {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.node-detail__neighbors-title {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
}

.node-detail__neighbors-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.node-detail__neighbors-list li {
  display: flex;
  align-items: center;
  gap: 10px;
}

.node-detail__neighbor-snr {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  color: var(--muted);
  font-size: 0.95rem;
}

.node-detail__trace-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.node-detail__trace {
  display: flex;
  align-items: center;
  gap: 10px;
}

.node-detail__trace-arrow {
  color: var(--muted);
  font-size: 0.95rem;
}

.node-detail__waypoint-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.node-detail__waypoint {
  line-height: 1.5;
}

.node-detail__section h3 {
  margin: 0 0 10px;
  font-size: 1.1rem;
}

.node-detail__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.node-detail__row {
  display: grid;
  grid-template-columns: minmax(140px, 200px) 1fr;
  gap: 8px;
  margin-bottom: 6px;
}

.node-detail__row dt {
  font-weight: 600;
  color: var(--muted);
  margin: 0;
}

.node-detail__row dd {
  margin: 0;
  font-family: ui-monospace, Menlo, Consolas, monospace;
}

/* Node detail spec sheet (SPEC PD1, Post-Deploy review 01): a single-record
   view is a spec sheet, not a table — grouped dt/dd (reusing .node-detail__row),
   nothing hidden, no horizontal scroll, reflowing to one column on mobile. */
.node-detail-sheet {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px 40px;
}

.node-detail-sheet__group-title {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  border-bottom: 1px solid var(--line);
  padding-bottom: 5px;
}

@media (max-width: 659px) {
  .node-detail-sheet {
    grid-template-columns: 1fr;
  }
}

.node-detail__list li {
  margin-bottom: 4px;
  font-family: ui-monospace, Menlo, Consolas, monospace;
}

.short-info-content {
  margin: 0;
}

.meta-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 0 8px;
}

.meta-controls .filter-input {
  flex: 1 1 auto;
  display: inline-flex;
  align-items: center;
  position: relative;
}

.filter-input input[type="text"] {
  flex: 1 1 auto;
  width: 100%;
  padding-right: 28px;
}

.filter-clear {
  position: absolute;
  right: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: none;
  background: transparent;
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  border-radius: 50%;
  cursor: pointer;
  padding: 0;
}

.filter-clear:hover {
  background: rgba(255, 255, 255, 0.12);
}


.filter-clear:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

button:not(.chat-tab):not(.sort-button):not(.node-extra-toggle):not(.identity-disclosure):not(.legend-item):not(.map-toolbar button) {
  padding: 6px 10px;
  border: 1px solid var(--line-strong);
  background: var(--input-bg);
  border-radius: 6px;
  cursor: pointer;
  color: var(--fg);
}

.icon-button {
  width: 36px;
  height: 36px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

.protocol-toggle-btn {
  width: auto;
  min-width: 32px;
  height: 32px;
  gap: 4px;
  padding: 0 6px;
  border: 1px solid var(--line-strong);
  background: var(--input-bg);
  border-radius: 6px;
  cursor: pointer;
}

/* Per-protocol node count on the toggle (audit follow-up 04). Mirrors the
   legend column count (7-day active) so the same protocol shows the same
   number in both places. Hidden when empty so a countless button stays 32px. */
.protocol-toggle-count {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--fg);
}

.protocol-toggle-count:empty {
  display: none;
}

.protocol-toggle-btn[aria-pressed="true"] {
  background: #2a2a2a;
}

.protocol-toggle-icon {
  display: block;
  transition: filter 0.15s ease;
}

button:not(.chat-tab):not(.sort-button):not(.node-extra-toggle):not(.identity-disclosure):not(.legend-item):not(.map-toolbar button):hover {
  background: var(--line-strong);
}

.sort-button {
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.sort-button:hover {
  background: none;
}

.sort-button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.sort-header {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  user-select: none;
  color: inherit;
}

.sort-header:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.sort-indicator {
  font-size: 0.75em;
  opacity: 0.6;
}

th[aria-sort] .sort-indicator {
  opacity: 1;
}

label {
  font-size: 14px;
  color: var(--fg);
}

input[type="text"] {
  padding: 6px 10px;
  border: 1px solid var(--input-border);
  border-radius: 6px;
  background: var(--input-bg);
  color: var(--input-fg);
  transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease,
    box-shadow 160ms ease;
}

input[type="text"]::placeholder {
  color: var(--input-placeholder);
}

input[type="text"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--accent);
}

input[type="checkbox"],
input[type="radio"] {
  accent-color: var(--control-accent);
  transition: accent-color 160ms ease, background-color 160ms ease;
}

/* Wrapper that places the toggle button to the LEFT of the legend panel. */
.legend-outer {
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  gap: 4px;
  /* Reset Leaflet control chrome so only the inner elements have styling. */
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0 !important;
}

.legend {
  position: relative;
  background: var(--overlay-bg);
  color: var(--fg);
  padding: 8px 10px 10px;
  border: 1px solid var(--overlay-line);
  border-radius: 8px;
  font-size: 12px;
  line-height: 18px;
  min-width: 160px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
  /* fitLegendToMap caps the height to the map's; the rest scrolls (SPEC ML3). */
  overflow-y: auto;
}

/* Mesh activity card (SPEC MA-F1…MA-F6): a bottom-left map overlay, sibling to
   the roles legend at bottom-right. Chrome matches .legend so the two map
   corners read as a pair; the sparkline is an explicit placeholder (MA-F5). */
.map-activity-outer {
  /* Reset Leaflet control chrome so only the inner card carries styling. */
  background: transparent !important;
  border: none !important;
  box-shadow: none !important;
  padding: 0 !important;
}

.map-activity-card {
  background: var(--overlay-bg);
  color: var(--fg);
  padding: 8px 10px 10px;
  border: 1px solid var(--overlay-line);
  border-radius: 8px;
  font-size: 12px;
  line-height: 18px;
  min-width: 178px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45);
}

.map-activity-card--hidden {
  display: none;
}

.map-activity-card__header {
  display: flex;
  align-items: center;
  gap: 4px;
  margin-bottom: 6px;
  font-weight: 600;
}

.map-activity-card__pulse {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
  animation: mesh-activity-pulse 2.4s ease-in-out infinite;
}

.map-activity-card__title {
  font-size: 13px;
}

.map-activity-card__total {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin-bottom: 6px;
}

.map-activity-card__total-value {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 26px;
  line-height: 26px;
  font-weight: 600;
  color: #fff;
  font-variant-numeric: tabular-nums;
}

.map-activity-card__total-unit {
  color: var(--muted);
  font-size: 12px;
}

.map-activity-card__spark {
  display: block;
  margin-bottom: 8px;
}

.map-activity-card__spark-area {
  fill: rgba(95, 168, 255, 0.18);
}

.map-activity-card__spark-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linejoin: round;
}

.map-activity-card__rows {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.map-activity-card__row {
  display: flex;
  align-items: center;
  gap: 6px;
}

.map-activity-card__row-name {
  flex: 1 1 auto;
  color: var(--fg-soft);
}

.map-activity-card__row-bar {
  width: 44px;
  height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.12);
  overflow: hidden;
}

.map-activity-card__row-bar-fill {
  display: block;
  height: 100%;
  background: var(--accent);
}

.map-activity-card__row-rate {
  min-width: 30px;
  text-align: right;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  color: #fff;
}

@keyframes mesh-activity-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.35;
  }
}

@media (prefers-reduced-motion: reduce) {
  .map-activity-card__pulse {
    animation: none;
  }
}

/* Mesh activity card → full-width caption strip on phones (design 1d): the full
   card covers ~a third of a small map, so span the map width (8px inset) with
   the protocol split pushed to the far edge, so it reads as a map caption
   rather than covering it. 659px matches the app's mobile band (UX9). */
@media (max-width: 659px) {
  /* Span the full map width instead of the content-width Leaflet corner. Only
     the activity card lives bottom-left, so this scopes cleanly to it. */
  .leaflet-bottom.leaflet-left {
    left: 0;
    right: 0;
  }
  .map-activity-outer {
    margin: 0 8px 8px !important;
  }
  .map-activity-card {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 8px;
    padding: 4px 9px;
    min-width: 0;
    min-height: 32px;
    line-height: 1.4;
  }
  /* Keep the hidden state hidden: re-declared *after* the flex rule so it wins
     on source order (equal specificity), since an author `display` also beats
     the UA sheet's `[hidden]`. Without this the idle card paints an empty pill
     over the map, breaking MA-F3. */
  .map-activity-card--hidden {
    display: none;
  }
  .map-activity-card__header,
  .map-activity-card__total {
    margin-bottom: 0;
    gap: 4px;
  }
  .map-activity-card__title {
    display: none; /* the numbers make the label redundant in a strip */
  }
  .map-activity-card__total-value {
    font-size: 15px;
    line-height: 1.4;
  }
  .map-activity-card__spark {
    display: none; /* the trend line needs vertical room the strip lacks */
  }
  .map-activity-card__rows {
    flex-direction: row;
    gap: 8px;
    margin-left: auto; /* push the protocol split to the far edge (caption) */
  }
  .map-activity-card__row {
    gap: 3px;
  }
  .map-activity-card__row-name,
  .map-activity-card__row-bar {
    display: none; /* icon + rate is enough in the strip */
  }
}

.legend-header {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 4px;
  margin-bottom: 6px;
  font-weight: 600;
}

.legend-title {
  font-size: 13px;
}

.legend-items {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.legend-items--columns {
  flex-direction: row;
  align-items: stretch;
  gap: 8px;
}

.legend-column {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

/* First legend column (SPEC LS1): the MeshCore group above the Reticulum
   group, 8px apart, the gap between the columns. A flex gap sits only between
   laid-out items, so a hidden group leaves no gap and a lone group top-aligns
   with the Meshtastic column (FU12). */
.legend-column--stack {
  gap: 8px;
}

/* One protocol in the first column: its header and role filters, spaced as a
   column spaces them. No margin or padding: the stack's gap and the Reticulum
   sub-headline's margin below are the only space between groups. */
.legend-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* The Reticulum sub-headline under the MeshCore group (SPEC LS1, amended
   2026-10-08): a 12px top margin over the stack's 8px gap, so the stacked
   groups read as two legends. A margin stays behind when the group above it
   hides, so it applies only while the stack is not `legend-column--lone`,
   which applyProtocolVisibility sets while fewer than two groups are in view:
   a lone group keeps the column top and top-aligns with Meshtastic (FU12). */
.legend-column--stack:not(.legend-column--lone) > .legend-group + .legend-group > .legend-column-header {
  margin-top: 12px;
}

.legend-protocol-count {
  font-weight: 400;
  opacity: 0.7;
}

.legend-column-header {
  display: flex;
  align-items: center;
  gap: 4px;
  font-weight: 600;
  font-size: 11px;
  padding: 2px 6px 4px;
  color: var(--fg);
}

.legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font: inherit;
  color: inherit;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 4px;
  padding: 3px 6px;
  cursor: pointer;
  /* Full width including padding and border, as a <button> already is: the
     Federation legend's <div> items overflowed the scrolling panel by 4 px,
     which drew a horizontal scrollbar there (SPEC ML3). */
  box-sizing: border-box;
  width: 100%;
  justify-content: flex-start;
  text-align: left;
}

.legend-item:hover {
  background: rgba(255, 255, 255, 0.1);
}

.legend-item:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Quiet default (SPEC ML1): a shown chip (`aria-pressed="true"`) is plain
   text, so the swatches carry the key; a hidden one is muted and struck
   through, its swatch faded. The generic button reset skips `.legend-item`,
   so `.legend-item` and these rules paint the chip. */
button.legend-item[aria-pressed="true"] {
  font-weight: 400;
}

button.legend-item[aria-pressed="false"] {
  color: var(--muted);
  text-decoration: line-through;
}

button.legend-item[aria-pressed="false"] .legend-swatch {
  opacity: 0.35;
}

/* Clear filters turns accent while a role filter or a protocol toggle is
   active: updateLegendRoleFiltersUI sets the attribute on the panel. */
#mapLegend[data-has-active-filters] .legend-reset {
  color: var(--accent);
}

/* Role swatch = the marker at legend size (Legend & Chat Fix): colour keys
   role, shape keys protocol. Both carry the marker's 1px ring; the shape
   variants below add the circle / rotated-diamond geometry. */
.legend-swatch {
  display: inline-block;
  box-sizing: border-box;
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  border: 1px solid rgba(0, 0, 0, 0.7);
  border-radius: 2px;
}

/* Meshtastic — the circle marker at legend size. */
.legend-swatch--circle {
  border-radius: 50%;
}

/* MeshCore — the rotated-diamond chip at legend size. Equal optical area to the
   circle (the map's `side = radius × 1.78` → an 11px square ≈ a 12px circle),
   rotated 45° with the chip's 3px corners. The 2px side margin gives it a 16px
   slot so the ~15.6px diagonal never clips the label. */
.legend-swatch--diamond {
  width: 11px;
  height: 11px;
  margin: 0 2px;
  border-radius: 3px;
  transform: rotate(45deg);
}

/* Reticulum — the hexagon chip at legend size (SPEC RD6). Geometry is the
   design's own clip-path rather than a re-derivation. The border cannot follow
   a clip-path, so it is dropped here and the shape carries itself; at 12px the
   silhouette reads as clearly as the bordered circle beside it. */
.legend-swatch--hexagon {
  border: none;
  border-radius: 0;
  clip-path: polygon(50% 0%, 100% 27%, 100% 73%, 50% 100%, 0% 73%, 0% 27%);
}

.protocol-icon {
  display: inline-block;
  vertical-align: middle;
  margin-right: 2px;
}

.nav-protocol-icon {
  margin-right: 3px;
}

.legend-hidden {
  display: none !important;
}

.legend-toggle {
  margin-top: 8px;
}

.legend-toggle-button {
  font-size: 12px;
}

/* Overlay chrome (SPEC ML5). The generic button reset outranks a bare class,
   so `#map` lifts the colours over it; the reset still sets the box. */
#map .legend-toggle-button {
  color: var(--fg);
  background: var(--overlay-bg);
  border-color: var(--overlay-line);
}

/*
 * Single dimming veil over the whole tile pane. `0.5625 = 0.75 × 0.75` is the
 * *effective* brightness the map rendered at before the basemap became two
 * stacked layers (the pane and each layer were each dimmed 0.75, multiplying).
 * Dimming once at the pane — with the layers themselves left opaque — makes the
 * result independent of how many layers are in the pane (the CARTO base, the HOT
 * overlay, and, on the dashboard, a possible offline placeholder), so the stack
 * composites to exactly that same brightness.
 */
#map .leaflet-tile-pane {
  opacity: 0.5625;
}

/*
 * Both basemap providers are natively colourful, light-background raster maps
 * (HOT overlay over a CARTO Voyager base). This one static filter greys and
 * inverts them to match the dark UI, applied identically to the HOT layer
 * (`.map-tiles-hot`) and the CARTO base layer (`.map-tiles-fallback`) so a
 * viewport mixing an arrived HOT tile and a not-yet-arrived cell (CARTO showing
 * through) renders as one coherent dark basemap instead of a light/dark
 * checkerboard. Leaflet stamps a layer's `className` on its container
 * (`.leaflet-layer`), and `filter` on the container greys that provider's whole
 * tile set as a group — so these per-layer classes carry the filter. Offline
 * placeholder tiles carry neither class and stay unfiltered.
 */
#map .leaflet-layer.map-tiles-hot,
#map .leaflet-layer.map-tiles-fallback {
  filter: grayscale(1) invert(1) brightness(0.9) contrast(1.08);
  -webkit-filter: grayscale(1) invert(1) brightness(0.9) contrast(1.08);
}

/* Popups in the map overlay chrome (SPEC ML5). leaflet.css loads after this
   file, so a bare `.leaflet-*` rule loses to it (the Federation popups painted
   white); `#map` outranks it. Its link colour (#0078A8) is too dark here. */
#map .leaflet-popup-content-wrapper,
#map .leaflet-popup-tip {
  background: var(--overlay-bg);
  color: var(--fg);
  box-shadow: 0 3px 14px rgba(0, 0, 0, 0.8);
}

#map .leaflet-popup-content a {
  color: var(--accent);
}

/* Leaflet zoom control as an overlay pill on every map (SPEC ML2): 36 px
   buttons, 44 px on a coarse pointer. The focus ring sits inside the pill,
   which clips anything outside it. */
#map .leaflet-bar {
  border: 1px solid var(--overlay-line);
  border-radius: 999px;
  overflow: hidden;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  background-clip: border-box;
}

#map .leaflet-bar a {
  background-color: var(--overlay-bg);
  color: var(--fg);
  width: 36px;
  height: 36px;
  font: 600 18px/36px system-ui;
  border-bottom-color: var(--overlay-line);
}

#map .leaflet-bar a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -3px;
}

#map .leaflet-bar a.leaflet-disabled {
  color: var(--muted);
}

@media (pointer: coarse) {
  #map .leaflet-bar a {
    width: 44px;
    height: 44px;
    line-height: 44px;
  }
}

#nodes {
  font-size: 12px;
}

.nodes-col--protocol {
  width: 20px;
  min-width: 20px;
  text-align: center;
  padding-inline: 4px;
}

.app-footer {
  position: fixed;
  inset-block-end: 0;
  inset-inline: 0;
  box-sizing: border-box;
  background: var(--panel);
  border-top: 1px solid var(--line-strong);
  color: var(--fg);
  font-size: 12px;
  z-index: 4100;
  /* Lift the fixed footer over long scrolling pages (audit follow-up 08): the
     opaque chrome now renders on every route, so a soft top shadow separates
     it from body copy that scrolls beneath it. */
  box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.35);
}

.app-footer .footer-content {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 auto;
  width: 100%;
  max-width: 960px;
  padding: 6px var(--pad);
  text-align: center;
  box-sizing: border-box;
}

/* Dot separators (Post-Deploy review 03): a dot wants a touch more air and less
   weight than an em dash. */
.app-footer .footer-separator {
  margin: 0 5px;
  color: var(--muted);
}

/* The links are their own footer tier (Post-Deploy review 02): flex-basis 100%
   drops them onto their own line inside the wrapping .footer-content, so the row
   can no longer strand the version→links separator at the end of line one.
   Above 1024 px they join the brand group's row when both fit (SPEC SR2). */
.app-footer .footer-links {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  justify-content: center;
  flex-basis: 100%;
}

.app-footer .footer-brand {
  font-weight: 600;
}

.app-footer a {
  color: inherit;
}

/* The brand, its separator and the version (SPEC SR2). At 1024 px and below
   the group lays out no box, so they stay the row's own items. */
.app-footer .footer-brand-group {
  display: contents;
}

/* Above 1024 px the brand group and the links share a row when both fit
   (SPEC SR2): the group is one item, and the links drop the tier's basis and
   keep their one-line width. They join its row past a 24 px gap, without a
   separator, or move whole onto a row of their own, as PD2's tier. */
@media (min-width: 1025px) {
  .app-footer .footer-content {
    column-gap: 24px;
  }

  .app-footer .footer-brand-group {
    display: inline-flex;
    align-items: center;
    gap: 6px;
  }

  .app-footer .footer-links {
    flex-basis: auto;
  }
}

/* The slim (transparent, borderless) footer variant is retired (audit
   follow-up 08): it left the still-`position: fixed` footer floating without a
   background over the scrolling body copy on Charts, Federation and /pages/*.
   Every route now uses the one opaque `.app-footer` chrome above. */

.meta-active-nodes {
  font-size: 0.85em;
  color: var(--muted);
  white-space: nowrap;
}

@media (max-width: 600px) {
  /* The phone footer scrolls with the page (SH1): pinned, its stack was taller
     than the 112 px reserve and covered the end of every page. Only shells
     that render a footer drop the reserve; a full-screen shell keeps its zero
     bottom padding (SR1), so the full-screen chat panel keeps its height. */
  .app-footer {
    position: static;
    box-shadow: none;
  }

  .page-shell:not(.page-shell--full-screen) {
    padding-bottom: var(--pad);
  }

  .app-footer .footer-content {
    padding: 10px 12px;
    justify-content: center;
    gap: 4px 8px;
  }

  /* The links row moves into the phone menu's Pages group (SH2). */
  .app-footer .footer-links {
    display: none;
  }

  .app-footer .footer-separator {
    display: none;
  }
}


@media (max-width: 1024px) {
  .row {
    flex-direction: column;
    align-items: stretch;
    gap: var(--pad);
  }

  .site-title img {
    width: 44px;
    height: 44px;
  }

  .map-row {
    flex-direction: column;
  }

  .meta-controls {
    flex-wrap: wrap;
    gap: 8px;
  }

  .meta-controls .filter-input {
    flex: 1 1 100%;
    order: -1;
  }

  #map {
    order: 1;
    width: 100%;
    max-width: 100%;
    height: 50vh;
  }

  .chat-panel {
    order: 2;
    flex: none;
    max-width: 100%;
    height: 30vh;
  }

  .legend {
    max-width: min(240px, 80vw);
  }
}

/* Tablet split (SPEC CD7, design review rc3 C5): above the phone band the
   stacked chat gets 40vh under a 45vh map instead of 30vh under 50vh, still
   ordered after the map (UX13). Phones keep the split above until R1. */
@media (min-width: 660px) and (max-width: 1024px) {
  #map {
    height: 45vh;
  }

  .chat-panel {
    height: 40vh;
  }
}

@media (max-width: 1679px) {
  .nodes-col--node-id,
  .nodes-col--frequency,
  .nodes-col--modem-preset {
    display: none;
  }
}

@media (max-width: 1559px) {
  .nodes-col--temperature,
  .nodes-col--humidity,
  .nodes-col--pressure {
    display: none;
  }
}

@media (max-width: 1319px) {
  .nodes-col--latitude,
  .nodes-col--longitude,
  .nodes-col--last-position {
    display: none;
  }
}

@media (max-width: 1109px) {
  .nodes-col--voltage,
  .nodes-col--air-util-tx,
  .nodes-col--altitude {
    display: none;
  }
}

@media (max-width: 899px) {
  .nodes-col--uptime,
  .nodes-col--frequency,
  .nodes-col--modem-preset {
    display: none;
  }
}

@media (max-width: 659px) {
  /* Curated mobile survivors (SPEC UX9): Protocol/Short/Long/Last Seen/
     Battery. Role hides; Battery is the operator's column. */
  .nodes-col--role,
  .nodes-col--channel-util,
  .nodes-col--hw-model {
    display: none;
  }
}















.protocol-toggle-btn:hover {
  background: var(--line-strong);
}








#map .legend-toggle-button:hover {
  background: var(--line-strong);
}









/* ===========================
   Federation Page Styles
   =========================== */

.header-federation {
  display: flex;
  align-items: center;
  gap: 12px;
}

.federation-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.5rem;
  text-decoration: none;
  padding: 4px 8px;
  border-radius: 8px;
  transition: background-color 160ms ease, transform 160ms ease;
}

.federation-link:hover {
  background: var(--hover-bg, rgba(255, 255, 255, 0.1));
  transform: scale(1.1);
}

.federation-page {
  padding: 0 0 var(--pad);
  max-width: none;
  margin: 0;
  width: 100%;
}

.federation-page__content {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.federation-page__map-row {
  width: 100%;
  height: 50vh;
  min-height: 320px;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--line-strong);
}

.federation-page__map-row .map-panel {
  height: 100%;
}

/* The row draws the frame; a second border on the map inside it overflowed
   the row's content box (SPEC GF6). */
.federation-page__map-row #map {
  height: 100%;
  width: 100%;
  border: 0;
  border-radius: 0;
}

.instances-table-wrapper {
  width: 100%;
  overflow-x: auto;
}

/* The nodes table's density (SPEC DV5): 12px type, 5px 8px cells. Zebra rows
   come from the shared `tbody tr:nth-child(even)` rule. The sticky header
   below never pins: the wrapper's `overflow-x: auto` makes it the scrollport,
   and it does not scroll vertically. */
#instances {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
}

#instances th,
#instances td {
  padding: 5px 8px;
  text-align: left;
  border-bottom: 1px solid var(--border);
}

#instances th {
  font-weight: 600;
  background: var(--table-header-bg, var(--surface));
  position: sticky;
  top: 0;
  z-index: 1;
}

#instances tbody tr:hover {
  background: var(--hover-bg, rgba(255, 255, 255, 0.05));
}

#instances a {
  color: var(--accent);
  text-decoration: none;
}

#instances a:hover {
  text-decoration: underline;
}

.instances-col--name {
  min-width: 140px;
}

.instances-col--domain {
  min-width: 180px;
}

.instances-col--contact {
  min-width: 160px;
  white-space: pre-wrap;
  word-break: break-word;
}

.instances-col--version {
  min-width: 80px;
}

.instances-col--channel,
.instances-col--frequency {
  min-width: 100px;
}

.instances-col--nodes {
  min-width: 110px;
}

.instances-col--latitude,
.instances-col--longitude {
  min-width: 100px;
}

.instances-col--last-update {
  min-width: 90px;
}

@media (max-width: 900px) {
  .federation-page__map-row {
    height: 300px;
  }

  .header-federation {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }

  .federation-link {
    align-self: flex-end;
  }
}

/* ── Static pages (markdown content) ────────────────────────── */

.static-page {
  max-width: 800px;
  margin: 0 auto;
  padding: 32px 16px;
}

/* Fill the column, padding included (SPEC DV8): with `margin: 0 auto` in the
   column-flex `.page-main` the page shrank to fit its widest `pre` line and
   widened the phone viewport; at full width the `pre` scrolls inside. */
.static-page {
  width: 100%;
  box-sizing: border-box;
}

.static-page__content.markdown-body {
  color: var(--fg);
  line-height: 1.7;
  font-size: 15px;
}

.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4 {
  color: var(--fg);
  margin: 1.6em 0 0.6em;
  font-weight: 600;
  line-height: 1.3;
}

.markdown-body h1 { font-size: 1.6em; }
.markdown-body h2 { font-size: 1.3em; border-bottom: 1px solid var(--line); padding-bottom: 0.3em; }
.markdown-body h3 { font-size: 1.1em; }
.markdown-body h4 { font-size: 1em; }

.markdown-body h1:first-child,
.markdown-body h2:first-child,
.markdown-body h3:first-child {
  margin-top: 0;
}

.markdown-body p {
  margin: 0.8em 0;
}

.markdown-body a {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

.markdown-body a:hover {
  opacity: 0.85;
}

.markdown-body ul,
.markdown-body ol {
  margin: 0.8em 0;
  padding-left: 1.8em;
}

.markdown-body li {
  margin: 0.3em 0;
}

.markdown-body blockquote {
  margin: 1em 0;
  padding: 0.5em 1em;
  border-left: 3px solid var(--accent);
  background: var(--bg2);
  border-radius: 4px;
  color: var(--muted);
}

.markdown-body blockquote p {
  margin: 0.4em 0;
}

.markdown-body code {
  /* The app's one monospace stack, as `.mono` (SPEC DV7). */
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.9em;
  background: var(--bg2);
  padding: 0.15em 0.4em;
  border-radius: 4px;
  /* A key or hash has no break opportunity (SPEC PO2): inline it may break
     anywhere, so it never widens the page. In a `pre` the code keeps
     `white-space: pre`, never wraps, and the pre scrolls (DV8). */
  overflow-wrap: anywhere;
}

.markdown-body pre {
  margin: 1em 0;
  padding: 12px 16px;
  background: var(--bg2);
  border-radius: 6px;
  overflow-x: auto;
}

.markdown-body pre code {
  background: none;
  padding: 0;
  font-size: 0.85em;
  line-height: 1.5;
}

.markdown-body table {
  width: 100%;
  border-collapse: collapse;
}

/* A table wider than the column scrolls inside its own box (SPEC PO3):
   pages.rb wraps every table in `.markdown-table-wrapper`, which takes the
   table's margin, font size and sideways scroll, so the page never scrolls
   sideways. The table keeps its table display and fills the column when it
   fits: `display: block` on the table would shrink it to its content (the
   About page's tables from 767 to 325.5 px at 1366 px). The bare `th` rule's
   sticky header now pins inside the wrapper, not to the window. */
.markdown-body .markdown-table-wrapper {
  margin: 1em 0;
  font-size: 0.9em;
  overflow-x: auto;
}

.markdown-body th,
.markdown-body td {
  padding: 8px 12px;
  border: 1px solid var(--line);
  text-align: left;
}

.markdown-body th {
  background: var(--bg2);
  font-weight: 600;
}

.markdown-body hr {
  border: none;
  border-top: 1px solid var(--line);
  margin: 2em 0;
}

.markdown-body img {
  max-width: 100%;
  height: auto;
  border-radius: 6px;
}

@media (max-width: 600px) {
  .static-page {
    padding: 20px 12px;
  }

  .markdown-body h1 { font-size: 1.3em; }
  .markdown-body h2 { font-size: 1.15em; }
}

/* Live-update highlight (SPEC LV1/LV2/LV3, amends VF5): a ~1.2s fade applied to a
   node row / chat message row when a live SSE update lands on it. It starts
   white, fades through the element's role colour (the --flash-role-color custom
   property stamped at render time) with increasing transparency, then clears.
   An inset box-shadow overlay means there is no layout shift; each element runs
   its own animation, so concurrent updates stack independently (LV2). */
@keyframes live-flash-fade {
  0% { box-shadow: inset 0 0 0 100vmax rgba(255, 255, 255, 0.9); }
  30% { box-shadow: inset 0 0 0 100vmax var(--flash-role-color, rgba(255, 255, 255, 0.55)); }
  100% { box-shadow: inset 0 0 0 100vmax transparent; }
}

.live-flash {
  animation: live-flash-fade 1.2s ease-out;
}

/* Map-marker wave (SPEC LV5): an expanding ring emitted from a flashed marker.
   The ring grows from ~12px radius and fades toward the role colour over ~1.2s.
   Rendered as a Leaflet divIcon, so it is a positioned overlay with no layout
   shift; the marker layer removes it after the animation completes. */
@keyframes live-flash-wave {
  0% { transform: scale(1); opacity: 0.85; }
  100% { transform: scale(3.5); opacity: 0; }
}

.live-flash-wave {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid var(--flash-role-color, rgba(255, 255, 255, 0.85));
  pointer-events: none;
  animation: live-flash-wave 1.2s ease-out forwards;
}

/* Honor reduced-motion: data still updates live, only the highlight/wave is
   withheld. */
@media (prefers-reduced-motion: reduce) {
  .live-flash,
  .live-flash-wave {
    animation: none;
  }

  /* Motion the LV9 gate never covered (SPEC UX11, audit D-036): Leaflet's
     zoom easing and the chat tab strip's smooth scrolling. */
  .leaflet-zoom-anim .leaflet-zoom-animated {
    transition: none !important;
  }

  .chat-tablist {
    scroll-behavior: auto;
  }
}

/* ===========================
   UX audit remediation (SPEC UX2–UX14)
   =========================== */

/* Degenerate-state voice (UX4, D-001): the noscript notice and the
   server-rendered waiting row must read as "waiting", not as a broken UI. */
.noscript-notice {
  margin: 0;
  padding: 10px var(--pad);
  background: var(--card);
  border-bottom: 1px solid var(--line);
  color: var(--fg);
}

/* Condensed nodes table (audit follow-up 06): the generic `th, td` padding is
   shadowed by the unscoped `thead th` / `tbody td` 8px rules, so row density is
   scoped to #nodes here — ~20px rows instead of 30px (a third more nodes per
   screen) without shrinking the 12px type. */
#nodes thead th {
  padding: 5px 8px;
}

#nodes tbody td {
  padding: 3px 8px;
  line-height: 1.35;
}

#nodes .nodes-group-header th {
  padding: 3px 8px;
}

/* Phone density (SPEC DV9): at <= 659px the five surviving columns and the
   `+` column needed 373px in the dashboard's 343px column, so the page scrolled
   sideways. 4px inline padding fits them, also with the battery sort glyph
   shown. The wrapper stays a plain block: a scrolling wrapper would become the
   sticky header's scrollport (FU10) and take the page scroll that the
   reader-place fallback adjusts (DR-A8). Same specificity as the rules above,
   so this block must follow them. */
@media (max-width: 659px) {
  #nodes thead th,
  #nodes tbody td,
  #nodes .nodes-group-header th {
    padding-inline: 4px;
  }
}

/* Identifiers break inside a word while the table is squeezed (SPEC PO1):
   one long name without a break opportunity (`mf.mt.turm.ak.ber.sixtopia.net`)
   or hardware model (`HELTEC_MESH_NODE_T114`) can set the table's min-content
   past its column, at the start of each tier that shows more columns, and the
   page scrolled sideways. `main/nodes-table-fit.js` adds the class after each
   render and on resize while the table, unbroken, is wider than its wrapper.
   `anywhere`, not `break-word`, lowers the cells' min-content, so the
   auto-layout table shrinks into the column; the wrapper stays a plain block
   (DV9, FU10). Without the class the cells keep their words, so a table that
   fits keeps every column width. */
#nodes.nodes-table--squeezed td.nodes-col--long-name,
#nodes.nodes-table--squeezed td.nodes-col--hw-model {
  overflow-wrap: anywhere;
}

/* The waiting row keeps generous padding. It is inside #nodes tbody, so the
   tight `#nodes tbody td` rule above would otherwise apply — an id-scoped
   selector is needed to outrank it (a bare `.nodes-empty-row td` class would
   lose to the id). */
.nodes-empty-row td {
  color: var(--muted);
  font-style: italic;
}

#nodes .nodes-empty-row td {
  padding: 12px 8px;
}

/* Absent table values render the overlay's muted dash (UX4, D-020). */
.cell-empty {
  color: var(--muted);
  /* Pin the font: `#nodes td.num` is monospace while every other column is the
     body sans, so the identical em dash rendered as two visibly different
     glyphs down the same table. The placeholder has no number to align, so it
     loses nothing by leaving the tabular face. */
  font-family: system-ui, Segoe UI, Roboto, Ubuntu, Arial, sans-serif;
  font-variant-numeric: normal;
}

/* Freshness buckets (UX5, D-005): stale rows dim, live rows carry a 3 px
   accent rule on their first cell; text stays fully readable via opacity on
   the cells, not the row background. */
#nodes tbody tr[data-age="stale"] > td {
  opacity: 0.55;
}

#nodes tbody tr[data-age="live"] > td:first-child {
  box-shadow: inset 3px 0 0 0 var(--accent);
}

/* Row affordance (UX9, D-017): rows respond to hover and read as clickable;
   the waiting, disclosure, and "show all" control rows opt out (the last has
   its own button affordance, so a full-row pointer would be a dead zone). */
#nodes tbody tr:not(.node-extra):not(.nodes-empty-row):not(.nodes-show-all-row):hover > td {
  background: var(--hover-bg);
  cursor: pointer;
}

/* "Show all nodes" control (render scale): the node table renders only the
   top-N rows; this full-width control reveals the rest. The button fills the
   cell so the whole row is the target (no dead zone) and reads as a control
   rather than a raw UA button, matching the disclosure toggle's tokens. */
.nodes-show-all-cell {
  padding: 8px;
  text-align: center;
}

.nodes-show-all {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--card);
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.nodes-show-all:hover {
  background: var(--hover-bg);
}

/* Numeric columns (UX9, D-023): right-aligned tabular mono so vertical
   comparison works; headers share the alignment. */
#nodes td.num,
#nodes th.num {
  text-align: right;
}

#nodes td.num {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
}

/* The unit of a numeric column, named once in its header (SPEC DV1). */
#nodes .nodes-col__unit {
  color: var(--muted);
}

/* Grouped header row (UX9, D-006): quiet group scent above the columns.
   Both header rows pin (audit follow-up b): the base `th` rule already sticks
   the column row at top:0, so the group row must stick too — otherwise it
   scrolls away and leaves the column row floating unbacked on the /nodes page.
   The group row pins at the top and the column row just beneath it. The group
   th uses border-box + a fixed height so the column row's offset is exact
   despite the page's non-global box-sizing. */
.nodes-group-header th {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  background: var(--table-head-bg);
  border-bottom: 1px solid var(--line);
  border-left: 1px solid var(--line);
  text-align: left;
  position: sticky;
  top: 0;
  z-index: 3;
  box-sizing: border-box;
  /* The 24px height and the column row's `top: 24px` below are a coupled pair
     (Post-Deploy review, "fragile"): keep the group row exactly one line tall
     so the two sticky rows can never overlap. `white-space: nowrap` stops a
     longer label, a translation, or a narrow tier from wrapping the group row
     and breaking that invariant. */
  height: 24px;
  white-space: nowrap;
}

/* Column header row pins directly below the 24px group row (see above). */
#nodes thead tr:not(.nodes-group-header) th {
  top: 24px;
  z-index: 2;
}

.nodes-group-header th:first-child {
  border-left: none;
}

/* Mobile disclosure column (UX9, D-007): hidden while every column fits,
   revealed as soon as the first responsive tier hides data. */
.nodes-col--more {
  width: 42px;
  min-width: 42px;
  text-align: center;
  /* Always present: it now also carries the identity-group caret (SPEC RA1),
     which must stay reachable at every width. A table column cannot be shown
     per row, so the column is constant and the `+` keeps its own rule below. */
  display: table-cell;
  padding-inline: 2px;
  white-space: nowrap;
}

/* The `+` still appears only once a responsive tier has hidden something —
   unchanged behaviour, moved from the column to the control. */
@media (min-width: 1680px) {
  .node-extra-toggle {
    display: none;
  }
}

/* Row controls sit inside the row's own line box: a 24 px min-height pushed a
   ~24 px row to ~33 px, so a Reticulum parent stood taller than every other
   row. The hit area is restored on coarse pointers below, where it matters. */
.node-extra-toggle,
.identity-disclosure {
  /* Explicitly square and flex-centred. The global
     `button:not(.chat-tab):not(.sort-button)` rule applies `padding: 6px 10px`,
     so resetting only the height left a stretched box with the glyph off its
     centre line. A fixed box plus flex centring is independent of the glyph's
     own metrics, which differ between `+`, `−`, `▸` and `▾`. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  width: 18px;
  height: 18px;
  min-width: 0;
  min-height: 0;
  padding: 0;
  border-radius: 4px;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 12px;
  line-height: 1;
  vertical-align: middle;
  border: 1px solid var(--line-strong);
  background: var(--input-bg);
  color: var(--fg);
  cursor: pointer;
}

.node-extra-toggle:hover,
.identity-disclosure:hover {
  background: var(--line-strong);
}

@media (pointer: coarse) {
  .node-extra-toggle,
  .identity-disclosure {
    position: relative;
  }

  /* Transparent 44 px target centred on the control, without affecting layout. */
  .node-extra-toggle::after,
  .identity-disclosure::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }
}

.node-extra > td {
  background: var(--card);
  border-bottom: 1px solid var(--line);
}

.node-extra__list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 2px 12px;
  margin: 6px 0;
}

.node-extra__list dt {
  color: var(--muted);
}

.node-extra__list dd {
  margin: 0;
}

/* Disclosure-row fallback when a node reported none of the hidden fields
   (audit follow-up 07). */
.node-extra__empty {
  margin: 6px 0;
  color: var(--muted);
  font-style: italic;
}

/* Join strip (UX12, D-002; audit follow-up 04): the radio settings a newcomer
   needs, moved from the meta row to the footer. */
.join-line {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  font-family: ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.85em;
  color: var(--muted);
}

.join-line__entry {
  overflow: hidden;
  text-overflow: ellipsis;
}

.join-line__proto {
  color: var(--fg);
}

/* Footer placement: centre the strip and let it wrap on narrow viewports
   rather than clipping the way the single-line meta row did. */
.footer-join {
  justify-content: center;
  flex-wrap: wrap;
  overflow: visible;
  white-space: normal;
  padding: 6px var(--pad) 0;
}

.footer-join .join-line__entry {
  overflow: visible;
}

/* Live/paused control (UX6, D-010): visible text state — accent while the
   stream is live, muted while frozen. */
#autorefreshToggle {
  font-family: ui-monospace, Menlo, Consolas, monospace;
  white-space: nowrap;
  border-radius: 999px; /* a pill, so it reads as a control (CT4) */
  margin-left: auto; /* row end: counts filling in never move it (CT3) */
}

#autorefreshToggle[aria-pressed="false"] {
  color: var(--accent);
}

#autorefreshToggle[aria-pressed="true"] {
  color: var(--muted);
  border-color: var(--accent);
}

/* Promoted day count in the vital-sign line (UX11, D-026). */
.meta-active-nodes__today {
  color: var(--fg);
  font-weight: 600;
}

/* `hidden` hides these (CT3). An author `display` beats the UA sheet's
   `[hidden]`, and each sets one (`.icon-button`, `.filter-clear` and
   `.map-toolbar button` inline-flex, `.header-federation` flex), so a hidden
   toggle painted as an empty box from first paint. */
.protocol-toggle-btn[hidden],
.filter-clear[hidden],
.header-federation[hidden],
.map-toolbar button[hidden] {
  display: none;
}

@media (max-width: 659px) {
  /* The vital-sign line owns a reserved row, so the meta row's height is
     final at first paint (CT3). */
  .meta-active-nodes {
    order: 1;
    flex-basis: 100%;
    line-height: 1.4;
    min-height: 1.4em;
  }

  /* The toggle's action hides from view, so pausing does not wrap the toggle
     row; the aria-label still names it (CT4). */
  .autorefresh-toggle__action {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
}

/* One accent focus ring for every button and `role="button"` control (CT2).
   At (0,1,1) the component rules (0,2,0) keep their own ring. */
button:focus-visible,
[role="button"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The tab strip scrolls, and a scroll container clips an outset ring. */
.chat-tab:focus-visible {
  outline-offset: -2px;
}

/* 44 px touch targets on coarse pointers (CT1, extends FU11 and RA11).
   Standalone controls grow. Three dense controls keep their box and get a
   transparent hit box: the filter's × the chat badge's centred one, the
   short-info close one that grows from inside its popover's corner, and a sort
   button its whole header cell. None of these reaches a node row or another
   button. Legend chips keep their box: they touch, so a hit box would only
   move each chip's tap area onto its neighbour. Fine pointers keep the tight
   targets. The Leaflet zoom and the mobile-menu links and close are sized by
   their own rules. */
@media (pointer: coarse) {
  .icon-button:not(.mobile-menu__close),
  .map-toolbar button,
  .node-detail-overlay__close {
    min-width: 44px;
    min-height: 44px;
  }

  .protocol-toggle-btn,
  #autorefreshToggle,
  .legend-toggle-button,
  .chat-tab,
  .chat-tab-select,
  .filter-input input[type="text"] {
    box-sizing: border-box; /* the text input is content-box */
    min-height: 44px;
  }

  /* The text stops short of the ×'s hit box, which reaches 38 px in from
     the box's right edge (inset 6 px, 20 px wide, 44 px box). */
  .filter-input input[type="text"] {
    padding-right: 40px;
  }

  /* The × is already absolutely positioned. */
  .filter-clear::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100%;
    height: 100%;
    min-width: 44px;
    min-height: 44px;
    transform: translate(-50%, -50%);
  }

  /* The close sits 5 px inside the popover's top-right corner. Its hit box
     grows down and left from 3 px inside that corner, so it stays within the
     popover's 8 px rounding instead of reaching the rows beneath. */
  .short-info-overlay .short-info-close::after {
    content: "";
    position: absolute;
    top: -2px;
    right: -2px;
    width: 44px;
    height: 44px;
  }

  /* The column row grows to 45 px, a 44 px padding box over its 1 px rule
     (it is 28 px where no label wraps). The sticky `th` is the containing
     block of the sort button's hit box, so the box fills the cell and never
     reaches the group row above or the first node row below. */
  #nodes thead tr:not(.nodes-group-header) th {
    box-sizing: border-box;
    height: 45px;
  }

  #nodes thead .sort-button::after {
    content: "";
    position: absolute;
    inset: 0;
  }
}

/* MeshCore marker chips (UX7, D-013; audit follow-up 03): shape encodes
   protocol. The fill is a round(radius × 1.78) square rotated 45° into an
   equal-area diamond with rounded corners; the rotated diagonal exceeds the
   divIcon box, so the container keeps overflow visible. */
.node-marker-chip {
  background: transparent;
  border: none;
  overflow: visible;
}

.node-marker-chip__fill {
  display: block;
  box-sizing: border-box;
  border: 1px solid rgba(0, 0, 0, 0.7);
  border-radius: 3px;
  transform: rotate(45deg);
}

/* Reticulum map chip (SPEC RD6) — drawn for an ingestor's own host, the one
   RNS node with a position (SPEC RP7). It overrides the diamond rather than
   restructuring it, so MeshCore's chip is untouched (FU-A3). */
.node-marker-chip__fill--hexagon {
  border: none;
  border-radius: 0;
  transform: none;
  clip-path: polygon(50% 0%, 100% 27%, 100% 73%, 50% 100%, 0% 73%, 0% 27%);
}

/* Legend line-style key (UX7, D-014). */
.legend-line-sample {
  vertical-align: middle;
  margin-right: 4px;
}

/* Colocated hub (UX11, D-031): 32 px hit box centring the 16 px glyph. */
.colocated-spider-hub {
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Region selector behind a compact toggle (UX11, D-029). */
.instance-selector {
  gap: 6px;
}

/* Region toggle (SH5, SH6; audit follow-up 02): an inline SVG globe in the
   muted text colour on a 28 px pill. The accent frame marks the open state
   (aria-expanded flips in instance-selector.js). The id selector outranks the
   generic `button:not(…)` reset, which otherwise sets this button's padding,
   border and colour and hid the frame (FU2). */
#instanceSelectToggle {
  height: 28px;
  padding: 0 8px;
  border: 1px solid var(--input-border);
  border-radius: 999px;
  color: var(--muted);
}

#instanceSelectToggle[aria-expanded="true"] {
  border-color: var(--accent);
  background: var(--input-bg);
}

/* Site title clamp (UX11, D-015): long instance names shrink and ellipsize
   instead of colliding with the nav or the hamburger. */
.site-title {
  font-size: clamp(18px, 2.2vw, 28px);
}

.site-title-text {
  max-width: 40vw;
}

.chat-entry-msg,
.chat-entry-node {
  /* Break unbreakable tokens (Legend & Chat Fix): `.chat-tabpanel` is
     `overflow-y: auto`, so its x-axis computes to `auto` — a horizontal scroll
     container. Nothing else in base.css breaks long tokens, so one hex payload,
     long URL, or 39-char long name overflows the panel. `anywhere` (not
     `break-word`) also lowers the entry's min-content width, so it can never
     demand more width than the panel — wrapped fragments continue in the
     line's body column. No `overflow-x: hidden`, which would only mask the
     next regression. */
  overflow-wrap: anywhere;
}

/* One chat line grid (SPEC CD1, LA1; design review rc3 C1 and rc4 L1): the
   time in a 5ch column and the rest of the line in one body column, so
   wrapped lines land under the body, for messages and Log announcements
   alike. The node page keeps its date-bearing `[YYYY-MM-DD HH:MM]`, 18
   characters, since it lists days without dividers. */
.chat-entry-msg,
.chat-entry-node {
  display: grid;
  grid-template-columns: 5ch minmax(0, 1fr);
  column-gap: 8px;
}

.chat-panel--node-detail .chat-entry-msg {
  grid-template-columns: 18ch minmax(0, 1fr);
}

/* The time is quiet; its title carries seconds, frequency and preset. */
.chat-entry-time {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A line that needs a radio tag shows the preset code alone, faint, with
   frequency, preset and protocol in its title (SPEC LA5). */
.chat-entry-radio {
  color: var(--fg-faint);
}

/* The kind word that follows an announcement's badge, where a colour emoji
   used to lead (SPEC LA2); the detail after it keeps the line's colour. */
.chat-entry-kind {
  color: var(--muted);
}

/* One click affordance in the chat (SPEC LA6, as DV2 in #nodes): the badge
   beside a new node's long name opens the same node, so the name underlines
   only on hover or keyboard focus. Longhands keep the 1px / 2px thickness. */
.chat-panel .node-long-link {
  text-decoration-line: none;
}

.chat-panel .node-long-link:hover,
.chat-panel .node-long-link:focus-visible {
  text-decoration-line: underline;
}

/* Route chip (SPEC SC7, CD3): hop count and flood scope after the message
   text, as quiet text behind a middle dot that assistive technology skips.
   inline-block keeps it whole in the line, and without nowrap a long region
   name still wraps inside it on a narrow panel (LC3). */
.chat-route-chip {
  display: inline-block;
  text-indent: 0;
  padding: 0;
  border: 0;
  color: var(--muted);
  font-size: 0.9em;
}

.chat-route-chip::before {
  content: "· ";
  content: "· " / "";
}

/* Unverified-sender marker (SPEC SV3, amended 2026-10-08): an "unverified"
   chip after the sender badge of a line whose sender is named only by its
   text, in the line's body (SPEC CD1). The chat renders it with `hidden`, so
   its class and title stay for inspection while nothing shows; the rule keeps
   the chip's look for a marker shown without it. It resets the inherited
   hanging indent like the route chip, and has no nowrap, so the line still
   wraps on a narrow panel (LC3). The dashed border sets it apart from the
   route chip. */
.chat-sender-unverified {
  display: inline-block;
  text-indent: 0;
  padding: 0 4px;
  border: 1px dashed var(--muted);
  border-radius: 4px;
  color: var(--muted);
  font-size: 0.85em;
  cursor: help;
}

/* An author `display` beats the UA sheet's `[hidden]`, so the hidden marker
   needs its own rule: no box, no space. */
.chat-sender-unverified[hidden] {
  display: none;
}

/* Verified-sender tag (SPEC SV3, amended 2026-10-08): a "verified" chip after
   the sender badge of a line served with `sender_verified: true`, in the
   line's body (SPEC CD1). It keeps the chip chrome the route chip had before
   SPEC CD3; it resets the inherited hanging indent and has no nowrap, so the
   line still wraps (LC3). */
.chat-sender-verified {
  display: inline-block;
  text-indent: 0;
  padding: 0 4px;
  border: 1px solid var(--line);
  border-radius: 4px;
  color: var(--muted);
  font-size: 0.85em;
}

/* Chat date dividers separate days spatially (UX11, D-040): a centred rule
   with the day, such as "Thu 8 Oct" (SPEC CD5, design review rc3 C3), in
   11px of the page's sans-serif. */
.chat-entry-date {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  color: var(--muted);
  font-size: 11px;
}

.chat-entry-date::before,
.chat-entry-date::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* Mobile chat channel navigation is the dropdown alone (UX11, D-025):
   the fiddly 12 px tab strip and its 28 px arrows hide ≤ 900 px. */
@media (max-width: 900px) {
  .chat-tablist,
  .chat-tab-scroll-btn {
    display: none;
  }
}

/* Announcement banner wraps on narrow screens (UX11, D-030): up to three whole
   lines, then an ellipsis (SH4). The line clamp ends the text, so the banner
   grows to fit it; a height cap centred the overflow and cut the first and
   last lines. */
@media (max-width: 600px) {
  .announcement-banner {
    height: auto;
    max-height: none;
  }

  .announcement-banner__content {
    white-space: normal;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
  }
}

/* Federation table: coordinates fold away on narrow viewports (UX9, D-032). */
@media (max-width: 1319px) {
  .instances-col--latitude,
  .instances-col--longitude {
    display: none;
  }
}

/* Zebra striping for the nodes table is class-driven (SPEC UX9): the hidden
   disclosure rows occupy nth-child slots, so structural even/odd would stop
   alternating visible rows. :where() keeps the neutralizer's specificity
   below the class-driven stripe and the disclosure row's card background. */
#nodes tbody :where(tr:nth-child(even)) > td {
  background: none;
}

#nodes tbody tr.row-alt > td {
  background: var(--row-alt);
}

#nodes tbody tr.node-extra > td {
  background: var(--card);
}

/* --- Reticulum identity groups (SPEC RA1/RA2) ---------------------------- */

/* The disclosure replaces the tile glyph on a grouped identity, so it carries
   the tile's violet inset itself: a collapsed group must still read as
   Reticulum before it opens (RA1). */
.identity-disclosure {
  appearance: none;
  border: 0;
  cursor: pointer;
  font: inherit;
  margin-right: 2px;
  /* No violet fill. RD5 pins #7b61ff as failing the UX2 4.5:1 floor as a text
     background (its best case is 4.39:1), and the row now keeps its protocol
     tile, so the caret no longer needs to carry the protocol's colour. Plain
     foreground-on-transparent clears the floor by the page's own contrast. */
  border-radius: 3px;
}

.identity-disclosure:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 1px;
}

/* One chip per aspect on a parent row's Role cell. Colours are inlined from
   reticulumRoleColors (RD5) with their computed contrast text (UX2); only the
   box belongs here. */
.role-chip {
  display: inline-block;
  border-radius: 4px;
  padding: 0 4px;
  margin-right: 2px;
  font-size: 0.85em;
  white-space: nowrap;
}

/* The overflow chip counts roles the budget hid, so it stays neutral rather
   than borrowing any one role's colour. */
.role-chip--overflow {
  background: var(--card);
  color: var(--muted);
  border: 1px solid var(--muted);
}

/* Sub-rows sit visually inside their parent: indented, quieter, and without
   the parent's hover affordance since they are not themselves navigable. */
#nodes tbody tr.nodes-subrow > td {
  background: var(--card);
}

/* The row-hover rule does not exclude sub-rows and outranks the block above on
   specificity, so a sub-row offered a pointer and a hover tint while the click
   handler ignores it -- an affordance for something that does nothing. */
#nodes tbody tr.nodes-subrow:hover > td {
  background: var(--card);
  cursor: default;
}

.nodes-subrow__spacer {
  border-left: 2px solid #7b61ff;
}

.nodes-subrow__aspect {
  color: var(--fg);
  padding-left: 8px;
}

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

/* Destinations table on the identity page (SPEC RA5). The full 32-hex hashes
   are long, so the table scrolls inside its own container rather than forcing
   the page to. */
.node-detail__destinations {
  overflow-x: auto;
}

.destinations {
  width: 100%;
  border-collapse: collapse;
}

.destinations th,
.destinations td {
  text-align: left;
  padding: 4px 8px;
  white-space: nowrap;
}

.destinations th {
  color: var(--muted);
  font-weight: 600;
}

.destinations__id {
  font-size: 0.9em;
}

/* Destination identifiers are links (SPEC RL5) but sit inside dense table
   cells, so they stay quiet until hovered rather than painting every row with
   link colour. */
.nodes-subrow__id,
.destinations__id a {
  color: var(--muted);
  text-decoration: none;
}

.nodes-subrow__id:hover,
.destinations__id a:hover {
  color: var(--fg);
  text-decoration: underline;
}
