/* netmap UI chrome.

   Built against the `design-direction: soft-depth` spec declared in this
   repo's CLAUDE.md - read that first. The short version: every colour,
   radius, spacing step, shadow and duration below is a token, floating
   chrome reads as physically layered above the graph canvas, and machine
   values render in mono. Themes are driven by [data-theme] on <body>; each
   one redefines the full token set. The graph canvas itself is styled by
   Cytoscape in app.js so both stay in sync. */

/* ─────────────── Tokens ─────────────── */

:root {
  /* Shape. One family, used everywhere: sm = controls, md = floating
     panels, lg = popovers/modals, pill = badges only. */
  --r-sm: 6px; --r-md: 10px; --r-lg: 14px; --r-pill: 999px;

  /* Space, on a 4px grid. Nothing off this scale ships. */
  --s1: 4px; --s2: 8px; --s3: 12px; --s4: 16px; --s5: 24px; --s6: 32px;

  /* Motion. Two durations, one easing, both dropped entirely under
     prefers-reduced-motion (see the bottom of this file). */
  --t-fast: 120ms; --t: 180ms; --ease: cubic-bezier(.2, .7, .3, 1);

  --font: "Segoe UI", system-ui, -apple-system, sans-serif;
  --mono: "Cascadia Mono", Consolas, "SFMono-Regular", Menlo, monospace;

  /* Type scale: 11 / 12 / 13 / 16 / 20. Weights 400 and 600 only. */
  --fs-micro: 11px; --fs-small: 12px; --fs-body: 13px;
  --fs-head: 16px; --fs-title: 20px;

  /* NOC dark - the default theme. */
  --bg: #0e1117; --panel: #161b22; --panel-2: #1c2230; --panel-3: #232b3b;
  --line: #2b3444; --line-soft: #222a38;
  --text: #d6deeb; --text-strong: #f0f4fa; --muted: #7d8aa0; --faint: #55607a;
  --accent: #4c9aff; --accent-2: #37e0a0; --danger: #ff6b6b; --warning: #ffcb6b;
  --sh-1: 0 1px 2px rgba(0,0,0,.30);
  --sh-2: 0 2px 6px rgba(0,0,0,.28), 0 10px 24px rgba(0,0,0,.34);
  --sh-3: 0 4px 10px rgba(0,0,0,.30), 0 18px 44px rgba(0,0,0,.42);
  --sh-4: 0 8px 18px rgba(0,0,0,.34), 0 28px 64px rgba(0,0,0,.50);
  --scrim: rgba(0,0,0,.55);
}

body[data-theme="blueprint"] {
  --bg:#0a1f3c; --panel:#0d2b52; --panel-2:#12385f; --panel-3:#17436f;
  --line:#1e4d80; --line-soft:#17406e;
  --text:#cfe4ff; --text-strong:#ffffff; --muted:#7fa8d8; --faint:#5d86b4;
  --accent:#5cc8ff; --accent-2:#ffd166; --danger:#ff8095; --warning:#ffd166;
}

body[data-theme="terminal"] {
  --bg:#04140a; --panel:#07220f; --panel-2:#0a2f16; --panel-3:#0d3a1c;
  --line:#12431f; --line-soft:#0f3a1a;
  --text:#9dffb0; --text-strong:#d8ffe2; --muted:#4f9c63; --faint:#3a7449;
  --accent:#39ff88; --accent-2:#c8ff5a; --danger:#ff6b6b; --warning:#ffd166;
}

/* Light themes need a far weaker, cooler shadow ramp - the dark ramp reads
   as dirty smudges on a white surface. */
body[data-theme="light"] {
  --bg:#f4f6fb; --panel:#ffffff; --panel-2:#eef2f8; --panel-3:#e4eaf3;
  --line:#d6deea; --line-soft:#e6ecf4;
  --text:#1f2a3d; --text-strong:#0d1420; --muted:#68758c; --faint:#94a0b4;
  --accent:#2f6df6; --accent-2:#0a8f63; --danger:#d43b3b; --warning:#96620a;
  --sh-1: 0 1px 2px rgba(24,34,56,.08);
  --sh-2: 0 2px 6px rgba(24,34,56,.07), 0 10px 24px rgba(24,34,56,.10);
  --sh-3: 0 4px 10px rgba(24,34,56,.09), 0 18px 44px rgba(24,34,56,.14);
  --sh-4: 0 8px 18px rgba(24,34,56,.12), 0 28px 64px rgba(24,34,56,.18);
  --scrim: rgba(20,28,44,.35);
}

/* High contrast deliberately opts out of the elevation ramp: a shadow is a
   low-contrast cue by definition, so borders carry the layering instead. */
body[data-theme="contrast"] {
  --bg:#000000; --panel:#0a0a0a; --panel-2:#141414; --panel-3:#1e1e1e;
  --line:#4a4a4a; --line-soft:#333333;
  --text:#ffffff; --text-strong:#ffffff; --muted:#c0c0c0; --faint:#9a9a9a;
  --accent:#ffd400; --accent-2:#00e5ff; --danger:#ff5a5a; --warning:#ffd400;
  --sh-1: none;
  --sh-2: 0 0 0 1px var(--line);
  --sh-3: 0 0 0 2px var(--line);
  --sh-4: 0 0 0 2px var(--accent);
  --scrim: rgba(0,0,0,.75);
}

/* ─────────────── Shell ─────────────── */

* { box-sizing: border-box; }
html, body { margin: 0; height: 100%; font-family: var(--font); overflow: hidden; }

body { background: var(--bg); color: var(--text); font-size: var(--fs-body);
  line-height: 1.5; display: grid;
  grid-template-columns: 236px 1fr; grid-template-rows: minmax(56px, auto) 1fr;
  grid-template-areas: "top top" "side main"; }

/* #topbar and .btn-group both wrap (rather than a fixed nowrap row) so a
   narrow desktop window - a snapped half screen, a small laptop, anything
   under ~950px - grows the toolbar to a second row instead of silently
   pushing the rightmost buttons (Snapshot, Legend, Views…) off the edge of
   the viewport with no scrollbar to reach them. At normal desktop widths
   nothing wraps, so this has zero visual effect there; grid-template-rows
   above uses minmax(56px, auto) so the row grows to match. */
#topbar { grid-area: top; display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--s4); padding: var(--s2) var(--s4); background: var(--panel);
  border-bottom: 1px solid var(--line); box-shadow: var(--sh-1);
  position: relative; z-index: 30; }
.brand { display: flex; align-items: center; gap: var(--s2); font-weight: 600;
  letter-spacing: .2px; color: var(--text-strong); font-size: var(--fs-head);
  white-space: nowrap; }
.brand-mark { width: 20px; height: 20px; flex-shrink: 0; color: var(--accent); }
.control { display: flex; align-items: center; gap: var(--s2); }
.control.grow { flex: 1; min-width: 140px; }
.control label { color: var(--muted); font-size: var(--fs-small); }

/* Signed-in-as / sign-out control - only ever injected into #topbar by
   app.js's renderAuthControl(), and only when /api/whoami reports
   auth_enabled:true. Absent from the DOM entirely (not just hidden) when
   auth is off, so this rule has zero visual effect on the default,
   auth-disabled deployment. */
.auth-control { gap: var(--s3); margin-left: auto; }
.auth-user { color: var(--muted); font-size: var(--fs-small); white-space: nowrap; }

.section-label { display: block; color: var(--muted); font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: .08em; margin: var(--s4) 0 var(--s2); }

/* ─────────────── Controls ─────────────── */

select, input, button { background: var(--panel-2); color: var(--text);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: var(--s2) var(--s3); font-size: var(--fs-body); font-family: inherit;
  line-height: 1.35; }
select, input { min-height: 32px; }
select:hover, input:hover { border-color: var(--faint); }
select:focus-visible, input:focus-visible, button:focus-visible, a:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px; }
button { cursor: pointer; min-height: 32px;
  transition: border-color var(--t-fast) var(--ease), color var(--t-fast) var(--ease),
              background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease); }
button:hover { border-color: var(--faint); background: var(--panel-3); color: var(--text-strong); }
button:active { background: var(--panel-2); }
/* Non-admin accounts (OIDC just-in-time-provisioned users default to role
   "user") get the Edit toggle disabled client-side - server.py 403s the
   write endpoints regardless, this is just so it doesn't look clickable. */
button:disabled { cursor: not-allowed; opacity: .45; }
button:disabled:hover { border-color: var(--line); color: inherit; background: var(--panel-2); }

/* Icon buttons: svg + optional text label, consistent sizing/spacing. */
.icon-btn { display: inline-flex; align-items: center; gap: var(--s2); }
.icon-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
#hamburger svg { width: 18px; height: 18px; }
.btn-group { gap: var(--s1); flex-wrap: wrap; }
.btn-group .btn-label { font-size: var(--fs-small); }
/* Active toolbar toggles: the accent carries the state, not a coloured box. */
.icon-btn.active { border-color: var(--accent); color: var(--accent); background: var(--panel-3); }
.icon-btn.active:hover { color: var(--accent); }
#refresh.spinning svg { animation: spin .9s linear infinite; }
#refresh:disabled { opacity: .7; cursor: default; }

/* Search box with an inline icon and a clear button. */
.search-wrap { position: relative; width: 100%; display: flex; align-items: center; }
.search-icon { position: absolute; left: var(--s3); width: 15px; height: 15px;
  color: var(--muted); pointer-events: none; }
#search { width: 100%; padding-left: var(--s6); padding-right: var(--s5); }
.search-clear { position: absolute; right: var(--s1); padding: var(--s1); min-height: 0;
  border: none; background: none; color: var(--muted); opacity: 0; pointer-events: none;
  transition: opacity var(--t-fast) var(--ease); }
.search-clear.show { opacity: 1; pointer-events: auto; }
.search-clear:hover { color: var(--accent); background: none; }
.search-clear svg { width: 13px; height: 13px; }

/* Refresh / loading spinner, only visible while a fetch is in flight. */
#loading { width: 18px; opacity: 0; transition: opacity var(--t-fast) var(--ease); }
#loading.show { opacity: 1; }
.spinner { display: inline-block; width: 15px; height: 15px; border-radius: var(--r-pill);
  border: 2px solid var(--line); border-top-color: var(--accent); animation: spin .7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* ─────────────── Sidebar ─────────────── */

#sidebar { grid-area: side; background: var(--panel); border-right: 1px solid var(--line);
  overflow-y: auto; padding: var(--s4); box-shadow: var(--sh-1); }
#sidebar h3 { margin: var(--s5) 0 var(--s2); font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
#sidebar section:first-child h3 { margin-top: 0; }
.filters { display: flex; flex-direction: column; gap: 2px; }
.filters label { display: flex; align-items: center; gap: var(--s2); font-size: var(--fs-body);
  cursor: pointer; padding: var(--s1) var(--s2); border-radius: var(--r-sm);
  transition: background var(--t-fast) var(--ease); }
.filters label:hover { background: var(--panel-2); }
.filters input[type=checkbox] { accent-color: var(--accent); width: 14px; height: 14px;
  min-height: 0; flex-shrink: 0; }
.swatch { width: 18px; height: 18px; border-radius: var(--r-sm); display: inline-block;
  flex-shrink: 0; background-repeat: no-repeat; background-position: center; background-size: 62%; }
#site-filter { width: 100%; }
#sidebar h3 .section-hint { text-transform: none; letter-spacing: 0; color: var(--faint);
  font-weight: 400; }

/* Subnet highlight list. Addressing is the one thing the topology drawing
   cannot show: a device hardcoded to the wrong subnet looks perfectly normal
   sitting on the wrong switch. Lighting up one /24 at a time is what makes
   the odd one out visible. */
.subnet-list { display: flex; flex-direction: column; gap: 2px; max-height: 220px; overflow-y: auto; }
.subnet-row { display: flex; align-items: center; gap: var(--s2); width: 100%; min-height: 0;
  padding: var(--s1) var(--s2); border: none; background: none; border-radius: var(--r-sm);
  color: var(--text); font-family: var(--mono); font-size: var(--fs-small);
  text-align: left; cursor: pointer; }
.subnet-row:hover { background: var(--panel-2); color: var(--accent); border: none; }
.subnet-row.active { background: var(--panel-3); color: var(--accent); }
.subnet-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  font-variant-numeric: tabular-nums; }
.subnet-count { color: var(--muted); font-variant-numeric: tabular-nums; }
.subnet-row.active .subnet-count { color: var(--accent); }
.subnet-empty { font-size: var(--fs-small); color: var(--muted); padding: 2px var(--s1); }

/* The IP value in the detail panel is the fastest way into the same view. */
.subnet-link { background: none; border: none; min-height: 0; padding: 0; margin: 0;
  font-family: var(--mono); font-size: var(--fs-micro); color: var(--accent);
  text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
.subnet-link:hover { color: var(--accent-2); background: none; border: none; }
#stats { font-size: var(--fs-small); color: var(--muted); line-height: 1.7;
  margin-top: var(--s5); padding-top: var(--s3); border-top: 1px solid var(--line-soft); }
#stats > div:first-child { color: var(--text-strong); font-weight: 600; margin-bottom: var(--s1);
  font-family: var(--mono); font-variant-numeric: tabular-nums; }
#stats .src-ok { color: var(--accent-2); }
#stats .src-bad { color: var(--danger); cursor: help; }

/* ─────────────── Canvas ─────────────── */

#cy { grid-area: main; background: var(--bg); position: relative; }

/* Ambient concentric-ring backdrop while the radial Spider layout is active -
   pure decoration (Cytoscape draws to canvas so this can't literally track
   node radii), just enough texture to sell the "web" read without claiming
   precision. Off for every other layout. */
#cy.spider-mode {
  background-image: radial-gradient(circle at 50% 50%, transparent 0, transparent 77px,
      var(--line-soft) 78px, transparent 79px, transparent 155px,
      var(--line-soft) 156px, transparent 157px, transparent 233px,
      var(--line-soft) 234px, transparent 235px, transparent 311px,
      var(--line-soft) 312px, transparent 313px);
  background-repeat: no-repeat;
  background-position: center;
  transition: background-image var(--t) var(--ease);
}

/* Empty / error state shown over the canvas when there is nothing to draw. */
#empty-state { position: absolute; inset: 0; display: none; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--s3); color: var(--muted);
  text-align: center; padding: var(--s5); pointer-events: none; }
#empty-state.show { display: flex; }
#empty-state svg { width: 48px; height: 48px; opacity: .45; }
#empty-state p { max-width: 340px; font-size: var(--fs-body); margin: 0; }

/* ─────────────── Floating panel family ───────────────
   Legend, detail and (on mobile) the compass sheet share one head: a drag
   handle, a title, a dock-back-to-default pin, and a close. app.js's
   makeDraggable() picks the handle up via [data-drag-handle] and only ever
   writes left/top + the .floating class, so a panel's docked position stays
   pure CSS and the pin is a one-line reset rather than a second layout. */

.panel-head { display: flex; align-items: center; gap: var(--s2);
  padding: var(--s2) var(--s2) var(--s2) var(--s3); border-bottom: 1px solid var(--line-soft);
  flex-shrink: 0; cursor: grab; touch-action: none; user-select: none; }
.panel-head:active { cursor: grabbing; }
.panel-grip { width: 14px; height: 14px; flex-shrink: 0; color: var(--faint); }
.panel-head-title, .legend-title { flex: 1; min-width: 0; font-size: var(--fs-micro);
  font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.panel-head .icon-btn { padding: var(--s1); min-height: 0; border: none; background: none;
  color: var(--muted); cursor: pointer; }
.panel-head .icon-btn:hover { color: var(--accent); background: none; border: none; }
.panel-head .icon-btn svg { width: 14px; height: 14px; }
/* The pin only means something once a panel has actually been dragged off
   its dock, so it stays out of the way until then. */
.panel-pin { display: none; }
.floating > .panel-head .panel-pin { display: inline-flex; }

/* A dragged panel is positioned by JS in its offsetParent's coordinates, so
   every docked-side offset has to be released - otherwise the panel is
   pinned to two opposite edges at once and simply stretches. These are
   written ID-scoped on purpose: the docked positions below are ID rules, and
   a bare .floating class would lose to them on specificity. */
#legend-panel.floating, #detail.floating {
  left: var(--float-x); top: var(--float-y);
  right: auto; bottom: auto; margin: 0; }
#legend-panel.floating.dragging, #detail.floating.dragging {
  box-shadow: var(--sh-4); transition: none; }

/* Overview minimap: a second, tiny Cytoscape instance mirroring the main
   graph's node positions (no labels/icons, dot-scale) with a rectangle
   showing the current viewport. Essential once the IT Glue layer (~2,700
   nodes) or a multi-hub Spider view is on screen - lets you see the whole
   web shape while zoomed into one leg, and drag to jump anywhere on it. */
#minimap { position: absolute; left: var(--s4); bottom: var(--s4); width: 190px; height: 140px;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md);
  overflow: hidden; z-index: 20; box-shadow: var(--sh-2);
  opacity: .9; transition: opacity var(--t-fast) var(--ease); cursor: crosshair; touch-action: none; }
#minimap:hover, #minimap:active { opacity: 1; }
#minimap.hidden { display: none; }
#minimap-cy { position: absolute; inset: 0; pointer-events: none; }
#minimap-viewport { position: absolute; border: 1.5px solid var(--accent-2);
  border-radius: 2px; box-shadow: 0 0 0 1px var(--scrim);
  pointer-events: none; min-width: 4px; min-height: 4px; }

/* Always-on-screen legend. Kind rows double as filter toggles - clicking one
   hides/shows that kind by mutating the same state.hiddenKinds set the
   sidebar's "Kinds" checkboxes use (renderLegend()/setKindHidden() keep both
   in sync), so on mobile - where the sidebar is a hidden-by-default drawer -
   this is also the fastest way to isolate one kind. Edge styles and the
   dashed/glow/green-border node markers are otherwise only explained in code
   comments, so they get a static key here too. Sits bottom-right, mirroring
   the minimap's bottom-left corner, rebuilt by renderLegend() on every filter
   change - until it is dragged somewhere else. */
#legend-panel { position: absolute; right: var(--s4); bottom: 70px; z-index: 19;
  width: 212px; max-height: min(60vh, 430px); display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--r-md);
  box-shadow: var(--sh-2); overflow: hidden; }
#legend-panel.hidden { display: none; }
.legend-body { overflow-y: auto; padding: var(--s2) var(--s3) var(--s3); }
.legend-section + .legend-section { margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--line-soft); }
.legend-section-title { font-size: var(--fs-micro); text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted); margin-bottom: var(--s1); }
.legend-hint { text-transform: none; letter-spacing: 0; color: var(--faint); }
.legend-rows { display: flex; flex-direction: column; gap: 2px; }
.legend-row { display: flex; align-items: center; gap: var(--s2); font-size: var(--fs-small);
  padding: var(--s1); min-height: 0; border-radius: var(--r-sm); background: none; border: none;
  color: var(--text); text-align: left; width: 100%; cursor: pointer; font-family: inherit; }
button.legend-row:hover { background: var(--panel-2); color: var(--accent); border: none; }
.legend-row.legend-static { cursor: default; }
.legend-row.legend-static:hover { background: none; color: var(--text); }
.legend-row.off { opacity: .45; }
.legend-row.off .legend-label { text-decoration: line-through; }
.legend-swatch { width: 16px; height: 16px; border-radius: var(--r-sm); flex-shrink: 0;
  background-repeat: no-repeat; background-position: center; background-size: 62%; }
.legend-line { width: 20px; height: 0; border-top: 2.4px solid; flex-shrink: 0; }
.legend-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend-empty { font-size: var(--fs-small); color: var(--muted); padding: 2px var(--s1); }
.legend-mark { width: 16px; height: 16px; border-radius: var(--r-sm); flex-shrink: 0;
  background: var(--panel-2); }
.legend-mark.hub-mark { border: 2px solid var(--text-strong); }
.legend-mark.manual-mark { border: 2px dashed var(--warning); }
.legend-mark.edited-mark { border: 2px solid var(--accent-2); }
.legend-mark.contradicted-mark { border: 2px dashed var(--danger); }

/* Evidence blocks in the detail panel: what the live network says that the
   documentation does not, and which duplicate records this node absorbed. */
.evidence { margin: 0 0 var(--s3); padding: var(--s2) var(--s3); border-radius: var(--r-sm);
  background: var(--panel-2); border: 1px solid var(--line-soft);
  font-size: var(--fs-small); line-height: 1.5; }
.evidence strong { font-weight: 600; }
.evidence-warn { border-color: var(--danger); }
.evidence-warn strong { color: var(--danger); }
.evidence-list { margin: var(--s1) 0 0; padding-left: var(--s4); }
.evidence-list li { margin-bottom: 2px; }
.evidence-meta { color: var(--muted); font-family: var(--mono); font-size: var(--fs-micro); }

/* "What is on this network" - the question you are actually asking when you
   click one. Each row jumps to that device, same as a port-map occupant. */
.connections { margin-top: var(--s4); border-top: 1px solid var(--line-soft); padding-top: var(--s3); }
.connections-head { font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase;
  letter-spacing: .08em; color: var(--muted); margin-bottom: var(--s2);
  display: flex; align-items: center; gap: var(--s2); }
.connections-count { font-family: var(--mono); font-weight: 400; color: var(--faint);
  font-variant-numeric: tabular-nums; }
.connections-list { display: flex; flex-direction: column; gap: 1px;
  max-height: 320px; overflow-y: auto; }
.conn-row { display: flex; align-items: center; gap: var(--s2); width: 100%; min-height: 0;
  padding: var(--s1) var(--s2); border: none; background: none; border-radius: var(--r-sm);
  color: var(--text); text-align: left; cursor: pointer; font-family: inherit;
  font-size: var(--fs-small); }
.conn-row:hover { background: var(--panel-2); color: var(--accent); border: none; }
.conn-swatch { width: 15px; height: 15px; border-radius: var(--r-sm); flex-shrink: 0;
  background-repeat: no-repeat; background-position: center; background-size: 62%; }
.conn-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.conn-ip { font-family: var(--mono); font-size: var(--fs-micro); color: var(--muted);
  font-variant-numeric: tabular-nums; flex-shrink: 0; }
.conn-via { font-size: var(--fs-micro); color: var(--faint); flex-shrink: 0;
  max-width: 38%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.connections-more { font-size: var(--fs-micro); color: var(--muted); padding: var(--s1) var(--s2); }
.conn-section-title { display: flex; align-items: center; gap: var(--s2);
  font-size: var(--fs-micro); color: var(--faint); margin: var(--s2) 0 var(--s1);
  text-transform: uppercase; letter-spacing: .06em; }
.conn-section-title:first-of-type { margin-top: 0; }

/* One device, several collectors. Each account is its own block rather than a
   merged table, so where two sources disagree the disagreement is visible. */
.source-reports { margin-top: var(--s4); border-top: 1px solid var(--line-soft); padding-top: var(--s3); }
.source-reports-head { font-size: var(--fs-micro); font-weight: 600; text-transform: uppercase;
  letter-spacing: .08em; color: var(--muted); margin-bottom: var(--s2); }
.source-report { margin-bottom: var(--s3); border: 1px solid var(--line-soft);
  border-radius: var(--r-sm); overflow: hidden; }
.source-report:last-child { margin-bottom: 0; }
.source-name { padding: var(--s1) var(--s2); background: var(--panel-2);
  border-bottom: 1px solid var(--line-soft); font-size: var(--fs-micro);
  font-weight: 600; color: var(--accent); }
.source-report table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); }
.source-report td { padding: var(--s1) var(--s2); border-bottom: 1px solid var(--line-soft);
  vertical-align: top; }
.source-report tr:last-child td { border-bottom: none; }

/* Compass overlay chrome (hint caption + close button) only ever appears
   inside the mobile full-screen sheet variant of #minimap below - hidden
   here so it has zero effect on the desktop corner minimap. */
#minimap-hint, #minimap-close { display: none; }
#compass-backdrop { display: none; }

/* ─────────────── Popovers ───────────────
   Snapshot and Saved views share one recipe. Position (top/left) is set by
   JS rather than CSS anchoring, because the toolbar wraps to a variable
   height on mobile - a fixed offset would drift under the button as soon as
   the toolbar grows past one row. */

#snapshot-panel, #views-panel, #hidden-panel { position: fixed; z-index: 70; max-width: calc(100vw - 16px);
  background: var(--panel-2); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: var(--s4); box-shadow: var(--sh-3); display: flex; flex-direction: column;
  gap: var(--s3); }
#snapshot-panel { width: 244px; }
#views-panel { width: 288px; }
#snapshot-panel.hidden, #views-panel.hidden, #hidden-panel.hidden { display: none; }

/* Hidden-devices manager. Hiding is only safe if it is obviously reversible,
   so the panel always shows what was taken off the map, which customer it
   belongs to, and a restore button - and a second layer answers "what IS
   this" without having to restore it just to find out. */
/* Width only. Do NOT set position here: this panel is placed by
   positionPopover(), which writes VIEWPORT coordinates, so it only means
   anything while the panel stays `fixed` (set in the shared popover rule
   above). Overriding it to `relative` reinterprets those same numbers as
   offsets from the panel's in-flow position in the toolbar and throws it
   clean off screen - the button appears dead. The detail layer inside does
   not need a relative parent either: a fixed element is already a containing
   block for its absolutely positioned descendants. */
#hidden-panel { width: 340px; }
.hidden-note { font-size: var(--fs-micro); color: var(--muted); line-height: 1.45; }
.hidden-list { display: flex; flex-direction: column; gap: 1px; max-height: 340px;
  overflow-y: auto; border-top: 1px solid var(--line-soft); padding-top: var(--s2); }
.hidden-row { display: flex; align-items: center; gap: var(--s2); padding: var(--s1);
  border-radius: var(--r-sm); }
.hidden-row:hover { background: var(--panel-3); }
.hidden-open { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px;
  background: none; border: none; min-height: 0; padding: 0; text-align: left;
  cursor: pointer; color: var(--text); font-family: inherit; }
.hidden-open:hover { background: none; border: none; color: var(--accent); }
.hidden-name { font-size: var(--fs-small); font-weight: 600; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.hidden-meta { font-size: var(--fs-micro); color: var(--muted); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.hidden-restore { flex-shrink: 0; padding: var(--s1) var(--s2); font-size: var(--fs-micro);
  min-height: 0; border-radius: var(--r-sm); }
.hidden-restore.wide { width: 100%; margin-top: var(--s3); justify-content: center;
  padding: var(--s2); font-size: var(--fs-small); }

/* The layer above the list. Sits inside the panel so it reads as drilling in
   rather than as a second window that has lost its context. */
.hidden-detail { position: absolute; inset: 0; z-index: 1; display: flex;
  flex-direction: column; padding: var(--s4); border-radius: var(--r-lg);
  background: var(--panel-2); box-shadow: var(--sh-3); overflow-y: auto; }
.hidden-detail.hidden { display: none; }
.hidden-detail-head { display: flex; align-items: center; gap: var(--s2);
  margin-bottom: var(--s2); }
.hidden-detail-title { flex: 1; min-width: 0; font-size: var(--fs-body); font-weight: 600;
  color: var(--text-strong); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#hidden-detail-close { padding: var(--s1); min-height: 0; border: none; background: none;
  color: var(--muted); }
#hidden-detail-close:hover { color: var(--accent); background: none; border: none; }
#hidden-detail-close svg { width: 14px; height: 14px; }
.hidden-detail table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); }
.hidden-detail td { padding: var(--s1); border-bottom: 1px solid var(--line-soft);
  vertical-align: top; }
.hidden-detail td.k { color: var(--muted); width: 38%; }
.hidden-detail td.v { font-family: var(--mono); font-size: var(--fs-micro); word-break: break-word; }
.hidden-detail td.v.v-text { font-family: var(--font); font-size: var(--fs-small); }

/* Count on the toolbar button, so you can see something is hidden without
   opening the panel to find out. */
.count-badge { display: none; min-width: 16px; padding: 0 var(--s1); border-radius: var(--r-pill);
  background: var(--accent); color: var(--bg); font-size: var(--fs-micro); font-weight: 600;
  font-family: var(--mono); text-align: center; }
.count-badge.show { display: inline-block; }
.icon-btn.gone { display: none; }
.snap-title, .views-title { font-size: var(--fs-micro); text-transform: uppercase;
  letter-spacing: .08em; color: var(--muted); font-weight: 600; }
.snap-row { display: flex; gap: var(--s4); flex-wrap: wrap; }
.snap-row label, .snap-check { display: flex; align-items: center; gap: var(--s2);
  font-size: var(--fs-body); cursor: pointer; }
.snap-row input, .snap-check input { accent-color: var(--accent); width: 14px; height: 14px;
  min-height: 0; flex-shrink: 0; }
#snap-download { justify-content: center; width: 100%; }
.views-save-row { display: flex; gap: var(--s2); }
.views-save-row input { flex: 1; min-width: 0; }
.views-save-row button { flex-shrink: 0; padding: var(--s2); }
.views-list { display: flex; flex-direction: column; gap: 2px; max-height: 260px;
  overflow-y: auto; border-top: 1px solid var(--line-soft); padding-top: var(--s2); }
.views-empty { font-size: var(--fs-small); color: var(--muted); line-height: 1.5; padding: var(--s1) 2px; }
.view-row { display: flex; align-items: center; gap: var(--s2); padding: var(--s2);
  border-radius: var(--r-sm); cursor: pointer; transition: background var(--t-fast) var(--ease); }
.view-row:hover { background: var(--panel-3); }
.view-row:focus-visible { background: var(--panel-3); outline: 2px solid var(--accent); outline-offset: -2px; }
.view-row-text { flex: 1; min-width: 0; }
.view-name { font-size: var(--fs-body); font-weight: 600; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.view-meta { font-size: var(--fs-micro); color: var(--muted); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.view-delete { flex-shrink: 0; padding: var(--s1); min-height: 0; border: none;
  background: none; color: var(--muted); }
.view-delete:hover { color: var(--danger); border: none; background: none; }
.view-delete svg { width: 14px; height: 14px; }

/* ─────────────── Focus breadcrumb ───────────────
   Shows the ancestor chain from a node's hub down to the clicked node, click
   any crumb to re-focus on that ancestor. Sits top-left of the canvas (same
   inset as the minimap's bottom-left) so it never competes with the same
   corner, and shrinks to its content instead of stretching full-width so a
   two-hop chain doesn't look like an empty bar. Long chains scroll
   horizontally rather than wrapping. */

#breadcrumb { position: absolute; top: var(--s3); left: var(--s4); z-index: 18;
  display: none; align-items: center; gap: 2px; flex-wrap: nowrap; overflow-x: auto;
  max-width: min(70%, 600px); padding: var(--s1) var(--s2); border-radius: var(--r-md);
  background: var(--panel); border: 1px solid var(--line);
  box-shadow: var(--sh-2); font-size: var(--fs-small); }
#breadcrumb.show { display: flex; }
.crumb-title { color: var(--muted); font-weight: 600; font-size: var(--fs-micro);
  text-transform: uppercase; letter-spacing: .08em; margin-right: var(--s1); flex-shrink: 0; }
.crumb { background: none; border: none; min-height: 0; color: var(--text);
  padding: var(--s1) var(--s2); border-radius: var(--r-sm); font-size: var(--fs-small);
  white-space: nowrap; flex-shrink: 0; }
.crumb:hover { background: var(--panel-2); color: var(--accent); border: none; }
.crumb.current { color: var(--accent); font-weight: 600; cursor: default; }
.crumb.current:hover { background: none; }
.crumb-sep { color: var(--faint); flex-shrink: 0; }
.crumb-count { color: var(--muted); margin-left: var(--s1); white-space: nowrap; flex-shrink: 0;
  font-family: var(--mono); }
/* Keyboard-nav hint: only meaningful on a real keyboard, so it's hidden on
   touch/mobile where a physical arrow-key press can't happen anyway. */
.crumb-kbd-hint { color: var(--faint); font-size: var(--fs-micro); margin-left: var(--s2);
  padding-left: var(--s2); border-left: 1px solid var(--line-soft); white-space: nowrap; flex-shrink: 0; }
#focus-exit { margin-left: var(--s1); flex-shrink: 0; padding: var(--s1); min-height: 0;
  border: none; background: none; color: var(--muted); }
#focus-exit:hover { color: var(--accent); border: none; background: none; }
#focus-exit svg { width: 14px; height: 14px; }

/* ─────────────── Detail panel ───────────────
   z-index note: #legend-panel (19) and #minimap (20) are ambient, freely
   toggleable chrome; #detail is what you get by deliberately tapping a node,
   so it must never end up buried under them where they overlap - on a phone
   the legend's bottom strip and the detail bottom-sheet occupy the exact
   same corner. 21 sits just above both. #edit-bar (22) still wins over
   #detail, and #toast (23) stays on top of all of them so a confirmation is
   never silently hidden behind the mode toolbar that triggered it. */

#detail { position: absolute; top: 68px; right: var(--s3); width: 336px; max-height: 78vh;
  display: flex; flex-direction: column; background: var(--panel);
  border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden;
  box-shadow: var(--sh-2); z-index: 21; }
#detail.hidden { display: none; }
#detail-body { overflow-y: auto; padding: var(--s4); }
.detail-head { display: flex; align-items: center; gap: var(--s3); }
.detail-icon { width: 30px; height: 30px; border-radius: var(--r-sm); flex-shrink: 0;
  background-repeat: no-repeat; background-position: center; background-size: 60%; }
#detail h2 { margin: 0; font-size: var(--fs-head); font-weight: 600; line-height: 1.25;
  color: var(--text-strong); word-break: break-word; }
#detail .kind-pill { display: inline-block; padding: 2px var(--s2); border-radius: var(--r-pill);
  font-size: var(--fs-micro); font-weight: 600; margin: var(--s2) 0 var(--s3); }
#detail .port-chip { display: inline-block; padding: 2px var(--s2); border-radius: var(--r-pill);
  font-size: var(--fs-micro); font-family: var(--mono); margin: 2px var(--s1) 2px 0;
  background: var(--panel-2); border: 1px solid var(--line-soft); }
#detail .no-ports { color: var(--muted); font-style: italic; }
#detail table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); }
#detail td { padding: var(--s1) var(--s1); vertical-align: top;
  border-bottom: 1px solid var(--line-soft); }
#detail tr:last-child td { border-bottom: none; }
#detail td.k { color: var(--muted); width: 38%; }
/* Machine values are mono; the value column is where every IP, MAC, id,
   serial and firmware string lands. */
#detail td.v { font-family: var(--mono); font-size: var(--fs-micro); word-break: break-word;
  font-variant-numeric: tabular-nums; }
#detail td.v.v-text { font-family: var(--font); font-size: var(--fs-small); }
#detail .notes-edit { width: 100%; min-height: 56px; margin-top: var(--s2); resize: vertical; }
#save-notes { margin-top: var(--s2); }
/* The name a device ended up with is the whole point of the naming pipeline,
   so the panel says which source won rather than leaving it a mystery. */
.name-source { display: inline-flex; align-items: center; gap: var(--s1);
  font-size: var(--fs-micro); color: var(--muted); margin: 0 0 var(--s3); }
.name-source .name-source-tag { font-family: var(--mono); color: var(--accent-2); }

/* Switch port map: "PC plugged into port 24" at a glance from the switch's
   own detail panel - every physical port the collector saw (occupied or
   not), not just the ones with a device on them. */
.port-map { margin: 2px 0 var(--s3); border: 1px solid var(--line-soft);
  border-radius: var(--r-sm); overflow: hidden; background: var(--panel-2); }
.port-map-head { display: flex; align-items: center; justify-content: space-between;
  padding: var(--s2) var(--s3); font-size: var(--fs-micro); font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
  border-bottom: 1px solid var(--line-soft); }
.port-map-count { font-weight: 400; text-transform: none; letter-spacing: normal;
  font-family: var(--mono); }
.port-map-scroll { max-height: 220px; overflow-y: auto; }
.port-map-table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); }
.port-map-table th { position: sticky; top: 0; background: var(--panel-2); text-align: left;
  padding: var(--s1) var(--s2); font-size: var(--fs-micro); font-weight: 600;
  text-transform: uppercase; letter-spacing: .06em; color: var(--muted);
  border-bottom: 1px solid var(--line-soft); }
.port-map-table td { padding: var(--s1) var(--s2); border-bottom: 1px solid var(--line-soft);
  vertical-align: top; }
.port-map-table tr:last-child td { border-bottom: none; }
.port-map-table tr.port-vacant { opacity: .55; }
.port-map-table td.port-num { font-weight: 600; white-space: nowrap; width: 1%;
  font-family: var(--mono); font-variant-numeric: tabular-nums; }
.port-map-table td.port-vlan, .port-map-table td.port-poe { white-space: nowrap; width: 1%;
  color: var(--muted); font-family: var(--mono); font-variant-numeric: tabular-nums; }
.port-map-table td.port-link { white-space: nowrap; width: 1%; }
.port-map-table td.port-link.port-up { color: var(--accent-2); }
.port-map-table td.port-link.port-down { color: var(--muted); }
.port-empty { color: var(--muted); font-style: italic; }
.port-note { color: var(--muted); font-size: var(--fs-micro); margin-top: 2px; }
.port-occupant { background: none; border: none; min-height: 0; padding: 0; margin: 0 2px 0 0;
  color: var(--accent); font-size: var(--fs-small); text-decoration: underline;
  text-underline-offset: 2px; cursor: pointer; }
.port-occupant:hover { color: var(--accent-2); background: none; border: none; }

/* ─────────────── Edit bar & toast ─────────────── */

#edit-bar { position: absolute; bottom: var(--s4); left: 50%; transform: translateX(-50%);
  display: flex; gap: var(--s2); align-items: center; background: var(--panel-2);
  border: 1px solid var(--line); border-radius: var(--r-pill); padding: var(--s2) var(--s4);
  box-shadow: var(--sh-3); z-index: 22; }
#edit-bar.hidden { display: none; }
.edit-hint { color: var(--accent); font-weight: 600; font-size: var(--fs-small); }
#edit-status { color: var(--muted); font-size: var(--fs-small); }
body.editing #cy { cursor: crosshair; }

#toast { position: absolute; bottom: var(--s5); right: var(--s5); background: var(--panel-2);
  border: 1px solid var(--line); color: var(--text-strong); padding: var(--s3) var(--s4);
  border-radius: var(--r-lg); font-size: var(--fs-body); box-shadow: var(--sh-3);
  opacity: 0; transform: translateY(4px); pointer-events: none; z-index: 23;
  transition: opacity var(--t) var(--ease), transform var(--t) var(--ease); }
#toast.show { opacity: 1; transform: translateY(0); }

/* Hamburger + backdrop exist only for the mobile drawer; hidden on desktop. */
#hamburger { display: none; padding: var(--s2) var(--s3); }
#backdrop { display: none; }

/* ─────────────── Mobile / narrow screens ─────────────── */

@media (max-width: 820px) {
  /* Single column: toolbar on top (wraps), graph fills the rest. */
  body { grid-template-columns: 1fr; grid-template-rows: auto 1fr;
         grid-template-areas: "top" "main"; }

  #hamburger { display: inline-flex; }
  .brand { font-size: var(--fs-body); }

  /* Toolbar wraps onto as many rows as it needs; search takes its own row. */
  #topbar { flex-wrap: wrap; height: auto; padding: var(--s2) var(--s3); gap: var(--s2); }
  .control.grow { order: 10; flex-basis: 100%; }
  #topbar select, #topbar input, #topbar button { min-height: 42px; font-size: var(--fs-body); }
  #search { padding-left: var(--s6); }

  /* Icon-only buttons on narrow screens - the title attribute keeps them
     accessible, and it saves the row space text labels would cost. */
  .btn-group .btn-label { display: none; }
  .btn-group { gap: var(--s2); margin-left: auto; }
  .icon-btn svg { width: 19px; height: 19px; }

  #empty-state svg { width: 40px; height: 40px; }

  /* Overview minimap costs screen real estate the phone doesn't have, and
     pinch/drag already covers navigation there - desktop/tablet only. */
  #minimap { display: none; }

  /* Legend becomes a compact bottom strip instead of a corner card - full
     width is cheaper than a narrow floating box on a small screen, and it
     starts collapsed (see the isMobileViewport() check in app.js) so it
     never eats screen space on first load. Dragging is disabled at this
     width (app.js skips wiring it), so .floating never applies here. */
  #legend-panel { left: var(--s2); right: var(--s2); bottom: var(--s2); width: auto; max-height: 42vh; }

  /* Sidebar becomes an off-canvas drawer. */
  #sidebar { position: fixed; z-index: 60; top: 0; bottom: 0; left: 0; width: 82%;
    max-width: 300px; transform: translateX(-102%);
    transition: transform var(--t) var(--ease); box-shadow: var(--sh-3); }
  body.nav-open #sidebar { transform: translateX(0); }
  body.nav-open #backdrop { display: block; position: fixed; inset: 0; z-index: 55;
    background: var(--scrim); }
  #sidebar .filters label { padding: var(--s2) 0; font-size: var(--fs-body); }
  #sidebar input[type=checkbox] { width: 18px; height: 18px; }

  /* Detail: bottom sheet instead of a floating side panel. */
  #detail { top: auto; left: 0; right: 0; bottom: 0; width: 100%; max-height: 62vh;
    border-radius: var(--r-lg) var(--r-lg) 0 0; border-bottom: none; }
  #detail-body { padding: var(--s4); }
  #detail table td { padding: var(--s2) var(--s1); font-size: var(--fs-body); }
  #detail td.v { font-size: var(--fs-small); }
  #detail .notes-edit { min-height: 70px; font-size: var(--fs-body); }
  .port-map-scroll { max-height: 34vh; }
  .port-occupant { padding: 3px 0; }
  .panel-head .icon-btn { padding: var(--s2); }
  .panel-head .icon-btn svg { width: 18px; height: 18px; }

  /* Edit bar: wrap and hug the bottom so buttons stay tappable. */
  #edit-bar { flex-wrap: wrap; justify-content: center; left: var(--s2); right: var(--s2);
    transform: none; border-radius: var(--r-lg); }
  #edit-bar button { min-height: 40px; }

  /* The legend strip spans full-width up to 42vh from the very bottom of the
     canvas - on a phone that's tall enough to sit directly under Edit mode's
     floating bar (also bottom-anchored), so with both open at once the edit
     bar rendered on top of the legend's own rows. Editing is the rarer, more
     deliberate mode of the two, so when it's on, give it the bottom strip
     and shrink the legend to make room above it instead of overlapping. */
  body.editing #legend-panel { bottom: 76px; max-height: 30vh; }
}

/* Coarse-pointer tweaks (touch), independent of width. */
@media (pointer: coarse) {
  button, select { min-height: 38px; }
  .crumb-kbd-hint { display: none; }
}

/* ─────────────── Mobile "Compass" overview sheet ───────────────
   The corner minimap is flatly hidden on mobile - there's no screen real
   estate to spare for a permanent panel - but that left phone users with
   zero orientation aid on a large web (the "Map" toggle button was still
   there, tappable, and silently did nothing). This repurposes that same
   button on mobile: instead of toggling a corner panel, it opens #minimap as
   a temporary full-screen sheet on top of a backdrop, reusing the exact same
   miniCy instance/drag-to-jump logic the desktop minimap already has
   (wireMinimap's pointer handlers measure #minimap-cy's live bounding box,
   so they work unchanged at any size). Tap the backdrop, the close button,
   or Escape to dismiss. */

@media (max-width: 820px) {
  .crumb-kbd-hint { display: none; }
  body.compass-open #compass-backdrop { display: block; position: fixed; inset: 0;
    z-index: 65; background: var(--scrim); }
  body.compass-open #minimap {
    display: block; position: fixed; z-index: 70;
    left: 4%; right: 4%; top: 12vh; bottom: 12vh; width: auto; height: auto;
    opacity: 1; border-radius: var(--r-lg); box-shadow: var(--sh-3);
  }
  body.compass-open #minimap-hint { display: block; position: absolute; left: 0; right: 0;
    bottom: 0; z-index: 1; padding: var(--s5) var(--s3) var(--s3); text-align: center;
    font-size: var(--fs-small); color: var(--muted);
    background: linear-gradient(transparent, var(--panel) 55%); }
  body.compass-open #minimap-close { display: inline-flex; position: absolute;
    top: var(--s2); right: var(--s2); z-index: 2; padding: var(--s2);
    background: var(--panel); border: 1px solid var(--line); }
  body.compass-open #minimap-close svg { width: 18px; height: 18px; }

  /* Breadcrumb and popover touch-sizing: both are already width-aware
     (max-width/clamped left), this just gives fingers a fairer target than
     the desktop-tuned padding. */
  #breadcrumb { max-width: 92%; }
  .crumb { padding: var(--s2); font-size: var(--fs-body); }
  #focus-exit { padding: var(--s2); }
  #focus-exit svg { width: 16px; height: 16px; }
  #snapshot-panel { width: min(320px, calc(100vw - 24px)); }
  #views-panel { width: min(300px, calc(100vw - 24px)); }
  .snap-row input, .snap-check input { width: 18px; height: 18px; }
  .snap-row label, .snap-check { font-size: var(--fs-body); }
}

/* ─────────────── Reduced motion ───────────────
   Everything above animates only to show cause and effect, so it is all
   safely droppable. app.js checks the same query before starting the hub
   pulse and before animating pan/zoom. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}
