
:root {
  /* ════════════════════════════════════════════════════════════════
     Brand — electric blue (canonical, syncs with bright5g.com).
     Replaces the previous Private Wave gold theme. The legacy
     --gold* alias tokens were removed in #202 — every callsite was
     renamed to --accent / --accent-bright / --accent-dim /
     --accent-border across all CSS + JS files. grep "gold" in
     public/assets/{css,js} now returns only historical comments.
     ════════════════════════════════════════════════════════════════ */
  --accent:        #2F6BFF;                       /* primary electric blue */
  --accent-bright: #5B9BFF;                       /* highlights, gradient end, active glow */
  --accent-deep:   #1B3FB0;                       /* gradients, depth */
  --accent-glow:   rgba(47, 107, 255, 0.35);      /* shadows / glow */
  --accent-rgb:    47, 107, 255;                  /* for ad-hoc rgba(var(--accent-rgb), N) */
  --accent-dim:    rgba(47, 107, 255, 0.14);      /* low-alpha fill */
  --accent-border: rgba(47, 107, 255, 0.32);      /* low-alpha border */

  /* Sidebar (dark, blue-tinted) */
  --sb-bg:#0A0E16; --sb-hover:#121A2A; --sb-active:#1A2540; --sb-border:#1E2A45;
  /* Bumped from #6b7280 → #8A94A6 to match the new muted-text token
     and keep AA contrast on inactive section labels. */
  --sb-text:#8A94A6; --sb-text-active:#E8EEF8;
  /* Topbar — near-black with blue undertone to match the page */
  --tb-bg:#0F1622; --tb-border:#1E2A45;
  /* Content (light theme defaults — dark theme overrides below) */
  --bg:#f0f2f5; --bg-card:#ffffff; --bg-card2:#f8f9fb;
  --text:#1a2332; --text2:#4a5568; --text3:#8a9bb0;
  --border-light:#e2e8f0; --border-mid:#d0d7e0;
  /* Dark-theme tokens — use these on any dark surface. --border-light
     and --border-mid above are the ORIGINAL light-theme tokens and
     stay as-is for light-mode rules; blue-tinted variants below are
     the dark-theme equivalents used by SAS/WAN/etc. cards. */
  --border-subtle:rgba(91, 155, 255, 0.18);
  --row-hover-bg: rgba(47, 107, 255, 0.06);
  --row-open-bg:  rgba(47, 107, 255, 0.10);
  /* Status. Two pairs per color:
     - foreground (`--green`, `--amber`, ...) keeps the saturated value
       so it still works as a solid-fill (`.tb-health-dot`, `.mfg` bar,
       `.cell-ok` text) where the bg is the page itself.
     - text-on-tint (`--green-fg`, ...) is the lighter value used by
       `.tag-*`, `.status-pill.sp-*`, `.ni-badge` etc. so the text reads
       cleanly against the new dark-tinted bg.
     - bg (`--green-bg`, ...) was previously light-mode near-white
       (#f0fdf4) which on the dark dashboard rendered as a jarring white
       sticky-note with dark-green text. Now tinted rgba over the page
       background so the surface reads as "subtle highlight" not "alien
       white pane".
     - border (`--green-border`, ...) tracks the tint with a stronger
       alpha so the chip outline stays visible on dark. */
  --green:#16a34a;  --green-fg:#86efac;  --green-bg:rgba(34,197,94,0.12);   --green-border:rgba(34,197,94,0.40);
  --amber:#d97706;  --amber-fg:#fcd34d;  --amber-bg:rgba(245,158,11,0.14);  --amber-border:rgba(245,158,11,0.45);
  --red:#dc2626;    --red-fg:#fca5a5;    --red-bg:rgba(220,38,38,0.12);     --red-border:rgba(220,38,38,0.45);
  --blue:#2563eb;   --blue-fg:#93c5fd;   --blue-bg:rgba(96,165,250,0.12);   --blue-border:rgba(96,165,250,0.45);
  --teal:#0d9488;   --teal-fg:#5eead4;   --teal-bg:rgba(45,212,191,0.12);   --teal-border:rgba(45,212,191,0.45);
  --purple:#7c3aed; --purple-fg:#c4b5fd; --purple-bg:rgba(167,139,250,0.12);--purple-border:rgba(167,139,250,0.45);
  /* Type */
  --mono:'JetBrains Mono',monospace; --sans:'Inter',sans-serif; --display:'Bebas Neue',sans-serif;
}

*{margin:0;padding:0;box-sizing:border-box;}
html,body{height:100%;overflow:hidden;}
body{background:var(--bg);color:var(--text);font-family:var(--sans);font-size:13px;display:flex;flex-direction:column;}

/* ════ TOP BAR ════ */
/* Topbar — flat surface. Previously baked a gold-era hexagonal tile
   pattern (rgba(197,163,89,0.09) gold at 9% opacity) into a
   background-image data URL that survived PR #155's blue theme.
   Removed the data URL so the topbar reads as a clean dark-blue band
   matching the foundation. The container properties (height, border,
   flex layout) stay !important because the gc-overlay sheets target
   them and need to keep winning over any per-view inline overrides. */
.topbar{height:56px!important;background-color:var(--tb-bg)!important;border-bottom:1px solid var(--tb-border);display:flex;align-items:center;padding:0 16px 0 0;flex-shrink:0;z-index:200;position:relative;}
.tb-logo{width:260px;flex-shrink:0;display:flex;align-items:center;padding:0 16px;border-right:1px solid var(--sb-border);}
/* ── Topbar scope trail (Phase-1 nav redesign) ──────────────────────
   Compact "Global › Org › Site" trail INSIDE the topbar row (replaces
   the retired .breadcrumb-bar). Quiet: 12px, muted text; segments are
   <button> elements styled as inline links; current segment is brighter.
   Hidden on Global Overview (JS sets [hidden]) and on the login wall
   (auth.css `body.unauthenticated .topbar`). */
.tb-scope-trail{display:flex;align-items:center;gap:2px;flex-shrink:0;padding:0 16px;font-family:var(--sans);font-size:12px;color:var(--sb-text);min-width:0;overflow:hidden;white-space:nowrap;}
.tb-scope-trail[hidden]{display:none;}
.tb-trail-seg{background:none;border:none;padding:3px 6px;margin:0;font-family:inherit;font-size:12px;color:var(--sb-text);font-weight:500;cursor:pointer;border-radius:4px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:background .12s,color .12s;}
.tb-trail-seg:hover{color:var(--sb-text-active,#e2eaf4);background:rgba(255,255,255,0.05);}
.tb-trail-seg:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
.tb-trail-seg-current{color:var(--accent-bright);font-weight:600;}
.tb-trail-sep{color:#3f3f46;flex-shrink:0;}
@media (max-width:900px){.tb-scope-trail{display:none;}}
/* .tb-divider, .tb-guardian removed 2026-06-04 — orphans
   (topbar logo+text uses .tb-logo + UI.brandMark inline SVG). */
.tb-center{flex:1;display:flex;align-items:center;padding:0 20px;gap:12px;}
/* Topbar search wrap — anchors the result palette dropdown. */
.tb-search-wrap{position:relative;width:100%;max-width:380px;}
.tb-search{display:flex;align-items:center;gap:8px;background:rgba(255,255,255,.04);border:1px solid var(--sb-border);border-radius:5px;padding:6px 10px;cursor:text;transition:border-color .15s,background .15s;}
.tb-search:focus-within{border-color:var(--accent-border);background:rgba(255,255,255,.06);}
.tb-search input{background:transparent;border:none;outline:none;color:#e2eaf4;font-size:12px;font-family:var(--sans);width:100%;min-width:0;}
.tb-search input::placeholder{color:var(--sb-text);}
.tb-search svg{color:var(--sb-text);flex-shrink:0;}
.tb-search-kbd{flex-shrink:0;font-family:var(--mono);font-size:10px;color:var(--sb-text);background:rgba(255,255,255,.04);border:1px solid var(--sb-border);border-radius:3px;padding:1px 5px;line-height:1;letter-spacing:0;}
.tb-search:focus-within .tb-search-kbd{display:none;}

.tb-search-results{position:absolute;top:calc(100% + 6px);left:0;right:0;background:var(--gc-surface-rail,#0C1220);border:1px solid var(--sb-border);border-radius:6px;box-shadow:0 16px 40px rgba(0,0,0,0.5),0 2px 6px rgba(0,0,0,0.35);padding:4px;z-index:300;max-height:380px;overflow-y:auto;font-family:var(--sans);}
.tb-search-results[hidden]{display:none;}
.tb-search-results::-webkit-scrollbar{width:6px;}
.tb-search-results::-webkit-scrollbar-thumb{background:rgba(47, 107, 255,0.18);border-radius:3px;}
.tb-search-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:4px;cursor:pointer;color:var(--sb-text-active);font-size:12px;}
.tb-search-row.sel{background:rgba(47, 107, 255,0.10);}
.tb-search-row-icon{color:var(--sb-text);flex-shrink:0;display:flex;align-items:center;justify-content:center;}
.tb-search-row.sel .tb-search-row-icon{color:var(--accent-bright);}
.tb-search-row-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;}
.tb-search-row-sub{flex-shrink:0;font-size:11px;color:var(--sb-text);font-family:var(--mono);}
.tb-search-empty{padding:12px;color:var(--sb-text);font-size:12px;text-align:center;}

/* ════ Empty-state component ════ */
/* Used wherever a view legitimately has zero rows. The icon disc
   inherits its tint from the tone modifier so a healthy zero (no
   alerts) reads green and a missing-data zero reads neutral gray.
   A small action button (when supplied) keeps the user moving. */
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:6px;padding:32px 20px;color:var(--text2);font-family:var(--sans);}
.empty-state-icon{width:44px;height:44px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-bottom:6px;}
.empty-state-title{font-size:13px;font-weight:600;color:var(--text);letter-spacing:0;}
.empty-state-msg{font-size:12px;color:var(--text3);max-width:340px;line-height:1.5;}
.empty-state .btn-sm{margin-top:10px;}
/* Tones */
/* .empty-state-positive/-neutral/-warning variants removed
   2026-06-04 — orphans (no markup emits them). */

/* ════ Loading card ════
 * Non-table loading state. Same shimmer as skeleton placeholders,
 * sized for an in-card "we're fetching" affordance. The screen-
 * reader-only message text gives assistive tech something to
 * announce; visual users see only the bars. */
.loading-card{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px 20px;gap:0;font-family:var(--sans);}
.loading-card-msg{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}

/* ════ Tooltip system ════
 * Pure CSS hover tooltip on any element with [data-tooltip="…"] +
 * an optional [data-tooltip-side="top|bottom|left|right"] (default
 * top). 280ms delay so accidental cursor-passes don't flicker the
 * tooltip; hides instantly on leave. Renders via a ::before
 * pseudo-element + ::after arrow. Pointer-events: none so it
 * never steals the click target underneath.
 *
 * For long descriptions, prefer a real tooltip component with
 * positioning logic — this is for short hints (button labels,
 * status dot meanings, "Open in new tab" affordances). */
[data-tooltip]{position:relative;}
[data-tooltip]::before,
[data-tooltip]::after{
  pointer-events:none;
  opacity:0;
  transition:opacity .12s ease-out, transform .12s ease-out;
  transition-delay:0s;
  position:absolute;
  z-index:400;
}
[data-tooltip]::before{
  content:attr(data-tooltip);
  font-family:var(--sans);
  font-size:11px;
  font-weight:500;
  line-height:1.4;
  color:#E8EEF8;
  background:#1a1a1a;
  border:1px solid var(--sb-border);
  border-radius:5px;
  padding:5px 9px;
  white-space:nowrap;
  box-shadow:0 6px 18px rgba(0,0,0,0.35);
  letter-spacing:0;
}
[data-tooltip]::after{
  content:"";
  width:0;height:0;
  border:4px solid transparent;
}
[data-tooltip]:hover::before,
[data-tooltip]:hover::after,
[data-tooltip]:focus-visible::before,
[data-tooltip]:focus-visible::after{
  opacity:1;
  transition-delay:.28s;
}
/* Default = top */
[data-tooltip]::before{bottom:calc(100% + 8px);left:50%;transform:translateX(-50%) translateY(2px);}
[data-tooltip]::after{bottom:calc(100% + 4px);left:50%;transform:translateX(-50%);border-top-color:#1a1a1a;}
[data-tooltip]:hover::before,
[data-tooltip]:focus-visible::before{transform:translateX(-50%) translateY(0);}
/* Side modifiers */
[data-tooltip-side="bottom"]::before{bottom:auto;top:calc(100% + 8px);transform:translateX(-50%) translateY(-2px);}
[data-tooltip-side="bottom"]::after{bottom:auto;top:calc(100% + 4px);border-top-color:transparent;border-bottom-color:#1a1a1a;}
[data-tooltip-side="bottom"]:hover::before,
[data-tooltip-side="bottom"]:focus-visible::before{transform:translateX(-50%) translateY(0);}

[data-tooltip-side="right"]::before{bottom:auto;left:calc(100% + 8px);top:50%;transform:translate(-2px,-50%);}
[data-tooltip-side="right"]::after{bottom:auto;left:calc(100% + 4px);top:50%;transform:translateY(-50%);border-top-color:transparent;border-right-color:#1a1a1a;}
[data-tooltip-side="right"]:hover::before,
[data-tooltip-side="right"]:focus-visible::before{transform:translate(0,-50%);}

[data-tooltip-side="left"]::before{bottom:auto;right:calc(100% + 8px);left:auto;top:50%;transform:translate(2px,-50%);}
[data-tooltip-side="left"]::after{bottom:auto;right:calc(100% + 4px);left:auto;top:50%;transform:translateY(-50%);border-top-color:transparent;border-left-color:#1a1a1a;}
[data-tooltip-side="left"]:hover::before,
[data-tooltip-side="left"]:focus-visible::before{transform:translate(0,-50%);}

@media (prefers-reduced-motion: reduce){
  [data-tooltip]::before,
  [data-tooltip]::after{transition:none;transform:none!important;}
}
/* .tb-badges / .tb-badge / .tb-badge-a / .tb-pulse removed
   2026-06-04 — orphans, gold-era tokens (.tb-badge-a referenced
   var(--accent-bright), --gold-border, --gold-dim). */
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
/* Topbar right cluster — was a flex row with a left vertical hairline
   border that read as visual noise next to the permanent grey health
   dot. Border dropped, gap tightened, padding gone. */
.tb-right{display:flex;align-items:center;gap:10px;margin-left:auto;}
/* .tb-icon-btn removed 2026-06-04 — orphan (topbar buttons emit
   different markup; no remaining markup uses .tb-icon-btn). */

/* ── User account: Google-app-account avatar pattern ──────────────
   The previous design rendered initials in a 28px gold-tinted circle
   regardless of whether the identity provider supplied a photo. The
   new pattern uses the Auth0 user.picture (Google / GitHub / etc.
   carry it through) as the main affordance — initials only as a
   fallback when no picture is available. 36px circle with a 2px gold
   ring on hover, presence dot (online) in the bottom-right corner.
   Identical pattern to gmail.com / google.com / a dozen others; the
   page reads as "Yes I'm signed in as this person" at a glance. */
.tb-account{position:relative;}
.tb-avatar{
  width:36px; height:36px;
  border-radius:50%;
  background:var(--accent-dim);
  border:1px solid var(--accent-border);
  display:flex; align-items:center; justify-content:center;
  font-size:13px; font-weight:600; color:var(--accent-bright);
  cursor:pointer; padding:0;
  font-family:var(--sans);
  overflow:hidden; position:relative;
  transition:transform .12s, border-color .15s, box-shadow .15s;
}
.tb-avatar img,
.tb-avatar .tb-avatar-initials{
  width:100%; height:100%;
  display:flex; align-items:center; justify-content:center;
  object-fit:cover;
}
.tb-avatar .tb-avatar-initials{ line-height:1; }
.tb-avatar:hover{
  transform:scale(1.05);
  border-color:var(--accent);
  box-shadow:0 0 0 2px rgba(47, 107, 255,0.18);
}
.tb-avatar:focus-visible{outline:none;border-color:var(--accent);box-shadow:0 0 0 2px rgba(47, 107, 255,0.40);}
/* Presence dot — Google-style "you are online" indicator. Small green
   dot in the bottom-right corner of the avatar. Uses a thicker outer
   ring (background-color of the topbar) so it reads as separate from
   the avatar, not part of it. */
.tb-avatar-presence{
  position:absolute; right:1px; bottom:1px;
  width:9px; height:9px; border-radius:50%;
  background:#22c55e;
  box-shadow:0 0 0 2px var(--tb-bg, #0f0f0f);
  pointer-events:none;
}

/* Account menu — wider, Google-flavored. Header centers a big avatar +
   name + email + "Manage your account" link. Below the divider sit
   the existing settings affordances. Sign-out gets the danger-red
   tint at the bottom. */
.tb-account-menu{
  position:absolute; top:calc(100% + 10px); right:0;
  min-width:300px; max-width:320px;
  background:var(--gc-surface-card,#121A2A);
  border:1px solid var(--accent-border);
  border-radius:10px;
  box-shadow:0 16px 48px rgba(0,0,0,0.55), 0 0 0 1px rgba(47, 107, 255,0.08);
  padding:8px; z-index:300;
  color:var(--sb-text-active);
  font-family:var(--sans);
  animation:tbAccountMenuEnter 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes tbAccountMenuEnter{
  from{ opacity:0; transform:translateY(-6px) scale(0.98); }
  to  { opacity:1; transform:translateY(0) scale(1); }
}
.tb-account-menu[hidden]{display:none;}
.tb-account-hdr{
  display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:18px 18px 14px;
  text-align:center;
}
.tb-account-avatar-big{
  width:64px; height:64px;
  border-radius:50%;
  background:var(--accent-dim);
  border:1px solid var(--accent-border);
  overflow:hidden;
  display:flex; align-items:center; justify-content:center;
  margin-bottom:6px;
}
.tb-account-avatar-big img{
  width:100%; height:100%;
  object-fit:cover;
}
.tb-avatar-big-initials{
  font-size:22px; font-weight:600;
  color:var(--accent-bright);
  font-family:var(--sans);
  line-height:1;
}
.tb-account-name{
  font-size:14px; font-weight:600;
  color:var(--sb-text-active);
  max-width:100%;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.tb-account-email{
  font-size:12px;
  color:#7d8b9c;
  max-width:100%;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  font-family:var(--mono);
}
/* Subtle pill button under the email — opens User Directory for
   platform_admins, identity card for everyone else. Same visual
   weight Google uses for "Manage your Google Account". */
.tb-account-profile-link{
  margin-top:8px;
  padding:6px 14px;
  font-size:12px; font-weight:500;
  color:var(--accent-bright);
  background:rgba(47, 107, 255,0.10);
  border:1px solid var(--accent-border);
  border-radius:99px;
  cursor:pointer;
  font-family:var(--sans);
  transition:background .12s, color .12s, border-color .12s;
}
.tb-account-profile-link:hover{
  background:rgba(47, 107, 255,0.18);
  color:var(--accent);
  border-color:var(--accent);
}
.tb-account-profile-link:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }

.tb-account-sep{height:1px;background:var(--sb-border);margin:6px 4px;}
.tb-account-item{
  width:100%; display:flex; align-items:center; gap:12px;
  padding:10px 14px;
  background:transparent; border:none; border-radius:6px;
  color:var(--sb-text-active);
  font-size:13px; font-family:var(--sans);
  cursor:pointer; text-align:left;
  transition:background .12s, color .12s;
}
.tb-account-item:hover,.tb-account-item:focus-visible{background:rgba(47, 107, 255,0.10);outline:none;}
.tb-account-item svg{flex-shrink:0;color:var(--sb-text);}
.tb-account-item:hover svg{color:var(--accent-bright);}
.tb-account-item .tb-account-state{margin-left:auto;font-family:var(--mono);font-size:11px;color:var(--sb-text);}
.tb-account-item:hover .tb-account-state{color:var(--accent-bright);}
.tb-account-item .tb-account-state kbd{
  font-family:var(--mono); font-size:10px;
  background:#1a1a1a;
  border:1px solid var(--sb-border);
  border-bottom-width:2px;
  border-radius:3px;
  padding:1px 5px;
  color:var(--accent-bright);
}
/* Sign-out gets the red danger tint — Google's pattern, makes the
   destructive action read at a glance. */
.tb-account-item-danger{ color:#fca5a5; }
.tb-account-item-danger svg{ color:#f87171; }
.tb-account-item-danger:hover,.tb-account-item-danger:focus-visible{
  background:rgba(220,38,38,0.10);
  color:#fecaca;
}
.tb-account-item-danger:hover svg{ color:#fca5a5; }

/* ════ Category badges (org portfolio) ════ */
/* Inline-SVG icon + label, monochrome, color is driven by the
   surrounding pill's color. Replaces the old emoji icons (🎓 🏌️ 🎪)
   which clashed with the rest of the inline-SVG icon system and
   rendered differently across OSes. */
.cat-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:700;padding:2px 7px;border-radius:4px;letter-spacing:.06em;border:1px solid;font-family:var(--sans);}
.cat-badge svg{flex-shrink:0;}

/* Category filter button row above the org grid. Pills inherit
   the muted palette of their category but fall back to a neutral
   look when inactive — mirrors the way Meraki / Verkada filter
   chips read at a glance without competing for attention. */
.cat-filter-btn{display:inline-flex;align-items:center;gap:5px;font-family:var(--sans);font-size:10px;font-weight:600;padding:5px 10px;border-radius:4px;border:1px solid var(--border-light);background:var(--bg-card);color:var(--text2);cursor:pointer;transition:background .15s,border-color .15s,color .15s;letter-spacing:.04em;line-height:1;}
.cat-filter-btn:hover{background:var(--bg-card2);border-color:var(--border-mid);color:var(--text);}
.cat-filter-btn svg{flex-shrink:0;opacity:.85;}
.cat-filter-btn.active{background:var(--text);color:var(--bg-card);border-color:var(--text);}
.cat-filter-btn.active svg{opacity:1;}
.cat-filter-btn.cat-filter-edu.active{background:#2563eb;border-color:#2563eb;color:#fff;}
.cat-filter-btn.cat-filter-resort.active{background:#0d9488;border-color:#0d9488;color:#fff;}
.cat-filter-btn.cat-filter-event.active{background:#7c3aed;border-color:#7c3aed;color:#fff;}

/* ════ Skeleton placeholders ════ */
/* Animated gray bars stand in for async-loaded values until the
   patcher in dashboard.js#_overviewFetchAndPatch fills them in. The
   "—" + "Loading…" text we used to show read as broken on first
   paint; a shimmering bar tells the user the system is computing,
   not stuck. Inherit currentColor so usage on dark/light surfaces
   doesn't require palette work. */
@keyframes skeleton-shimmer{
  0%   { background-position: -200% 0; }
  100% { background-position:  200% 0; }
}
.skeleton{display:inline-block;vertical-align:middle;border-radius:3px;background:linear-gradient(90deg, rgba(0,0,0,0.05) 0%, rgba(0,0,0,0.10) 50%, rgba(0,0,0,0.05) 100%);background-size:200% 100%;animation:skeleton-shimmer 1.4s ease-in-out infinite;}
/* .skeleton-num, .skeleton-sub removed 2026-06-04 — orphans
   (skeleton placeholders use .skeleton-cell / .skeleton-pill). */
.skeleton-cell{height:12px;width:64px;}
.skeleton-pill{height:9px;width:42px;border-radius:6px;}

/* ════ Keyboard-shortcuts help overlay ════ */
/* Standalone — doesn't reuse .modal-overlay because the cheat sheet
   has its own keyboard lifecycle (Esc closes it, ? toggles it) and
   no business sharing the lifecycle of regular modals. Centered
   card, dimmed scrim, monospace key chips. */
.kbd-help-overlay{position:fixed;inset:0;background:rgba(10,10,10,0.65);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:600;font-family:var(--sans);}
.kbd-help-overlay[hidden]{display:none;}
.kbd-help-card{background:var(--gc-surface-rail,#0C1220);border:1px solid var(--sb-border);border-radius:8px;box-shadow:0 24px 64px rgba(0,0,0,0.6);min-width:360px;max-width:480px;color:var(--sb-text-active);}
.kbd-help-hdr{display:flex;align-items:center;justify-content:space-between;padding:14px 16px 10px;border-bottom:1px solid var(--sb-border);}
.kbd-help-title{font-family:var(--sans);font-size:13px;font-weight:600;letter-spacing:.04em;color:var(--sb-text-active);}
.kbd-help-close{background:transparent;border:none;color:var(--sb-text);font-size:22px;line-height:1;cursor:pointer;padding:0 4px;border-radius:3px;transition:color .15s,background .15s;}
.kbd-help-close:hover{color:var(--sb-text-active);background:rgba(255,255,255,0.05);}
.kbd-help-body{padding:14px 16px 16px;}
.kbd-help-table{width:100%;border-collapse:collapse;font-size:12px;}
.kbd-help-table td{padding:6px 0;vertical-align:middle;}
.kbd-help-table td:first-child{width:120px;color:var(--sb-text);}
.kbd-help-table td:last-child{color:var(--sb-text-active);}
.kbd-help-table .kbd-help-rule td{padding:6px 0 0;border-top:1px solid var(--sb-border);}
.kbd-help-hint{font-size:11px;color:var(--sb-text);margin-top:10px;}
.kbd-help-hint kbd, .kbd-help-table kbd{display:inline-block;font-family:var(--mono);font-size:10px;line-height:1;padding:3px 6px;background:#1a1a1a;border:1px solid var(--sb-border);border-bottom-width:2px;border-radius:3px;color:var(--accent-bright);min-width:16px;text-align:center;}
/* .tb-clock removed 2026-05-30 — duplicated the OS / browser tab
   clock and added visual noise next to the avatar. The HTML emits a
   hidden #clock stub so legacy textContent updaters keep working. */

/* ════ Topbar health pill ════
 * 14px tinted button between clock and avatar. Color reflects
 * /api/health/deep status; click opens a per-component detail modal.
 * Defaults to gray (unknown) until the first poll resolves. */
/* Platform health pip. Default boot state was rendering a permanent
   grey dot that read as "broken pixel"; now hidden until the first
   health check resolves to something the operator should actually
   know about. The CSS rule below selects only ok/degraded/fail and
   uses display:none for unknown — the BUTTON disappears entirely
   instead of becoming a quiet grey wart on every page load. */
.tb-health{width:14px;height:14px;padding:0;border-radius:50%;border:1px solid var(--sb-border);background:transparent;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:border-color .15s,background .15s,transform .12s;}
.tb-health[data-status="unknown"]{display:none;}
.tb-health:hover{transform:scale(1.12);}
.tb-health:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
.tb-health-dot{width:7px;height:7px;border-radius:50%;background:var(--text3);transition:background .15s;}
.tb-health[data-status="ok"]      .tb-health-dot{background:var(--green);box-shadow:0 0 6px rgba(22,163,74,0.55);}
.tb-health[data-status="degraded"].tb-health-dot{background:var(--amber);box-shadow:0 0 6px rgba(217,119,6,0.55);}
.tb-health[data-status="fail"]    .tb-health-dot{background:var(--red);box-shadow:0 0 8px rgba(220,38,38,0.65);animation:pulse 1.5s ease-in-out infinite;}
.tb-health[data-status="unknown"] .tb-health-dot{background:var(--text3);}
.tb-health[data-status="ok"]      {border-color:rgba(22,163,74,0.30);}
.tb-health[data-status="degraded"]{border-color:rgba(217,119,6,0.30);}
.tb-health[data-status="fail"]    {border-color:rgba(220,38,38,0.45);}

/* ════ Density modes ════
 * `body.density-compact` tightens paddings + font sizes across the
 * platform's most-rendered surfaces — tables, section cards, KPI
 * cards, sidebar nav items. Verkada/Meraki convention: a compact
 * mode for power users who scan dense tables, comfortable for
 * everyone else.
 *
 * Only the surfaces below are touched. Modal forms, login wall,
 * page headers, and breadcrumbs keep their original sizing so the
 * compact mode doesn't make text-entry feel cramped.
 */
body.density-compact .data-table thead th{padding:6px 14px;font-size:9.5px;}
body.density-compact .data-table td{padding:6px 14px;font-size:12px;line-height:1.35;}
body.density-compact .data-table thead th:first-child,
body.density-compact .data-table td:first-child{padding-left:16px;}
body.density-compact .data-table thead th:last-child,
body.density-compact .data-table td:last-child{padding-right:16px;}

body.density-compact .sc-hdr{padding:9px 14px 8px;}
body.density-compact .sc-hdr-title{font-size:12.5px;}
body.density-compact .section-card + .section-card{margin-top:10px;}

body.density-compact .nav-item{padding:6px 12px 6px 14px;gap:9px;}
body.density-compact .ni-label{font-size:11.5px;}

body.density-compact .empty-state{padding:24px 18px;}
body.density-compact .empty-state-icon{width:38px;height:38px;}

/* Compact-mode tag/pill — drops the leading whitespace by 1px so
   a row of pills inside a tight table cell doesn't visually push
   the row taller than the row text. */
body.density-compact .tag,
body.density-compact .status-pill{font-size:9.5px;padding:1px 7px;}

/* Sidebar density carries through to section labels too — compact
   users typically want more nav items visible without scrolling. */
body.density-compact .sb-section-label{padding:6px 14px;font-size:10px;}
body.density-compact .sb-section-items{margin-bottom:2px;}

/* ════ BODY LAYOUT ════ */
.shell{display:flex;flex:1;overflow:hidden;}

/* ════ LEFT SIDEBAR ════ */
/* .sidebar is a flex column with a fixed-height nav-area in the
   middle and a pinned .sidebar-footer at the bottom. overflow-y MUST
   live on .sidebar-nav-area, not here — putting it on .sidebar itself
   caused short-viewport bugs where the flex layout compressed inner
   content to fit the available height (scrollHeight ended up EQUAL to
   clientHeight) so the scrollbar never triggered and expanded section
   items silently clipped. */
/* Sidebar — flat surface. Same gold-era hexagonal tile data URL as
   the topbar (rgba(197,163,89,0.07) gold at 7% opacity) was tinting
   the sidebar with a subtle gold pattern. Removed for a clean dark-
   blue rail matching the foundation. */
.sidebar{width:240px;background:var(--sb-bg);border-right:1px solid var(--sb-border);display:flex;flex-direction:column;flex-shrink:0;overflow-x:hidden;}
/* The middle scroll region. min-height:0 is critical — by default a
   flex child's min-height is auto (content-driven), which prevents
   overflow-y from ever firing when content is tall. Setting it to 0
   lets the child shrink below its intrinsic content size so the inner
   scrollbar takes over. */
/* The scroll region itself is a positioning context for the bottom
   fade overlay; without position:relative the ::after sits in the
   wrong place when the sidebar is the page's first painted column. */
.sidebar-nav-area{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;overflow-y:auto;overflow-x:hidden;position:relative;}
.sidebar-nav-area::-webkit-scrollbar{width:6px;}
.sidebar-nav-area::-webkit-scrollbar-track{background:transparent;}
.sidebar-nav-area::-webkit-scrollbar-thumb{background:rgba(47, 107, 255,0.18);border-radius:3px;}
.sidebar-nav-area::-webkit-scrollbar-thumb:hover{background:rgba(47, 107, 255,0.32);}
.sidebar-nav-area{scrollbar-width:thin;scrollbar-color:rgba(47, 107, 255,0.22) transparent;}
/* Soft fade at the bottom of the scroll region — signals "more
   content below" without taking up space. Only visible when the
   nav-area actually overflows; otherwise it sits behind the spacer
   and is invisible. */
.sidebar::after{content:"";position:absolute;left:0;right:0;bottom:0;height:24px;pointer-events:none;background:linear-gradient(to bottom, rgba(10,10,10,0), rgba(10,10,10,0.95));z-index:1;}
/* Direct children of the nav area must not shrink — otherwise the
   flex layout inside the scroll region compresses them instead of
   overflowing. The spacer keeps its flex:1 growth so it still pushes
   nothing when content is short, but it won't force siblings smaller. */
.sidebar-nav-area > *{flex-shrink:0;}
/* Kept so the mobile media-query below — which re-applies overflow-y
   directly to .sidebar — still gets a styled scrollbar. */
.sidebar{position:relative;}
.sidebar::-webkit-scrollbar{width:6px;}
.sidebar::-webkit-scrollbar-thumb{background:rgba(47, 107, 255,0.18);border-radius:3px;}

/* Context selectors block (.ctx-block / .ctx-label / .ctx-selector /
   .ctx-icon / .ctx-section) removed 2026-06-04 — orphaned. Grep
   confirms zero references in any JS file or HTML markup. The org
   and site selectors now use .nav-item / .nav-item-drop instead. */
/* .ctx-* family (icon/name/sub/chevron/selector/dropdown/option/opt-*)
   removed 2026-06-04 — orphaned. Org/site selectors use .nav-item /
   .nav-item-drop. Grep confirms zero references in JS/HTML markup. */

/* .nav-section, .nav-section-label removed 2026-06-04 — orphans
   (sidebar uses .sb-section + .sb-section-label). */
/* UniFi-style collapsible section header. Click the row (label + chevron)
   to toggle the .sb-section-items container beneath it.
   Visually distinct from nav items: subtle background band, heavier
   font, extra top padding for group separation. NEVER shows the amber
   left-border bracket — that's reserved for the single active nav item
   so one selection never reads as two rows of highlight. */
.sb-section-label{display:flex;align-items:center;justify-content:space-between;padding:18px 14px 6px 18px;margin:6px 6px 2px;font-size:10px;letter-spacing:.05em;color:#6b7280;font-weight:600;text-transform:uppercase;user-select:none;cursor:pointer;border-radius:4px;transition:background .12s,color .12s;background:rgba(255,255,255,0.03);}
.sb-section-label:first-of-type{margin-top:10px;padding-top:14px;}
.sb-section-label:hover{background:rgba(255,255,255,0.055);color:#8a92a0;}
/* has-active-child is still applied by JS so we have a hook if we ever
   want a subtle parent-expand cue — but no visual effect today so
   there's never two highlighted rows for one selection. */
.sb-section-label.has-active-child{ /* intentionally empty */ }
.sb-section-chev{color:#6b7280;transition:transform .15s ease-out;flex-shrink:0;}
.sb-section-label.expanded .sb-section-chev{transform:rotate(90deg);}
/* Expanded max-height is effectively unbounded (2000px) so sections
   with many child items never clip their last row. The previous 480px
   cap was fine for today's sections (<=6 items), but if a new section
   ever grows past that, items would silently disappear.
   overflow:hidden stays so the collapsed→expanded max-height
   transition has something to animate against. */
.sb-section-items{overflow:hidden;transition:max-height .18s ease-out,opacity .15s ease-out;max-height:2000px;opacity:1;}
.sb-section-items.collapsed{max-height:0;opacity:0;pointer-events:none;}

/* ── Phase-1 nav redesign: SITE / ORG / PLATFORM scope groups ───────
   Each group header (.sb-group-hdr) stacks the collapse label
   (.sb-group-label — a .sb-section-label) above the inline scope
   selector (.sb-group-selector). The label still owns the collapse
   toggle + chevron; the selector is the org/site dropdown trigger. */
.sb-group-hdr{display:flex;flex-direction:column;flex-shrink:0;}
.sb-group-hdr .sb-group-label{margin-bottom:0;}
/* Inline scope selector — a compact .nav-item-drop. Sits just under its
   group label, visually tied to the group rather than the nav rows
   below. Reuses .nav-item-drop's chevron rule (.ni-chevron). */
.sb-group-selector{height:30px!important;min-height:30px!important;max-height:30px!important;margin:0 6px 4px!important;padding:0 10px 0 12px!important;background:rgba(255,255,255,0.03)!important;border-radius:6px!important;}
.sb-group-selector:hover{background:rgba(47,107,255,0.08)!important;}
.sb-group-selector .ni-label{font-size:12px;font-weight:600;color:var(--sb-text-active,#e2eaf4);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.sb-group-selector.open .ni-chevron{transform:rotate(180deg)!important;}
/* Org-scoped-view dimming: when the active view is org-scoped, the SITE
   group's header + selector are dimmed to signal the site selector
   doesn't apply. Pointer stays enabled so the user can still switch
   site, but the muted treatment + tooltip make the scope clear. */
.sb-group-hdr.scope-inactive{opacity:0.4;transition:opacity .15s ease-out;}
.sb-group-hdr.scope-inactive .sb-group-selector{cursor:default;}
.nav-item{display:flex;align-items:center;gap:10px;padding:8px 12px 8px 14px;cursor:pointer;transition:background .12s,color .12s;position:relative;}
.nav-item:hover{background:var(--sb-hover);}
.nav-item:focus-visible{outline:none;background:var(--sb-hover);}
.nav-item:focus-visible::before{content:'';position:absolute;left:0;top:4px;bottom:4px;width:3px;background:var(--accent-border);border-radius:0 2px 2px 0;}
.nav-item.active{background:var(--sb-active);color:var(--sb-text-active);}
/* Meraki-style accent left-stripe — bumped 2px → 3px and stretched
   slightly so the active item reads at a glance, even when a
   section is collapsed. */
.nav-item.active::before{content:'';position:absolute;left:0;top:4px;bottom:4px;width:3px;background:var(--accent);border-radius:0 2px 2px 0;}
.nav-item.active .ni-icon{color:var(--accent-bright);}
.nav-item.active .ni-label,.nav-item.selected .ni-label{color:var(--accent-bright);}
.ni-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center;color:var(--sb-text);flex-shrink:0;transition:color .12s;}
.ni-label{font-size:14px;font-weight:600;color:#f1f5f9;flex:1;transition:color .12s;}
.nav-item:hover .ni-icon,.nav-item:hover .ni-label{color:var(--sb-text-active);}
/* .ni-badge, .ni-badge-a removed 2026-06-04 — orphans
   (no nav-item emits a badge with these classes). */

.sidebar-spacer{flex:1;}
/* Pinned to the bottom of .sidebar. flex:0 0 auto keeps it at its
   natural height regardless of how tall the scrollable nav-area
   above it grows. Border-top gives a visual break between the
   scroll region and the pinned footer. */
/* .sidebar-footer + .sf-* family removed 2026-06-04 — orphans
   (no markup emits .sidebar-footer; sidebar footer slot renders
   different content via JS). */

/* ════ MAIN CONTENT ════ */
.main{flex:1;display:flex;flex-direction:column;overflow:hidden;}
/* Page transitions — every view renderer wraps its body in a
   `<div id="view-xxx">` and replaces #main-content's innerHTML.
   The default behavior is a hard cut between views; on a fast
   machine it reads as "did I click the right thing?". A short
   opacity+translateY fade smooths the transition without slowing
   it. Respects prefers-reduced-motion. */
@keyframes view-enter{
  from{opacity:0;transform:translateY(4px);}
  to  {opacity:1;transform:translateY(0);}
}
.main > [id^="view-"]{animation:view-enter .18s ease-out both;}
/* prefers-reduced-motion override for .main > [id^="view-"] animation
   removed 2026-06-04 — the global rule at ~line 1283 already sets
   animation-duration: 0.01ms !important on every element, which
   makes this specific override redundant. */

/* Page header strip */
.page-hdr{background:var(--bg-card);padding:0 24px;flex-shrink:0;display:flex;align-items:center;gap:0;}
.page-title-wrap{padding:14px 0;border-right:none;padding-right:20px;margin-right:0;}
.page-title{font-size:18px;font-weight:600;color:var(--text);line-height:1;}
.page-subtitle{font-size:11px;color:#cbd5e1;margin-top:2px;}
.page-tabs{display:flex;gap:0;padding-left:4px;flex:1;}
.pt{font-size:12px;font-weight:500;color:var(--text3);padding:0 16px;height:52px;display:flex;align-items:center;border-bottom:2px solid transparent;cursor:pointer;transition:all .15s;white-space:nowrap;}
.pt:hover{color:var(--text2);}
.pt.active{color:var(--blue);border-bottom-color:var(--blue);}
.page-actions{margin-left:auto;display:flex;align-items:center;gap:8px;}
/* "Updated Xs ago" data-freshness badge — opt-in per page via
 * UI.pageHeader({ updated: { id } }) + UI.markUpdated(id) after
 * every successful fetch. Sits inside .page-actions before any
 * buttons so the order reads timestamp → primary action. */
.page-updated-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:rgba(34,197,94,0.06);border:1px solid rgba(34,197,94,0.18);font-size:11px;color:var(--text3);font-family:var(--mono);letter-spacing:.02em;user-select:none;}
.page-updated-badge .page-updated-dot{width:6px;height:6px;border-radius:50%;background:#22c55e;flex:0 0 auto;}
.page-updated-badge.page-updated-pulse .page-updated-dot{animation:guardianUpdatedPulse 1.2s ease-out 1;}
.page-updated-badge .page-updated-text{color:var(--text2);}
@keyframes guardianUpdatedPulse{0%{transform:scale(1);box-shadow:0 0 0 0 rgba(34,197,94,0.45);}70%{transform:scale(1.6);box-shadow:0 0 0 8px rgba(34,197,94,0);}100%{transform:scale(1);}}
/* ════ Button system ════
 * Anatomy: every button starts with `.btn`, then a single variant
 * class (primary | secondary | ghost | danger | icon) and an
 * optional size class (.btn-sm). Reachable via keyboard with a
 * visible focus ring. `disabled` state is muted, not invisible.
 * Inline button styles in JS should migrate to these classes
 * progressively — replacing all 19 inline-styled buttons in one
 * go is a separate cleanup. */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:6px 14px;border-radius:5px;font-family:var(--sans);font-size:12px;font-weight:500;cursor:pointer;border:1px solid transparent;transition:background .15s,border-color .15s,color .15s,box-shadow .15s;white-space:nowrap;line-height:1.2;text-decoration:none;}
.btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
.btn:disabled,.btn[aria-disabled="true"]{opacity:0.5;cursor:not-allowed;}
.btn svg{flex-shrink:0;}

/* Sizes */
.btn-sm{padding:4px 10px;font-size:11px;border-radius:4px;}
/* .btn-lg removed 2026-06-04 — orphaned, no callsite. */

/* Variants */
/* Primary button = electric blue with WHITE text + a subtle accent
   glow that picks up the blue. The active-state hex is the deep
   token (--accent-deep) for the pressed feel. Replaces the previous
   gold-with-dark-text combo. */
.btn-primary{background:var(--accent);border-color:var(--accent);color:#FFFFFF;font-weight:600;box-shadow:0 4px 14px var(--accent-glow);}
.btn-primary:hover:not(:disabled){background:var(--accent-bright);border-color:var(--accent-bright);}
.btn-primary:active:not(:disabled){background:var(--accent-deep);border-color:var(--accent-deep);box-shadow:0 2px 6px rgba(47, 107, 255, 0.18);}

.btn-secondary{background:var(--bg-card);border-color:var(--border-mid);color:var(--text);font-weight:500;}
.btn-secondary:hover:not(:disabled){border-color:var(--text3);background:var(--bg-card2);}

.btn-ghost{background:transparent;border-color:var(--border-mid);color:var(--text2);}
.btn-ghost:hover:not(:disabled){background:var(--bg-card2);border-color:var(--text3);color:var(--text);}

/* .btn-danger and .btn-icon removed 2026-06-04 — orphaned. The .btn-inv
   family below covers danger/ghost variants used in the codebase. */

/* ── .btn-inv* — global button family ─────────────────────────
   Previously defined inside _invInjectStyles (only injected when
   the Inventory view mounted), which meant any other view using
   .btn-inv before /inventory was visited rendered default browser
   buttons (white box with system font) — operator-reported on the
   user permissions page. Pulled out so the styles ship on every
   page load.

   Visual: gold-tinted inviting filled button, transparent ghost
   variant, red danger variant, small size modifier. Distinct from
   .btn-primary (which is solid gold) — .btn-inv is the lighter
   inline-action treatment used inside modals + drawers + table
   action slots. */
.btn-inv { display: inline-block; padding: 8px 14px; background: var(--accent-dim); color: var(--accent-bright); border: 1px solid var(--accent-border); border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 500; text-decoration: none; font-family: var(--sans); }
.btn-inv:hover { background: rgba(47, 107, 255,0.22); color: var(--accent); }
.btn-inv.loading { opacity: 0.5; cursor: progress; }
.btn-inv.ghost { background: transparent; color: var(--text2); border-color: var(--border-light); }
.btn-inv.ghost:hover { background: rgba(255,255,255,0.03); color: var(--text); }
.btn-inv.danger { background: rgba(220,38,38,0.12); color: #f87171; border-color: rgba(220,38,38,0.32); }
.btn-inv.danger:hover { background: rgba(220,38,38,0.2); color: #ef4444; }
.btn-inv[disabled] { opacity: .5; cursor: not-allowed; }
.btn-inv-sm { padding: 5px 10px; font-size: 11px; }

/* Body text "link" buttons — kept for compat. Tone moved from
   the conflicting bluish #60a5fa to the platform's gold accent
   so it reads as a proper affordance instead of fighting the
   theme. */
.link-btn{font-size:12px;font-weight:600;color:var(--accent-bright);cursor:pointer;white-space:nowrap;background:none;border:none;padding:0;font-family:var(--sans);transition:color .15s;}
.link-btn:hover{color:var(--accent);text-decoration:underline;}
.link-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px;}

/* Scroll body */
.content-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:20px 24px;display:flex;flex-direction:column;gap:16px;}
.content-body::-webkit-scrollbar{width:6px;}
.content-body::-webkit-scrollbar-thumb{background:var(--border-mid);border-radius:3px;}

/* ════ STAT CARDS ════ */
/* .stat-row / .stat-card / .sc-* (icon/label/val/sub) removed
   2026-06-04 — every KPI bar in the codebase migrated to .gc-kpi-row
   + .gc-kpi (PR #160 foundation + #167-#170 view migrations). No
   class="stat-row" or class="stat-card" emit remains in any view. */

/* ════ SECTION CARD ════ */
/* ════ Section card ════
 * Standard chrome wrapper around content blocks (alerts, node table,
 * etc.). The header rule + the body share the same horizontal
 * padding so card content visually rhymes with the header. The
 * header SVG icon (when present) is muted by default and gold on
 * hover — same affordance language as the sidebar nav. */
.section-card{background:var(--bg-card);border:1px solid var(--border-light);border-radius:8px;overflow:hidden;font-family:var(--sans);}
/* Adjacent section-cards get a 14px gap by default. INSIDE
   .content-body the parent already supplies gap:16px via its flex
   layout, so this margin compounds to ~30px of dead space between
   the two cards — kill it there so the gap reads as intended. */
.section-card + .section-card{margin-top:14px;}
.content-body > .section-card + .section-card{margin-top:0;}
.sc-hdr{display:flex;align-items:center;justify-content:space-between;padding:12px 16px 11px;border-bottom:1px solid var(--border-light);background:var(--bg-card);gap:12px;}
.sc-hdr-title{font-size:13px;font-weight:600;color:var(--text);display:inline-flex;align-items:center;gap:8px;letter-spacing:0;line-height:1.2;}
.sc-hdr-title svg{color:var(--text3);}
.sc-hdr-right{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
/* Tags — small labels on cards/rows. The previous design used
   saturated dark backgrounds (e.g. rgba(5,150,105,0.5)) with light
   text — Bootstrap-era "filled" tags that clashed with the rest of
   the platform's lighter chip aesthetic on white cards. Now every
   tone uses the bg+border tokens from :root so a tag-g looks the
   same as the green-bordered status pill on the same card. */
.tag{display:inline-flex;align-items:center;gap:4px;font-family:var(--sans);font-size:10px;font-weight:600;padding:2px 8px;border-radius:4px;border:1px solid;white-space:nowrap;letter-spacing:.04em;line-height:1.4;}
.tag.tag-g{color:var(--green-fg);background:var(--green-bg);border-color:var(--green-border);}
.tag.tag-a{color:var(--amber-fg);background:var(--amber-bg);border-color:var(--amber-border);}
.tag.tag-r{color:var(--red-fg);background:var(--red-bg);border-color:var(--red-border);}
.tag.tag-b{color:var(--blue-fg);background:var(--blue-bg);border-color:var(--blue-border);}
.tag.tag-t{color:var(--teal-fg);background:var(--teal-bg);border-color:var(--teal-border);}
.tag.tag-p{color:var(--purple-fg);background:var(--purple-bg);border-color:var(--purple-border);}
.tag.tag-gold{color:var(--accent-bright);background:var(--accent-dim);border-color:var(--accent-border);}
.tag.tag-neutral{color:var(--text2);background:var(--bg-card2);border-color:var(--border-light);}
/* .link-btn moved to the button system block above. */

/* ════ TABLE ════ */
/* ════ Data tables ════
 * Meraki/Verkada hallmark: dense rows, subtle dividers, sticky
 * column headers, neutral hover. Click-rows get a barely-there
 * gold tint instead of the previous bluish #f5f7fa so the table
 * doesn't look like it belongs to a different product than the
 * cards above it. */
.data-table-wrap{position:relative;}
.data-table{width:100%;border-collapse:separate;border-spacing:0;font-family:var(--sans);font-size:12.5px;color:var(--text);}
.data-table thead th{position:sticky;top:0;z-index:1;font-family:var(--sans);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text3);text-align:left;padding:9px 16px;background:var(--bg-card2);border-bottom:1px solid var(--border-mid);box-shadow:inset 0 -1px 0 var(--border-mid);white-space:nowrap;}
.data-table thead th:first-child{padding-left:18px;}
.data-table thead th:last-child{padding-right:18px;}
/* Sortable column header — keep the tap target large but the
   visual cue subtle (chevron only on hover / when sorted). */
.data-table thead th.sortable{cursor:pointer;user-select:none;}
.data-table thead th.sortable:hover{color:var(--text);}
.data-table thead th.sorted-asc::after,
.data-table thead th.sorted-desc::after{display:inline-block;margin-left:6px;color:var(--accent-bright);font-family:var(--mono);font-size:9px;}
.data-table thead th.sorted-asc::after{content:'▲';}
.data-table thead th.sorted-desc::after{content:'▼';}

.data-table td{padding:9px 16px;border-bottom:1px solid var(--border-light);vertical-align:middle;line-height:1.4;}
.data-table td:first-child{padding-left:18px;}
.data-table td:last-child{padding-right:18px;}
.data-table tbody tr:last-child td{border-bottom:none;}
.data-table tbody tr.row-click{cursor:pointer;transition:background .12s;}
.data-table tbody tr.row-click:hover td{background:rgba(47, 107, 255,0.04);}
.data-table tbody tr.row-click:hover .cell-name{color:var(--accent-bright);}
.data-table tbody tr.row-click:focus-within td{background:rgba(47, 107, 255,0.06);}
/* Numeric / monospace alignment — let callers opt in via .num. */
.data-table td.num,
.data-table thead th.num{text-align:right;font-family:var(--mono);}
/* LAN page tables need more horizontal room than the 638px section-card
   affords. Wrap in .lan-table-scroll (overflow-x:auto) and floor the
   table at 1000px so columns like TX Power / Firmware / Uptime / Status
   always reach their natural width; horizontal scrollbar appears only
   when the viewport is narrower. */
/* .lan-table-scroll, .lan-name-cell removed 2026-06-04 — orphans
   (LAN table no longer wrapped in a scroll container; cells use
   generic .cell-name/.cell-sub). */
.cell-name{font-weight:600;font-size:13px;color:var(--text);transition:color .12s;}
.cell-sub{font-size:10px;color:#94a3b8;margin-top:2px;font-family:var(--mono);}
.cell-mono{font-family:var(--mono);font-size:11px;color:var(--text2);}
/* .cell-ok / .cell-warn / .cell-crit removed 2026-06-04 — orphans
   (data-table emits status via different class system). */

/* Status pill */
/* Status pills — pill-shaped status with a colored leading dot.
   Same tone-token unification as .tag above; the dot is tinted to
   currentColor so it matches the surrounding text without inline
   style overrides. */
.status-pill{display:inline-flex;align-items:center;gap:6px;font-family:var(--sans);font-size:10px;font-weight:600;padding:2px 9px;border-radius:20px;white-space:nowrap;border:1px solid transparent;letter-spacing:.04em;line-height:1.4;}
.status-pill.sp-g{color:var(--green-fg);background:var(--green-bg);border-color:var(--green-border);}
.status-pill.sp-a{color:var(--amber-fg);background:var(--amber-bg);border-color:var(--amber-border);}
.status-pill.sp-r{color:var(--red-fg);background:var(--red-bg);border-color:var(--red-border);}
.status-pill.sp-b{color:var(--blue-fg);background:var(--blue-bg);border-color:var(--blue-border);}
.status-pill.sp-gray{color:var(--text2);background:var(--bg-card2);border-color:var(--border-light);}
.sp-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0;}
/* Pulse only on .blink — used for "live" / "online" status dots.
   Kept as a slow 1.5s ease so it doesn't draw attention away from
   the rest of the page. */
.sp-dot.blink{animation:pulse 1.5s ease-in-out infinite;}

/* .mbar / .mbf / .mfg / .mfa / .mfr removed 2026-06-04 — orphan
   mini-bar widget (no markup emits these). */

/* Two-col grid */
.two-col{display:grid;grid-template-columns:1fr 1fr;gap:16px;}

/* Gauge cards (.gauge-grid, .gauge-card) and legacy gauge children
   (.gc-label/.gc-val/.gc-unit/.gc-bar/.gc-fill) removed 2026-06-04 —
   orphaned. The legacy .gc-* gauge names also collide with the
   foundation .gc-* namespace, so removing them prevents accidental
   matches. */

/* Alert items */
.alert-list{display:flex;flex-direction:column;}
.alert-item{display:flex;align-items:flex-start;gap:12px;padding:12px 18px;border-bottom:1px solid var(--border-light);cursor:pointer;transition:background .12s;}
.alert-item:last-child{border-bottom:none;}
.alert-item:hover{background:var(--bg-card2);}
.ai-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:3px;}
.ai-title{font-size:13px;font-weight:500;color:var(--text);}
/* .ai-meta, .ai-time removed 2026-06-04 — orphans (markup uses
   .alert-item with only .ai-dot + .ai-title, no meta/time). */

/* .int-list / .int-item / .int-name / .int-sub / .int-status
   removed 2026-06-04 — orphans (no markup emits any .int-* class). */

/* Client cards */
.client-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;}
.client-card{background:var(--bg-card);border:1px solid var(--border-light);border-radius:8px;overflow:hidden;cursor:pointer;transition:all .18s;}
.client-card:hover{border-color:var(--accent-border);box-shadow:0 4px 16px rgba(47, 107, 255,.1);}
/* .cc-top, .cc-top-amber removed 2026-06-04 — orphans, gold-era
   token references (var(--accent), var(--amber)). */
.cc-body{padding:16px;}
.cc-name{font-size:16px;font-weight:700;color:var(--text);}
.cc-type{font-size:11px;color:var(--text3);margin-top:2px;}
.cc-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:12px 0;}
.cc-stat{background:var(--bg-card2);border-radius:5px;padding:8px 10px;}
.cc-stat-label{font-size:9px;text-transform:uppercase;letter-spacing:.08em;color:var(--text3);margin-bottom:3px;}
.cc-stat-val{font-size:13px;font-weight:600;color:var(--text);font-family:var(--mono);}
.cc-footer{display:flex;align-items:center;justify-content:space-between;padding-top:10px;border-top:1px solid var(--border-light);}
.cc-loc{font-size:11px;color:var(--text3);}
.cc-drill{font-size:11px;font-weight:500;color:var(--accent-bright);}

/* .site-list-item + .sli-* family removed 2026-06-04 — orphans
   (site list rendered via different markup elsewhere). */

/* Cameras */
.cam-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:14px 18px;}
/* .cam-cell / .cam-ov / .cam-lbl removed 2026-06-04 — orphans
   (cameras.js renders cards with .cam-card, not .cam-cell). */
.cam-live{position:absolute;top:4px;right:4px;font-size:8px;font-weight:700;color:#ef4444;background:rgba(0,0,0,.7);border:1px solid rgba(239,68,68,.4);padding:1px 5px;border-radius:2px;animation:pulse 2s infinite;font-family:var(--mono);}

/* .sensor-grid / .sensor-tile / .st-* family removed 2026-06-04 —
   orphans (no markup emits any .sensor-* / .st-* class). */

/* .sim-table removed 2026-06-04 — orphan (no markup emits it). */

/* .report-item, .radio-item and the .ri-* family removed
   2026-06-04 — orphans (no markup emits any of them). */

/* Legacy WAN-card layout (.wan-item, .wan-col, .wan-name, .wan-row,
   .wan-key, .wan-val) removed 2026-06-04 — orphans. Current WAN
   view uses table markup with .wan-row (different element type),
   .wan-chev-cell, .wan-type-cell, .wan-expand-row, etc. */

/* .chart-wrap removed 2026-06-04 — orphan stub never rendered. */

/* Legacy topology canvas styles (.topo-area, .topo-canvas-wrap,
   #topoCanvas) removed 2026-06-04 — orphans. Current topology
   renders into .topo-stack-card sections inside .topo-stacks via
   _topoRenderStack(); no <canvas> element exists. */
/* .topo-legend / .leg-* / .topo-filters / .tf removed 2026-06-04 —
   orphans (current topology view doesn't emit a legend or filter
   strip with these classes). */
/* .tf:hover / .tf.active removed 2026-06-04 — leftover from .tf
   removal in PR #187 (only the bare selector was removed). */

/* .drawer + .dw-* family removed 2026-06-04 — orphan (current
   drawers use .nx-drawer-* in dashboard.js, not the legacy
   .drawer/.dw-hdr/.dw-body pattern). */

/* Modal — Cisco Webex Control Hub pattern (matches users-modal,
   alerts-modal, inv-modal, nd-modal across the rest of the site).
   4px backdrop blur, gold-tinted border + shadow, fade+slide entry.
   The earlier flat backdrop + neutral border read as "system dialog"
   rather than "Guardian Command". */
@keyframes modalOverlayFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes modalEnter {
  from { opacity: 0; transform: translateY(-8px) scale(0.98); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.modal-overlay{position:fixed;inset:0;background:rgba(8,10,16,0.62);z-index:500;display:none;align-items:center;justify-content:center;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);padding:20px;}
.modal-overlay.open{display:flex;animation:modalOverlayFadeIn 0.16s ease-out;}
.modal{background:var(--gc-surface-card,#121A2A);border:1px solid var(--accent-border);border-radius:10px;width:500px;max-width:calc(100vw - 32px);max-height:80vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 24px 64px rgba(0,0,0,0.45),0 0 0 1px rgba(47, 107, 255,0.06);font-family:var(--sans);animation:modalEnter 0.18s cubic-bezier(0.16, 1, 0.3, 1);}
.modal-hdr{padding:14px 18px 13px;border-bottom:1px solid var(--border-light);display:flex;align-items:center;justify-content:space-between;flex-shrink:0;gap:12px;}
.modal-title{font-family:var(--sans);font-size:14px;font-weight:600;color:var(--text);letter-spacing:0;line-height:1.2;}
.modal-close{background:transparent;border:1px solid transparent;color:var(--text3);cursor:pointer;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:5px;padding:0;transition:background .12s,color .12s,border-color .12s;flex-shrink:0;}
.modal-close:hover{color:var(--text);background:var(--bg-card2);}
.modal-close:focus-visible{outline:none;border-color:var(--accent);}
.modal-close svg{display:block;}
.modal-body{padding:18px 20px;overflow-y:auto;flex:1;font-size:13px;color:var(--text);}
.modal-body::-webkit-scrollbar{width:6px;}
.modal-body::-webkit-scrollbar-thumb{background:var(--border-mid);border-radius:3px;}

/* ════ Form field system ════
 * Used inside modals + inline forms. Standard label, input, select,
 * textarea, plus .form-row for two-column layouts and .form-help
 * for the muted hint text underneath a field. Auto-applied to
 * every <input>/<select>/<textarea> inside .modal-body so existing
 * modals get the new look without per-call markup changes. */
/* Form field styling. The legacy .form-row/.form-field/.form-label/
   .form-input/.form-select/.form-textarea/.form-help/.form-error
   selectors were removed 2026-06-04 — no markup emits them. The
   .modal-body * variants below auto-style every input inside any
   .modal-body without per-call markup, which is how every modal in
   the app picks up these styles. Focus border switched to var(--blue)
   from the gold-era var(--accent). */
.modal-body input[type="text"],
.modal-body input[type="email"],
.modal-body input[type="password"],
.modal-body input[type="number"],
.modal-body input[type="search"],
.modal-body input[type="url"],
.modal-body input[type="tel"],
.modal-body select,
.modal-body textarea{
  display:block;width:100%;
  font-family:var(--sans);font-size:13px;color:var(--text);
  background:var(--bg-card);
  border:1px solid var(--border-mid);
  border-radius:5px;
  padding:8px 10px;
  line-height:1.4;
  transition:border-color .15s,box-shadow .15s;
  box-sizing:border-box;
}
.modal-body input:focus,
.modal-body select:focus,
.modal-body textarea:focus{
  outline:none;
  border-color:var(--blue,#2F6BFF);
  box-shadow:0 0 0 3px rgba(47, 107, 255,0.15);
}
.modal-body input::placeholder,
.modal-body textarea::placeholder{
  color:var(--text3);
}
.modal-body input:disabled,
.modal-body select:disabled,
.modal-body textarea:disabled{
  background:var(--bg-card2);color:var(--text3);cursor:not-allowed;
}
.modal-body textarea{min-height:88px;resize:vertical;}
/* Native select dropdown chevron — hide the default and draw our own
   so light/dark surfaces both look intentional. */
.modal-body select{
  appearance:none;-webkit-appearance:none;-moz-appearance:none;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238a96a8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
  background-repeat:no-repeat;background-position:right 10px center;
  padding-right:32px;
}

/* Modal footer — for primary/cancel button rows. Right-aligned, with
   a top divider so it reads as a real action area not a free-floating
   button strip. */
/* .modal-footer removed 2026-06-04 — orphan (modals close via
   header X / Esc and inline action buttons, no fixed footer). */
.modal-section{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;color:var(--text3);margin:14px 0 8px;padding-bottom:5px;border-bottom:1px solid var(--border-light);}
.modal-row{display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid rgba(255,255,255,0.05);}
.modal-row:last-child{border-bottom:none;}
.modal-key{font-size:12px;color:var(--text3);}
.modal-val{font-family:var(--mono);font-size:12px;color:var(--text);}
.modal-desc{font-size:12px;color:var(--text2);line-height:1.7;margin-bottom:14px;}
.modal-btn{display:block;width:100%;margin-top:8px;padding:8px 0;border-radius:5px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid var(--border-mid);background:var(--bg-card2);color:var(--text2);transition:all .15s;text-align:center;}
.modal-btn:hover{border-color:var(--blue);color:var(--blue-fg);background:var(--blue-bg);}
.modal-input{width:100%;background:var(--bg-card2);border:1px solid var(--border-mid);color:var(--text);padding:8px 12px;border-radius:5px;font-size:12px;margin-bottom:12px;outline:none;font-family:var(--sans);}
.modal-input:focus{border-color:var(--blue);}
.modal-select{width:100%;background:var(--bg-card2);border:1px solid var(--border-mid);color:var(--text);padding:8px 12px;border-radius:5px;font-size:12px;margin-bottom:12px;outline:none;}

/* .dev-hero* family removed 2026-06-04 — orphan, gold-era tokens. */

/* .content-tabs + .ct family removed 2026-06-04 — orphan (no
   markup emits .content-tabs / .ct). */

/* .event-row + .ev-* family removed 2026-06-04 — orphan (events
   render via .pe-event-row instead). */

/* Empty state */
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;color:var(--text3);text-align:center;}
/* .es-icon, .es-text removed 2026-06-04 — orphans. */

/* ════════════════════════════════════
   MOBILE HAMBURGER BUTTON
════════════════════════════════════ */
.hamburger{display:none;flex-direction:column;gap:5px;cursor:pointer;padding:8px;border:none;background:transparent;flex-shrink:0;}
.hamburger span{display:block;width:20px;height:2px;background:var(--sb-text-active);border-radius:2px;transition:all .25s;}
.hamburger.open span:nth-child(1){transform:translateY(7px) rotate(45deg);}
.hamburger.open span:nth-child(2){opacity:0;}
.hamburger.open span:nth-child(3){transform:translateY(-7px) rotate(-45deg);}

/* Sidebar overlay for mobile */
.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:149;backdrop-filter:blur(2px);}
.sidebar-overlay.open{display:block;}

/* ════════════════════════════════════
   RESPONSIVE BREAKPOINTS
════════════════════════════════════ */
@media (max-width: 768px) {
  /* Unlock scroll on body */
  html,body{overflow:auto;height:auto;}

  /* Topbar */
  .topbar{padding:0 12px;position:sticky;top:0;z-index:200;}
  .tb-logo{width:auto;padding:0;border-right:none;margin-right:0;}
  .tb-center{padding:0 8px;flex:1;}
  .tb-search{width:100%;max-width:180px;}
  /* .tb-badges and .tb-divider were dropped 2026-06-04 — orphans
     (removed in #188). .tb-clock retained — it's still emitted. */
  .tb-clock{display:none;}
  .hamburger{display:flex;}

  /* Shell — stack vertically on mobile */
  .shell{flex-direction:column;overflow:visible;}

  /* Sidebar — slide in from left as overlay */
  .sidebar{
    position:fixed;
    top:0;left:0;bottom:0;
    width:260px;
    z-index:150;
    transform:translateX(-100%);
    transition:transform .25s ease;
    overflow-y:auto;
  }
  .sidebar.open{transform:translateX(0);}

  /* Main content — full width */
  .main{width:100%;overflow:visible;}

  /* Page header */
  .page-hdr{padding:16px 14px;flex-wrap:wrap;height:auto;}
  .page-title-wrap{padding:10px 0;border-right:none;padding-right:0;width:100%;border-bottom:1px solid var(--border-light);}
  .page-title{font-size:16px;}
  .page-tabs{overflow-x:auto;padding:0;-webkit-overflow-scrolling:touch;}
  .page-tabs::-webkit-scrollbar{display:none;}
  .pt{padding:0 12px;height:40px;font-size:11px;white-space:nowrap;}
  .page-actions{padding:8px 0;width:100%;}

  /* Content body */
  .content-body{padding:12px;gap:12px;}

  /* Section cards */
  .sc-hdr{padding:10px 14px;flex-wrap:wrap;gap:6px;}
  .sc-hdr-title{font-size:12px;}

  /* Tables — horizontal scroll */
  .data-table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}
  .data-table{min-width:600px;}
  .data-table th,.data-table td{padding:8px 10px;}

  /* Two-col → single col */
  .two-col{grid-template-columns:1fr !important;}

  /* Gauge grid — 2 cols */
  .gauge-grid{grid-template-columns:1fr 1fr;}

  /* Client grid — single col */
  .client-grid{grid-template-columns:1fr !important;padding:12px;}

  /* Camera grid — 2 cols */
  .cam-grid{grid-template-columns:1fr 1fr !important;}

  /* Sensor grid — 2 cols */
  .sensor-grid{grid-template-columns:1fr 1fr !important;}

  /* Alerts — full width */
  .alert-item{padding:10px 12px;}

  /* Legacy topology media-query overrides (.topo-area, .topo-filters,
     .tf, .topo-legend, .drawer) removed 2026-06-04 — all
     orphans, sweep matches the main rules removed in earlier PRs. */
  .drawer.open{width:100% !important;}

  /* Modal */
  .modal{width:92vw;max-height:85vh;margin:0 auto;}

  /* Tables wrap in scroll */
  .section-card table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch;}

  /* Int list */
  .int-item{padding:10px 12px;}

  /* Nav RF tabs */
  .page-tabs{border-bottom:1px solid var(--border-light);}

  /* SIM table */
  .sim-table{min-width:500px;}

  /* Report list */
  .report-item{flex-wrap:wrap;gap:8px;}
  .ri-date{margin-left:0;}
}

@media (max-width: 480px) {
  /* .gauge-grid and .sensor-grid dropped 2026-06-04 — orphans
     (removed in #186/#187). .cam-grid retained — cameras.js
     still emits it. */
  .cam-grid{grid-template-columns:1fr 1fr !important;}
  .tb-search{max-width:140px;}
  .page-title{font-size:15px;}
}

/* ══════════════════════════════════════════════════════════════════
   Mobile adjustments for the surfaces shipped this week:
     - Topology tier gutter (78 px is 22% of a 360px viewport, too
       greedy — drop to 56 px on phones; tier labels read compact
       single-line without the subtitle line).
     - /diagnostics tables (8 columns don't fit on phones — let
       them horizontal-scroll cleanly with a tap-and-drag hint).
     - WAN debug modal (same treatment).
   ══════════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {
  /* Topology tier gutter — compact on phones. JS reads the body
     class to pick the gutter width; the visual width here just
     needs to match the JS constant. We don't change the JS at
     this breakpoint (re-running layout on resize is expensive);
     instead the gutter visually narrows but the layout numbers
     stay the same — labels just hug the right side of a wider
     gutter and the gap above the first card looks tighter.
     If a future pass wants per-breakpoint geometry, run
     ResizeObserver → _topoFetchAndRender(siteId, true). */
  .topo-stack-body-tiered { padding-left: 56px; }
  .topo-tier-gutter       { width: 56px; }
  .topo-tier-label-main   { font-size: 9px; letter-spacing: 0.08em; }
  .topo-tier-label-sub    { display: none; }  /* subtitle hides on phones */

  /* Diagnostics + WAN debug tables — let them horizontal-scroll
     with momentum on touch. The outer .dx-card stays full-width
     and the table inside scrolls. The hairline shadow on the
     right edge hints that more is offscreen. */
  .dx-card-body { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .dx-table     { min-width: 640px; font-size: 11px; }
  .dx-table th, .dx-table td { padding: 6px 8px; }
  .dx-env-key   { width: 120px; font-size: 9.5px; }

  .wandbg-table { min-width: 680px; }
  .wandbg-source-url { display: none; }   /* save horizontal room */
  .wandbg-source-sample { display: none; }
}

@media (max-width: 480px) {
  /* Phones — drop the tier gutter further so single-tier sites
     don't look like 30% gutter. Subtitle was already hidden. */
  .topo-stack-body-tiered { padding-left: 48px; }
  .topo-tier-gutter       { width: 48px; }
  .topo-tier-label-main   { font-size: 8.5px; }

  /* Diagnostics — env table key column compresses further. */
  .dx-env-key   { width: 96px; }
}

/* ════════════════════════════════════
   DEVICE DETECTION & PLATFORM CSS
════════════════════════════════════ */

/* iOS safe area (notch / Dynamic Island / home indicator) */
@supports (padding-top: env(safe-area-inset-top)) {
  .topbar {
    padding-top: env(safe-area-inset-top);
    height: calc(56px + env(safe-area-inset-top));
  }
  .sidebar {
    padding-top: env(safe-area-inset-top);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .content-body {
    padding-bottom: calc(12px + env(safe-area-inset-bottom));
  }
}

/* iOS standalone (added to home screen) */
@media (display-mode: standalone) {
  .topbar {
    padding-top: env(safe-area-inset-top);
    height: calc(56px + env(safe-area-inset-top));
    position: sticky;
    top: 0;
  }
  /* Hide browser chrome indicators */
  .tb-badge { display: none; }
}

/* Tablet (iPad, Android tablet) — sidebar visible but narrower */
@media (min-width: 769px) and (max-width: 1024px) {
  .sidebar { width: 200px; }
  .tb-badges { display: none; }
  .tb-search { max-width: 180px; }
  .client-grid { grid-template-columns: repeat(2, 1fr) !important; }
  .gauge-grid { grid-template-columns: 1fr 1fr; }
  .cam-grid { grid-template-columns: 1fr 1fr !important; }
  .sensor-grid { grid-template-columns: repeat(3, 1fr) !important; }
  .page-tabs { overflow-x: auto; }
  .pt { padding: 0 12px; font-size: 11px; }
}

/* Landscape phone — optimize for wide but short screens */
@media (max-width: 768px) and (orientation: landscape) {
  html, body { overflow: auto; }
  .topbar { height: 44px; }
  .sidebar { padding-top: 0; }
  .content-body { padding: 8px 12px; gap: 8px; }
  .page-title { font-size: 14px; }
  .page-hdr { padding: 0 12px; }
  /* .topo-area media-query removed 2026-06-04 — orphan. */
}

/* High-res / retina displays */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
  .sidebar { background-size: 28px 50px; }
}

/* Pointer: coarse = touch device — larger tap targets */
/* Sidebar nav-item / ctx-selector intentionally NOT bumped here — the sidebar uses a
   compact 32px height by design, and bumping them to 44px on touch caused padding/margin
   conflicts with the override block below (previously worked around with !important). */
@media (pointer: coarse) {
  .btn { padding: 9px 16px; min-height: 44px; }
  .row-click td { padding: 12px 10px; }
  .alert-item { padding: 12px 14px; }
  .int-item { padding: 12px 14px; min-height: 52px; }
  .cam-cell { cursor: default; }
  .tf { padding: 8px 14px; min-height: 36px; }
  .tb-icon-btn { width: 40px; height: 40px; }
  .hamburger { padding: 10px; }
  .modal-btn { padding: 12px 0; min-height: 44px; }
  .ctx-option { min-height: 40px; }
  .sf-row { min-height: 40px; }
}

/* Pointer: fine = mouse — keep original compact sizing */
@media (pointer: fine) {
  .hamburger { display: none !important; }
}

/* Hover: none = touch device (no hover states needed) */
@media (hover: none) {
  .nav-item:hover { background: transparent; }
  .row-click:hover td { background: transparent; }
  .nav-item:hover .ni-icon,
  .nav-item:hover .ni-label { color: var(--sb-text); }
}

/* Dark mode OS preference — already dark so just ensure consistency */
@media (prefers-color-scheme: dark) {
  /* Already dark — no change needed */
}

/* Reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
  /* .tb-pulse dropped 2026-06-04 — orphan class, removed in #188. */
  .sp-dot.blink, .pulse { animation: none !important; opacity: 1; }
}

/* Print — clean layout */
@media print {
  .topbar, .sidebar, .hamburger, .sidebar-overlay { display: none !important; }
  .shell { display: block; }
  .main { width: 100%; }
  .content-body { padding: 0; }
  .section-card { break-inside: avoid; border: 1px solid #ccc; margin-bottom: 12px; }
  body { overflow: visible; background: white; color: black; }
  .data-table th, .data-table td { color: black; border-color: #ccc; }
}

/* ════════════════════════════════════
   THEME — single dark theme only. The previous light/dark dual-mode
   scaffolding (body.light-mode + body.dark-mode token blocks +
   light-theme defaults in :root) was removed 2026-06-04. The app
   no longer surfaces a light/dark toggle, so light-mode rendering
   was unreachable except via stale localStorage; the dark-mode
   declarations are now the canonical source of truth.

   The toggle UI was removed in #191. applyTheme() was kept around
   to honor localStorage, but every other view's chrome assumed
   dark-mode anyway — the light-mode block just gave a half-broken
   render to the handful of users with guardian-theme=light saved.
   Now the body always renders dark; saved preferences are ignored.
════════════════════════════════════ */
body.dark-mode,
body {
  /* Electric-blue dark surfaces. */
  --bg:           #0A0E16;
  --bg-card:      #121A2A;
  --bg-card2:     #0F1622;
  --text:         #E8EEF8;
  --text2:        #B6C0D2;
  --text3:        #8A94A6;
  --border-light: rgba(91, 155, 255, 0.18);
  --border-mid:   rgba(91, 155, 255, 0.28);
}

/* Dark-mode tokens previously had TWO declaration blocks (gold-theme
   leftover — one for page surfaces, one for card chrome). Merged into
   the canonical block above. The card/table-specific dark-mode rules
   that depended on the second block continue to work via the existing
   `body.dark-mode <selector>` rules below — those don't redeclare
   tokens, they just apply per-element overrides. */

/* Dark mode card/table adjustments. .gauge-card and .sensor-tile
   removed from the selector list 2026-06-04 — those classes are
   orphan (the gauge-grid and sensor-grid layouts were dropped in
   earlier sweeps; no markup emits them). */
body.dark-mode .section-card,
body.dark-mode .client-card,
body.dark-mode .modal {
  background: var(--bg-card);
  border-color: var(--border-light);
}

body.dark-mode .data-table th {
  background: var(--bg-card2);
  color: var(--text3);
}

body.dark-mode .data-table td {
  border-color: var(--border-light);
}

body.dark-mode .data-table tr.row-click:hover td {
  background: var(--bg-card2);
}

body.dark-mode .page-hdr {
  background: var(--bg-card);
}

/* body.dark-mode .content-tabs removed 2026-06-04 — .content-tabs
   was orphan-removed in #186. */

body.dark-mode .cc-stat {
  background: var(--bg-card2);
}

body.dark-mode .cc-footer {
  border-color: var(--border-light);
}

/* Dark-mode list-item hover. Dropped .int-item / .report-item /
   .radio-item / .site-list-item from the selector list 2026-06-04
   — those families are orphan (removed in earlier sweeps). */
body.dark-mode .alert-item:hover {
  background: var(--bg-card2);
}

body.dark-mode .modal-body,
body.dark-mode .modal-hdr {
  background: var(--bg-card);
  border-color: var(--border-light);
}

/* body.dark-mode .modal-row, .modal-key, .modal-val removed
   2026-06-04 — those bare classes are not emitted (modals use
   .nd-modal-row, .modal-input/.modal-select). */

body.dark-mode .modal-input,
body.dark-mode .modal-select {
  background: var(--bg-card2);
  border-color: var(--border-mid);
  color: var(--text);
}

/* body.dark-mode .dw-body, body.dark-mode .drawer removed
   2026-06-04 — those families were removed in #186. Current
   drawers use .nx-drawer-* and .users-drawer / .users-drawer-*. */

/* body.dark-mode .topo-area / .topo-legend overrides and the
   second .theme-toggle definition removed 2026-06-04 — all
   orphan (parent selectors removed in earlier PRs). */


.cat-filter-btn.active {
  box-shadow: 0 0 0 2px rgba(0,0,0,0.15) inset;
  opacity: 1 !important;
  font-weight: 700 !important;
}
.cat-filter-btn:not(.active) { opacity: 0.7; }
.cat-filter-btn:hover { opacity: 1 !important; transform: translateY(-1px); }



/* ── Scroll fix — view containers were overflow:hidden at fixed height ── */
[id^="view-"] {
  overflow-y: auto !important;
  overflow-x: hidden !important;
  height: 100% !important;
  -webkit-overflow-scrolling: touch !important;
}
/* .section-card height:auto/overflow:visible override is redundant
   with the same !important pair set in the foundation block below
   (~line 1612). Dropped to avoid double-specifying. */
.content-body { height: auto !important; overflow: visible !important; }
/* .topo-hint hide rule retired alongside the orphan removal above. */


/* Gold-era token shadowing removed 2026-06-04. This catch-all rule
   used to force --bg / --bg-card to the gold-era browns (#0c0b08 /
   #131109), shadowing the foundation-aligned values defined in
   body.dark-mode above (#0A0E16 / #121A2A). With it gone, the
   dark-mode block at line 1452 wins and var(--bg) / var(--bg-card)
   now resolve to the foundation surfaces everywhere they're
   referenced. --border-color stays available via the existing
   :root + body.dark-mode token blocks.

   Body background pinned to var(--bg) so the cascade flows from
   body.dark-mode → :root.dark-mode → foundation token. !important
   stays so per-view inline background overrides on .view-* roots
   can't paint a dark-mode body in a light shade. */
html,body{background:var(--bg,#0A0E16)!important}
/* Re-enabled by feat/ui-polish: the topbar search is now built and
   the dark-mode override no longer hides it. */
.tb-logo{width:240px!important;min-width:240px!important;max-width:240px!important;border-right:none!important}
.page-tabs{display:none!important}
/* 2026-06-04: backgrounds now use the foundation base-surface token
   (--gc-surface-base = #0A0E16) instead of the gold-era #0c0b08
   brown. Also dropped the forced height:32px / min-height:32px /
   padding:0 24px from .page-hdr — those were locking the header
   into a tight 32px row that defeated the foundation
   .gc-page-title's 24px font + the foundation .gc-page-hdr's
   natural --gc-space-5 padding (which the PR #171 page-header
   migration intended to ship). The flex layout + remove-borders
   stays !important because the original sub-pixel border was
   visibly cutting through the page-tabs row on a few views. */
.page-hdr,.page-title-wrap,.page-actions{background-color:var(--gc-surface-base,#0A0E16)!important}
.page-hdr{display:flex;align-items:center;gap:12px;flex-shrink:0;border:none!important;border-top:none!important;border-bottom:none!important;outline:none!important;box-shadow:none!important;background:var(--gc-surface-base,#0A0E16);}
/* Org / site selectors render as nav-items (same .nav-item class) with a right-side
   chevron affordance for the dropdown. */
.nav-item-drop .ni-chevron{margin-left:auto!important;opacity:0.5!important;color:var(--sb-text)!important;flex-shrink:0!important;transition:transform .2s!important;}
.nav-item-drop.open .ni-chevron{transform:rotate(180deg)!important;}
/* "You are here" breadcrumb tint — accent text + icon on current org/site pills
   when the user is on any sub-page. No background or left-bar (that's reserved
   for .nav-item.active). Suppressed on the Global Overview view since no
   specific org context is "active" there.
   Compound selector (.nav-item.nav-item-context-active) gives higher specificity
   than the generic .nav-item .ni-label rule in the override block below, so this
   rule wins without needing to re-order the whole block. */
/* Org/site rows used to tint amber when a child view was active. That
   read as a second highlight beside the real .nav-item.active ::before
   bracket — confusing. Now they stay muted; only the clicked child
   item gets the bracket + gold tint. Class is still applied so we can
   reintroduce a distinct "context" affordance later if needed. */
.nav-item.nav-item-context-active .ni-label,
.nav-item.nav-item-context-active .ni-icon,
.nav-item.nav-item-context-active .ni-icon svg{ /* intentionally empty */ }
/* Org + site pickers render INLINE in the sidebar flex flow. Expanding a
   dropdown grows the sidebar's scroll height; items below are pushed
   down, never hidden. If the expanded content runs past the viewport
   the sidebar itself (overflow-y:auto) scrolls — every nav item stays
   reachable.
   We previously tried position:absolute (overlay) here — the submenu
   painted on top of the rows below, hiding Carpinteria / NETWORK. This
   is that regression reverted. See the CSS sketch the user provided:
   max-height animation + sidebar-scroll, no absolute positioning. */
.sb-dropdown{
  display:block;
  max-height:0;
  overflow:hidden;
  margin:0 6px;
  padding:0;
  background:rgba(47, 107, 255,0.04);
  border:1px solid transparent;
  border-radius:6px;
  transition:max-height .18s ease-out, padding .18s ease-out, margin-top .18s ease-out, margin-bottom .18s ease-out, border-color .18s ease-out;
}
.sb-dropdown.open{
  max-height:480px;
  padding:4px 0;
  margin-top:2px;
  margin-bottom:2px;
  border-color:rgba(47, 107, 255,0.15);
  overflow-y:auto;
}
/* Keep the trigger rows (and every other direct sidebar child) from
   getting flex-compressed when the sidebar runs out of space — we
   WANT the sidebar to scroll rather than shrink items. */
.nav-item,.sb-section-label{flex-shrink:0;}
/* Sidebar dropdown rows — base = SITE rows */
.sb-dropdown-row{display:flex;align-items:center;padding:6px 14px;font-size:13px;font-weight:500;color:#cbd5e1;cursor:pointer;transition:all .12s;}
.sb-dropdown-row:hover{background:rgba(255,255,255,0.04);color:#ffffff;}
.sb-dropdown-row.active,
.sb-dropdown-row.selected,
.sb-dropdown-row[aria-selected="true"]{color:#ffffff;font-weight:600;}

/* ORG rows: 3-child rows in .sb-dropdown (icon + name + type-pill).
   Sites have 2 children. Distinguish via :has() — Chrome 105+,
   Safari 15.4+, FF 121+ (~95% of users per caniuse). */
.sb-dropdown-row:has(> *:nth-child(3)):not(.sb-dropdown-add){
  color:#f1f5f9;
  font-weight:600;
  font-size:14px;
  border-top:1px solid rgba(255,255,255,0.06);
  margin-top:4px;
  padding-top:8px;
}
.sb-dropdown-row:has(> *:nth-child(3)):not(.sb-dropdown-add) > span:not([class]){
  color:#f1f5f9;
}
.sb-dropdown-row:has(> *:nth-child(3)):not(.sb-dropdown-add):first-child{
  border-top:none;
  margin-top:0;
  padding-top:6px;
}

/* ORG TYPE PILLS (MSP/EDU): the 3rd child of an org row */
.sb-dropdown-row:has(> *:nth-child(3)) > *:nth-child(3){
  color:#fbbf24;
  font-size:9px;
  font-weight:700;
  letter-spacing:0.6px;
  text-transform:uppercase;
  background:rgba(251,191,36,0.15);
  border:1px solid rgba(251,191,36,0.3);
  padding:2px 6px;
  border-radius:3px;
  margin-left:8px;
}

/* "+ Add" rows */
.sb-dropdown-row.sb-dropdown-add{color:#fbbf24;font-weight:600;border-top:1px solid rgba(255,255,255,0.06);margin-top:2px;padding-top:7px;}
.sb-dropdown-row.sb-dropdown-add:hover{color:#fcd34d;background:rgba(251,191,36,0.08);}
/* ──────────────────────────────────────────────────────────────────
   Gold-era surface block — updated 2026-06-04.

   This `!important` block was originally added to force the dark
   gold-era brown surfaces (#0c0b08 shell / #131109 cards / #080700
   sidebar / brown-tan table cell color). PR #155 (electric blue
   theme) refreshed the brand palette but the !important rules here
   kept forcing the old browns, so .section-card / .client-card /
   .kpi-item rendered BROWN while my new .gc-card foundation rendered
   BLUE — visible inconsistency across the dashboard.

   Each `!important` color is now wired to the foundation surface
   tokens (--gc-surface-base / --gc-surface-card / --gc-surface-rail)
   defined in gc-tokens.css. Layout rules (heights, paddings, font
   sizes that were forced to gold-era-tight values) are dropped so
   the natural dashboard.css + .gc-page-* foundation rules apply.
   That fixes the second-order bug where .gc-page-title (foundation
   24px) couldn't beat the legacy .page-title font-size:15px
   !important — operators were never actually seeing the bigger
   title size the page-header migration was supposed to ship.
   ────────────────────────────────────────────────────────────────── */
.sidebar{background:var(--gc-surface-rail,#0C1220)!important;border-right:none!important;width:240px!important;padding-top:9px!important}
.nav-item{height:32px!important;min-height:32px!important;max-height:32px!important;display:flex!important;align-items:center!important;padding:0 12px 0 14px!important;margin:1px 6px!important;border:none!important;border-radius:6px!important;cursor:pointer!important;box-sizing:border-box!important;overflow:hidden!important;position:relative!important}
.nav-item:hover .ni-label{color:#ffffff!important}
.nav-item:hover{background:rgba(47, 107, 255,0.06)!important}
.nav-item.active{background:rgba(47, 107, 255,0.1)!important}
/* .sb-section bare-class !important rule removed 2026-06-04. Grep
   confirms no view emits class="sb-section" — only the -label,
   -items, -break, -chev compound classes are used, and each has
   its own non-!important rule earlier in the file. */
#org-sub,#site-sub{font-size:10px!important;color:var(--text2,#4A5568)!important}
.shell,.main,[id^="view-"],.content-body{background:var(--gc-surface-base,#0A0E16)!important}
.section-card{background:var(--gc-surface-card,#121A2A)!important;border:1px solid rgba(47, 107, 255,0.1)!important;border-radius:10px!important;overflow:visible!important;height:auto!important}
.client-card{background:var(--gc-surface-card,#121A2A)!important;border-left:1px solid rgba(47, 107, 255,0.08)!important;border-right:1px solid rgba(47, 107, 255,0.08)!important;border-bottom:1px solid rgba(47, 107, 255,0.08)!important;border-radius:10px!important}
.kpi-item,.kpi-card,.gauge-card,.sensor-tile,[class*="kpi-item"],[class*="gauge-card"]{background:var(--gc-surface-card,#121A2A)!important;border:1px solid rgba(47, 107, 255,0.08)!important;border-radius:8px!important}
table th,thead th{color:#94a3b8;font-weight:700;text-transform:uppercase;letter-spacing:.5px;font-size:11px;}
table tr{border-bottom:1px solid rgba(47, 107, 255,0.05)!important}
table td{color:var(--gc-text-strong,#E8EEF8)!important}
.client-card span[style*="font-size:10px"][style*="font-weight:700"][style*="padding:2px"]{display:none!important}

/* Legacy topology CSS removed 2026-06-04 — the dead-code chunk that
   styled .topo-card / .topo-card-* / .topo-panel / .topo-grid /
   .topo-col / .topo-tier-hdr / .topo-tier-body / .topo-actions /
   .topo-content / .topo-filter-btn / .topo-filter-group /
   .topo-site-select / .topo-search (class) / .topo-area-inner /
   .topo-links-svg / .topo-status-pill (duplicate) / .topo-vendor-pill
   plus two responsive media queries. The five-column layout these
   styled was removed in the PR that deleted _topoCard/_topoTier/
   _topoDrawLinks etc. The current renderer (.topo-stack-card +
   .topo-node-card + tier-gutter pattern) is unaffected. */

body { padding-top: 0 !important; }

@media (max-width: 768px) {
  /* Mobile sidebar — wired to the SAME open contract the JS uses
     (toggleSidebar/closeSidebar toggle `.open` on #sidebar and
     #sidebarOverlay). A previous paste of this block targeted
     `.sidebar.gc-open` / `#gc-sb-overlay.gc-open`, selectors that
     exist nowhere in the markup or JS — so the `!important` rules
     below pinned the sidebar off-screen at left:-270px and the
     hamburger appeared to do nothing. Use translateX off-screen
     (left:0 anchored) and slide in on `.open`. */
  .sidebar { position:fixed!important;left:0!important;top:0!important;bottom:0!important;width:270px!important;z-index:9000!important;transform:translateX(-100%)!important;transition:transform 0.25s ease!important;overflow-y:auto!important; }
  .sidebar.open { transform:translateX(0)!important;box-shadow:8px 0 32px rgba(0,0,0,0.6)!important; }
  .sidebar-overlay { display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:8999; }
  .sidebar-overlay.open { display:block!important; }
  .shell { margin-left:0!important; }
  .content-body { padding:12px!important;overflow-y:auto!important; }
  .kpi-bar, #view-network .kpi-item { grid-template-columns:repeat(2,1fr)!important; }
  #view-network [style*="grid-template-columns:repeat(4"] { grid-template-columns:repeat(2,1fr)!important; }
  #view-network [style*="grid-template-columns:repeat(auto-fit"] { grid-template-columns:1fr!important; }
  .client-grid { grid-template-columns:1fr!important; }
  .tb-status-pills,.tb-clock { display:none!important; }
  .hamburger { display:flex!important; }
}

/* ════ COLLECTORS PAGE ════ */
/* Legacy collector-card layout (.collector-card, .collector-meta-*,
   .collector-svc-*, .collector-actions) removed 2026-06-04 —
   orphans. Current collectors view renders via .section-card +
   .col-card and emits .collector-btn / .collector-popover (both
   retained below). */
.collector-btn {
  font-family:var(--sans);
  font-size:11px;
  font-weight:600;
  padding:6px 12px;
  border-radius:4px;
  border:1px solid var(--border-mid);
  background:var(--bg-card);
  color:var(--text);
  cursor:pointer;
  transition:all .12s;
}
.collector-btn:hover:not(:disabled) {
  border-color:var(--accent);
  background:var(--accent-dim);
  color:var(--text);
}
.collector-btn:disabled {
  opacity:.5;
  cursor:not-allowed;
}
.collector-popover {
  background:var(--bg-card);
  border:1px solid var(--border-mid);
  border-radius:6px;
  box-shadow:0 8px 24px rgba(0,0,0,0.15);
  overflow:hidden;
  min-width:180px;
  max-width:320px;
}
.cpop-row {
  padding:8px 14px;
  font-size:12px;
  font-family:var(--mono);
  cursor:pointer;
  border-bottom:1px solid var(--border-light);
  color:var(--text);
}
.cpop-row:last-child { border-bottom:none; }
.cpop-row:hover { background:var(--accent-dim); }

/* ════ /node detail — power system visualization ════ */

/* Small amber pill at the top of the Power Source card signaling the
   data stream is simulated rather than live. Reads as dev-aware
   status rather than "DEMO DATA" scare-speak. */
.sim-banner {
  display: inline-block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 4px;
  background: rgba(47, 107, 255,0.12);
  border: 1px solid rgba(47, 107, 255,0.32);
  color: var(--accent-bright);
  margin-bottom: 12px;
}

/* Power-flow SVG shell — scales with the card; components and flow
   lines live inside the SVG viewBox for crisp scaling at any width. */
.pf-diagram { margin-bottom: 4px; }
.pf-svg { width: 100%; max-width: 760px; height: auto; display: block; }
.pf-box {
  fill: var(--gc-surface-card,#121A2A);
  stroke: rgba(47, 107, 255,0.35);
  stroke-width: 1;
  transition: stroke .2s, fill .2s;
}
.pf-box-warn { stroke: var(--amber); fill: rgba(217,119,6,0.06); }
.pf-title    { fill: var(--gc-text-strong,#E8EEF8); font-family: var(--mono); font-size: 12px; font-weight: 600; }
.pf-metric   { fill: var(--gc-text-muted,#B6C0D2); font-family: var(--mono); font-size: 11px; }
.pf-label    { fill: var(--text3); font-family: var(--mono); font-size: 11px; }
/* .pf-watt removed 2026-06-04 — orphan, was gold-era #e8dfc8. */
.pf-arrow-fill { fill: rgba(47, 107, 255,0.6); }
.pf-flow     { fill: none; stroke: rgba(47, 107, 255,0.55); stroke-width: 1.5; transition: stroke .2s, stroke-width .2s; }
.pf-flow-active { stroke: rgba(47, 107, 255,0.85); stroke-width: 2; animation: pfFlowPulse 2.2s ease-in-out infinite; }
.pf-flow-dim    { stroke: rgba(47, 107, 255,0.18); stroke-width: 1; }
@keyframes pfFlowPulse {
  0%, 100% { stroke-opacity: 0.55; }
  50%      { stroke-opacity: 1; }
}

/* DP-10i distribution strip cells — filled when on, hollow when off.
   Hover brightens so operators can eyeball which circuit is which. */
.pf-circuit-rect {
  fill: var(--gc-surface-base,#0A0E16);
  stroke: rgba(47, 107, 255,0.28);
  stroke-width: 1;
  transition: fill .25s, stroke .25s;
}
.pf-circuit-rect[data-state="on"]  { fill: rgba(22,163,74,0.22);  stroke: rgba(22,163,74,0.55); }
.pf-circuit-rect[data-state="off"] { fill: var(--gc-surface-base,#0A0E16); stroke: rgba(47, 107, 255,0.2); }
.pf-circuit-cell:hover .pf-circuit-rect { stroke: var(--accent); stroke-width: 1.5; }
.pf-circuit-num { fill: var(--gc-text-muted,#B6C0D2); font-family: var(--mono); font-size: 11px; font-weight: 600; }

/* SOC bar inside the battery-direct flow SVG — color shifts with
   remaining charge. Track is a muted backdrop. */
.pf-soc-track { fill: rgba(255,255,255,0.05); }
.pf-soc-fill  { fill: #22c55e; transition: width .35s ease; }
.pf-soc-fill.pf-soc-amber { fill: var(--amber); }
.pf-soc-fill.pf-soc-red   { fill: #ef4444; }

/* Distribution Circuits table — one row per circuit, inline Cycle/
   Off/On per row. Flash animation plays on cycle for ~2s. */
.circuit-table { width: 100%; border-collapse: collapse; font-size: 12px; font-family: var(--mono); }
.circuit-table th {
  text-align: left;
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--text3);
  padding: 8px 10px;
  border-bottom: 1px solid var(--border-light);
}
.circuit-row td {
  padding: 8px 10px;
  border-bottom: 1px solid rgba(255,255,255,0.04);
  vertical-align: middle;
}
.circuit-row:last-child td { border-bottom: none; }
.circuit-row.circuit-flash td { background: rgba(239,68,68,0.15); transition: background .25s; }
.circuit-n { color: var(--text3); width: 28px; }
.circuit-label { color: var(--text); }
.circuit-draw  { color: var(--text2); }
.circuit-breaker { color: var(--text3); }
.circuit-pill {
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 10px;
  display: inline-block;
}
.circuit-pill.circuit-on  { background: rgba(22,163,74,0.18); color: #16a34a; }
.circuit-pill.circuit-off { background: rgba(138,155,176,0.14); color: var(--text3); }
.circuit-actions { text-align: right; display: flex; gap: 4px; justify-content: flex-end; flex-wrap: wrap; }
.circuit-btn {
  padding: 4px 10px;
  font-size: 11px;
  font-weight: 500;
  font-family: var(--sans);
  border: 1px solid var(--accent-border, rgba(47, 107, 255,0.32));
  background: var(--accent-dim);
  color: var(--accent-bright);
  border-radius: 4px;
  cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
.circuit-btn:hover { background: rgba(47, 107, 255,0.22); color: var(--accent); }
.circuit-footnote {
  margin-top: 10px;
  font-size: 11px;
  color: var(--text3);
  font-style: italic;
}

/* Runtime card — two big side-by-side number blocks with an SOC
   bar under each. Footer carries the secondary metrics. */
.rt-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; }
@media (max-width: 620px) { .rt-grid { grid-template-columns: 1fr; } }
.rt-section-label {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--text3);
  margin-bottom: 6px;
}
.rt-big-number {
  font-size: 32px;
  font-weight: 700;
  font-family: var(--mono);
  color: var(--text);
  line-height: 1.1;
  margin-bottom: 8px;
}
.rt-soc-bar {
  height: 8px;
  background: rgba(255,255,255,0.06);
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 8px;
}
.rt-soc-bar-fill {
  height: 100%;
  width: 0;
  background: #22c55e;
  border-radius: 4px;
  transition: width .35s ease, background .2s;
}
.rt-soc-bar-fill.rt-soc-amber { background: var(--amber); }
.rt-soc-bar-fill.rt-soc-red   { background: #ef4444; }
.rt-secondary {
  font-size: 12px;
  color: var(--text3);
  font-family: var(--mono);
}
.rt-footer {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--border-light);
  font-size: 12px;
  color: var(--text3);
  line-height: 1.6;
}
.rt-warning {
  margin-top: 10px;
  color: var(--amber);
  font-weight: 500;
}
.rt-warning.rt-warning-red { color: #ef4444; }

/* Recent Power Events — vertical list with colored dot per event type.
   Timestamp line is slightly dimmer than the detail line to emphasize
   the "what happened" copy. */
.pe-list { display: flex; flex-direction: column; gap: 10px; }
.pe-event-row {
  display: grid;
  grid-template-columns: 14px 1fr;
  gap: 12px;
  align-items: start;
  padding: 6px 0;
}
.pe-dot {
  width: 10px; height: 10px; border-radius: 50%;
  margin-top: 5px;
  background: var(--text3);
  box-shadow: 0 0 4px rgba(255,255,255,0.1);
}
.pe-transfer     { background: var(--amber); box-shadow: 0 0 4px rgba(217,119,6,0.45); }
.pe-overcurrent  { background: #ef4444;      box-shadow: 0 0 4px rgba(239,68,68,0.45); }
.pe-circuit_cycle{ background: #3b82f6;      box-shadow: 0 0 4px rgba(59,130,246,0.45); }
.pe-restart      { background: var(--text3); }
.pe-lora_uplink  { background: #14b8a6;      box-shadow: 0 0 4px rgba(20,184,166,0.4); }
.pe-charge       { background: #22c55e;      box-shadow: 0 0 4px rgba(34,197,94,0.4); }
.pe-poe_link     { background: #a855f7;      box-shadow: 0 0 4px rgba(168,85,247,0.4); }
.pe-body { min-width: 0; }
.pe-line1 { font-size: 12px; color: var(--text3); font-family: var(--mono); }
.pe-clock { color: var(--text2); }
.pe-detail { font-size: 12px; color: var(--text); line-height: 1.5; margin-top: 2px; }

/* LoRaWAN Telemetry card — dev-EUI + last uplink + signal + sparkline
   of recent voltage readings. Sparkline uses the same polyline/polygon
   pattern /wan's throughput chart uses. */
.lora-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  margin-bottom: 12px;
}
@media (max-width: 620px) { .lora-grid { grid-template-columns: 1fr; } }
.lora-deveui {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--text);
  font-weight: 500;
}
.lora-uplink {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--text);
  margin-bottom: 3px;
}
.lora-signal {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text3);
}
.lora-meta {
  font-size: 12px;
  color: var(--text3);
  font-family: var(--mono);
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border-light);
  margin-bottom: 10px;
}
.lora-sparkline { width: 100%; height: 60px; display: block; }
.lora-axis {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  color: var(--text3);
  font-family: var(--mono);
  margin-top: 2px;
}

/* Page-local toast (Cycle / Off / On action feedback). Bottom-right
   stack, fades in/out; does not depend on any other toast helper. */
#nd-toast-stack {
  position: fixed;
  bottom: 20px;
  right: 20px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 200;
  pointer-events: none;
}
.nd-toast {
  padding: 10px 14px;
  border-radius: 6px;
  font-size: 12px;
  background: #1f2937;
  border: 1px solid var(--accent-border, rgba(47, 107, 255,0.32));
  color: var(--text);
  box-shadow: 0 4px 14px rgba(0,0,0,0.4);
  opacity: 0;
  transform: translateY(8px);
  transition: all .2s;
  pointer-events: auto;
  max-width: 360px;
}
.nd-toast.show { opacity: 1; transform: translateY(0); }

/* ════ /collectors — expandable collector cards ════ */
.col-card { margin-bottom: 12px; overflow: hidden; }
.col-row {
  display: grid;
  grid-template-columns: 180px 1fr auto 24px;
  gap: 16px;
  align-items: center;
  padding: 14px 18px;
  cursor: pointer;
  transition: background .15s;
}
.col-row:hover { background: rgba(47, 107, 255,0.03); }
.col-row-status { display: flex; flex-direction: column; gap: 4px; }
.col-status-pill {
  display: inline-block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 3px 10px;
  border-radius: 12px;
  width: fit-content;
}
.col-status-pill.col-status-online  { background: rgba(34,197,94,0.15);  color: #22c55e; box-shadow: 0 0 8px rgba(34,197,94,0.25); }
.col-status-pill.col-status-stale   { background: rgba(217,119,6,0.15);  color: var(--amber); box-shadow: 0 0 8px rgba(217,119,6,0.2); }
.col-status-pill.col-status-offline { background: rgba(239,68,68,0.15);  color: #ef4444; box-shadow: 0 0 8px rgba(239,68,68,0.2); }
.col-last-seen { font-size: 11px; color: var(--text3); font-family: var(--mono); }
.col-row-main { min-width: 0; }
.col-row-primary { font-size: 14px; font-weight: 600; color: var(--text); }
.col-row-secondary { font-size: 11px; color: var(--text3); font-family: var(--mono); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.col-row-meta { display: flex; align-items: center; gap: 10px; }
.col-agent-badge {
  font-size: 10px;
  font-weight: 600;
  font-family: var(--mono);
  padding: 3px 8px;
  border-radius: 4px;
  background: rgba(47, 107, 255,0.12);
  color: var(--accent-bright);
  border: 1px solid rgba(47, 107, 255,0.25);
}
.col-dots { display: flex; gap: 4px; align-items: center; }
.col-dots-empty { font-size: 10px; color: var(--text3); font-style: italic; }
.col-svc-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  display: inline-block;
}
.col-svc-dot-green  { background: #22c55e; box-shadow: 0 0 4px rgba(34,197,94,0.5); }
.col-svc-dot-amber  { background: var(--amber); box-shadow: 0 0 4px rgba(217,119,6,0.4); }
.col-svc-dot-red    { background: #ef4444; box-shadow: 0 0 4px rgba(239,68,68,0.4); }
.col-svc-dot-grey   { background: #6b7280; }
.col-chev {
  color: var(--text3);
  font-size: 13px;
  transition: transform .2s;
}
.col-card.open .col-chev { transform: rotate(180deg); }

/* Expandable panel — max-height transition so open/close reads smooth
   without committing to a fixed panel height. Cap is generous (3000px)
   for the biggest cards seen in practice. */
.col-panel {
  max-height: 0;
  overflow: hidden;
  transition: max-height .25s ease;
  border-top: 1px solid var(--border-light);
}
.col-card.open .col-panel { max-height: 3000px; }
.col-panel-inner {
  padding: 16px 18px 18px;
  border-left: 3px solid rgba(47, 107, 255,0.35);
  background: rgba(0,0,0,0.12);
}
.col-panel-section { margin-bottom: 18px; }
.col-panel-section:last-child { margin-bottom: 0; }
.col-panel-section-hdr {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--text3);
  margin-bottom: 10px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--border-light);
}
.col-kv { display: grid; grid-template-columns: 160px 1fr; gap: 8px 16px; font-size: 12px; }
.col-kv-k { color: var(--text3); }
.col-kv-v { color: var(--text); font-family: var(--mono); word-break: break-all; }
.col-kv-sub { color: var(--text3); font-size: 11px; margin-left: 4px; }

.col-svc-list { display: flex; flex-direction: column; }
.col-svc-header,
.col-svc-row {
  display: grid;
  grid-template-columns: 28px minmax(180px, 2fr) 90px 70px 70px 70px minmax(210px, auto);
  gap: 10px;
  align-items: center;
  padding: 8px 6px;
}
.col-svc-header {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--text3);
  border-bottom: 1px solid var(--border-light);
  padding-bottom: 6px;
  margin-bottom: 2px;
}
.col-svc-header-label,
.col-svc-header-meta { color: var(--text3); }
.col-svc-header-label { grid-column: 1 / span 2; }

.col-svc-block {
  border-bottom: 1px solid rgba(255,255,255,0.04);
  padding-bottom: 6px;
  margin-bottom: 2px;
}
.col-svc-block:last-child { border-bottom: none; margin-bottom: 0; }

.col-svc-icon { font-size: 16px; line-height: 1; text-align: center; }
.col-svc-label {
  font-family: var(--mono);
  color: var(--text);
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.col-svc-schedule,
.col-svc-run {
  font-family: var(--mono);
  color: var(--text2);
  font-size: 11px;
}
.col-svc-actions { display: flex; gap: 4px; justify-content: flex-end; flex-wrap: wrap; }
.col-btn-sm { padding: 4px 8px; font-size: 11px; }

/* Second line: description (dim) + writes-to chip row. Indented so
   the reader reads the table as blocks of 2. */
.col-svc-sub {
  display: grid;
  grid-template-columns: minmax(0, 2fr) auto;
  gap: 12px;
  padding: 0 6px 8px 40px;  /* indent past the icon column */
  font-size: 11px;
  color: var(--text3);
  align-items: start;
}
.col-svc-sub-desc { line-height: 1.45; }
.col-svc-sub-writes {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  justify-content: flex-end;
  font-size: 10px;
  color: var(--text3);
}
.col-writes-chip {
  font-family: var(--mono);
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 3px;
  background: rgba(47, 107, 255,0.08);
  color: var(--accent-bright);
  border: 1px solid rgba(47, 107, 255,0.2);
}
.col-writes-empty { color: var(--text3); font-style: italic; }

.col-svc-empty {
  color: var(--text3);
  font-size: 12px;
  padding: 14px 8px;
  font-style: italic;
  text-align: center;
  border: 1px dashed var(--border-light);
  border-radius: 4px;
}

.col-dots-count {
  margin-left: 8px;
  font-size: 10px;
  color: var(--text3);
  font-family: var(--mono);
  text-transform: lowercase;
  letter-spacing: .04em;
}

.col-activity { display: flex; flex-direction: column; gap: 8px; }
.col-activity-row {
  padding: 6px 10px;
  border-radius: 4px;
  background: rgba(0,0,0,0.18);
  border-left: 3px solid transparent;
  font-size: 11px;
}
.col-activity-row.failed { border-left-color: #ef4444; background: rgba(239,68,68,0.06); }
.col-activity-line { display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
.col-activity-time { font-family: var(--mono); color: var(--text3); min-width: 80px; font-size: 10px; }
.col-activity-event { color: var(--text); font-family: var(--mono); font-weight: 500; }
.col-activity-args { color: var(--text3); font-family: var(--mono); font-size: 10px; }
/* .col-activity-status removed 2026-06-04 — orphan. */
.col-activity-sub { color: var(--text3); font-family: var(--mono); font-size: 10px; margin-top: 3px; padding-left: 90px; }
/* Tone-coded status pill on each command row — replaces the bare
   colored text. Same shape as the WAN view's connected/down pill so
   the dashboard reads consistently across views. */
.col-activity-pill {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 9px; font-weight: 700; padding: 2px 8px;
  border-radius: 999px; text-transform: uppercase; letter-spacing: .08em;
  margin-left: auto; font-family: var(--mono);
}
.col-activity-pill .col-activity-pill-dot {
  width: 5px; height: 5px; border-radius: 50%; flex: 0 0 auto;
}
.col-activity-pill-ok   { background: rgba(34,197,94,0.10);  border: 1px solid rgba(34,197,94,0.40);  color: #86efac; }
.col-activity-pill-ok   .col-activity-pill-dot { background: #22c55e; }
.col-activity-pill-warn { background: rgba(245,158,11,0.10); border: 1px solid rgba(245,158,11,0.45); color: #fcd34d; }
.col-activity-pill-warn .col-activity-pill-dot { background: #f59e0b; }
.col-activity-pill-err  { background: rgba(220,38,38,0.10);  border: 1px solid rgba(220,38,38,0.45);  color: #fca5a5; }
.col-activity-pill-err  .col-activity-pill-dot { background: #dc2626; }
.col-activity-pill-info { background: rgba(96,165,250,0.10); border: 1px solid rgba(96,165,250,0.45); color: #bfdbfe; }
.col-activity-pill-info .col-activity-pill-dot { background: #60a5fa; }
/* Duration chip beside the verb when the agent reported one — useful
   to spot the run that took 8s vs. the run that took 200ms. */
.col-activity-dur {
  font-family: var(--mono); font-size: 10px; color: var(--text3);
  padding: 2px 6px; border-radius: 3px; background: rgba(255,255,255,0.04);
}

.col-wide-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* ════ RADIOS (SAS CBSD) PAGE ════ */
.radio-card { margin-bottom:16px; }
.radio-meta-grid {
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
  gap:10px 24px;
  font-size:12px;
}
.radio-meta-k {
  color:var(--text3);
  font-size:10px;
  text-transform:uppercase;
  letter-spacing:.08em;
  margin-right:8px;
  font-weight:600;
}
.radio-meta-v { color:var(--text); font-family:var(--mono); font-size:11px; }

/* ════ WAN PAGE — unified connection table + row expansion ════
   Pattern: overflow-x on the wrapper, min-width on the table itself,
   so a long table gets a horizontal scrollbar on narrow viewports
   instead of columns bleeding off the card's right edge. Mirrors the
   LAN page's .lan-table-scroll treatment. */
/* .wan-table-scroll wrapper + .wan-table th/td rules removed
   2026-06-04 — WAN page renders the uplinks table without a
   .wan-table class; .wan-row / .wan-chev-cell / .wan-name-wrap
   etc. do the styling. */
/* The chevron cell carries the expand affordance + the row name/subtitle.
   Flex lives on the INNER div — not the td itself, which would break the
   table layout algorithm and pull the data row's columns out of alignment
   with the header row (the BUG-1 cause where only "LINK" and "TYPE"
   headers appeared to render). */
.wan-chev-cell{min-width:220px;}
.wan-chev-inner{display:flex;align-items:flex-start;gap:10px;}
.wan-chev{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  width:16px;
  height:16px;
  color:var(--text3);
  margin-top:2px;
  transition:transform .2s ease;
  flex-shrink:0;
}
.wan-row.open .wan-chev{transform:rotate(90deg);color:var(--accent);}
.wan-row{cursor:pointer;transition:background .12s;}
.wan-row:hover td{background:var(--row-hover-bg);}
.wan-row.open td{background:var(--row-open-bg);}
.wan-name-wrap{min-width:0;flex:1;}
.wan-name-wrap .cell-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wan-name-wrap .cell-sub{font-size:11px;color:var(--text3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
/* Type cell: flex stays on the inner div, same reason as chev-cell
   above. The <td> itself is a plain table cell that the header row
   can align against. */
.wan-type-cell{font-size:12px;color:var(--text);}
.wan-type-inner{display:flex;align-items:center;gap:6px;}
.wan-type-icon{display:inline-flex;color:var(--text3);}
.wan-row.open .wan-type-icon{color:var(--accent);}
/* Kill any stray focus outline / ring on non-interactive pills inside
   the WAN table — a green :focus-visible ring was leaking around the
   Live pill in the Clients column when the row received keyboard
   focus. Rows themselves still highlight via .wan-row.open. */
/* .wan-table :focus / :focus-visible removed 2026-06-04 — orphans. */

/* Expansion row lives in the same <tbody>. Collapsed state = 0 height
   + 0 padding + no border-top so it's visually invisible. Opening
   animates max-height of the inner container — mirrors the sidebar
   section toggles introduced earlier so the transitions feel cohesive. */
.wan-expand-row{background:rgba(47, 107, 255,0.04);}
.wan-expand-cell{padding:0 !important;border-bottom:1px solid var(--border-subtle);}
.wan-expand-inner{
  max-height:0;
  overflow:hidden;
  transition:max-height .2s ease, padding .2s ease;
  padding:0 18px;
}
.wan-expand-row.open .wan-expand-inner{
  max-height:560px;
  padding:16px 18px;
}
.wan-exp-grid{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:16px 28px;
}
@media (max-width: 900px){
  .wan-exp-grid{grid-template-columns:1fr;}
}
.wan-exp-col{display:flex;flex-direction:column;gap:6px;}
.wan-exp-col-empty{
  color:var(--text3);
  font-size:11px;
  font-style:italic;
  display:flex;
  align-items:center;
}
.wan-exp-row{display:grid;grid-template-columns:120px 1fr;gap:12px;font-size:12px;line-height:1.5;}
.wan-exp-k{
  color:var(--text3);
  font-size:10px;
  text-transform:uppercase;
  letter-spacing:.08em;
  font-weight:600;
  padding-top:2px;
}
.wan-exp-v{color:var(--text);font-family:var(--mono);font-size:11px;overflow-wrap:break-word;}

/* Sparkline strip — three mini-cards full width under the expanded
   row. Today the body is just a "No 24h series yet" placeholder; when
   we wire real series, the empty span becomes an inline SVG polyline
   and this wrapper doesn't need to change. */
.wan-sparks{
  display:grid;
  grid-template-columns:repeat(3, 1fr);
  gap:12px;
  margin-top:16px;
  padding-top:14px;
  border-top:1px dashed var(--border-light);
}
@media (max-width: 900px){
  .wan-sparks{grid-template-columns:1fr;}
}
.wan-spark{
  background:rgba(255,255,255,0.02);
  border:1px solid var(--border-subtle);
  border-radius:6px;
  padding:10px 12px;
}
.wan-spark-title{
  font-size:10px;
  text-transform:uppercase;
  letter-spacing:.08em;
  color:var(--text3);
  font-weight:600;
  margin-bottom:6px;
}
.wan-spark-body{
  height:40px;
  display:flex;
  align-items:center;
  justify-content:center;
}
.wan-spark-empty{
  font-size:10px;
  color:var(--text3);
  font-style:italic;
}

/* ════ SAS DEVICE CARD — Google Portal Status panel layout ════ */
/* Grid layout splits the card into a left content column (name,
   subtitle, state, chips, grants, map) and a right reference column
   (portal link + 3-line reference stack) that spans the name + state
   rows, so the right-side stack visually sits next to the upper-left
   content block. Chips + everything below them span the full width
   so they can center across the whole card. */
.sas-card {
  padding: 12px 20px;
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  gap: 14px;  /* gap between the top cluster and grants/map below */
}
/* Top cluster = friendly name + state + chips on the left, portal
   link + install-params stack on the right. Flex row, align-items
   center so neither side floor-justifies — whichever stack is
   shorter sits vertically centered relative to the taller one. */
.sas-card-top {
  display: flex;
  flex-direction: row;
  align-items: flex-start;           /* top-align all three columns */
  justify-content: space-between;    /* left → far-left, chips → center,
                                        right → far-right */
  gap: 16px;
}
.sas-card-top-left {
  flex: 0 1 auto;            /* hug content width instead of filling */
  min-width: 0;
  align-self: flex-start;    /* don't stretch to card height */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;  /* consistent breathing room between name/subtitle and state */
}
.sas-card-title {
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  letter-spacing: 0;
}
/* .sas-card-title-sep removed 2026-06-04 — orphan. */
/* Right cluster: portal-link icon + 2-line install-params stack as
   a single right-aligned flex column. Both card columns top-align via
   .sas-card-top align-items:flex-start, so the right stack's arrow
   sits at y=0 with the text lines stacked immediately below — no
   dead space above, below, or between. */
.sas-card-header-right {
  flex: 0 1 auto;              /* hug content width */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  align-self: flex-start;      /* don't stretch to card height */
  justify-content: flex-start; /* arrow + text stack sit at top of column */
  gap: 6px;
  min-width: 0;
}
.sas-reg-strip {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0;
  min-width: 0;
  max-width: 100%;
}
.sas-reg-line {
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 400;
  color: var(--text2);
  line-height: 1.6;
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.sas-portal-link {
  flex-shrink: 0;
  background: transparent;
  border: none;
  color: var(--text3);
  width: 28px;
  height: 28px;
  border-radius: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  opacity: 0.7;
  padding: 0;
  transition: opacity .12s, color .12s, background .12s;
}
.sas-portal-link:hover { opacity: 1; color: var(--accent); background: rgba(47, 107, 255,0.1); }
.sas-portal-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 1024px) {
  /* Narrow viewports: stack the top row vertically so the right
     cluster drops below the left content instead of being squeezed. */
  .sas-card-top { flex-direction: column; align-items: stretch; }
  .sas-card-header-right { align-items: flex-start; }
  .sas-reg-strip { align-items: flex-start; }
}

/* Device state pill — mirrors the Portal's "✓ Authorized" line.
   Color comes from inline style (green / amber stale / gray / red)
   because the state-to-color mapping lives in the JS renderer. */
.sas-device-state {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
}
.sas-device-state .sas-state-icon {
  font-size: 16px;
  line-height: 1;
}

/* Stat chips — horizontal row below the state pill. Subtle
   bordered (not filled) so they read as inline stats rather than
   buttons. Each chip is two lines: value on top, label below.
   Centered horizontally in the card body, full-width grid cell so
   centering actually reaches both edges of the card. */
.sas-stat-chips {
  /* Chips are now a top-level column of .sas-card-top — sit in the
     middle slot between left and right (space-between handles the
     horizontal placement) and top-align with the other columns. */
  flex: 0 1 auto;
  align-self: flex-start;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
}
.sas-stat-chip {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 8px 12px;
  /* Subtle green tint ties the chip cluster visually to the
     ✓ Authorized state chip above — reads as "live healthy data"
     without filling loud. Border is also green-tinted so the chip
     boundary stays visible against the tinted fill. Opacities
     bumped from 0.06/0.25 → 0.08/0.35 so the wash is actually
     visible against the dark card background. */
  border: 1px solid rgba(16, 185, 129, 0.35);
  border-radius: 6px;
  background: rgba(16, 185, 129, 0.08);
  min-width: 0;
}
.sas-stat-chip-value {
  font-size: 14px;
  font-weight: 500;
  color: var(--text);
  line-height: 1.2;
  white-space: nowrap;
}
.sas-stat-chip-label {
  font-size: 11px;
  font-weight: 400;
  color: var(--text2);
  line-height: 1.2;
  white-space: nowrap;
}
/* Tone variants — apply on chips that carry a quality signal
   (latency, signal strength, uptime). Default chip stays green
   so the cluster still reads as "live healthy data" overall. */
.sas-stat-chip.tone-ok   { border-color: rgba(16,185,129,0.45); background: rgba(16,185,129,0.10); }
.sas-stat-chip.tone-warn { border-color: rgba(245,158,11,0.50); background: rgba(245,158,11,0.10); }
.sas-stat-chip.tone-warn .sas-stat-chip-value { color: #fcd34d; }
.sas-stat-chip.tone-err  { border-color: rgba(220,38,38,0.50);  background: rgba(220,38,38,0.10); }
.sas-stat-chip.tone-err  .sas-stat-chip-value { color: #fca5a5; }
.sas-stat-chip.tone-info { border-color: rgba(96,165,250,0.45); background: rgba(96,165,250,0.10); }
.sas-stat-chip.tone-info .sas-stat-chip-value { color: #bfdbfe; }
.sas-stat-chip.tone-muted{ border-color: rgba(255,255,255,0.14); background: rgba(255,255,255,0.04); }
.sas-stat-chip.tone-muted .sas-stat-chip-value { color: var(--text3); }
/* Accent chip — small uppercased badge for type-of-link labels
   (LTE / 5G / Fiber / Cable / Gbps). Sits inline in the chip row
   without grabbing visual weight from the throughput numbers. */
.sas-stat-chip.chip-accent { padding: 6px 10px; }
.sas-stat-chip.chip-accent .sas-stat-chip-value {
  font-size: 11px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; font-family: var(--mono);
}
.sas-stat-chip.chip-accent .sas-stat-chip-label { font-size: 10px; }
/* Single-line variant — used in table cells whose column header
   already labels the value (Uptime / Band / Type). Drops the inner
   label, tightens padding so the chip stays compact in row height. */
.sas-stat-chip.chip-compact {
  padding: 3px 9px;
  align-items: center;
}
.sas-stat-chip.chip-compact .sas-stat-chip-value { line-height: 1.3; }
.sas-stat-chip.chip-accent.chip-compact { padding: 3px 9px; }

/* Carrier badge — small accent pill next to the WAN id. Reads like a
   brand tag ("Helium" / "Cox Cable" / "T-Mobile") so the operator
   knows whose pipe this is without scanning the subtitle line. */
.wan-carrier-badge {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: .04em;
  color: var(--gc-accent-bright,#5B9BFF);
  background: rgba(47, 107, 255,0.12);
  border: 1px solid rgba(47, 107, 255,0.40);
  border-radius: 999px;
  font-family: var(--sans);
  text-transform: none;
}

/* ── WAN head-bar stat tiles ──────────────────────────────────
   Flat Meraki-style stats with subtle vertical dividers between
   adjacent tiles. Replaces the pill-shaped .sas-stat-chip chrome
   in the WAN row head, which read as "lines in bubbles" when
   the inline sparkline was wrapped in the same chip class. */
.cc-wan-stats {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0;
  justify-content: center;
  min-width: 0;
}
.cc-wan-stat {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 2px 14px;
  position: relative;
  min-width: 0;
}
.cc-wan-stat + .cc-wan-stat::before,
.cc-wan-stat + .cc-wan-spark::before,
.cc-wan-spark + .cc-wan-stat::before {
  content: '';
  position: absolute;
  left: 0;
  top: 20%;
  bottom: 20%;
  width: 1px;
  background: var(--border-subtle);
}
.cc-wan-stat-value {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.15;
  font-family: var(--mono);
  white-space: nowrap;
}
.cc-wan-stat-label {
  font-size: 10px;
  font-weight: 500;
  color: var(--text3);
  text-transform: uppercase;
  letter-spacing: .06em;
  line-height: 1.2;
  white-space: nowrap;
}
.cc-wan-stat.tone-ok    .cc-wan-stat-value { color: #86efac; }
.cc-wan-stat.tone-warn  .cc-wan-stat-value { color: #fcd34d; }
.cc-wan-stat.tone-err   .cc-wan-stat-value { color: #fca5a5; }
.cc-wan-stat.tone-info  .cc-wan-stat-value { color: #93c5fd; }
.cc-wan-stat.tone-muted .cc-wan-stat-value { color: var(--text3); }
/* Lead "tech" tag — same tile shape but the value renders as a
   bordered gold pill so 5G / LEO / Fiber reads as a type marker
   ahead of the throughput numbers. */
.cc-wan-stat.cc-wan-stat-accent { padding: 0 12px; }
.cc-wan-stat.cc-wan-stat-accent .cc-wan-stat-value {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--gc-accent-bright,#5B9BFF);
  background: rgba(47, 107, 255,0.10);
  border: 1px solid rgba(47, 107, 255,0.35);
  border-radius: 4px;
  padding: 2px 8px;
}
.cc-wan-stat.cc-wan-stat-accent .cc-wan-stat-label { font-size: 9px; margin-top: 2px; }
/* Inline sparkline strip — sits at the end of the stat row,
   no chip chrome. Renders the same 15-minute download trace
   that used to live inside a .sas-stat-chip wrapper. */
.cc-wan-spark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 12px;
  position: relative;
  opacity: 0.9;
  min-width: 0;
}
.cc-wan-spark svg { display: block; }

.sas-active-section { margin-top: 0; }
.sas-section-label {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--text3);
  margin-bottom: 10px;
}

/* 15-channel grid, 3550 → 3700 MHz in 10 MHz cells. Filled cells
   are green to match the Portal's convention; empty cells are
   slightly lifted from the card bg so the grid reads as a
   quantized axis even when no grants exist. */
.sas-ch-axis-labels {
  display: flex;
  justify-content: space-between;
  font-size: 9px;
  color: var(--text3);
  font-family: var(--mono);
  margin-bottom: 6px;
}
.sas-ch-grid {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  gap: 2px;
  height: 28px;
}
.sas-ch-cell {
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.06);
  border-radius: 3px;
}
.sas-ch-filled {
  background: rgba(34,197,94,0.45);
  border-color: rgba(34,197,94,0.7);
  box-shadow: inset 0 0 0 1px rgba(34,197,94,0.3);
}
.sas-ch-numbers {
  display: grid;
  grid-template-columns: repeat(15, 1fr);
  gap: 2px;
  font-size: 9px;
  color: var(--text3);
  font-family: var(--mono);
  text-align: center;
  margin-top: 4px;
}
.sas-ch-num { line-height: 1.2; }

/* Grant list. Collapsed rows = a single line with frequency range
   + authorized check + chevron. Expanded rows show the detail
   table matching the Portal's layout. Uses native <details> for
   keyboard + screen-reader accessibility. */
/* Legacy SAS grant details/summary disclosure UI removed
   2026-06-04 — orphan family. Current SAS view renders grants as
   .sas-grant-card + .sas-grant-card-* children (see dashboard.js
   ~line 5650). Removed: .sas-grant-list, .sas-grant-details,
   .sas-grant-summary, .sas-grant-range, .sas-grant-status,
   .sas-grant-chev, .sas-grant-detail-body, .sas-grant-detail-row. */

/* ════ SAS FRIENDLY NAME + GRANT CARDS ════ */

/* Card header layout: name + subtitle on the left, icons on the right.
   Replaces the old single-line header. */
.sas-card-title-wrap { min-width: 0; }
.sas-friendly-name {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 22px;
}
.sas-friendly-name-text {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: .01em;
}
.sas-friendly-name-placeholder {
  font-size: 14px;
  font-style: italic;
  color: var(--text3);
  cursor: pointer;
  transition: color .12s;
}
.sas-friendly-name-placeholder:hover { color: var(--accent); }
.sas-edit-pencil {
  background: transparent;
  border: none;
  color: var(--text3);
  width: 22px;
  height: 22px;
  padding: 0;
  border-radius: 3px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: .3;
  transition: opacity .12s, color .12s, background .12s;
}
.sas-card-header:hover .sas-edit-pencil,
.sas-friendly-name:hover .sas-edit-pencil,
.sas-edit-pencil:focus-visible { opacity: 1; }
.sas-edit-pencil:hover { color: var(--accent); background: rgba(47, 107, 255,0.1); }
.sas-friendly-input {
  font-family: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(47, 107, 255,0.4);
  border-radius: 4px;
  padding: 3px 8px;
  outline: none;
  min-width: 240px;
  max-width: 420px;
}
.sas-friendly-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(47, 107, 255,0.18); }
/* Tier 2 subtitle: vendor · model · radio tech. Sits directly below
   the friendly name. Proportional font (was mono when it showed
   serial/FCC), larger + lighter than the old version per the new
   information hierarchy. */
.sas-card-subtitle {
  font-size: 13px;
  color: var(--text2);
  margin-top: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Grant cards grid — replaces the <details> collapsible rows. Auto-fill
   so cards wrap naturally on narrow viewports; 280px min keeps the
   two-column KV rows readable. Cards stretch vertically so a row of
   three is the same height regardless of per-card content length. */
.sas-grant-cards {
  margin-top: 16px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 18px;
  align-items: stretch;
}
.sas-grant-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  background: rgba(255,255,255,0.02);
  border: 2px solid rgba(34,197,94,0.35);  /* green = authorized default */
  border-radius: 8px;
  transition: border-color .2s;
}
/* Expire-aware border color. Ticker toggles these classes every 10s. */
.sas-grant-card--warn { border-color: rgba(217,119,6,0.6); }
.sas-grant-card--expired { border-color: rgba(220,38,38,0.65); }
.sas-grant-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.sas-grant-card-range {
  font-family: var(--mono);
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: .01em;
}
.sas-grant-card-ch {
  font-size: 10px;
  font-weight: 600;
  color: var(--text3);
  text-transform: uppercase;
  letter-spacing: .08em;
  white-space: nowrap;
}
/* Bandwidth bar — same green palette as the channel-grid filled
   cells at the top of the card so the visual vocabulary reads
   consistently between the two. */
.sas-grant-card-bar {
  height: 5px;
  background: rgba(34,197,94,0.45);
  border: 1px solid rgba(34,197,94,0.7);
  border-radius: 3px;
}
.sas-grant-card--warn .sas-grant-card-bar {
  background: rgba(217,119,6,0.45);
  border-color: rgba(217,119,6,0.7);
}
.sas-grant-card--expired .sas-grant-card-bar {
  background: rgba(220,38,38,0.4);
  border-color: rgba(220,38,38,0.6);
}
.sas-grant-card-bw {
  font-size: 10px;
  color: var(--text3);
  font-family: var(--mono);
  margin-top: -4px;
}
.sas-grant-card-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  align-self: flex-start;
  font-size: 11px;
  font-weight: 600;
  padding: 3px 9px;
  border-radius: 3px;
  border: 1px solid transparent;  /* actual color set inline per state */
  letter-spacing: .01em;
}
.sas-grant-card-kv {
  display: grid;
  grid-template-columns: auto 1fr;
  row-gap: 4px;
  column-gap: 12px;
  font-size: 11px;
}
.sas-grant-card-kv > span:nth-child(odd) {
  color: var(--text3);
  text-transform: none;
  letter-spacing: 0;
}
.sas-grant-card-kv > span:nth-child(even) {
  color: var(--text);
  font-family: var(--mono);
  text-align: right;
}
.sas-grant-card-divider {
  height: 1px;
  background: rgba(255,255,255,0.06);
  margin: 2px -4px;
}
.sas-grant-card--expired .sas-grant-tx-text { color: #f87171; }
.sas-grant-warn {
  color: #d97706;
  margin-left: 4px;
  font-size: 11px;
}
.sas-grant-card--expired .sas-grant-warn { color: #f87171; }

/* ════ SAS CBSD LOCATION MAP ════ */
.sas-location-section { margin-top: 0; }
.sas-location-map {
  display: block;
  width: 100%;
  height: 280px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid rgba(47, 107, 255,0.12);
  background: var(--gc-surface-base,#0A0E16);  /* fallback while the image loads */
}
.sas-location-caption {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 8px;
  font-size: 11px;
  color: var(--text2);
}
.sas-location-caption-sep { color: var(--text3); }
.sas-location-placeholder {
  width: 100%;
  height: 280px;
  border: 1px dashed rgba(47, 107, 255,0.15);
  border-radius: 6px;
  background: rgba(255,255,255,0.02);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 11px;
  color: var(--text3);
  font-style: italic;
  text-align: center;
  padding: 20px;
}

/* ════ /topology — Phase 1 stack cards + node cards + edge overlay ════
   One card per backend stack (Broadband / Cellular / Security). Cards
   sit side-by-side at ≥900px and stack vertically below that. Inside
   each card, layers render as horizontal flex rows (one per node kind);
   edges are drawn by an absolutely-positioned SVG that computes paths
   from the rendered node-card geometry on load + debounced resize. */
.topo-stacks {
  display: grid;
  /* Column minimum tightened to 720 px — enough for a 4-wide lane
     (4×160 + 3×20 = 700 px plus padding), which covers the lower
     lanes of every site. Wider monitors (>1440 px content area)
     still get two columns via auto-fit; Demo Venue has a single
     stack so its card stretches to full content width, letting the
     6-upstream lane spread comfortably. */
  grid-template-columns: repeat(auto-fit, minmax(720px, 1fr));
  gap: 14px;
}
.topo-stack-card { min-width: 0; position: relative; }
/* Override .section-card's global overflow:hidden for topology cards.
   Two compounding clips (this + .topo-stack-body) were hiding the
   absolute-positioned node cards even though they had valid rects
   and computed opacity:1. */
.topo-stack-card.section-card { overflow: visible; }
/* Top accent bar per stack — color-codes the stack at a glance.
   Each stack-id gets its own hue: broadband=blue (WAN/internet),
   cellular=amber (radio), lan=teal (local), security=red (cams).
   Falls through to gold for unknown stacks. */
.topo-stack-card::before {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 2px;                 /* slimmer — 3px read as a heavy banner */
  border-radius: 8px 8px 0 0;
  background: var(--accent);
  opacity: 0.85;
  z-index: 4;
}
/* Per-stack category accents. The flagship Private Cellular stack now
   wears the electric-blue brand accent instead of the legacy gold→amber
   gradient (gold era removed in #202; the gold bar was the single
   cheapest-reading element on the topology page). The other categories
   keep their on-palette NOC hues (broadband=blue, lan=green,
   security=red, wireless=purple) — those already harmonize. */
.topo-stack-card[data-stack-id="broadband"]::before { background: linear-gradient(90deg,#5B9BFF,#2F6BFF); }
.topo-stack-card[data-stack-id="cellular"]::before  { background: linear-gradient(90deg,#2F6BFF,#5B9BFF); }
.topo-stack-card[data-stack-id="lan"]::before       { background: linear-gradient(90deg,#34d399,#10b981); }
.topo-stack-card[data-stack-id="security"]::before  { background: linear-gradient(90deg,#f87171,#dc2626); }
.topo-stack-card[data-stack-id="wireless"]::before  { background: linear-gradient(90deg,#a78bfa,#7c3aed); }
.topo-stack-card .sc-hdr {
  padding-top: 14px;  /* leave room for the accent bar */
  display: flex;
  align-items: center;
  gap: 10px;
}
.topo-stack-card .sc-hdr-title {
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text2);
  font-weight: 700;
}
/* Let the body scroll horizontally for wider-than-container trees,
   but NOT clip vertically — overflow-y:hidden was trimming absolute
   descendants whose containing-block math lined up right on the edge
   of body's padding box. has-overflow still adds a right-edge mask
   as a scroll hint when the tree overflows horizontally. */
.topo-stack-body {
  overflow-x: auto;
  overflow-y: visible;
  scrollbar-width: thin;
  scrollbar-color: rgba(47, 107, 255,0.25) transparent;
}
.topo-stack-body.has-overflow {
  mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
}
/* z-index stack: edges under cards. Cards' z-index:3 vs SVG's
   z-index:2 must compete in the SAME stacking context, so
   .topo-nodes-layer deliberately does NOT set a z-index here —
   adding one (plus the shell's position:relative) formed a new
   stacking context that trapped cards inside it, which interacted
   with the overflow clips above to render them invisible even with
   z-index:9999 !important forced at the card level. */
.topo-edge-layer { z-index: 2; pointer-events: none; animation: topoEdgeFadeIn 0.3s ease-out 0.2s both; }
.topo-edge { transition: stroke-width .15s, stroke-opacity .15s; }
/* Active-edge traffic animation. A dashed stroke that flows from
   parent to child to indicate live data movement. Only applied to
   edges whose target node is active (drop-shadow filter is set in
   _topoDrawAllEdges). Without the animation, active and offline
   edges read as visually identical — same color, same width. */
.topo-edge[filter*="drop-shadow"] {
  stroke-dasharray: 6 8;
  animation: topoTrafficFlow 1.8s linear infinite;
  stroke-opacity: 0.9;
}
@keyframes topoTrafficFlow {
  to { stroke-dashoffset: -14; }
}
.topo-nodes-layer {
  position: relative;  /* anchor for absolute card top/left */
  display: block;
  /* no z-index — cards compete with SVG in body's stacking context */
}

/* ══════════════════════════════════════════════════════════════════
   TOPOLOGY — TIERED LAYOUT
   ══════════════════════════════════════════════════════════════════
   Five-tier hierarchy modeling how a network operator actually
   describes a site, top-to-bottom:

     INTERNET  carrier + ISP — public network, the upstream signal
     WAN       edge uplinks — Peplink WAN ports, Starlink, etc.
     EDGE      the edge gateway / router — site demarcation
     LAN       everything on the local network: Wi-Fi APs, switches,
               Private Cellular, cameras (Wi-Fi is a wireless LAN,
               not a sibling of LAN)
     CLIENTS   end devices

   Visual language: zero color bands, just a left-gutter tier label
   per range + thin horizontal divider lines between tiers. A slightly
   stronger divider at the WAN/Edge and Edge/LAN boundaries draws the
   "outside vs inside" demarcation operators look for first. The
   tinted-band approach this replaces was too busy and conflated
   Wi-Fi with LAN as separate tiers (it isn't — it's a child).

   Single-stack sites collapse the per-stack title since "Topology"
   + the site subtitle already say what the page shows; dropping the
   "Broadband" / "Network" sub-heading keeps the tier hierarchy as
   the dominant visual element.
   ══════════════════════════════════════════════════════════════════ */

/* The tiered body reserves a wider left padding to make room for
   tier labels in the gutter. Width matches TOPO_TIER_GUTTER_PX in
   dashboard.js — keep both numbers in sync. */
.topo-stack-body-tiered {
  position: relative;
  padding: 14px 18px 18px 78px;
  overflow-x: auto;
  overflow-y: visible;
  scrollbar-width: thin;
  scrollbar-color: rgba(47, 107, 255,0.25) transparent;
}
.topo-stack-body-tiered.has-overflow {
  mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
}

/* Single-stack sites hide the per-stack title — the page subtitle
   already names the site + node count, the tier gutter already
   provides hierarchy, and a redundant "Broadband" / "Network"
   header just competes for attention. Multi-stack sites still
   render the title so operators can tell categories apart. */
.topo-single-stack .topo-stack-card > .sc-hdr {
  display: none;
}
.topo-single-stack .topo-stack-card::before {
  border-radius: 8px 8px 0 0;   /* keep the top-accent rounded */
}

/* ── Tier label gutter ─────────────────────────────────────────────
   Absolute-positioned column on the left edge of the stack body.
   Each tier gets one label anchored at its vertical center, so the
   label reads as "this whole range of nodes is the LAN tier". The
   element occupies the left padding area of the body — nodes never
   collide with it. */
.topo-tier-gutter {
  position: absolute;
  top: 14px;
  bottom: 18px;
  left: 0;
  width: 78px;            /* === .topo-stack-body-tiered padding-left */
  pointer-events: none;
  z-index: 1;
}
.topo-tier-label {
  position: absolute;
  left: 10px;
  right: 6px;
  transform: translateY(-50%);   /* vertically center on the tier */
  font-family: var(--mono);
  user-select: none;
}
.topo-tier-label-main {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--text2);
  text-transform: uppercase;
  line-height: 1.1;
  margin-bottom: 3px;
}
.topo-tier-label-sub {
  font-size: 9px;
  letter-spacing: 0.04em;
  color: var(--text3);
  line-height: 1.2;
  text-transform: none;
  font-family: var(--sans);
}
/* The Edge tier is the site demarcation — give its label a subtle
   gold accent so the operator's eye lands on it first when scanning
   "where does the site itself live in this picture?". */
.topo-tier-label--edge .topo-tier-label-main {
  color: var(--accent-bright, #5b9bff);
}

/* ── Tier dividers ────────────────────────────────────────────────
   Thin horizontal lines BETWEEN tiers; sit behind the node cards
   and edge SVG (z:0) so they never visually obstruct connections.
   The demarc variant marks the WAN→Edge / Edge→LAN boundary —
   the line that separates "outside the site" from "inside the
   site". Slightly stronger so it reads first.  */
.topo-tier-dividers {
  position: absolute;
  top: 14px;
  bottom: 18px;
  left: 14px;
  right: 14px;
  pointer-events: none;
  z-index: 0;
}
.topo-tier-divider {
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: rgba(255,255,255,0.06);
}
.topo-tier-divider-demarc {
  background: linear-gradient(to right,
    transparent 0,
    rgba(47, 107, 255,0.18) 8%,
    rgba(47, 107, 255,0.32) 50%,
    rgba(47, 107, 255,0.18) 92%,
    transparent 100%);
  height: 1px;
  box-shadow: 0 0 8px rgba(47, 107, 255,0.10);
}
/* .topo-layer-label-tag, .topo-layer, .topo-layer-label,
   .topo-layer-cards removed 2026-06-04 — orphans (the comment
   even said they were kept "while we finish the rollout"; the
   rollout is finished). Current layout uses .topo-tier-* family. */
/* Meraki/UniFi-style topology card. Width pinned by Topology.DEFAULTS
   (208 px). Tall enough to carry icon + label + sublabel + metric
   chip + status pill without crowding. Status-tinted left rail (6 px
   wide) reads health at a glance; the rest of the card stays neutral.
   Hover lifts; click selects (gold ring); selected stays lifted. */
/* Topology node card — Meraki Dashboard styling.
   Compact, content-driven height. Tight padding. Subtle hairline
   border. No giant shadows. Status communicated via the 3 px left
   rail (already present) — no extra chrome. */
.topo-node-card {
  position: absolute;
  z-index: 3;
  /* No min-height — let content drive size. The Walker layout sets
     nodeHeight in JS but the actual card sizes to its content; we
     fix uniform height in the Topology.DEFAULTS adjustment below. */
  padding: 9px 12px 9px 14px;
  border-radius: 7px;
  background: var(--gc-surface-card, #121A2A);
  /* Bumped from rgba 0.06 → 0.12 — the resting border was so faint
     the cards looked free-floating against the stack body. 0.12 alpha
     reads as a defined edge without competing with the status rail. */
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: 0 1px 3px rgba(0,0,0,0.32), 0 1px 0 rgba(255,255,255,0.04) inset;
  font-family: var(--sans);
  cursor: pointer;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  overflow: hidden;
  transition:
    left .22s cubic-bezier(.4,0,.2,1),
    top  .22s cubic-bezier(.4,0,.2,1),
    border-color .12s,
    box-shadow .12s,
    background .12s,
    transform .12s;
}
.topo-node-card:hover {
  border-color: rgba(47, 107, 255,0.48);
  box-shadow: 0 4px 14px rgba(0,0,0,0.45), 0 0 0 1px rgba(47, 107, 255,0.28), 0 1px 0 rgba(255,255,255,0.06) inset;
  background: var(--gc-surface-raised, #18223A);
  transform: translateY(-1px);
}
.topo-node-card:focus-visible {
  outline: none;
  border-color: var(--accent, #2f6bff);
  box-shadow: 0 0 0 2px rgba(47, 107, 255,0.35);
}
/* When a card is selected via click, persistent accent ring + lift */
.topo-node-card.topo-node-selected {
  border-color: var(--accent, #2f6bff);
  box-shadow: 0 8px 24px rgba(0,0,0,0.55), 0 0 0 2px rgba(47, 107, 255,0.55);
  background: var(--gc-surface-raised, #18223A);
}
/* Path-highlight state — when another card is hovered/selected and
   this card is on its lineage (ancestor or descendant). */
.topo-node-card.topo-node-on-path {
  border-color: rgba(47, 107, 255,0.50);
  background: var(--gc-surface-raised, #18223A);
}
.topo-node-card.topo-node-dim {
  opacity: 0.42;
}

/* Topology page controls — search input, kind-filter pills, drawer
   chips. Inline styles handle the static look in renderTopologyView;
   these rules add the hover + focus polish that's easier to express
   in CSS than inlined. */
.topo-controls input[type="search"] {
  transition: border-color 0.12s ease, background 0.12s ease;
}
.topo-controls input[type="search"]:hover,
.topo-controls input[type="search"]:focus {
  border-color: rgba(47, 107, 255,0.32) !important;
  background: rgba(255,255,255,0.06) !important;
  outline: none;
}
.topo-pill {
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.topo-pill:hover {
  border-color: rgba(47, 107, 255,0.28) !important;
  background: rgba(47, 107, 255,0.08) !important;
  color: var(--text) !important;
}
.topo-pill:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* Drawer link chips — buttons that jump to a neighbor node. Gold
   accent on hover matches the rest of the topology page's
   interaction language. */
button.topo-drawer-link-chip {
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--text2);
  padding: 5px 10px;
  border-radius: 99px;
  font-size: 11px;
  font-family: inherit;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
button.topo-drawer-link-chip:hover {
  background: rgba(47, 107, 255,0.10);
  border-color: rgba(47, 107, 255,0.32);
  color: var(--text);
}
button.topo-drawer-link-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.topo-node-rail {
  position: absolute;
  top: 8px;
  bottom: 8px;
  left: 0;
  width: 3px;                    /* thinner — Meraki uses ~3px accents */
  border-radius: 0 2px 2px 0;
  opacity: 0.92;
}

.topo-node-top {
  display: flex;
  align-items: center;           /* center icon + text vertically */
  gap: 9px;
  min-width: 0;
}
.topo-node-icon-wrap {
  flex: 0 0 auto;
  width: 24px;                   /* smaller — content density up */
  height: 24px;
  border-radius: 5px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255,255,255,0.85);
}
.topo-node-text {
  min-width: 0;
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
  overflow: hidden;
}
.topo-node-label {
  font-size: 12.5px;
  font-weight: 600;
  color: rgba(255,255,255,0.92);
  line-height: 1.25;
  letter-spacing: -0.005em;
  white-space: nowrap;             /* single line — no 2-line wrap */
  overflow: hidden;
  text-overflow: ellipsis;
}
.topo-node-sublabel {
  font-size: 10.5px;
  /* Bumped from rgba 0.42 → 0.55 — sublabels (vendor/role) were
     reading as nearly disabled at 0.42; 0.55 keeps them clearly
     secondary to the label without rendering as ghost text. */
  color: rgba(255,255,255,0.55);
  font-family: var(--sans);        /* sans not mono — Meraki-style */
  font-weight: 400;
  letter-spacing: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Tiny status pill — Meraki uses a solid dot, not a "ACTIVE" word.
   Pill is smaller; the colored left rail already carries status.
   Pill is also right-aligned via the parent flex layout. */
.topo-status-pill {
  font-size: 8.5px;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 999px;
  letter-spacing: .05em;
  text-transform: uppercase;
  white-space: nowrap;
  flex-shrink: 0;
  align-self: center;
  opacity: 0.85;
}
.topo-node-bottom {
  display: flex;
  align-items: center;
  gap: 6px;
  padding-left: 33px;     /* icon-width 24 + gap 9 = 33 */
  margin-top: 2px;
  min-height: 0;
}
.topo-node-bottom:empty { display: none; }
.topo-node-metric {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  padding: 1px 7px;
  border-radius: 4px;            /* slightly less pillshape, more chip */
  background: rgba(47, 107, 255,0.08);
  border: 1px solid rgba(47, 107, 255,0.22);
  color: var(--accent-bright, var(--gc-accent-bright, #5B9BFF));
  letter-spacing: .02em;
}
/* Uplink redundancy chip — shown on cards whose data layer expresses
   multiple incoming parents (e.g. SpeedFusion bond, dual-path uplinks).
   Meraki/Catalyst Center convention: surface redundancy as a badge
   instead of drawing N converging edges to the same child. Slightly
   cooler hue than the gold metric chip so the two read as different
   semantic categories at a glance. */
.topo-node-uplink-chip {
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 500;
  padding: 1px 7px;
  border-radius: 4px;
  background: rgba(56,189,248,0.10);
  border: 1px solid rgba(56,189,248,0.30);
  color: #7dd3fc;
  letter-spacing: .02em;
  white-space: nowrap;
}

/* ── Node-detail inline expansion row ───────────────────────────
   Replaces the legacy MODALS['node-detail'] dialog. Used by the
   Node Health table (/network-overview) and the Routers / Switches
   / Wireless APs tables on /lan. The host row gets aria-expanded
   when open; the sibling tr renders the detail grid in place. */
.node-expand-row td.node-expand-cell {
  background: rgba(0,0,0,0.22);
  padding: 14px 18px 18px;
  border-top: 1px solid var(--border-light);
}
.node-expand-inner { max-width: 620px; }
.node-expand-section {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--text3);
  margin: 10px 0 8px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--border-light);
}
.node-expand-section:first-child { margin-top: 0; }
tr.row-click[aria-expanded="true"] { background: rgba(47, 107, 255,0.05); }
tr.row-click[aria-expanded="true"] td { border-bottom: none; }

/* topoNodeFadeIn removed — cards now render at opacity 1 from the
   start. Keyframe restarts on every DOM replace were the root cause
   of cards flickering invisibly. */
@keyframes topoEdgeFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
/* (Legacy single-line .topo-node-icon / .topo-node-dot rules removed —
   the new Meraki-style card uses .topo-node-icon-wrap + .topo-status-pill
   + .topo-node-rail and the modern .topo-node-label / .topo-node-sublabel
   defined earlier in this block.) */

/* Edge highlight states — driven by JS adding classes during hover /
   selection. The base topo-edge-path keeps the inline stroke color
   from the node status; these classes layer opacity + a gold glow on
   top so the lineage of the hovered node reads at a glance. */
.topo-edge-path { transition: stroke-opacity .15s, filter .15s; }
.topo-edge-on-path {
  stroke-opacity: 1 !important;
  filter: drop-shadow(0 0 4px rgba(47, 107, 255,0.55));
}
.topo-edge-dim {
  stroke-opacity: 0.20 !important;
  filter: none;
}

/* ── Topology side drawer ─────────────────────────────────────────
   Slide-in panel from the right edge that shows the clicked node's
   full payload. Built lazily, single instance lives on body. */
.topo-drawer-overlay {
  position: fixed; inset: 0;
  z-index: 240;
  pointer-events: none;
}
.topo-drawer-overlay.open { pointer-events: auto; }
.topo-drawer-scrim {
  position: absolute; inset: 0;
  background: rgba(0,0,0,0.45);
  opacity: 0;
  transition: opacity .2s ease-out;
}
.topo-drawer-overlay.open .topo-drawer-scrim { opacity: 1; }
.topo-drawer {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 440px; max-width: 92vw;
  background: var(--gc-surface-card,#121A2A);
  border-left: 1px solid var(--border-light);
  box-shadow: -12px 0 32px rgba(0,0,0,0.5);
  transform: translateX(100%);
  transition: transform .25s ease-out;
  display: flex; flex-direction: column;
  overflow: hidden;
}
.topo-drawer-overlay.open .topo-drawer { transform: translateX(0); }
.topo-drawer-hdr {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid var(--border-light);
}
.topo-drawer-hdr-label {
  font-size: 10px; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text3);
  font-family: var(--mono);
}
.topo-drawer-close {
  background: transparent; border: none; color: var(--text3);
  cursor: pointer; font-size: 15px; padding: 4px 10px;
}
.topo-drawer-close:hover { color: var(--text); }
.topo-drawer-body {
  padding: 18px;
  overflow-y: auto;
  display: flex; flex-direction: column; gap: 14px;
}
.topo-drawer-head {
  display: flex; align-items: flex-start; gap: 12px;
}
.topo-drawer-icon {
  width: 40px; height: 40px;
  border-radius: 8px;
  background: rgba(47, 107, 255,0.10);
  border: 1px solid rgba(47, 107, 255,0.30);
  color: var(--accent-bright, var(--gc-accent-bright, #5B9BFF));
  display: flex; align-items: center; justify-content: center;
  flex: 0 0 auto;
}
.topo-drawer-titles { flex: 1 1 auto; min-width: 0; }
.topo-drawer-title {
  font-size: 16px; font-weight: 600; color: var(--text);
  word-break: break-word;
}
.topo-drawer-subtitle {
  font-size: 11px; color: var(--text3);
  font-family: var(--mono);
  margin-top: 2px;
}
/* .topo-drawer-section / -detail / -row / -k / -v / -empty /
   -links-label removed 2026-06-04 — orphans. Current drawer
   uses .topo-drawer-links / .topo-drawer-links-row /
   .topo-drawer-link-chip (retained below). */
.topo-drawer-links { display: flex; flex-direction: column; gap: 6px; }
.topo-drawer-links-row { display: flex; flex-wrap: wrap; gap: 6px; }
.topo-drawer-link-chip {
  font-family: var(--mono); font-size: 11px;
  padding: 3px 9px;
  border-radius: 999px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--border-light);
  color: var(--text2);
}
.topo-drawer-link-chip.more { color: var(--text3); font-style: italic; }

/* ═══════════════════════════════════════════════════════════════════
   NODE STACK PANEL — the showcase slide-in. Renders a Guardian node as
   a literal top-down vertical stack of its gear tiers (Power → Backhaul
   → 5G → LAN → Serves). Premium-by-design: this panel is what gets
   screenshotted for customers. Built on gc tokens; status-tinted rails;
   a single connector line threads the tiers so the "stack" reads
   instantly. Interaction mirrors the users-drawer / topo-drawer
   (scrim + slide-from-right).
═══════════════════════════════════════════════════════════════════ */
.node-stack-overlay {
  position: fixed; inset: 0;
  z-index: 250;
  pointer-events: none;
}
.node-stack-overlay.open { pointer-events: auto; }
.node-stack-scrim {
  position: absolute; inset: 0;
  background: rgba(4, 7, 14, 0.62);
  backdrop-filter: blur(2px);
  opacity: 0;
  transition: opacity var(--gc-dur-slow, 240ms) var(--gc-ease-out, cubic-bezier(.16,1,.3,1));
}
.node-stack-overlay.open .node-stack-scrim { opacity: 1; }
.node-stack-panel {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: 680px; max-width: 94vw;
  background:
    radial-gradient(120% 60% at 100% 0%, rgba(47,107,255,0.07), transparent 60%),
    var(--gc-surface-card, #121A2A);
  border-left: 1px solid rgba(255,255,255,0.10);
  box-shadow: -24px 0 60px rgba(0,0,0,0.55);
  transform: translateX(100%);
  transition: transform var(--gc-dur-slow, 240ms) var(--gc-ease-out, cubic-bezier(.16,1,.3,1));
  display: flex; flex-direction: column;
  overflow: hidden;
}
.node-stack-overlay.open .node-stack-panel { transform: translateX(0); }

/* ── Header ── node identity + honest worst-of health roll-up ── */
.node-stack-hdr {
  display: flex; align-items: flex-start; gap: 14px;
  padding: 22px 24px 18px;
  border-bottom: 1px solid var(--gc-divider, rgba(255,255,255,0.06));
  background: linear-gradient(180deg, rgba(255,255,255,0.02), transparent);
}
.node-stack-hdr-main { flex: 1 1 auto; min-width: 0; }
.node-stack-title {
  font-family: var(--sans);
  font-size: 21px; font-weight: 700;
  color: var(--gc-text-strong, #E8EEF8);
  letter-spacing: -0.01em; line-height: 1.15;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.node-stack-sub {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin-top: 6px;
  font-size: 12.5px; color: var(--gc-text-muted, #B6C0D2);
}
.node-stack-dot-sep { color: var(--gc-text-dim, #8A94A6); }
.node-stack-health {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px; border-radius: var(--gc-radius-pill, 999px);
  font-size: 11px; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; white-space: nowrap;
  flex: 0 0 auto; align-self: center;
}
.node-stack-health-dot { width: 8px; height: 8px; border-radius: 50%; }
.node-stack-close {
  flex: 0 0 auto;
  background: transparent; border: none;
  color: var(--gc-text-dim, #8A94A6);
  cursor: pointer; font-size: 17px; line-height: 1;
  padding: 6px 8px; border-radius: var(--gc-radius-control, 8px);
  transition: color .12s, background .12s;
}
.node-stack-close:hover { color: var(--gc-text-strong, #E8EEF8); background: rgba(255,255,255,0.05); }

/* ── Body ── the vertical stack ───────────────────────────────── */
.node-stack-body {
  position: relative;
  flex: 1 1 auto; overflow-y: auto;
  padding: 24px 24px 8px 40px;   /* extra left pad makes room for the
                                    connector line + node pips */
}
.node-stack-loading, .node-stack-error {
  display: flex; align-items: center; gap: 10px;
  padding: 40px 8px; color: var(--gc-text-muted, #B6C0D2); font-size: 13px;
}
.node-stack-error a { color: var(--gc-accent-bright, #5B9BFF); margin-left: 4px; }

/* The connector line — single vertical thread behind every tier's pip,
   giving the unmistakable "stack" read. Sits under the pips. */
.node-stack-rail-line {
  position: absolute;
  left: 24px; top: 40px; bottom: 28px;
  width: 2px;
  background: linear-gradient(180deg,
    rgba(91,155,255,0.10),
    rgba(91,155,255,0.42) 12%,
    rgba(91,155,255,0.42) 88%,
    rgba(91,155,255,0.10));
  border-radius: 2px;
  pointer-events: none;
}

.node-stack-tier {
  position: relative;
  margin-bottom: 14px;
  padding-left: 24px;            /* room for the pip column */
}
.node-stack-tier-last { margin-bottom: 18px; }
/* Node pip on the connector line, status-tinted */
.node-stack-node {
  position: absolute;
  left: -16px;                   /* lands on the rail-line at body left:24 */
  top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px;
}
.node-stack-node-pip {
  display: block; width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--gc-surface-card, #121A2A);
  border: 2px solid currentColor;
  margin: 2px;
  box-shadow: 0 0 0 4px var(--gc-surface-card, #121A2A);
}

/* The tier card itself */
.node-stack-card {
  position: relative;
  display: flex; align-items: center; gap: 14px;
  padding: 15px 18px 15px 20px;
  border-radius: var(--gc-radius-card, 12px);
  background: var(--gc-surface-raised, #18223A);
  border: 1px solid var(--gc-divider, rgba(255,255,255,0.07));
  box-shadow: var(--gc-shadow-1, 0 1px 3px rgba(0,0,0,0.32));
  overflow: hidden;
  transition: border-color .15s, box-shadow .15s, transform .15s;
}
.node-stack-card:hover {
  border-color: rgba(47,107,255,0.30);
  box-shadow: 0 6px 20px rgba(0,0,0,0.42);
  transform: translateY(-1px);
}
.node-stack-rail {
  position: absolute; left: 0; top: 10px; bottom: 10px;
  width: 3px; border-radius: 0 2px 2px 0;
  background: currentColor; opacity: 0.9;
}
.node-stack-icon {
  flex: 0 0 auto;
  width: 42px; height: 42px;
  border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.08);
  color: var(--gc-text-strong, #E8EEF8);
}
.node-stack-text { flex: 1 1 auto; min-width: 0; }
.node-stack-tier-hdr { display: flex; align-items: center; gap: 8px; }
.node-stack-tier-title {
  font-size: 14px; font-weight: 600;
  color: var(--gc-text-strong, #E8EEF8);
  letter-spacing: -0.005em;
}
.node-stack-count {
  font-family: var(--mono);
  font-size: 10px; font-weight: 600;
  padding: 1px 6px; border-radius: var(--gc-radius-chip, 6px);
  background: rgba(255,255,255,0.05);
  color: var(--gc-text-muted, #B6C0D2);
}
.node-stack-devices {
  margin-top: 3px;
  font-size: 12px; color: var(--gc-text-muted, #B6C0D2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 280px;
}
.node-stack-right {
  flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
  text-align: right;
}
.node-stack-metric { display: flex; flex-direction: column; align-items: flex-end; line-height: 1.1; }
.node-stack-metric-val {
  font-family: var(--mono);
  font-size: 17px; font-weight: 600;
  color: var(--gc-text-strong, #E8EEF8);
  letter-spacing: -0.01em;
}
.node-stack-metric-lbl {
  font-size: 9.5px; font-weight: 600; letter-spacing: .07em;
  text-transform: uppercase; color: var(--gc-text-dim, #8A94A6);
  margin-top: 2px;
}
.node-stack-metric-empty .node-stack-metric-val {
  font-family: var(--sans); font-size: 12px; font-weight: 500;
  color: var(--gc-text-dim, #8A94A6); font-style: italic;
}
.node-stack-status {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 10.5px; font-weight: 600; letter-spacing: .03em;
  color: var(--gc-text-muted, #B6C0D2);
}
.node-stack-status-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.node-stack-status-none { color: var(--gc-text-dim, #8A94A6); font-style: italic; }

/* Empty tier — dimmed so the stack concept stays end-to-end legible
   even where no gear is deployed (truth over appearance). */
.node-stack-tier-empty .node-stack-card {
  background: rgba(255,255,255,0.012);
  border-style: dashed;
  border-color: rgba(255,255,255,0.07);
  box-shadow: none;
}
.node-stack-tier-empty .node-stack-card:hover { transform: none; box-shadow: none; border-color: rgba(255,255,255,0.10); }
.node-stack-tier-empty .node-stack-icon { opacity: 0.4; }
.node-stack-tier-empty .node-stack-tier-title,
.node-stack-tier-empty .node-stack-devices { opacity: 0.5; }
.node-stack-tier-empty .node-stack-node-pip { border-color: var(--gc-text-dim, #8A94A6); opacity: 0.5; }
.node-stack-tier-empty .node-stack-rail { opacity: 0.25; }

/* Status tones — drive rail (currentColor on .node-stack-rail), the
   pip border, the status dot, and the health pill. */
.node-stack-tone-ok      { color: var(--gc-ok, #22C55E); }
.node-stack-tone-warn    { color: var(--gc-warn, #F59E0B); }
.node-stack-tone-err     { color: var(--gc-err, #DC2626); }
.node-stack-tone-unknown { color: var(--gc-text-dim, #8A94A6); }
.node-stack-tone-none    { color: var(--gc-text-dim, #8A94A6); }
.node-stack-tone-ok      .node-stack-status,
.node-stack-tone-warn    .node-stack-status,
.node-stack-tone-err     .node-stack-status { color: currentColor; }
.node-stack-tier .node-stack-node { color: inherit; }
/* health pill backgrounds */
.node-stack-health.node-stack-tone-ok   { color: #86efac; background: rgba(34,197,94,0.14);  border: 1px solid rgba(34,197,94,0.40); }
.node-stack-health.node-stack-tone-warn { color: #fcd34d; background: rgba(245,158,11,0.14); border: 1px solid rgba(245,158,11,0.40); }
.node-stack-health.node-stack-tone-err  { color: #fca5a5; background: rgba(220,38,38,0.14);  border: 1px solid rgba(220,38,38,0.40); }
.node-stack-health.node-stack-tone-unknown { color: var(--gc-text-muted, #B6C0D2); background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.10); }
.node-stack-health.node-stack-tone-ok   .node-stack-health-dot { background: #22C55E; box-shadow: 0 0 7px rgba(34,197,94,0.6); }
.node-stack-health.node-stack-tone-warn .node-stack-health-dot { background: #F59E0B; box-shadow: 0 0 7px rgba(245,158,11,0.5); }
.node-stack-health.node-stack-tone-err  .node-stack-health-dot { background: #DC2626; box-shadow: 0 0 7px rgba(220,38,38,0.55); }
.node-stack-health.node-stack-tone-unknown .node-stack-health-dot { background: var(--gc-text-dim, #8A94A6); }

/* ── Footer actions ── into the deep view + inventory ──────────── */
.node-stack-foot {
  display: flex; gap: 10px; align-items: center;
  padding: 16px 24px;
  border-top: 1px solid var(--gc-divider, rgba(255,255,255,0.06));
  background: linear-gradient(0deg, rgba(255,255,255,0.02), transparent);
}
.node-stack-action {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 9px 16px; border-radius: var(--gc-radius-control, 8px);
  font-size: 12.5px; font-weight: 600; text-decoration: none;
  border: 1px solid var(--gc-divider, rgba(255,255,255,0.10));
  color: var(--gc-text-muted, #B6C0D2);
  background: rgba(255,255,255,0.02);
  cursor: pointer; transition: background .12s, border-color .12s, color .12s;
}
.node-stack-action:hover { color: var(--gc-text-strong, #E8EEF8); border-color: rgba(255,255,255,0.20); background: rgba(255,255,255,0.05); }
.node-stack-action-primary {
  color: #fff;
  background: var(--gc-accent, #2F6BFF);
  border-color: var(--gc-accent, #2F6BFF);
  box-shadow: 0 4px 14px var(--gc-accent-glow, rgba(47,107,255,0.28));
}
.node-stack-action-primary:hover { background: var(--gc-accent-bright, #5B9BFF); border-color: var(--gc-accent-bright, #5B9BFF); color: #fff; }

/* nx-card stack-open affordance (replaces the old accordion chevron) */
.nx-chev-stack {
  display: flex; align-items: center; justify-content: center;
  color: var(--gc-text-dim, #8A94A6);
  transition: color .12s, transform .12s;
}
.nx-card-head:hover .nx-chev-stack { color: var(--gc-accent-bright, #5B9BFF); transform: scale(1.06); }

@media (max-width: 820px) {
  .node-stack-panel { width: 100vw; max-width: 100vw; }
  .node-stack-devices { max-width: 160px; }
  .node-stack-body { padding-left: 36px; }
}

/* ════ /lan page — filter pill row + per-device type chip ════ */
.lan-filter-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 12px;
}
.lan-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border-radius: 999px;
  background: transparent;
  border: 1px solid var(--border-subtle);
  color: var(--text2);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: background .12s, border-color .12s, color .12s;
}
.lan-pill:hover { background: var(--row-hover-bg); color: var(--text); }
.lan-pill-active {
  border-color: var(--accent);
  color: var(--text);
  background: var(--accent-dim, rgba(47, 107, 255,0.14));
}
.lan-pill-active .lan-pill-count { color: var(--accent-bright, var(--accent)); }
.lan-pill-count {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text3);
  font-weight: 600;
}
.lan-pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* WiFi summary card — per-band breakdown chips. Each band gets a
   subtle accent border that matches the band's visual identity in
   network diagrams (2.4 = blue-grey, 5 = teal, 6 = purple). The
   default scheme stays neutral if a non-standard band shows up. */
.lan-wifi-bands-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; }
.lan-wifi-band { padding: 12px 14px; background: rgba(255,255,255,0.02); border: 1px solid var(--border-light); border-left: 3px solid var(--text3); border-radius: 6px; display: flex; flex-direction: column; gap: 4px; }
.lan-wifi-band.band-24 { border-left-color: #64748b; }
.lan-wifi-band.band-5  { border-left-color: #14b8a6; }
.lan-wifi-band.band-6  { border-left-color: #a855f7; }
.lan-wifi-band-label   { font-size: 10px; color: var(--text3); text-transform: uppercase; letter-spacing: .08em; font-weight: 600; font-family: var(--mono); }
.lan-wifi-band-stat    { display: flex; align-items: baseline; gap: 6px; }
.lan-wifi-band-num     { font-size: 20px; font-weight: 600; color: var(--text); letter-spacing: -0.02em; line-height: 1; }
.lan-wifi-band-unit    { font-size: 11px; color: var(--text3); }
.lan-wifi-band-traffic { font-size: 10px; color: var(--text3); font-family: var(--mono); }

/* Top APs list — ranked rows with a magnitude bar that fills
   proportional to the top AP's client count. Operators can scan
   and see "AP 1 has 12 clients, AP 2 has 9, AP 3 has 4" at a
   glance without parsing numbers. */
.lan-topaps-list { display: flex; flex-direction: column; gap: 4px; }
.lan-topap-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: rgba(255,255,255,0.02); border: 1px solid var(--border-light); border-radius: 6px; }
.lan-topap-row-clickable { cursor: pointer; transition: background var(--gc-dur-fast, 120ms) ease, border-color var(--gc-dur-fast, 120ms) ease; }
.lan-topap-row-clickable:hover { background: rgba(47, 107, 255,0.07); border-color: rgba(47, 107, 255,0.30); }
.lan-topap-row-clickable:focus-visible { outline: 2px solid var(--accent-bright); outline-offset: 2px; }
/* Transient highlight when a row is focused via deep-link (e.g. LAN Top
   clients → Clients). Pulses the accent then fades out. */
@keyframes row-focus-flash-kf { 0% { background: rgba(47, 107, 255,0.22); } 100% { background: transparent; } }
tr.row-focus-flash > td { animation: row-focus-flash-kf 1800ms ease-out; }
.lan-topap-rank { width: 22px; height: 22px; border-radius: 50%; background: rgba(47, 107, 255,0.10); color: var(--accent-bright); display: flex; align-items: center; justify-content: center; font-family: var(--mono); font-size: 11px; font-weight: 600; flex: 0 0 auto; }
.lan-topap-main { flex: 1; min-width: 0; }
.lan-topap-name { font-size: 12px; font-weight: 500; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lan-topap-meta { font-size: 10px; color: var(--text3); font-family: var(--mono); margin-top: 2px; }
.lan-topap-bar  { height: 3px; background: linear-gradient(90deg, var(--accent-border), var(--accent-bright)); border-radius: 2px; margin-top: 6px; width: var(--w, 0%); transition: width .3s; }
.lan-topap-clients { display: flex; flex-direction: column; align-items: flex-end; flex: 0 0 auto; }
.lan-topap-clients-num { font-size: 18px; font-weight: 600; color: var(--text); line-height: 1; letter-spacing: -0.02em; }
.lan-topap-clients-unit { font-size: 10px; color: var(--text3); text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }

/* Channel distribution chips. Co-channel jam (count > 1) gets a
   subtle warning glow so the operator notices it immediately. */
.lan-channels-row { display: flex; flex-wrap: wrap; gap: 6px; }
.lan-channel-chip { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 48px; padding: 6px 8px; background: rgba(255,255,255,0.02); border: 1px solid var(--border-light); border-top: 3px solid var(--text3); border-radius: 4px; cursor: help; transition: background .15s, border-color .15s; }
.lan-channel-chip:hover { background: rgba(255,255,255,0.05); }
.lan-channel-chip.band-24 { border-top-color: #64748b; }
.lan-channel-chip.band-5  { border-top-color: #14b8a6; }
.lan-channel-chip.band-6  { border-top-color: #a855f7; }
.lan-channel-chip.congested { box-shadow: 0 0 0 1px rgba(220,38,38,0.4) inset; background: rgba(220,38,38,0.04); }
.lan-channel-num  { font-size: 14px; font-weight: 600; color: var(--text); font-family: var(--mono); line-height: 1; }
.lan-channel-band { font-size: 9px; color: var(--text3); text-transform: uppercase; letter-spacing: .04em; margin-top: 2px; }
.lan-channel-count { font-size: 9px; font-weight: 700; color: #f87171; font-family: var(--mono); margin-top: 2px; }

/* Top talkers — RSSI bars + wired/wireless link badge. The signal
   bars use a stair-step heights so weak signal reads visually
   different from strong without reading the number. */
.lan-sigbars { display: inline-flex; align-items: flex-end; gap: 1px; margin: 0 4px; vertical-align: middle; }
.lan-sigbar { display: inline-block; width: 3px; border-radius: 1px; }
.lan-sigbar-on  { background: #22c55e; }
.lan-sigbar-off { background: rgba(138,155,176,0.25); }
.lan-talker-link { display: inline-block; font-family: var(--mono); font-size: 9px; font-weight: 600; letter-spacing: .04em; padding: 1px 5px; border-radius: 3px; background: rgba(138,155,176,0.14); color: var(--text3); margin-left: 6px; text-transform: uppercase; }
.lan-talker-link.wireless { background: rgba(14,165,233,0.14); color: #38bdf8; }

/* ════ /integrations connector cards ════
   Replaces the previous inline-styled cards with classes that share
   the .ic-* status pill + chip vocabulary from the design system.
   Same data, same actions, same call sites — pure visual cleanup. */
.int-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(47, 107, 255,0.10); border-radius: 10px; padding: 18px 20px; display: flex; align-items: flex-start; gap: 14px; transition: border-color .15s, background .15s; }
.int-card:hover { border-color: rgba(47, 107, 255,0.20); background: rgba(255,255,255,0.03); }
.int-card-configured { border-color: rgba(47, 107, 255,0.18); }
.int-card-logo { width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700; color: #fff; flex-shrink: 0; }
.int-card-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.int-card-hdr  { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.int-card-name { font-size: 14px; font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
.int-card-desc { font-size: 11px; color: var(--text3); line-height: 1.5; }
.int-card-meta { margin-top: 4px; }
.int-card-actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; align-items: center; }
.int-card-test-status { font-size: 10px; padding: 4px 0; color: var(--text3); font-family: var(--mono); }

.lan-type-chip {
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .08em;
  padding: 3px 8px;
  border-radius: 4px;
  flex-shrink: 0;
  white-space: nowrap;
}

/* ════ /wan page 24h throughput sparklines ════ */
.wan-spark { display: flex; flex-direction: column; padding: 4px 0; }
/* Side-by-side charts on desktop, stacked under ~720px; each chart
   carries its own 4-tick x-axis so alignment never drifts when the
   grid wraps to one column. */
.wan-spark-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 14px;
}
.wan-spark-chart { width: 100%; min-width: 0; }
.wan-spark-top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 11px;
  padding: 0 0 2px;
}
.wan-spark-label { color: var(--text3); font-weight: 500; }
.wan-spark-peak {
  color: var(--text2);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .02em;
}
.wan-spark-svg {
  width: 100%;
  height: 60px;
  display: block;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--border-subtle);
  border-radius: 4px;
}
.wan-spark-axis {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  font-family: var(--mono);
  font-size: 9px;
  color: var(--text3);
  letter-spacing: .02em;
}
.wan-spark-note {
  font-size: 11px;
  color: var(--text3);
  margin-top: 6px;
  min-height: 1em;
}

/* 200ms opacity pulse on the page-header UPDATED timestamp whenever a
   tick completes. Applied/removed from dashboard.js; force-reflow
   trick makes the animation replay on back-to-back ticks. */
.wan-updated-pulse { animation: wan-updated-pulse .2s ease; }
@keyframes wan-updated-pulse { from { opacity: 0.6; } to { opacity: 1; } }

/* ════ /wan page monitors list — inside each panel's Monitors section.
   CSS grid so the row wraps naturally on narrow viewports; no min-
   widths that could force horizontal overflow. */
.wan-monitors { display: flex; flex-direction: column; gap: 4px; }
.wan-monitor-row {
  display: grid;
  grid-template-columns: 10px minmax(0,1fr) 44px 64px 64px;
  gap: 10px;
  align-items: center;
  font-size: 12px;
  padding: 4px 0;
  font-family: var(--mono);
  min-height: 28px;
}
.wan-monitor-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}
.wan-monitor-target {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--text);
}
.wan-monitor-type {
  color: var(--text3);
  text-transform: uppercase;
  font-size: 10px;
  letter-spacing: .08em;
  font-family: var(--sans);
}
.wan-monitor-lat { text-align: right; }
.wan-monitor-pct { text-align: right; }
@media (max-width: 720px) {
  .wan-monitor-row { grid-template-columns: 10px minmax(0,1fr) 48px 60px; }
  .wan-monitor-pct { display: none; }
}

/* ════ COMMAND CENTER — expandable WAN rows (Uplinks card) ════
   Each WAN row is a collapsed head (4-col grid: name+primary,
   chips, connected+ip, chevron) + a max-height expand panel
   beneath. Only one row is expanded at a time; module-level JS
   state keeps that stable across the 30s polling refresh. Panel
   contents are vendor-aware — Starlink / Peplink / generic — and
   reuse the .sl-section + .sl-kv tokens from the dish detail panel. */
.cc-wan-row { display: flex; flex-direction: column; }
.cc-wan-head {
  display: grid;
  grid-template-columns: minmax(0,1.2fr) minmax(0,2fr) minmax(0,1fr) auto;
  gap: 14px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border-subtle);
  align-items: center;
  cursor: pointer;
  user-select: none;
  transition: background .12s;
}
.cc-wan-head:hover { background: var(--row-hover-bg); }
.cc-wan-head:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.cc-wan-head-left { min-width: 0; }
.cc-wan-head-chips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.cc-wan-head-right { text-align: right; min-width: 0; }
.cc-wan-chev {
  color: var(--text3);
  font-size: 14px;
  line-height: 1;
  padding: 0 4px;
  transition: transform .2s ease;
}
.cc-wan-row.open > .cc-wan-head > .cc-wan-chev { transform: rotate(180deg); }
.cc-wan-panel {
  overflow: hidden;
  max-height: 0;
  transition: max-height .2s ease;
}
.cc-wan-row.open > .cc-wan-panel { max-height: 1600px; } /* generous cap */
.cc-wan-panel-inner { padding: 4px 2px 12px; }

.cc-wan-monitors { display: flex; flex-direction: column; gap: 4px; }
.cc-wan-monitor-row {
  display: grid;
  grid-template-columns: 16px minmax(0,1fr) 44px 64px 56px;
  gap: 10px;
  align-items: center;
  font-size: 12px;
  padding: 3px 0;
  font-family: var(--mono);
}
.cc-wan-monitor-target { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
.cc-wan-monitor-type   { color: var(--text3); text-transform: uppercase; font-size: 10px; letter-spacing: .08em; }
.cc-wan-monitor-lat    { color: var(--text2); text-align: right; }
.cc-wan-monitor-pct    { color: var(--text2); text-align: right; }

/* ════ STARLINK DISH — expandable detail panel ════
   Collapsed: one summary row with state badge + uptime + firing-alert
   pills + chevron. Click the summary (or press Enter/Space) to expand
   the five-section inline panel below. State is preserved in module
   JS so the 30s polling refresh doesn't snap the panel closed. */
.sl-summary {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 4px;
  cursor: pointer;
  user-select: none;
  border-radius: 4px;
  transition: background .12s;
}
.sl-summary:hover { background: var(--row-hover-bg); }
.sl-summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sl-badge-ok    { color: #16a34a; font-weight: 600; font-size: 13px; white-space: nowrap; }
.sl-badge-warn  { color: var(--amber); font-weight: 600; font-size: 13px; white-space: nowrap; }
.sl-badge-error { color: #dc2626; font-weight: 600; font-size: 13px; white-space: nowrap; }
.sl-uptime {
  font-size: 12px;
  color: var(--text2);
  font-family: var(--mono);
  white-space: nowrap;
}
.sl-alert-chips { display: flex; flex-wrap: wrap; gap: 4px; flex: 1; min-width: 0; }
.sl-alert-pill {
  display: inline-block;
  font-size: 11px;
  padding: 2px 8px;
  background: rgba(217,119,6,.12);
  border: 1px solid rgba(217,119,6,.35);
  color: var(--amber);
  border-radius: 4px;
  white-space: nowrap;
}
.sl-chev {
  margin-left: auto;
  color: var(--text3);
  font-size: 14px;
  line-height: 1;
  transition: transform .2s ease;
}
.sl-chev.open { transform: rotate(180deg); }

.sl-panel {
  overflow: hidden;
  max-height: 0;
  transition: max-height .2s ease;
}
.sl-panel.open { max-height: 1200px; }  /* generous cap, exceeds all 5 sections */

.sl-section {
  padding: 10px 2px;
  border-top: 1px solid var(--border-subtle);
}
.sl-section-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .08em;
  color: var(--text3);
  font-weight: 600;
  margin-bottom: 8px;
}
.sl-kv {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 3px 0;
  font-size: 12px;
  gap: 12px;
}
.sl-kv-k { color: var(--text3); }
.sl-kv-v {
  font-family: var(--mono);
  color: var(--text);
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

/* ── Drawer-detail (.dd-*) — compact Meraki-style layout ──────
   Stat-strip hero + responsive grid of detail cards. Replaces the
   old stacked .sl-section blocks which created tons of empty
   horizontal space and tall narrow forms.
   Used by: LAN row drawer, WAN row drawers (Peplink/Starlink/
   generic ethernet), and any future per-row detail panel. */
.dd-hero {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 0;
  padding: 10px 4px 14px;
  border-bottom: 1px solid var(--border-subtle);
  margin-bottom: 10px;
}
.dd-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 4px 12px;
  position: relative;
  min-width: 0;
}
.dd-stat + .dd-stat::before {
  content: '';
  position: absolute;
  left: 0; top: 18%; bottom: 18%;
  width: 1px;
  background: var(--border-subtle);
}
.dd-stat-value {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.15;
  font-family: var(--mono);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}
.dd-stat-label {
  font-size: 10px;
  font-weight: 500;
  color: var(--text3);
  text-transform: uppercase;
  letter-spacing: .06em;
  line-height: 1.2;
}
.dd-stat.tone-ok    .dd-stat-value { color: #86efac; }
.dd-stat.tone-warn  .dd-stat-value { color: #fcd34d; }
.dd-stat.tone-err   .dd-stat-value { color: #fca5a5; }
.dd-stat.tone-muted .dd-stat-value { color: var(--text3); }

.dd-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 10px;
  padding: 0 4px 10px;
}
/* Wide variant — a single .dd-card that needs to span the full
   grid row (e.g. Starlink alerts which read better full-width
   than crammed into a 230px column). */
.dd-card-wide { grid-column: 1 / -1; }
.dd-card {
  background: rgba(255,255,255,0.015);
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  padding: 10px 12px;
  min-width: 0;
}
.dd-card-title {
  font-size: 10px;
  font-weight: 600;
  color: var(--accent-bright);
  text-transform: uppercase;
  letter-spacing: .08em;
  margin-bottom: 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--border-subtle);
}
.dd-kv {
  display: grid;
  grid-template-columns: 88px 1fr;
  gap: 8px;
  align-items: baseline;
  padding: 2px 0;
  font-size: 12px;
  min-width: 0;
}
.dd-kv-k {
  color: var(--text3);
  font-size: 11px;
  text-transform: lowercase;
}
.dd-kv-v {
  font-family: var(--mono);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}

.sl-alert-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 20px;
}
@media (max-width: 600px) {
  .sl-alert-grid { grid-template-columns: 1fr; }
}
.sl-alert-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  padding: 3px 0;
}
.sl-alert-ok   { color: var(--text2); }
.sl-alert-ok   .sl-alert-icon { color: #16a34a; }
.sl-alert-warn { color: var(--amber); font-weight: 600; }
.sl-alert-warn .sl-alert-icon { color: var(--amber); }


/* Legacy USERS VIEW styles removed 2026-05-29. views/users.js is
   lazy-loaded and injects all 103 .users-* class rules via its
   _injectStyles(). The 82 duplicates here were stale fallbacks from
   an era when users.js had no styles — they fought the view-module
   rules and caused several visible regressions:
     - .users-grid { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) }
       packed the 3 section cards into 3 columns; tables overflowed
       across adjacent cards. User saw this as "overlapping".
     - .users-id-row { flex-direction: column } forced the identity
       row to stack vertically instead of avatar-+-name side-by-side.
     - .users-team-table { border + border-radius + overflow:hidden }
       drew a double-border inside the card.
   Authoritative styles now live exclusively in views/users.js. */


/* Legacy ALERTS + AUDIT view styles removed 2026-05-29. Both views are
   lazy-loaded modules (views/alerts.js + views/audit.js) that inject
   their own authoritative styles via _injectStyles(). The duplicates
   here were stale from a pre-lazy-load era and were fighting the
   view-module rules — e.g. dashboard.css set
   .audit-row { display:grid; grid-template-columns:140px 1fr 160px 1fr 24px }
   which forced row-heads into a 140px first column even after audit.js
   set display:flex; flex-wrap:wrap. Result: the "huge cards / doesn't
   work design wise" the user kept reporting on /audit. Authoritative
   styles now live exclusively in their view modules. */


/* ── LAN view: connector-misconfigured empty state ─────────────────
   Cisco-style error card the operator sees when the UniFi Site Manager
   hostId for this site isn't visible to the active API key. Big icon,
   plain-English diagnosis, primary CTA button to fix it. Replaces the
   prior wall-of-text ordered list. */
.lan-conn-err {
  margin: 4px 0;
  padding: 22px 22px 20px;
  border: 1px solid rgba(220,38,38,0.32);
  background: linear-gradient(180deg, rgba(220,38,38,0.06) 0%, rgba(220,38,38,0.02) 100%);
  border-radius: 10px;
  display: flex; flex-direction: column; gap: 14px;
  font-family: var(--sans);
}
.lan-conn-err-hdr { display: flex; align-items: flex-start; gap: 14px; }
.lan-conn-err-icon {
  width: 40px; height: 40px;
  border-radius: 50%;
  background: rgba(220,38,38,0.12);
  color: #f87171;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  border: 1px solid rgba(220,38,38,0.32);
}
.lan-conn-err-text { flex: 1; min-width: 0; }
.lan-conn-err-title { font-size: 14.5px; font-weight: 600; color: #fca5a5; margin-bottom: 4px; letter-spacing: -0.01em; }
.lan-conn-err-msg { font-size: 12.5px; color: var(--text2); line-height: 1.55; }
.lan-conn-err-meta {
  display: flex; flex-wrap: wrap; gap: 18px;
  padding: 10px 14px;
  background: rgba(0,0,0,0.20);
  border-radius: 6px;
  font-size: 11px; color: var(--text3);
  font-family: var(--mono);
}
.lan-conn-err-meta code {
  background: rgba(255,255,255,0.04);
  padding: 1px 6px;
  border-radius: 3px;
  color: var(--text2);
}
.lan-conn-err-k { color: var(--text3); margin-right: 6px; text-transform: uppercase; letter-spacing: 0.05em; font-family: var(--sans); font-size: 10px; font-weight: 600; }
.lan-conn-err-actions { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.lan-conn-err-cta {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 14px;
  background: var(--accent);
  color: #0a0a0a;
  border: 1px solid var(--accent);
  border-radius: 6px;
  font-size: 12px; font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition: background .12s, transform .08s, box-shadow .12s;
}
.lan-conn-err-cta:hover {
  background: var(--accent-bright);
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(47, 107, 255,0.32);
}
.lan-conn-err-cta:active { transform: translateY(0); }
.lan-conn-err-cta svg { color: #0a0a0a; }
.lan-conn-err-link {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 6px 10px;
  background: transparent;
  color: var(--text2);
  border: 1px solid var(--border-light);
  border-radius: 6px;
  font-size: 12px; font-weight: 500;
  text-decoration: none;
  cursor: pointer;
  font-family: var(--sans);
  transition: background .12s, color .12s, border-color .12s;
}
.lan-conn-err-link:hover {
  background: rgba(47, 107, 255,0.06);
  color: var(--text);
  border-color: var(--accent-border);
}
.lan-conn-err-hint {
  font-size: 11.5px; color: var(--text3); line-height: 1.55;
  padding-top: 10px;
  border-top: 1px solid rgba(220,38,38,0.14);
}
.lan-conn-err-hint code {
  background: rgba(255,255,255,0.05);
  padding: 1px 6px;
  border-radius: 3px;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--accent-bright);
}

/* ── Scope pickers (owner-corrected IA 2026-06-07) ─────────────────
   Org + site are compact picker dropdowns at the top of the sidebar —
   "find a customer and their site" — separated from the functional
   nav sections below by a hairline. */
.sb-scope-pickers{padding:6px 0 8px;margin-bottom:6px;border-bottom:1px solid var(--gc-divider,rgba(255,255,255,0.06));}
/* Org-scoped views: the SITE picker doesn't apply. The first cut
   dimmed to 0.42 — operators read it as a broken/grey glitch
   ("why does the site label change to dark grey?"). Softer dim +
   an explicit inline note so the state explains itself without
   needing the hover tooltip. */
.nav-item-drop.scope-inactive{opacity:0.72;}
.nav-item-drop.scope-inactive .ni-label{text-decoration:none;}
.nav-item-drop .ni-scope-note{display:none;}
.nav-item-drop.scope-inactive .ni-scope-note{display:inline;margin-left:6px;font-size:9px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--gc-text-dim,#8A94A6);border:1px solid var(--gc-divider,rgba(255,255,255,0.08));border-radius:3px;padding:1px 4px;vertical-align:1px;}

/* Topology device-type chip — answers "what IS this card" at a glance
   (owner request). Quiet uppercase tag in the card's bottom row. */
.topo-node-kind{display:inline-flex;align-items:center;padding:1px 6px;border:1px solid var(--gc-divider,rgba(255,255,255,0.08));border-radius:4px;font-size:9px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--gc-text-dim,#8A94A6);background:rgba(255,255,255,0.03);}

/* Topology device-icon wraps render an SVG from DEVICE_VISUALS; size it
   to read crisply in the 24px slot. The wrap's color/background/border
   are set inline per device-family hue. */
.topo-node-icon-wrap svg{width:15px;height:15px;}

/* Topology device-type LEGEND — discoverable key behind the "Key"
   affordance in the controls row. Color = device family; status stays
   on the card rail/pill (DESIGN.md: status is never color alone). */
.topo-legend-btn{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);border-radius:6px;color:var(--gc-text-muted,#B6C0D2);font-size:11px;font-weight:600;letter-spacing:.04em;cursor:pointer;transition:background .15s,border-color .15s;}
.topo-legend-btn:hover{background:rgba(255,255,255,0.07);border-color:rgba(255,255,255,0.14);color:var(--gc-text-strong,#E8EEF8);}
.topo-legend-btn[aria-expanded="true"]{background:rgba(47,107,255,0.12);border-color:rgba(47,107,255,0.4);color:var(--gc-accent-bright,#5B9BFF);}
.topo-legend{margin-bottom:14px;background:var(--gc-surface-card,#121A2A);border:1px solid var(--gc-divider,rgba(255,255,255,0.08));border-radius:10px;padding:14px 16px;animation:gc-fade-in .18s ease;}
.topo-legend-note{font-size:11px;color:var(--gc-text-dim,#8A94A6);margin-bottom:12px;line-height:1.4;}
.topo-legend-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px 18px;}
.topo-legend-item{display:flex;align-items:center;gap:10px;min-width:0;}
.topo-legend-icon{flex:0 0 auto;width:30px;height:30px;border-radius:7px;display:flex;align-items:center;justify-content:center;border:1px solid transparent;}
.topo-legend-icon svg{width:17px;height:17px;}
.topo-legend-text{display:flex;flex-direction:column;min-width:0;}
.topo-legend-label{font-size:12px;font-weight:600;color:var(--gc-text-strong,#E8EEF8);line-height:1.25;}
.topo-legend-sub{font-size:10.5px;color:var(--gc-text-dim,#8A94A6);line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}

/* Node-stack tier icon slots render a DEVICE_VISUALS SVG; the inline
   per-tier color/background sets the device-family hue. */
.node-stack-icon svg{width:22px;height:22px;}
