:root {
  --bg: #0b0e14;
  /* fully OPAQUE so WebKit can occlusion-cull the animating map region beneath
     the panels instead of blending them over every fresh canvas frame (the
     40-100ms composite events). Translucency here bought nothing but paint cost. */
  --panel: #10141c;
  --line: #1e2632;
  --text: #d7dee8;
  --dim: #6b7688;
  --accent: #46d4b3;
  --warn: #e2a23b;
  /* faint magnitude bar behind the distance columns (item 15). Fixed colour — width
     alone carries magnitude, so it never compounds with the partial text-muting.
     Kept low-contrast: the number's legibility wins over the bar. */
  --distbar: rgba(120, 142, 170, 0.16);
}
* {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 13px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
}
#map {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 33vh; /* console occupies the bottom third */
}

/* MapLibre attribution readable on dark */
.maplibregl-ctrl-attrib {
  background: rgba(0,0,0,.5) !important;
}
.maplibregl-ctrl-attrib a {
  color: var(--dim) !important;
}

/* Left rail: the top-left overlay stack (stats/breakdown, then the flow legend)
   so they never overlap. Natural height, anchored to the top of the map; children
   keep their own width and pointer events pass through the gaps to the map. */
/* The old #left-rail stacked HUD + legend + hop-knowledge down the top-left, over the
   part of the map you are usually looking at. The counters are now top-right, the legend
   is a flat strip bottom-left, and hop knowledge moved into the console's analytics tab
   where it has room. */
#left-rail {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  max-width: 70vw;
  pointer-events: none;
  /* own GPU layer: a map-canvas repaint re-blends this cached texture instead of
     re-rasterising the overlay (decouples DOM overlays from the WebGL layer). */
  transform: translateZ(0);
}
#left-rail > * {
  pointer-events: auto;
}

#hud {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 6;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 5px 8px;
  font-size: 11px; /* same as the legend: these are reference readouts, not headlines */
  line-height: 1.35;
  text-align: right;
  transform: translateZ(0);
}
.hud-row {
  white-space: nowrap;
}
.hud-row + .hud-row {
  margin-top: 2px;
}
#fps,
#fpslow,
#pps {
  color: var(--accent);
  font-weight: 600;
}
.dim {
  color: var(--dim);
}

/* top-right node detail widget (shown on node click) */
#node-widget {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 6;
  width: 336px;
  min-width: 200px;
  max-width: 80vw;
  max-height: calc(100vh - 20px);
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  transform: translateZ(0); /* own GPU layer (see #left-rail) */
}
#node-widget[hidden] {
  display: none;
}
.nw-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  font-weight: 700;
  color: var(--accent);
}
#nw-close {
  background: none;
  border: none;
  color: var(--dim);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 0 2px;
}
#nw-close:hover {
  color: var(--text);
}
#nw-body {
  margin: 0;
  padding: 8px 12px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 3px 10px;
}
#nw-body dt {
  color: var(--dim);
}
#nw-body dd {
  margin: 0;
  text-align: right;
  min-width: 0;
  /* long values (dates, coords) wrap on boundaries rather than shattering */
  overflow-wrap: anywhere;
}
#nw-body dd.mono {
  font-size: 11px;
}
/* pubkey label + value each span the full grid width */
#nw-body dt.nw-full,
#nw-body dd.nw-full {
  grid-column: 1 / -1;
}
#nw-body dd.pk {
  text-align: left;
  color: var(--dim);
  /* full width available, so break-all wraps the hex to a few tidy lines */
  word-break: break-all;
}

/* direct-neighbours section under the detail grid */
#nw-neighbours {
  border-top: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.nw-nb-head {
  padding: 8px 12px 4px;
  color: var(--dim);
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: .5px;
}
.nw-nb-note {
  padding: 0 12px 8px;
  color: var(--dim);
  font-size: 11px;
}
.nw-nb-sub {
  padding: 0 12px 6px;
  color: var(--dim);
  font-size: 10px;
  opacity: 0.85;
}
/* item 27: range-envelope section in the node widget */
.nw-range-meta {
  padding: 0 12px 2px;
  color: var(--dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.nw-range-cap {
  padding: 6px 12px 8px;
  color: var(--dim);
  font-size: 10px;
  line-height: 1.35;
  border-left: 2px solid rgba(127, 176, 224, 0.4); /* the rose's own blue: this is its caveat */
  margin: 4px 12px 8px;
  opacity: 0.9;
}
.nw-nb-wrap {
  /* cap height and scroll internally so the widget never grows unbounded */
  max-height: 150px;
  overflow-y: auto;
}
table.nw-nb {
  width: 100%;
  border-collapse: collapse;
}
table.nw-nb th {
  position: sticky;
  top: 0;
  background: var(--panel);
  z-index: 1;
  text-align: left;
  color: var(--dim);
  font-weight: 500;
  font-size: 10px;
  padding: 2px 7px;
  border-bottom: 1px solid var(--line);
}
table.nw-nb td {
  padding: 2px 7px;
  border-bottom: 1px solid rgba(30, 38, 50, .5);
  white-space: nowrap;
  font-size: 11px;
}
table.nw-nb td.nb-name {
  max-width: 116px;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text);
}
table.nw-nb td.nb-name.unk {
  color: var(--dim);
}
/* item 21: reciprocal hover between a row and its map arc */
table.nw-nb tbody tr[data-nbkey]:hover,
table.nw-nb tbody tr.hl {
  background: rgba(70, 212, 179, 0.12);
}
/* a neighbour with no known location — can't be drawn on the map, so mark the row */
table.nw-nb tbody tr.nb-noloc td.nb-name {
  color: var(--dim);
  font-style: italic;
}
.nw-nb-note.dim {
  color: var(--dim);
}
table.nw-nb td.nb-snr,
table.nw-nb td.nb-ev,
table.nw-nb td.nb-dir {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
table.nw-nb td.nb-snr {
  color: var(--accent);
}
table.nw-nb td.nb-dir {
  color: var(--text);
  font-size: 10px;
}
table.nw-nb td.nb-dir.unk {
  color: var(--dim);
}
/* the → / ← direction headers */
table.nw-nb th:nth-child(2),
table.nw-nb th:nth-child(3) {
  text-align: right;
}
table.nw-nb tbody tr:hover {
  background: rgba(70, 212, 179, .06);
}

/* hover popup */
.maplibregl-popup-content {
  background: var(--panel) !important;
  color: var(--text) !important;
  border: 1px solid var(--line);
  border-radius: 6px;
  font: 12px/1.4 ui-monospace, monospace;
  padding: 6px 9px !important;
}
.maplibregl-popup-tip {
  display: none;
}
.np-name {
  color: var(--accent);
  font-weight: 700;
}
.np-sub {
  color: var(--dim);
}
/* the inference-evidence line on placeholder / unknown-relay tooltips (item 22a) */
.np-inf {
  color: var(--warn);
  font-size: 11px;
  margin-top: 3px;
  max-width: 240px;
}
.lk-h {
  color: var(--accent);
  font-weight: 700;
  margin-bottom: 3px;
}
.lk-b div {
  color: var(--dim);
}

/* analytics tab: hop knowledge, with the console's width instead of a 150px rail */
#analytics-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  padding: 8px 12px;
}
#analytics-scroll #state-hist {
  max-width: 720px;
}

#state-hist {
  /* no longer a section appended under the HUD rows, so it needs no divider rule */
  margin-top: 0;
}
.kn-head {
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  cursor: help;
}
/* item 34b: transition-corpus staleness — a backend fault, flagged amber so it reads as "we
   temporarily can't know", distinct from the neutral hop-knowledge stats below it. */
.kn-stale {
  color: #e0b050;
  font-size: 10px;
  line-height: 1.3;
  padding: 2px 0 4px;
  cursor: help;
}
/* hop byte-width stacked bar: 1B (ambiguous) → 4B (precise), brightening */
.kn-bar {
  display: flex;
  height: 8px;
  margin: 4px 0 2px;
  border-radius: 3px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.05);
}
.kn-seg {
  height: 100%;
}
.kn-b1 {
  background: #4d5769;
}
.kn-b2 {
  background: #7aa2f7;
}
.kn-b3 {
  background: #46d4b3;
}
.kn-b4 {
  background: #eafff8;
}
.kn-sub {
  color: var(--dim);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  margin-bottom: 4px;
}
.kn-stat {
  color: var(--dim);
  font-size: 11px;
  line-height: 1.5;
  cursor: help;
}
.kn-v {
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* node role legend (left rail, below the HUD): role = silhouette, observer = ring */
/* bottom-left, above the console drawer. A wide flat strip: the role glyphs run in one
   wrapping row and the three captions run inline, instead of stacking six rows tall. */
#node-legend {
  position: absolute;
  left: 10px;
  bottom: calc(33vh + 10px);
  z-index: 6;
  max-width: min(52vw, 640px);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 4px 8px;
  transform: translateZ(0);
}
.nl-head {
  color: var(--dim);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .5px;
  margin-bottom: 2px;
}
#nl-items {
  display: flex;
  flex-wrap: wrap;
  gap: 1px 10px;
}
.nl-item {
  display: flex;
  align-items: center;
  gap: 6px;
}
.nl-item canvas {
  width: 13px;
  height: 13px;
}
.nl-item span {
  color: var(--dim);
  font-size: 11px;
}
/* the three captions read as one wrapped line rather than three stacked rows */
.nl-cap {
  display: inline;
  color: var(--dim);
  font-size: 10px;
  opacity: 0.8;
}
.nl-cap + .nl-cap::before {
  content: " · ";
  opacity: 0.5;
}
#nl-caps {
  margin-top: 2px;
  line-height: 1.3;
}

/* packet info (shown on packet click) — sits below the left rail (its `top` is
   set in JS to the rail's bottom) and above the console drawer, scrolling
   internally so it never grows up over the stats/legend above it. The `top`
   fallback here applies until JS measures the rail. */
/* Packet detail is a tab of the bottom console, not a floating panel over the map.
   As a left-docked column it covered the map it was describing and had to have its
   top pinned to the left rail in JS; in the drawer it gets the full console width,
   scrolls with the other tabs, and needs no positioning code at all. */
#tab-packet {
  min-height: 0;
}
/* the head is a fixed strip; everything below it scrolls as one region, so the
   summary and the per-observer list can't fight over the drawer's height */
#pi-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  display: flex;
  flex-direction: column;
}
/* node-widget is right-docked, so its resize handle sits on the LEFT edge */
#nw-resize {
  position: absolute;
  top: 0;
  left: -3px;
  width: 8px;
  height: 100%;
  cursor: ew-resize;
  z-index: 2;
}
#nw-resize:hover {
  background: linear-gradient(to left, transparent, rgba(70, 212, 179, 0.35));
}
.pi-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 12px;
  border-bottom: 1px solid var(--line);
  font-weight: 700;
  color: var(--accent);
}
#pi-close {
  background: none;
  border: none;
  color: var(--dim);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
#pi-close:hover {
  color: var(--text);
}
#pi-body {
  /* One scroll region owns the tab (#pi-scroll), so the summary takes its natural
     height here. It used to shrink and scroll on its own, capped at half a
     fixed-height panel; inside a scrolling parent that made it collapse to a single
     row and hide all 28 stats. */
  flex: 0 0 auto;
  min-height: 0;
  margin: 0;
  padding: 8px 12px;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 3px 10px;
}
#pi-body dt {
  color: var(--dim);
}
#pi-body dd {
  margin: 0;
  text-align: right;
}
/* routing-overhead bar in the packet detail (item 8c) */
.pi-bar {
  display: inline-block;
  width: 64px;
  height: 7px;
  background: rgba(255, 255, 255, 0.09);
  border-radius: 3px;
  overflow: hidden;
  vertical-align: middle;
}
.pi-bar-fill {
  height: 100%;
  background: #e2a23b;
}
.pi-bar-lbl {
  font-variant-numeric: tabular-nums;
}

/* per-observation list: every observer that heard the packet + the route it saw.
   Scrolls with the summary inside #pi-scroll rather than owning its own height. */
#pi-obs {
  flex: 0 0 auto;
  border-top: 1px solid var(--line);
}
#pi-obs:empty {
  display: none;
}
.po-head {
  position: sticky;
  top: 0;
  padding: 6px 12px 4px;
  background: var(--panel);
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
/* [ structure | timing ] view toggle (item 13) */
.po-toggle {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: 5px;
  overflow: hidden;
  flex: 0 0 auto;
}
.po-toggle button {
  background: none;
  border: none;
  color: var(--dim);
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.03em;
  text-transform: lowercase;
  padding: 2px 8px;
  cursor: pointer;
}
.po-toggle button + button {
  border-left: 1px solid var(--line);
}
.po-toggle button.on {
  background: rgba(70, 212, 179, 0.16);
  color: var(--text);
}
/* item 19: play/pause button in the packet-detail header */
.po-play {
  flex: 0 0 auto;
  background: none;
  border: 1px solid var(--line);
  border-radius: 5px;
  color: var(--dim);
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.03em;
  padding: 2px 8px;
  margin-right: 6px;
  cursor: pointer;
}
.po-play:hover {
  color: var(--text);
  border-color: var(--dim);
}
.po-play.on {
  background: rgba(70, 212, 179, 0.16);
  color: var(--text);
  border-color: transparent;
}
.po-row {
  padding: 5px 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.04);
  cursor: pointer;
}
.po-row:hover {
  background: rgba(255, 255, 255, 0.04);
}
.po-row.on {
  background: rgba(56, 189, 248, 0.14);
}
.po-l1 {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  align-items: center;
}
.po-view {
  flex: 0 0 auto;
  background: none;
  border: 1px solid var(--line);
  color: var(--dim);
  border-radius: 5px;
  font: inherit;
  font-size: 10px;
  padding: 1px 7px;
  cursor: pointer;
}
.po-view:hover {
  color: var(--text);
  border-color: var(--dim);
}
.po-seen {
  color: var(--text);
  cursor: help;
  border-bottom: 1px dotted var(--dim);
}
.po-obs-link {
  color: var(--accent);
  cursor: pointer;
  border-bottom: 1px solid rgba(70, 212, 179, 0.4);
}
.po-obs-link:hover {
  border-bottom-color: var(--accent);
}
/* a hoverable hop/node name in a variant's path */
.po-hop {
  cursor: pointer;
}
.po-hop:hover {
  color: #fef08a;
}
/* per-hop confidence of OUR resolution (see resolveHopChain): green = a 2-byte+
   hash matching one node; amber = backed by observed routing; dim = a guess;
   faded/italic = unresolved (a bare 1-byte hop with no 2-byte evidence). */
.po-hop.m-confident { color: #34d399; }
.po-hop.m-likely    { color: #e2e06a; }
.po-hop.m-guess     { color: var(--dim); }
.po-hop.m-unknown   { color: #5a6473; font-style: italic; }
/* "≠" marker: the observer claimed a different node than we resolved. Matches the
   faint purple claimed-edge overlay on the map. */
.po-claim {
  cursor: pointer;
  color: #b083c9;
  font-weight: 600;
  padding: 0 2px;
}
.po-claim:hover { color: #d8b4fe; }
.po-name {
  color: var(--text);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.po-name.unloc {
  color: var(--dim);
  font-weight: 400;
}
.po-sig {
  color: var(--dim);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.po-l2 {
  margin-top: 1px;
  color: var(--dim);
  font-size: 11px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.po-route {
  color: var(--accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* ---- divergence tree + airtime waterfall (item 13) ---- */
.po-tree {
  position: relative; /* offset parent for the replay playhead (item 19) */
  padding: 2px 0 6px;
  font-size: 12px;
}
/* structure view: one line per tree node, indented by depth */
.po-line {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 2px 12px;
  min-height: 20px;
  white-space: nowrap;
}
.po-hopwrap {
  display: inline-flex;
  align-items: center;
}
/* the origin apex of the tree */
.po-origin {
  color: #34d399;
  font-weight: 600;
  cursor: default;
}
.po-origin:hover { color: #6ee7b7; }
.po-dim { color: var(--dim); font-weight: 400; }
/* a folded run of pass-through relays; click to expand */
.po-run {
  color: var(--dim);
  cursor: pointer;
  font-size: 11px;
  letter-spacing: 0.06em;
}
.po-run:hover { color: var(--text); }
/* unresolved hop (⚠): faded italic, matches the map's uncertain treatment */
.po-hop.unresolved { color: #5a6473; font-style: italic; }
/* a ◆ leaf = one distinct copy */
.po-leaf {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  border-radius: 4px;
  padding: 0 4px;
}
.po-leaf:hover { background: rgba(255, 255, 255, 0.05); }
.po-leaf.on { background: rgba(56, 189, 248, 0.16); }
.po-dia { color: #9aa4b2; flex: 0 0 auto; }
.po-leaf-meta {
  color: var(--dim);
  font-size: 11px;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* timing view: a shared time axis with one bar per hop, depth-first order */
.po-axis {
  position: sticky;
  top: 0;
  padding: 3px 12px 4px;
  color: var(--dim);
  font-size: 10px;
  letter-spacing: 0.03em;
  background: var(--panel);
  cursor: help;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.po-tline {
  display: grid;
  grid-template-columns: 92px 1fr;
  align-items: center;
  gap: 6px;
  padding: 2px 12px;
  min-height: 18px;
}
.po-tline:hover { background: rgba(255, 255, 255, 0.03); }
.po-tlabel {
  color: var(--text);
  font-size: 11px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.po-tlabel.dim { color: var(--dim); }
.po-runlbl { cursor: pointer; }
.po-runlbl:hover { color: var(--text); }
/* the bar track: a relative box; bars are positioned by % of the packet's span */
.po-track {
  position: relative;
  height: 12px;
  min-width: 0;
}
/* item 19: the replay playhead — a vertical line swept across the time axis in sync with
   the map animation (positioned in px by JS relative to the .po-tree). */
.po-playhead {
  position: absolute;
  top: 22px; /* clear the sticky axis caption */
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: #eafff8;
  box-shadow: 0 0 4px rgba(234, 255, 248, 0.7);
  pointer-events: none;
  z-index: 3;
}
/* item 25: the map animation ends where its located edges do (mapMaxT); past that the
   playhead runs on through the deep tail the map can't draw. Shade that tail + a dashed
   boundary so "the map stopped" is visible, not a mystery. Both are pointer-events:none so
   seeking still works across the whole track. */
.po-beyondmap {
  position: absolute;
  top: 22px;
  bottom: 0;
  background: repeating-linear-gradient(
    45deg,
    rgba(120, 132, 152, 0.12) 0 5px,
    rgba(120, 132, 152, 0.04) 5px 10px
  );
  pointer-events: none;
  z-index: 1;
}
.po-mapend {
  position: absolute;
  top: 22px;
  bottom: 0;
  width: 0;
  margin-left: -1px;
  border-left: 1px dashed rgba(154, 164, 178, 0.6);
  pointer-events: none;
  z-index: 2;
}
.po-mapcap {
  font-size: 10px;
  padding: 4px 2px 0;
  line-height: 1.3;
}
.po-bar {
  position: absolute;
  top: 2px;
  height: 8px;
  border-radius: 2px;
}
/* solid = fact convention: █ computed on-air is solid, ░ modelled backoff is hatched */
.po-bar-air { background: #46d4b3; }
.po-bar-backoff {
  background: repeating-linear-gradient(
    45deg,
    rgba(107, 118, 136, 0.55) 0 2px,
    rgba(107, 118, 136, 0.15) 2px 4px
  );
}
.po-bar-run {
  background: rgba(107, 118, 136, 0.35);
  border: 1px dashed rgba(154, 164, 178, 0.5);
}
/* a ◆ leaf inside the timing view: same element as structure, plus its reception time */
.po-leafline { justify-content: space-between; }
.po-whenline {
  color: var(--dim);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  flex: 0 0 auto;
  padding-left: 8px;
}

/* dock the packet disassembler as a bottom-right tooltip (its module ships as a
   centred modal; override to a non-blocking docked panel above the console) */
.pdis-backdrop {
  position: fixed !important;
  top: auto !important;
  left: auto !important;
  right: 10px !important;
  bottom: calc(33vh + 10px) !important;
  padding: 0 !important;
  display: block !important;
  overflow: visible !important;
  background: none !important;
  z-index: 40 !important;
  pointer-events: none !important;
}
.pdis-backdrop .pdis {
  pointer-events: auto;
  width: min(600px, 52vw) !important;
  max-height: 46vh !important;
}

/* bottom console drawer: full width, bottom third of the screen */
#panel {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 33vh;
  z-index: 5;
  display: grid;
  grid-template-columns: auto auto 1fr;
  grid-template-rows: auto auto 1fr;
  grid-template-areas:
    "header layers tabs"
    "search search search"
    "body   body   body";
  /* stretch (default) so the body row's table area is height-bounded and can
     scroll; the top-row items set their own align-self. */
  background: var(--panel);
  border-top: 1px solid var(--line);
  overflow: hidden;
  transform: translateZ(0); /* own GPU layer (see #left-rail) */
}
#panel header {
  grid-area: header;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px;
  border-right: 1px solid var(--line);
  align-self: stretch;
}
.brand {
  font-weight: 700;
  letter-spacing: .5px;
}
#conn {
  font-size: 11px;
  padding: 2px 8px;
  border-radius: 10px;
  border: 1px solid var(--line);
}
#conn.on {
  color: var(--accent);
  border-color: var(--accent);
}
#conn.off {
  color: var(--warn);
  border-color: var(--warn);
}
.layers {
  grid-area: layers;
  display: flex;
  gap: 12px;
  padding: 8px 14px;
  border-right: 1px solid var(--line);
  color: var(--dim);
  align-self: stretch;
  align-items: center;
}
.layers label {
  cursor: pointer;
  white-space: nowrap;
}
/* tabs sit in the top row, to the right of layers */
.tabs {
  grid-area: tabs;
  display: flex;
  gap: 4px;
  padding: 0 10px;
  align-self: stretch;
  align-items: flex-end;
}
.tab {
  background: none;
  border: none;
  color: var(--dim);
  font: inherit;
  cursor: pointer;
  padding: 5px 12px;
  border-radius: 6px 6px 0 0;
  border-bottom: 2px solid transparent;
}
.tab:hover {
  color: var(--text);
}
.tab.on {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* tab content fills the full-width bottom row; only the active one shows */
.tabc {
  grid-area: body;
  display: none;
  flex-direction: column;
  min-height: 0;
  border-top: 1px solid var(--line);
}
.tabc.on {
  display: flex;
}

.feed-head {
  padding: 8px 12px 4px;
  color: var(--dim);
  text-transform: uppercase;
  font-size: 11px;
  letter-spacing: .5px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.feed-head .thresh {
  margin-left: auto;
  text-transform: none;
  letter-spacing: 0;
}
.feed-head select {
  background: #0e131c;
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 4px;
  font: inherit;
  padding: 1px 4px;
}
.note {
  padding: 6px 12px;
  font-size: 11px;
  border-top: 1px solid var(--line);
}

/* shared tables */
.tbl-wrap {
  overflow-y: auto;
  flex: 1;
  min-height: 0;
}
table.tbl {
  width: 100%;
  border-collapse: collapse;
}
table.tbl thead th {
  position: sticky;
  top: 0;
  background: var(--panel);
  z-index: 1;
  text-align: left;
  color: var(--dim);
  font-weight: 500;
  font-size: 11px;
  padding: 4px 12px;
  border-bottom: 1px solid var(--line);
}
/* item 30: node search box — its own full-width grid row, above the table */
.node-search {
  grid-area: search;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border-top: 1px solid var(--line);
}
#node-search {
  flex: 1;
  min-width: 0;
  background: var(--bg, #0a0e14);
  border: 1px solid var(--line);
  border-radius: 4px;
  color: var(--text);
  font: inherit;
  font-size: 12px;
  padding: 4px 8px;
}
#node-search:focus {
  outline: none;
  border-color: var(--accent);
}
#node-search-count {
  font-size: 10px;
  white-space: nowrap;
}
/* item 30: nodes tab table — sortable headers, activity bar, distinct status colours */
#nodes-tbl th[data-sort] {
  cursor: pointer;
  user-select: none;
}
#nodes-tbl th[data-sort]:hover {
  color: var(--text);
}
#nodes-tbl th.sorted::after {
  content: " ▲";
  font-size: 8px;
}
#nodes-tbl th.sorted.desc::after {
  content: " ▼";
}
#nodes-tbl tr.clk {
  cursor: pointer;
}
#nodes-tbl tbody tr:hover {
  background: rgba(255, 255, 255, 0.04);
}
td.nn-name {
  color: var(--text);
  max-width: 160px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
td.ro,
td.rg {
  color: var(--dim);
  font-size: 11px;
}
td.na {
  min-width: 54px;
}
td.na .na-bar {
  display: inline-block;
  height: 6px;
  min-width: 1px;
  background: var(--accent);
  border-radius: 2px;
  opacity: 0.65;
  vertical-align: middle;
}
td.st {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
td.st-real {
  color: #46d4b3;
}
td.st-placeholder {
  color: #e0b050;
}
td.st-unlocated {
  color: var(--dim);
}
td.ob {
  text-align: center;
}
td.ob.unk {
  color: var(--dim);
}
/* item 18: a subdued header spanning the four distance columns — states "km" once and
   groups them as one family. Two sticky header rows: the group row on top, the column
   labels just beneath it. */
.grp-row th {
  top: 0;
  padding: 5px 12px 2px;
}
.grp-row th[colspan="9"] {
  border-bottom: none;
}
.dist-grp {
  text-align: center;
  font-size: 10px;
  letter-spacing: 0.05em;
  opacity: 0.7;
}
table.tbl thead tr:last-child th {
  top: 22px; /* the column-label row sticks just below the group row */
}
table.tbl td {
  padding: 3px 12px;
  border-bottom: 1px solid rgba(30,38,50,.5);
  white-space: nowrap;
}
table.tbl tbody tr:hover {
  background: rgba(70,212,179,.06);
}
td.tt {
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}
td.hh {
  color: var(--accent); /* item 26b: overridden inline per-row with the packet's hash colour */
  font-variant-numeric: tabular-nums;
}
/* item 26c: a feed row pulses in its packet's own hash hue while that packet's trace animates
   on the map — so the two views are unmistakably the same packet. Class-toggled (see
   setRowTracing); the hue comes from the row's --trace-hue custom property. background-color
   is GPU-composited and only a handful of rows pulse at once, so this stays cheap. */
@keyframes tracePulse {
  0%, 100% { background-color: hsla(var(--trace-hue, 0), 88%, 60%, 0); }
  50%      { background-color: hsla(var(--trace-hue, 0), 88%, 60%, 0.22); }
}
tr.tracing {
  animation: tracePulse 1.1s ease-in-out infinite;
}
td.ty {
  color: var(--dim);
  font-size: 11px;
}
td.rt {
  color: var(--dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
td.nn,
td.pp {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* distance columns (km); .part = a floor, not a total (unresolved hops excluded) */
td.ds {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--dim);
}
td.ds.part {
  color: #6b7688;
  font-style: italic;
}
td.ss {
  max-width: 130px;
  overflow: hidden;
  text-overflow: ellipsis;
}
td.ss.known {
  color: var(--text);
}
td.ss.amb {
  color: var(--warn);
}
td.ss.unk {
  color: var(--dim);
}
td.pp.unk {
  color: var(--dim);
}
td.hb {
  color: var(--dim);
  font-size: 11px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
td.hb.amb {
  color: var(--warn);
}
td.rg {
  color: var(--dim);
  font-size: 11px;
  letter-spacing: 0.02em;
}
td.spk {
  color: var(--accent);
  padding: 0 12px;
}
td.spk.unk {
  color: var(--dim);
}
.spark {
  display: block;
}
td.conf {
  text-transform: uppercase;
  font-size: 10px;
}
td.conf.strong {
  color: var(--accent);
}
td.conf.ok {
  color: #7aa2f7;
}
td.conf.low {
  color: var(--warn);
}
td.conf.none {
  color: var(--dim);
}

@media (max-width: 640px) {
  #map {
    bottom: 45vh;
  }
  #panel {
    height: 45vh;
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto auto 1fr;
    grid-template-areas: "header" "layers" "tabs" "search" "body";
  }
  #panel header,
  .layers {
    border-right: none;
    border-bottom: 1px solid var(--line);
  }
}
