/* ── Reset & Base ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

/* ── Global Checkbox ──────────────────────────────────────── */
input[type="checkbox"] {
  -webkit-appearance: none; appearance: none;
  width: 14px; height: 14px;
  border: 1px solid var(--border); border-radius: 3px;
  background: var(--surface2); cursor: pointer;
  position: relative; flex-shrink: 0;
}
input[type="checkbox"]:checked {
  background: var(--accent); border-color: var(--accent);
}
input[type="checkbox"]:checked::after {
  content: ''; position: absolute;
  left: 4px; top: 1px; width: 4px; height: 8px;
  border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
input[type="checkbox"]:disabled {
  opacity: 0.4; cursor: not-allowed;
}

/* Touch devices: prevent :hover from sticking after tap.
   iOS keeps :hover active until the next tap elsewhere, making buttons
   look permanently highlighted. This single block covers all interactive
   controls — add new selectors here instead of individual @media wrappers. */
@media (hover: none) {
  .dropdown-btn:not(.open):not(.active):hover { border-color: var(--border); }
  .refresh-btn:hover     { background: var(--surface2); border-color: var(--border); color: var(--text); }
  .clear-filters-btn:hover { border-color: var(--border); color: var(--text-muted); background: transparent; }
  .btn-primary:hover     { background: var(--accent); border-color: var(--accent); }
  .btn-secondary:hover   { border-color: var(--border); color: var(--text); }
  .btn-ghost:hover       { color: var(--text-muted); border-color: transparent; }
}

/* ── Custom Scrollbars ───────────────────────────────────────
   Thin, translucent scrollbars that blend with the dark theme.
   Uses both the standard `scrollbar-*` properties (Firefox) and
   WebKit pseudo-elements (Chrome, Edge, Safari) for full coverage. */

/* Standards-based (Firefox 64+) */
* {
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--text-muted-rgb), .35) transparent;
  /* --text-muted at 35% opacity for the thumb, transparent track */
}

/* WebKit / Blink (Chrome, Edge, Safari) */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: rgba(var(--text-muted-rgb), .25);
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
  background: rgba(var(--text-muted-rgb), .45);
  border: 2px solid transparent;
  background-clip: padding-box;
}
/* Hide scrollbar corner (where horizontal and vertical scrollbars meet) */
::-webkit-scrollbar-corner {
  background: transparent;
}

:root {
  --bg: #0d1117;
  --surface: #161b22;
  --surface2: #1f2937;
  --border: #30363d;
  --text: #e6edf3;
  --text-muted: #8b949e;
  --online: #3fb950;
  --online-rgb: 63, 185, 80;
  --offline: #f85149;
  --offline-rgb: 248, 81, 73;
  --warn: #d29922;
  --warn-rgb: 210, 153, 34;
  --accent: #00AEEF;
  --accent-rgb: 0, 174, 239;
  --border-rgb: 48, 54, 61;
  --text-muted-rgb: 139, 148, 158;
  --purple: #a855f7;
  --dev-banner: #7c5cff; /* dev-environment header strip — see .env-banner / header.js _renderEnvBanner */
  --accent-hover: #79b8ff;
  /* Site accent colors — used by SITE_COLORS in app.js for org badges/pills */
  --site-1: #58a6ff;
  --site-2: #3fb950;
  --site-3: #d29922;
  --site-4: #f78166;
  --site-5: #bc8cff;
  --site-6: #ff7b72;
  --radius: 8px;
  --shadow: 0 4px 16px rgba(0,0,0,.4);
  --control-h: 36px;
  /* Account/Portal content column width — single source of truth so the banner,
     summary badges, toolbar and device table all share the SAME max-width and
     stay edge-aligned. Used by .tenant-header/.tenant-summary/.tenant-controls/
     .tenant-grid/.tenant-list (was hardcoded 1180px in 4 places; pre-commit #5). */
  --tenant-width: 1180px;
  --summary-sticky-h: 0px;
  --controls-sticky-h: 0px;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;       /* fallback for browsers without dvh support */
  min-height: 100dvh;      /* iOS Safari: tracks actual visible viewport (URL bar aware) */
  display: flex;
  flex-direction: column;
  font-size: 14px;
  line-height: 1.5;
}

/* ── Header ───────────────────────────────────────────────── */
header {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 1000;
}

.header-inner {
  max-width: 1400px;
  margin: 0 auto;
  padding: 0 24px;
  height: 56px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
}

/* ── Dev-environment banner ───────────────────────────────────
   Full-bleed "DEVELOPMENT" strip pinned at the top of the (sticky)
   header so it's unmissable you're on the dev site — never on
   production. Rendered by _renderEnvBanner() in header.js only when
   health.buildBranch === 'dev'. The inner wrapper mirrors .header-inner
   so the label lines up with the logo while the colour goes edge to edge. */
.env-banner { background: var(--dev-banner); }
.env-banner-inner {
  max-width: 1400px;
  margin: 0 auto;
  padding: 5px 24px;
  display: flex;
  align-items: center;
  gap: 8px;
  color: #fff; /* white-on-colour is the allowed literal */
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.env-banner-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fff;
  flex-shrink: 0;
}
@media (max-width: 480px) {
  .env-banner-inner { padding-left: 12px; padding-right: 12px; }
}

/* Modifier for reusing .env-banner INSIDE the login card (public/login.html
   #login-env-banner) instead of full-bleed under the sticky header. Mirrors
   the header treatment: full width, flush with the top edge, no side/top
   inset. .login-card has padding:32px and overflow:hidden, so negative
   margins pull the banner out to the card's actual edges (align-self:stretch
   alone only reaches the PADDED width, not the card's outer edge) — the
   card's own overflow:hidden then clips it to the card's rounded top
   corners, same as .test-banner's merge-with-card treatment above. */
.env-banner-card {
  align-self: stretch;
  margin: -32px -32px 0 -32px;
  border-radius: 0;
}
.env-banner-card .env-banner-inner {
  max-width: none;
  margin: 0;
  padding: 6px 24px;
  justify-content: center;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 16px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none; /* brand is now an <a> tag — remove browser default underline */
}

.logo-img {
  width: 32px;
  height: 32px;
  object-fit: contain;
  flex-shrink: 0;
}

.logo-monogram {
  width: 30px;
  height: 30px;
  background: #8dc63f;
  color: #fff;
  border-radius: 6px;
  font-size: 11px;
  font-weight: 800;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  letter-spacing: -.5px;
}

.header-meta {
  display: flex;
  align-items: center;
  gap: 10px;
}

.meta-pill {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 3px 10px;
  font-size: 12px;
  color: var(--text-muted);
  white-space: nowrap;
}

/* ── Security indicator icons in header ────────────────────────
   3 independent Feather icons (lock, shield, globe).
   Each toggles independently: sec-on (green) or sec-off (grey).
   Icons are wrapped in a <button> inside .security-icons-wrap so
   clicking opens the .sec-summary-panel dropdown below. */
.security-icons-wrap { position: relative; }
/* Override default button reset — display the icons group as a pill button */
button.security-icons {
  background: none; border: none; cursor: pointer;
  padding: 4px 6px; border-radius: var(--radius);
  transition: background .15s;
}
button.security-icons:hover { background: var(--surface2); }
.security-icons {
  display: inline-flex; align-items: center; gap: 6px;
}
.sec-icon {
  width: 16px; height: 16px; flex-shrink: 0;
  transition: color .3s, opacity .3s;
}
.sec-on  { color: var(--online); opacity: 1; }
.sec-off { color: var(--text-muted); opacity: .4; }

/* Role-gated header status controls (Viewers): the Security icons and the
   status dot stay visible as indicators, but the click-to-expand detail
   dropdowns are disabled, so drop the clickable cursor/hover. Two-class
   selectors keep these above the base .dot-clickable / button.security-icons
   rules regardless of source order. Applied via JS (header.js
   _applyHeaderDropdownGate) once the Entra role resolves to 'viewer'. */
.security-icons.header-status-gated,
.dot-clickable.header-status-gated { cursor: default; }
button.security-icons.header-status-gated:hover { background: none; }

/* ── Security summary panel ──────────────────────────────────
   Dropdown below the security icons showing HTTPS / Auth / IP
   Allowlist status. Toggled by toggleSecurityPanel() in header.js.
   Row values reuse sec-on / sec-off classes for colour — no extra
   colour classes needed. Wider panel with dividers for cleaner look. */
.sec-summary-panel {
  position: absolute; right: 0; top: calc(100% + 6px);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  min-width: 220px; z-index: 1001; padding: 0;
}
.sec-summary-title {
  font-size: 10px; font-weight: 600; letter-spacing: .8px;
  color: var(--text-muted); padding: 10px 16px 8px;
  text-transform: uppercase;
  border-bottom: 1px solid var(--border);
}
.sec-summary-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 16px; font-size: 13px;
}
/* Subtle divider between rows — sits between items, not after last */
.sec-summary-row + .sec-summary-row {
  border-top: 1px solid rgba(var(--border-rgb), .5);
}
.sec-summary-row svg { flex-shrink: 0; }
.sec-summary-label { color: var(--text-muted); flex: 1; }
.sec-summary-val { text-align: right; white-space: nowrap; font-weight: 500; }
/* Footer status summary — green/yellow/red dot + text below divider */
.sec-summary-footer {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 16px 10px; font-size: 11px; color: var(--text-muted);
  border-top: 1px solid var(--border);
}
.sec-summary-dot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
.sec-summary-dot.sec-all-on  { background: var(--online); }
.sec-summary-dot.sec-partial { background: var(--warn); }
.sec-summary-dot.sec-all-off { background: var(--offline); }

/* ── Status summary panel ────────────────────────────────────
   Dropdown below the aggregated status dot. Same shape and layout
   conventions as .sec-summary-panel — Railway + Supply Chain + each
   vendor platform get one row, color-coded by severity. Toggled by
   toggleStatusPanel() in header.js. */
.status-icons-wrap { position: relative; display: inline-flex; align-items: center; }
.status-summary-panel {
  position: absolute; right: 0; top: calc(100% + 6px);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  min-width: 260px; z-index: 1001; padding: 0;
}
.status-summary-title {
  font-size: 10px; font-weight: 600; letter-spacing: .8px;
  color: var(--text-muted); padding: 10px 16px 8px;
  text-transform: uppercase;
  border-bottom: 1px solid var(--border);
}
.status-summary-row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 16px; font-size: 13px;
}
.status-summary-row + .status-summary-row {
  border-top: 1px solid rgba(var(--border-rgb), .5);
}
.status-summary-row svg { flex-shrink: 0; }
/* nowrap + ellipsis on the label so a long source name (e.g. "Webex Calling")
   doesn't word-wrap to multiple lines when the value side is wide. Combined
   with the panel's auto-grow (no max-width) this keeps every row on a single
   line and grows the panel just enough to fit the longest source. */
.status-summary-label {
  color: var(--text-muted); flex: 1;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.status-summary-val {
  text-align: right; white-space: nowrap; font-weight: 500;
  display: inline-flex; align-items: center; gap: 8px;
}
/* Section header (App Status / Platform Status Page / Integration Status).
   Smaller, muted text — visually distinct from the row labels but tied to
   them by indentation. Bordered top so the section break reads as a
   divider rather than a heading sitting in space. */
.status-summary-section {
  font-size: 10px; font-weight: 600; letter-spacing: .8px;
  color: var(--text-muted); padding: 10px 16px 6px;
  text-transform: uppercase;
  border-top: 1px solid var(--border);
}
.status-summary-section:first-child { border-top: 0; }
/* Clickable rows (every row with a `page` link). Cursor + subtle hover
   highlight tells the user the row is navigable. The whole row is the
   click target — no separate button needed. */
.status-summary-row-clickable { cursor: pointer; }
.status-summary-row-clickable:hover { background: var(--surface2); }
/* Per-row state coloring — mirrors dot color tiers (on/warn/off/muted). */
.status-summary-val.status-on    { color: var(--online); }
.status-summary-val.status-warn  { color: var(--warn); }
.status-summary-val.status-off   { color: var(--offline); }
.status-summary-val.status-muted { color: var(--text-muted); }
.status-row-dot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
.status-row-dot.status-on    { background: var(--online); }
.status-row-dot.status-warn  { background: var(--warn); }
.status-row-dot.status-off   { background: var(--offline); }
.status-row-dot.status-muted { background: var(--text-muted); }
/* Footer — aggregate count + optional "View →" link to a vendor page or settings. */
.status-summary-footer {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 16px 10px; font-size: 11px; color: var(--text-muted);
  border-top: 1px solid var(--border);
}
.status-summary-footer-text { flex: 1; }
.status-summary-link {
  color: var(--accent); text-decoration: none; font-weight: 500;
  cursor: pointer; background: none; border: none; padding: 0;
  font-size: 11px;
}
.status-summary-link:hover { text-decoration: underline; }
.status-summary-dot {
  width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
}
.status-summary-dot.status-on    { background: var(--online); }
.status-summary-dot.status-warn  { background: var(--warn); }
.status-summary-dot.status-off   { background: var(--offline); }
.status-summary-dot.status-muted { background: var(--text-muted); }

/* ── Header dropdown menu ─────────────────────────────────────
   Always-visible dropdown in the header. Contains account details (when
   OIDC is active), navigation links, server actions, and Sign Out. */
.header-dropdown { position: relative; }
.header-dropdown-btn {
  cursor: pointer; border: 1px solid var(--border); background: var(--surface2);
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 10px; border-radius: 20px;
  font-size: 12px; color: var(--text-muted);
}
.header-dropdown-btn:hover { background: var(--border); color: var(--text); }
.header-dropdown-panel {
  position: absolute; right: 0; top: calc(100% + 6px);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  min-width: 220px; z-index: 1000; padding: 4px 0;
}
/* Account details section — shown when OIDC user is signed in.
   Two columns: avatar on the left, text stack (name/email/IDP/role) on the right. */
.header-dropdown-account {
  padding: 10px 14px; display: flex; flex-direction: row; align-items: center; gap: 12px;
}
.header-account-text {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
}
.header-account-name {
  font-size: 13px; font-weight: 600; color: var(--text);
}
.header-account-detail {
  font-size: 11px; color: var(--text-muted);
}
/* IDP line as an anchor → myaccount.microsoft.com. Inherits the muted
   detail color until hover, where it lifts to the accent so it reads
   as a link. External-link icon sits inline to the right of the label. */
.header-account-link {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--text-muted); text-decoration: none;
  border-bottom: 1px dotted rgba(var(--text-muted-rgb), .5);
  transition: color .15s ease, border-color .15s ease;
}
.header-account-link:hover,
.header-account-link:focus-visible {
  color: var(--accent); border-bottom-color: var(--accent);
}
.header-account-link-icon {
  flex-shrink: 0; opacity: .8;
}
.header-account-link:hover .header-account-link-icon,
.header-account-link:focus-visible .header-account-link-icon {
  opacity: 1;
}
/* ── Profile avatar ──────────────────────────────────────────
   Base class shared by the small button avatar and the large panel avatar.
   Hosts either an <img> (Graph-sourced photo) or an initials <span>. */
.header-avatar {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; overflow: hidden;
  background: var(--accent); color: #fff;
  font-weight: 600; line-height: 1; flex-shrink: 0;
}
.header-avatar-img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
.header-avatar-initials {
  font-family: inherit; letter-spacing: .3px;
}
/* Small avatar in the dropdown button — replaces the kebab icon */
.header-avatar-btn {
  width: 22px; height: 22px; font-size: 10px;
  margin-left: -3px;  /* visually balance against the rounded pill */
}
/* Larger avatar inside the open dropdown panel */
.header-avatar-panel {
  width: 48px; height: 48px; font-size: 18px;
}
/* Role badges inside account details */
.header-role-badge {
  display: inline-block; padding: 1px 8px; border-radius: 10px;
  font-size: 10px; font-weight: 600; letter-spacing: .3px;
  margin-top: 2px;
}
.header-role-admin {
  background: rgba(var(--accent-rgb), .15); color: var(--accent);
}
.header-role-editor {
  background: rgba(var(--online-rgb), .15); color: var(--online);
}
.header-role-viewer {
  background: rgba(var(--text-muted-rgb), .15); color: var(--text-muted);
}
/* Legacy class kept as alias — old header used .header-dropdown-user */
.header-dropdown-user {
  padding: 8px 14px; font-size: 11px; color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.header-dropdown-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 8px 14px; font-size: 12px; color: var(--text);
  background: none; border: none; cursor: pointer; text-align: left;
}
/* Nav links inside dropdown — same style as items but styled as anchor tags */
a.header-dropdown-nav {
  text-decoration: none; color: var(--text);
}
.header-dropdown-item:hover { background: var(--surface2); }
.header-dropdown-item.header-dropdown-disabled {
  color: var(--text-muted); opacity: 0.5; cursor: not-allowed; pointer-events: none;
}
.header-dropdown-danger { color: var(--offline); }
.header-dropdown-danger:hover { background: rgba(var(--offline-rgb), .1); }
.header-dropdown-divider { border: none; border-top: 1px solid var(--border); margin: 4px 0; }

/* ── Status Dot (single source of truth) ─────────────────────
   One base class, size modifiers, state modifiers.
   Used everywhere: header, dashboard table, badges, pills, panels. */
.dot {
  display: inline-block;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--text-muted);
  flex-shrink: 0;
  vertical-align: middle;
  transition: background .3s;
}
.dot-xs  { width: 5px; height: 5px; }  /* badge pills */
.dot-sm  { width: 7px; height: 7px; }  /* site pills, compact indicators */

/* State modifiers — color, glow, pulse */
/* animation-delay: -999s forces all dots onto the same 2s cycle regardless
   of when they first render — eliminates the "Christmas lights" effect where
   each dot pulses out of phase with the others. */
.dot-on   { background: var(--online);  box-shadow: 0 0 5px rgba(var(--online-rgb),.5); animation: pulse-green 2s infinite; animation-delay: -999s; }
/* Static green — same as dot-on but no pulse. Use for boot-time / snapshot indicators
   where pulsing would wrongly suggest real-time status. */
.dot-on-static { background: var(--online); box-shadow: 0 0 5px rgba(var(--online-rgb),.5); }
.dot-off  { background: var(--offline); opacity: .7; }
.dot-warn { background: var(--warn); }
.dot-muted  { background: var(--text-muted); }
.dot-accent { background: var(--accent); }
/* Inherits color from parent (used inside .api-badge where parent sets color) */
.dot-inherit { background: currentColor; }
.dot-clickable { cursor: pointer; }

/* Cycle animation for the aggregated overall-status dot.
   When two non-green sources (e.g. Railway yellow + Supply Chain red) are
   active simultaneously, JS sets --cycle-c1 and --cycle-c2 on the element
   and applies .dot-cycle.

   Rhythm matches the dashboard's pulse-green dots (Webex/UniFi columns):
   2s per breath, opacity 1 ↔ .4 ease-in-out. Total cycle = 4s (one breath
   per color). Colors crossfade smoothly via continuous background +
   box-shadow color interpolation — no hard swap. The crossfade falls
   during the dim trough between breaths so it reads as a soft swell of
   the new color rather than a cutover. */
.dot-cycle {
  animation: status-cycle-breathe 4s ease-in-out infinite;
}
@keyframes status-cycle-breathe {
  /* Breath 1 — color A, dim → bright → dim */
  0%   { background: var(--cycle-c1, var(--text-muted)); box-shadow: 0 0 5px var(--cycle-c1, transparent); opacity: .4; }
  25%  { background: var(--cycle-c1, var(--text-muted)); box-shadow: 0 0 5px var(--cycle-c1, transparent); opacity: 1; }
  50%  { background: var(--cycle-c1, var(--text-muted)); box-shadow: 0 0 5px var(--cycle-c1, transparent); opacity: .4; }
  /* Breath 2 — color crossfades c1→c2 between 50% and 75% while opacity rises */
  75%  { background: var(--cycle-c2, var(--text-muted)); box-shadow: 0 0 5px var(--cycle-c2, transparent); opacity: 1; }
  100% { background: var(--cycle-c2, var(--text-muted)); box-shadow: 0 0 5px var(--cycle-c2, transparent); opacity: .4; }
  /* 100% → 0% loop crossfades c2→c1 at constant dim opacity — also seamless */
}

/* .status-indicator removed — header Railway dot now uses the unified .dot system
   (dot-on/dot-off/dot-warn/dot-muted) matching the settings page. */

/* ── Main ─────────────────────────────────────────────────── */
main {
  flex: 1;
  max-width: 1400px;
  width: 100%;
  margin: 0 auto;
  padding: 12px 24px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* ── Summary Cards ────────────────────────────────────────── */
/* Smooth compact: JS measures the expanded card height and sets
   --card-expanded-h so CSS can transition between explicit pixel values.
   (height:auto → 36px can't be animated — auto isn't interpolable.) */
.summary-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  position: sticky;
  top: 57px;
  z-index: 50;
  background: var(--bg);
  padding: 0;
  transition: gap .3s ease;
}

.summary-grid.compact {
  gap: 8px;
}

/* ── Laptop breakpoint (≤1024px) ─────────────────────────────── */
@media (max-width: 1024px) {
  main { padding: 12px 16px 24px; }
  .controls { gap: 8px; }
}

/* ── Tablet breakpoint (≤768px) ──────────────────────────────── */
/* Reclaim horizontal space for data-dense views (Activity Log, device table).
   All cards benefit uniformly — no per-component width overrides needed. */
@media (max-width: 768px) {
  main { padding: 12px 8px 24px; }
}


.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 20px 24px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  box-shadow: var(--shadow);
  border-left: 3px solid transparent;
}

/* Summary cards: explicit height from JS variable enables smooth transition.
   overflow:hidden prevents content spilling during height animation.
   Width is also transitioned for the phase-2 slide-left effect —
   JS sets explicit widths (like height) since flex can't be animated. */
.summary-grid .card {
  flex: 1 1 0;
  min-width: 0;
  height: var(--card-expanded-h, auto);
  overflow: hidden;
  transition: height .3s ease, padding .3s ease, gap .3s ease,
              box-shadow .3s ease, width .3s ease, opacity .15s ease;
}

/* Compact: slim horizontal pills. flex-direction:row is instant
   (not animatable) but the smooth height/padding transition masks it. */
.summary-grid.compact .card {
  flex-direction: row;
  align-items: center;
  padding: 0 14px;
  height: 36px;
  gap: 10px;
  box-shadow: none;
}

/* Compact-left: justify-content shifts cards to the left.
   Width animation is handled by JS (explicit px values).
   Card flex override keeps cards content-sized after JS cleanup
   clears inline styles — without this, flex:1 1 0 from the base
   rule snaps cards back to full width. */
.summary-grid.compact-left {
  justify-content: flex-start;
}
.summary-grid.compact-left .card {
  flex: 0 0 auto;
}

.card-label {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text-muted);
  transition: font-size .3s ease;
}

.summary-grid.compact .card-label { font-size: 10px; }

.card-value {
  font-size: 32px;
  font-weight: 700;
  line-height: 1;
  transition: font-size .3s ease;
}

.summary-grid.compact .card-value { font-size: 18px; }

.card-total { border-left-color: var(--accent); }
.card-total .card-value { color: var(--accent); }

.card-online { border-left-color: var(--online); }
.card-online .card-value { color: var(--online); }

.card-offline { border-left-color: var(--offline); }
.card-offline .card-value { color: var(--offline); }

/* ── Controls ─────────────────────────────────────────────── */
.controls {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  position: sticky;
  top: calc(57px + var(--summary-sticky-h));
  z-index: 9;
  background: var(--bg);
}

/* ── Dropdown (site + status filters) ─────────────────────── */
.dropdown {
  position: relative;
}

/* --control-h keeps dropdowns, search, and buttons the same height */
.dropdown-btn {
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--control-h, 36px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 0 12px;
  cursor: pointer;
  font-size: 13px;
  font-family: inherit;
  white-space: nowrap;
  transition: border-color .15s;
  min-width: 130px;
  /* Stop the iOS dark tap-highlight from lingering on adjacent buttons
     (share / export) when a finger drags past them during a column
     reorder gesture. Visible hover/active states still apply via the
     CSS rules below. */
  -webkit-tap-highlight-color: transparent;
}
.dropdown-btn:focus { outline: none; }
.dropdown-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Screen-reader-only text — accessible label for icon-only buttons */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/* Share dropdown btn — compact, no min-width needed (not a filter label) */
#export-dropdown .dropdown-btn { min-width: auto; }

/* Hide filter dropdown when its corresponding column is hidden */
.col-hidden-filter { display: none !important; }

.dropdown-btn:hover  { border-color: var(--accent); }
.dropdown-btn.open   { border-color: var(--accent); background: var(--surface2); }
/* Active filter indicator — accent border shows which filters have a selection */
.dropdown-btn.active { border-color: var(--accent); color: var(--accent); }

.dropdown-chevron {
  width: 12px;
  height: 12px;
  margin-left: auto;
  flex-shrink: 0;
  transition: transform .15s;
}
.dropdown-btn.open .dropdown-chevron { transform: rotate(180deg); }

.dropdown-panel {
  position: fixed;
  z-index: 200;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 8px 24px rgba(0,0,0,.5);
  min-width: 190px;
  max-height: 320px;
  overflow-y: auto;
  /* top/left/width are JS-set on open via _positionDropdownPanel. Default
     off-screen as a safety guard in case the panel is briefly rendered
     before positioning (e.g. flash of mispositioned content). */
  top: -9999px; left: -9999px;
}

/* ── Searchable dropdown ──────────────────────────────────────
   Convention (CLAUDE.md §UI Conventions): any dropdown rendering
   10+ items at runtime MUST use the searchable variant. Type-to-
   filter prevents the panel becoming an unscannable wall.

   Markup:
     <div class="dropdown dropdown-searchable">
       <button class="dropdown-btn">…</button>
       <div class="dropdown-panel hidden">
         <input class="dropdown-search" placeholder="Type to filter…" />
         <div class="dropdown-options-scroll">
           <div class="dropdown-option" data-search="alt text">…</div>
           …
         </div>
         <div class="dropdown-empty hidden">No matches.</div>
       </div>
     </div>

   wireDropdownSearch() in common.js handles the filter logic; it
   auto-fires for every .dropdown-searchable on DOMContentLoaded
   (and re-fires when the panel is opened, so dynamically-injected
   options pick it up). */
/* Searchable dropdowns override the max-height and add flex layout for
   the search input + scrollable options area. All dropdowns use position:
   fixed to avoid being clipped by ancestors with overflow: hidden (like
   .form-card). The positioning is handled per-open by _positionDropdownPanel()
   in common.js using the button's bounding rect. */
.dropdown-searchable .dropdown-panel {
  display: flex;
  flex-direction: column;
  max-height: 360px;
  overflow: hidden;          /* scroll lives on the inner container */
  min-width: 280px;
  padding: 0;
}
.dropdown-search {
  flex: 0 0 auto;
  margin: 8px;
  padding: 8px 10px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 12px;
  outline: none;
}
.dropdown-search:focus { border-color: var(--accent); }
.dropdown-options-scroll {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 0;             /* allow flex child to shrink */
}
.dropdown-empty {
  flex: 0 0 auto;
  padding: 14px 12px;
  font-size: 12px;
  color: var(--text-muted);
  text-align: center;
  font-style: italic;
}

.dropdown-option {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  cursor: pointer;
  font-size: 13px;
  color: var(--text-muted);
  transition: background .1s, color .1s;
  user-select: none;
}

.dropdown-option:hover   { background: var(--surface2); color: var(--text); }
.dropdown-option.selected { color: var(--text); }

.dropdown-check {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  border: 1.5px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  font-size: 11px;
  font-weight: 700;
  transition: background .15s, border-color .15s;
}

.dropdown-option.selected .dropdown-check {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Radio variant (status dropdown) */
.dropdown-check.radio { border-radius: 50%; }
.dropdown-option.selected .dropdown-check.radio {
  background: var(--accent);
  border-color: var(--accent);
}

/* Locked dropdown option — mandatory, can't be toggled off.
   Same .locked convention as .service-check-item.locked.
   Normal text, muted accent checkbox, no pointer interaction. */
.dropdown-option.locked {
  pointer-events: none; cursor: default; color: var(--text);
}
.dropdown-option.locked .dropdown-check {
  background: rgba(var(--accent-rgb),.35); border-color: rgba(var(--accent-rgb),.5); color: #fff;
}

/* Disabled dropdown option — used by the Share menu to show Email/Teams as
   greyed-out (rather than hidden) when the integration isn't configured, so the
   capability is discoverable. Stays in the DOM with a title tooltip explaining
   why; the click handler no-ops on .is-disabled. pointer-events kept on so the
   tooltip shows on hover. No colour literal — just dims the existing styles. */
.dropdown-option.is-disabled {
  opacity: .4;
  cursor: not-allowed;
}
.dropdown-option.is-disabled:hover {
  background: transparent;
  color: var(--text-muted);
}

.dropdown-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 4px 0;
}

/* Non-clickable group heading inside a dropdown panel — e.g. createExportMenu's
   "Download logs as…" / "Copy logs as…" sections. Muted small-caps label; not an
   option, so no hover/pointer. */
.dropdown-section-label {
  padding: 8px 14px 4px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .5px;
  color: var(--text-muted);
  user-select: none;
}

/* Per-row action in a filter dropdown (createFilterDropdown's itemAction) — e.g.
   the edit pencil on the Account picker. Only rendered when a consumer opts in,
   so it has no effect on plain pickers. The label takes the slack and the button
   pins to the trailing edge; the button is a sibling of the check/dot so it never
   overlaps them. */
.dropdown-option-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dropdown-option-action {
  margin-left: auto;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  color: var(--text-muted);
  cursor: pointer;
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.dropdown-option-action:hover {
  background: rgba(var(--accent-rgb), .14);
  border-color: rgba(var(--accent-rgb), .4);
  color: var(--accent);
}
.dropdown-option-action svg { width: 14px; height: 14px; }
/* Account Portal edit pencil — deep-links to admin-only Account Setup (portal.manage).
   The Account Portal page is now portal.read (viewers/editors can open it), so hide the
   pencil for everyone by default and reveal it only when account.js confirms the user
   holds portal.manage (body.portal-can-manage). Fail-closed: no class = no pencil, so a
   non-admin never sees an affordance that lands on a 403. Restores the base
   .dropdown-option-action display (inline-flex) when revealed. */
.tenant-edit-action { display: none; }
body.portal-can-manage .tenant-edit-action { display: inline-flex; }

/* Icon slot in action-menu dropdowns (Share menu) — replaces .dropdown-check */
.dropdown-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.dropdown-site-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

/* ── Drag-handle cursor affordance (generic) ──────────────
   Cursor semantics for ANY drag-to-reorder list, not just the column
   chooser. Rows keep their click affordance (e.g. .dropdown-option's
   `pointer`) — the grab hand lives only on the handle, so hovering a
   list reads as "select", not "drag", even when drag initiation still
   works from the whole row.
   - .drag-handle → the grip element the user grabs: grab / grabbing
   - .drag-live   → container class JS toggles for the duration of a
     drag; forces `grabbing` on everything inside, because mid-drag the
     pointer slides over row bodies and other grips, not just the handle
     it started from. `!important` is deliberate: a short-lived gesture
     state that must beat every static cursor rule (same precedent as
     .col-drag-ghost's opacity/transform overrides below).
   Consumers: createColumnChooser() in common.js — grips get .drag-handle,
   the panel gets .drag-live alongside .col-reordering. Reuse these
   classes in any future reorderable list instead of redefining cursors. */
.drag-handle { cursor: grab; }
.drag-handle:active { cursor: grabbing; }
.drag-live, .drag-live * { cursor: grabbing !important; }

/* ── Column chooser drag reorder ──────────────────────────
   Smooth drag-and-drop with sliding preview:
   - Sibling rows translateY to preview the new layout as you drag
   - Cubic-bezier easing makes the slide feel natural, not mechanical
   - Dragged item fades and stays in-place as a placeholder; the JS-driven
     translateY on sibling rows is the sole "what's moving" signal —
     no extra drop-indicator bar, because the slide already shows position */
.col-drag-item {
  /* No cursor rule — the row body keeps .dropdown-option's `pointer`
     (click toggles visibility). The grab affordance lives on the grip
     via the generic .drag-handle class (section above). */
  transition: transform .18s cubic-bezier(.2,.8,.2,1), background .12s;
  will-change: transform;
  /* PERMANENT iOS suppression — these MUST be baseline-applied, not
     scoped to .col-reordering. iOS Safari starts text selection and
     spins up the magnifier loupe DURING the 250 ms long-press, before
     any JS handler runs. By the time _activateDrag() adds a class,
     the loupe and range-selection are already on screen.
     (CLAUDE.md anti-pattern #30 — suppression scope = cause scope.) */
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}
/* Locked (alwaysOn) rows: body click is a no-op — no pointer affordance.
   The grip keeps .drag-handle's grab; locked columns can still be
   reordered, just not hidden. */
.col-drag-item.locked-vis { cursor: default; }
.col-grip {
  flex-shrink: 0;
  color: var(--text-muted);
  opacity: .55;
  transition: opacity .15s, color .15s;
}
.col-drag-item:hover .col-grip { opacity: 1; color: var(--accent); }
.col-drag-label { flex: 1; min-width: 0; }

/* The item being dragged — fades to a subtle placeholder. */
.col-drag-item.col-dragging {
  opacity: .25;
  background: rgba(var(--accent-rgb), .08);
  transition: opacity .15s, background .15s;
}

/* The floating chip rendered during a pointer-event drag (replaces the
   HTML5 setDragImage ghost — iOS Safari doesn't honour that API, so we
   own the cursor-follower ourselves with position:fixed and update
   top/left in pointermove). Compact: just grip icon + column name,
   anchored at its centre on the pointer. */
.dropdown-panel.col-reordering {
  /* Keep `touch-action: none` here ONLY for the duration of the drag
     so the panel can scroll normally otherwise. The text-selection /
     callout suppression that used to live on this selector moved to
     `.col-drag-item` permanently (see above) — that's the cause-scope
     fix per CLAUDE.md anti-pattern #30. */
  touch-action: none;
}
.col-drag-ghost {
  /* The floater is a clone of the actual row, so we keep its layout
     (display/flex/padding inherited from .col-drag-item / .dropdown-option)
     and only paint the "lifted" visual treatment on top. */
  background: var(--surface2);
  color: var(--text);
  border: 2px solid var(--accent);
  border-radius: var(--radius);
  box-shadow: 0 14px 30px rgba(0,0,0,.6), 0 0 0 1px rgba(var(--accent-rgb), .35);
  opacity: 1 !important;     /* override .col-dragging fade if it slipped onto the clone */
  pointer-events: none;
  z-index: 9999;
  transform: none !important; /* override the slide-preview translateY on the source */
}
.col-drag-ghost .col-grip { color: var(--accent); opacity: 1; }

.col-drag-item.locked-vis .dropdown-check {
  background: rgba(var(--accent-rgb),.35); border-color: rgba(var(--accent-rgb),.5); color: #fff;
}
.col-reset-order { color: var(--text-muted); font-size: 12px; gap: 8px; cursor: pointer; }
.col-reset-order:hover { color: var(--accent); }

.search-group {
  flex: 1;
  min-width: 200px;
}

/* Explicit height matches .dropdown-btn via --control-h */
#search {
  width: 100%;
  height: var(--control-h, 36px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0 12px;
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
  outline: none;
  transition: border-color .15s;
}

#search:focus { border-color: var(--accent); }
#search::placeholder { color: var(--text-muted); }

.refresh-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  height: var(--control-h, 36px);
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 0 16px;
  cursor: pointer;
  font-size: 13px;
  transition: background .15s, border-color .15s;
  white-space: nowrap;
}

.refresh-btn:hover { background: var(--surface); border-color: var(--accent); color: var(--accent); }
.refresh-btn svg { width: 14px; height: 14px; }

.clear-filters-btn {
  display: flex;
  align-items: center;
  gap: 5px;
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text-muted);
  padding: 7px 12px;
  cursor: pointer;
  font-size: 12px;
  white-space: nowrap;
  transition: background .15s, border-color .15s, color .15s;
}
.clear-filters-btn:hover { border-color: var(--offline); color: var(--offline); background: var(--surface2); }
.clear-filters-btn svg { flex-shrink: 0; }
.refresh-btn.spinning svg { animation: spin .6s linear infinite; }
.refresh-btn.spinning { pointer-events: none; opacity: 0.7; }

.refresh-countdown {
  font-size: 11px;
  color: var(--text-muted);
  background: var(--bg);
  border-radius: 10px;
  padding: 1px 7px;
  font-variant-numeric: tabular-nums;
  min-width: 32px;
  text-align: center;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* ── Banners ──────────────────────────────────────────────── */
.error-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  border-radius: var(--radius);
  padding: 12px 16px;
  font-size: 13px;
  font-weight: 500;
  background: rgba(var(--offline-rgb),.12);
  border: 1px solid rgba(var(--offline-rgb),.4);
  color: var(--offline);
}

.error-banner svg { width: 18px; height: 18px; flex-shrink: 0; }

/* Login column — vertical stack for test banner + login card */
.login-col {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: 340px;
  max-width: 100%;
}

/* Test mode banner — badge-style card above the login card.
   Sits outside .login-card so lockout/deploy overlays (position:absolute
   inside the card with overflow:hidden) don't cover it. */
.test-banner {
  display: flex;
  flex-direction: column;
  gap: 10px;
  border-radius: var(--radius) var(--radius) 0 0;
  padding: 14px 16px;
  font-size: 13px;
  font-weight: 500;
  background: rgba(var(--warn-rgb),.12);
  border: 1px solid rgba(var(--warn-rgb),.4);
  border-bottom: none;
  color: var(--warn);
}
/* When test banner is visible, login card loses top radius to merge visually */
.test-banner:not(.hidden) + .login-card {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  border-top-color: rgba(var(--warn-rgb),.4);
}
.test-banner-header {
  display: flex;
  align-items: center;
  gap: 8px;
}
.test-banner-header svg { flex-shrink: 0; }
.test-banner-title { font-weight: 700; font-size: 14px; }
.test-banner-actions {
  display: flex;
  gap: 8px;
}
.test-banner-btn {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 8px 12px;
  font-size: 13px;
  font-weight: 600;
  border-radius: 6px;
  border: 1px solid rgba(var(--warn-rgb),.4);
  background: rgba(var(--warn-rgb),.15);
  color: var(--warn);
  cursor: pointer;
}
.test-banner-btn svg { flex-shrink: 0; }
.test-banner-btn:hover { background: rgba(var(--warn-rgb),.3); }
.test-banner-btn-lock {
  color: var(--offline);
  border-color: rgba(var(--offline-rgb),.4);
  background: rgba(var(--offline-rgb),.15);
}
.test-banner-btn-lock:hover { background: rgba(var(--offline-rgb),.3); }
.test-banner-btn-refresh {
  color: var(--accent);
  border-color: rgba(var(--accent-rgb),.4);
  background: rgba(var(--accent-rgb),.15);
}
.test-banner-btn-refresh:hover { background: rgba(var(--accent-rgb),.3); }
.test-banner-link {
  text-align: right;
  white-space: nowrap;
  color: var(--accent);
  text-decoration: none;
  font-weight: 600;
  font-size: 12px;
}
.test-banner-link:hover { text-decoration: underline; }

.hidden { display: none !important; }

/* Card panel crossfade — config ↔ health view transitions.
   Used by showSetup()/hideSetup() when animate=true. Pure opacity
   fade — no height collapse — for a smooth dissolve between panels.
   Duration (400ms) must match the setTimeout delay in hideSetup()/
   showSetup() in settings.html. */
.card-panel-exit {
  opacity: 0;
  transition: opacity 400ms ease;
}
.card-panel-enter {
  animation: cardFadeIn 400ms ease forwards;
}
@keyframes cardFadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Config panel auto-close countdown bar — thin accent line that shrinks over
   SETUP_IDLE_MS (10s). Sits at the bottom of the setup section. Resets on
   any user interaction (focus, click, keydown). CSS transition on transform
   for GPU-accelerated animation — zero JS animation frames. */
.setup-countdown {
  height: 2px;
  background: var(--accent);
  opacity: 0.4;
  margin-top: 8px;
  transform-origin: left;
  /* transition set by JS when starting the countdown */
}
.setup-countdown.running {
  transform: scaleX(0);
}

/* ── Table ────────────────────────────────────────────────── */
.table-section {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.table-wrapper {
  position: relative;   /* create stacking context so thead z-index stays contained */
  z-index: 1;           /* below .controls (z-index: 9) and header (z-index: 1000) */
  overflow-x: auto;
  overflow-y: auto;
  /* STICKY CASCADE DESIGN (do not change without testing mobile):
     1. header sticks at top:0 (z-index:1000)
     2. .summary-grid sticks at top:57px (z-index:50)
     3. .controls sticks below summary (z-index:9)
     4. .table-wrapper sits below controls (z-index:1 — stacking context)
     5. thead th sticks at top:0 WITHIN this scroll container (z-index:10, contained)

     max-height makes the table scroll internally so the thead stays visible.
     On desktop: 70vh is enough since header+summary+controls fit easily.
     On mobile: calculated in @media to exactly fill remaining viewport,
     preventing page-level scroll that would push thead behind controls.
     See also: layout.js ResizeObserver for --summary-sticky-h, --controls-sticky-h. */
  max-height: 70vh;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
}

thead tr {
  background: var(--surface2);
  border-bottom: 1px solid var(--border);
}

th {
  padding: 10px 14px;
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text-muted);
  white-space: nowrap;
  user-select: none;
  position: sticky;
  top: 0;
  /* Sticky within the table-wrapper scroll container.
     Since .table-wrapper has overflow-y: auto, the header sticks to the top of that
     scrolling region, allowing the table to scroll independently while the header
     stays visible and the controls/summary remain visible above. */
  z-index: 10;
  background: var(--surface2);
}

th.sortable { cursor: pointer; }
th.sortable:hover { color: var(--text); }
th.sort-asc::after { content: ' ↑'; color: var(--accent); }
th.sort-desc::after { content: ' ↓'; color: var(--accent); }

tbody tr {
  border-bottom: 1px solid var(--border);
  transition: background .1s;
}

tbody tr:last-child { border-bottom: none; }
tbody tr:hover { background: var(--surface2); }

/* Offline rows highlighted */
tbody tr.row-offline {
  background: rgba(var(--offline-rgb),.05);
}
tbody tr.row-offline:hover { background: rgba(var(--offline-rgb),.10); }

/* ── Row selection (click-to-select) ────────────────────── */
/* Pointer cursor only when selection mode is active (editor/admin) */
#device-tbody.selectable tr[data-id] { cursor: pointer; user-select: none; }
tbody tr.row-selected {
  background: rgba(var(--accent-rgb), .10);
  box-shadow: inset 3px 0 0 var(--accent);
}
tbody tr.row-selected:hover { background: rgba(var(--accent-rgb), .15); }

/* ── Bulk action bar ────────────────────────────────────── */
/* ── Bulk Action Bar ───────────────────────────────────── */
.bulk-bar {
  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 10px;
  background: var(--surface2); border: 1px solid var(--accent);
  border-radius: calc(var(--radius) * 1.5);
  padding: 8px 14px; box-shadow: 0 4px 24px rgba(0,0,0,.5);
  z-index: 500; white-space: nowrap;
  animation: bulk-bar-in .15s ease;
}
.bulk-bar.hidden { display: none; }
@keyframes bulk-bar-in {
  from { opacity: 0; transform: translateX(-50%) translateY(8px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* Left side: checkbox toggle with count (replaces count + Select All) */
.bulk-toggle {
  display: flex; align-items: center; gap: 6px;
  background: none; border: none; color: var(--accent);
  font-size: 13px; font-weight: 600; cursor: pointer;
  padding: 4px 8px 4px 4px; border-radius: var(--radius);
  transition: background .15s;
}
.bulk-toggle:hover { background: rgba(var(--accent-rgb, 0, 164, 228), .12); }
.bulk-toggle svg { color: var(--accent); flex-shrink: 0; }

/* Desktop: pill groups with label on top, buttons below */
.bulk-actions { display: flex; align-items: center; gap: 8px; }
/* Capability-aware bulk bar: a menu row/section greys out when NO selected
   device supports its action (e.g. Tags / Account / Virtual Lines on a
   UniFi-Talk selection). pointer-events:none blocks the doomed Webex write;
   Share never greys. Per-item greying covers buttons gated tighter than their
   section (Apply Changes via data-cap="deviceCmd" — so Reboot stays active for
   UniFi while Apply Changes greys). Toggled by updateBulkBarCapabilities(). */
.bulk-menu-row.action-disabled,
.bulk-menu-section.action-disabled,
.bulk-menu-item.action-disabled,
/* role-disabled: greyed because the user's ROLE lacks the capability (e.g. an
   editor on Voicemail). Same look as action-disabled (device-source greying) but
   a separate class so updateBulkBarCapabilities() doesn't toggle it off. */
.bulk-menu-row.role-disabled,
.bulk-menu-section.role-disabled,
.bulk-menu-item.role-disabled {
  opacity: .4;
  pointer-events: none;
}
.bulk-clear {
  padding: 4px 6px; margin-left: auto; flex-shrink: 0;
}

/* ── Bulk bar dropdown menus (Assign / Device / Share) ───────────────────
   The bar is three verb dropdowns. Each opens a panel ABOVE its button (the
   bar is pinned to the bottom of the screen). Lead icon + label on desktop;
   the label hides on mobile so the bar collapses to icons. Fixed width — a new
   action slots into an existing verb, never widening the bar. */
.bulk-menu { position: relative; display: flex; align-items: center; }
.bulk-menu-btn {
  display: flex; align-items: center; gap: 6px; height: 46px;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--text); font-size: 12.5px; font-weight: 600;
  padding: 7px 12px; border-radius: var(--radius); cursor: pointer;
  transition: border-color .15s, color .15s, background .15s;
}
.bulk-menu-btn:hover,
.bulk-menu-btn.active {
  border-color: var(--accent); color: var(--accent);
  background: rgba(var(--accent-rgb, 0, 164, 228), .1);
}
.bulk-menu-btn svg { flex-shrink: 0; }
.bulk-menu-chev { transition: transform .15s; }
.bulk-menu-btn.active .bulk-menu-chev { transform: rotate(180deg); }

/* Panel opens upward, anchored to its button. Left-aligned by default; the
   right-most menu (Share) anchors right so the panel never overflows offscreen. */
.bulk-menu-panel {
  position: absolute; bottom: calc(100% + 8px); left: 0; min-width: 184px;
  background: var(--surface2); border: 1px solid var(--accent);
  border-radius: var(--radius); padding: 4px;
  box-shadow: 0 4px 20px rgba(0,0,0,.5);
  z-index: 510; white-space: nowrap;
  display: flex; flex-direction: column; gap: 1px;
}
.bulk-menu[data-menu="share"] .bulk-menu-panel { left: auto; right: 0; }

.bulk-menu-section { display: flex; flex-direction: column; gap: 1px; }
.bulk-menu-row { position: relative; display: flex; flex-direction: column; }

.bulk-menu-item {
  display: flex; align-items: center; gap: 9px; width: 100%;
  background: none; border: none; color: var(--text); text-align: left;
  font-size: 13px; padding: 8px 12px; border-radius: calc(var(--radius) - 2px);
  cursor: pointer; white-space: nowrap; transition: background .1s;
}
.bulk-menu-item:hover,
.bulk-menu-item.active { background: rgba(var(--accent-rgb, 0, 164, 228), .12); }
.bulk-menu-item svg { color: var(--text-muted); flex-shrink: 0; }
.bulk-menu-item.bulk-menu-danger { color: var(--offline); }
.bulk-menu-item.bulk-menu-danger svg { color: var(--offline); }
/* Chevron on rows that open a level-2 flyout (Hunt Group / Virtual Lines) */
.bulk-submenu-chev { margin-left: auto; }

/* Level-2 flyout — opens to the RIGHT of its row (Assign sits left of centre). */
.bulk-submenu {
  position: absolute; left: calc(100% + 6px); top: -4px; min-width: 156px;
  background: var(--surface2); border: 1px solid var(--accent);
  border-radius: var(--radius); padding: 4px; z-index: 520;
  box-shadow: 0 4px 20px rgba(0,0,0,.5);
  display: flex; flex-direction: column; gap: 1px;
}

/* ── Bulk Bar Mobile ─────────────────────────────────── */
@media (max-width: 600px) {
  .bulk-bar {
    left: 8px; right: 8px; bottom: 8px;
    transform: none; max-width: none;
    gap: 6px; padding: 8px 10px;
    border-radius: var(--radius);
  }
  @keyframes bulk-bar-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .bulk-toggle { font-size: 12px; gap: 4px; padding: 4px; flex-shrink: 0; }
  .bulk-actions { display: flex; gap: 4px; min-width: 0; }
  .bulk-clear { flex-shrink: 0; }

  /* Bulk menus on mobile: the button matches the group-label tap pill, and the
     level-2 submenu expands INLINE (indented) instead of a right-flyout, so it
     can never run off the narrow viewport. */
  .bulk-menu-btn {
    height: auto; padding: 6px 9px; font-size: 11px; color: var(--text-muted);
  }
  .bulk-submenu {
    position: static; left: auto; top: auto; min-width: 0;
    margin: 1px 0 3px 18px; padding: 0;
    border: none; box-shadow: none; background: none;
  }

  /* Collapse each dropdown to its lead icon only — hide the text label so the
     bar fits a narrow viewport (icon + chevron per dropdown). */
  .bulk-menu-label { display: none; }
}

/* ── BG Deploy modal ──────────────────────────────────── */
.bg-deploy-card { width: min(520px, 94vw); }
.bg-deploy-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 8px;
  max-height: 320px;
  overflow-y: auto;
  padding: 4px 0;
  margin: 8px 0;
}
.bg-deploy-grid .branding-library-item.bg-selected {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(var(--accent-rgb, 0, 164, 228), .35);
}
.bg-deploy-loading {
  color: var(--text-muted);
}
.bg-deploy-empty-msg {
  text-align: center;
  padding: 24px 0;
}
.bg-deploy-empty-msg a {
  color: var(--accent);
  text-decoration: underline;
}
.bg-deploy-apply-label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
  margin: 8px 0 4px;
}
.bg-deploy-apply-label input[type="checkbox"] {
  accent-color: var(--accent);
}
.bg-deploy-progress-bar {
  height: 4px;
  background: var(--surface);
  border-radius: 2px;
  overflow: hidden;
  margin: 8px 0;
}
.bg-deploy-progress-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 2px;
  transition: width .2s ease;
  width: 0;
}
.bg-deploy-default-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  color: var(--text-muted);
}
.bg-deploy-default-tile.bg-selected .bg-deploy-default-icon {
  color: var(--accent);
}

/* ── Tag pills in table cells ──────────────────────────── */
.tag-pill {
  display: inline-block; font-size: 11px; font-weight: 500;
  padding: 2px 7px; border-radius: 10px;
  background: rgba(var(--accent-rgb), .12); color: var(--accent);
  margin: 1px 2px; white-space: nowrap;
}
/* CID source badge (L/C/D) — subtle pill styling with policy colors */
.cid-source-badge {
  display: inline-block; font-size: 11px; font-weight: 600;
  padding: 2px 6px; border-radius: 10px;
  margin-right: 4px; white-space: nowrap;
}
.cid-badge-location {
  background: rgba(210, 153, 34, .12); color: #d29922; /* orange */
}
.cid-badge-custom {
  background: rgba(0, 164, 228, .12); color: #00a4e4; /* blue */
}
.cid-badge-direct {
  background: rgba(63, 185, 80, .12); color: #3fb950; /* green */
}
.hg-pill {
  background: rgba(var(--online-rgb), .12); color: var(--online);
}
.hg-pill-disabled {
  background: rgba(var(--text-muted-rgb), .12); color: var(--text-muted);
  border-color: rgba(var(--text-muted-rgb), .25);
}
.tag-suggestion-pill.hg-pill-disabled:hover {
  background: rgba(var(--text-muted-rgb), .20);
  border-color: rgba(var(--text-muted-rgb), .5);
}
.tag-suggestion-pill.hg-pill-disabled.selected {
  background: rgba(var(--text-muted-rgb), .25);
  border-color: var(--text-muted);
  color: var(--text);
}
/* ── Tag suggestion pills (modal) ──────────────────────── */
.tag-suggestions {
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 10px 0 4px;
}
.tag-suggestions.hidden { display: none; }
.tag-suggestion-pill {
  display: inline-flex; align-items: center;
  font-size: 11px; font-weight: 500; cursor: pointer;
  padding: 3px 10px; border-radius: 10px;
  background: rgba(var(--accent-rgb), .10);
  color: var(--accent);
  border: 1px solid rgba(var(--accent-rgb), .25);
  transition: background .12s, border-color .12s;
}
.tag-suggestions .suggestions-hint {
  font-size: 12px; color: var(--text-muted); width: 100%;
}
.tag-suggestion-pill:hover {
  background: rgba(var(--accent-rgb), .20);
  border-color: rgba(var(--accent-rgb), .5);
}
.tag-suggestion-pill.selected {
  background: rgba(var(--accent-rgb), .25);
  border-color: var(--accent);
  color: var(--text);
}
/* Disabled state — used by Validate Location Move modal to grey out a
   destination pill that matches the current location of any selected person
   (a same-location move is a no-op upstream). pointer-events:none drops
   click + hover handling. */
.tag-suggestion-pill.pill-disabled {
  opacity: .35;
  cursor: not-allowed;
  pointer-events: none;
}

td {
  padding: 12px 14px;
  font-size: 13px;
  color: var(--text);
  vertical-align: middle;
  white-space: nowrap;
}

/* (Dead badge classes removed — .badge, .badge-online, .badge-offline,
   .badge-dot were unused. Use .api-badge + .dot .dot-xs .dot-inherit instead.) */

@keyframes pulse-green {
  0%, 100% { opacity: 1; }
  50% { opacity: .4; }
}

/* Status dots (compact columns) — uses .dot base class with state modifiers */
.dot-col { text-align: center !important; width: 50px; padding: 0 4px !important; }
/* Dash placeholder when no dot status exists (e.g. no UniFi match) */
.dot-dash { color: var(--text-muted); font-size: 14px; line-height: 1; }

/* Source column — home-integration status dot + source label (consolidated
   multi-source status column; replaces the old Webex/UniFi status columns) */
.source-col { white-space: nowrap; }
.source-col .source-label { margin-left: 6px; vertical-align: middle; }

.unifi-warn {
  display: inline-flex;
  align-items: center;
  color: var(--warn);
  margin-left: 3px;
  vertical-align: middle;
  cursor: help;
}

/* Type chip */
.type-chip {
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 8px;
  font-size: 11px;
  color: var(--text-muted);
}

/* .owner-link wraps icon + device name → workspace OR user page in Control Hub.
   .device-link wraps phone icon + MAC → device detail page in Control Hub.
   Both share styling — they're the row's two finger-friendly tap targets.
   Padding extends the hit area beyond the visible text so each link meets the
   Apple HIG / WCAG 2.1 AAA 44px target on touch screens. Negative margin
   cancels the layout effect so visually the rows look unchanged. */
.owner-link, .device-link {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 2px;
  margin: -6px -2px;
}
.owner-link:hover, .device-link:hover { color: var(--accent); }
.owner-link .ext-icon {
  opacity: 0;
  transition: opacity .15s;
  flex-shrink: 0;
}
.owner-link:hover .ext-icon { opacity: .6; }
/* device-name: replaces inline style="font-weight:600" in app.js renderTable() */
.device-name { font-weight: 600; }

/* Owner / phone icons — inner glyph containers inside .owner-link / .device-link.
   Color cascades from the parent link via currentColor, so hover-accent works
   without per-element rules. */
.owner-icon, .phone-icon {
  display: inline-flex;
  align-items: center;
  margin-right: 4px;
  flex-shrink: 0;
}
.owner-icon svg, .phone-icon svg { display: block; }

/* Mobile: enlarge the tap target to Apple HIG / Material 44px without growing
   visible text. Padding inflates, negative margin compensates. The MAC line
   is the tighter of the two (11px monospace font, ~14px line height) so the
   padding has to be 15px each side to clear 44 — that overshoots for the
   name line, which is fine. */
@media (max-width: 480px) {
  .owner-link, .device-link, .mac-copy {
    padding: 15px 4px;
    margin: -15px -4px;
  }
}

.ip-text { font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent); }
.ip-mismatch { color: var(--warn); }
.ip-warn-icon { margin-left: 4px; vertical-align: middle; color: var(--warn); }
.mac-text { font-family: 'SF Mono', 'Fira Code', monospace; color: var(--text-muted); font-size: 11px; }
/* Neutral mono for numeric identifiers (Ext / DID columns). Distinct
   from .ip-text (accent-coloured, reads like a link) and .mac-text (muted/small) —
   primary-line numbers want normal text weight in monospace for easy scanning. */
.num-text { font-family: 'SF Mono', 'Fira Code', monospace; color: var(--text); }
/* The MAC in the device cell is click-to-copy on every row (all sources). Pointer +
   hover-accent mirrors the retired .device-link affordance; padding keeps a finger-
   friendly target (negative margin cancels the layout effect). .mac-copy is skipped
   by the row-selection handler so a copy never toggles row selection. */
.mac-copy {
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 6px 2px;
  margin: -6px -2px;
  border-radius: 4px;
}
.mac-copy:hover { color: var(--accent); }

/* cell-detail: smaller font for secondary table columns (location, tags, unifi) */
.cell-detail { font-size: 12px; }
/* cell-dash: muted placeholder for empty cells (em-dash "—") */
.cell-dash { color: var(--text-muted); }

.last-seen-text { color: var(--text-muted); }
/* Uptime column: bright when the device reports uptime; offline falls back to a
   faded .last-known absolute timestamp (same treatment as Connected/Location). */
.uptime-text { color: var(--text); }

/* Last-known UniFi data — faded to indicate device is offline, showing cached info */
.last-known { opacity: 0.45; font-style: italic; }

/* Loading row */
.loading-row td {
  text-align: center;
  padding: 48px 0;
  color: var(--text-muted);
  display: table-cell;
}

.loading-row td > * { display: inline-block; vertical-align: middle; margin: 0 8px; }

.spinner {
  width: 18px;
  height: 18px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .7s linear infinite;
}

/* Empty state */
.empty-state {
  text-align: center;
  padding: 60px 0;
  color: var(--text-muted);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}

.empty-state svg { width: 48px; height: 48px; opacity: .4; }
.empty-state p { font-size: 15px; }

/* ── Site Health Strip ────────────────────────────────────── */
.site-strip {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  padding: 8px 24px;
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.site-strip-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--text-muted);
  margin-right: 4px;
}

.site-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 600;
  border: 1px solid transparent;
}

/* Site pill dot uses .dot .dot-sm base — state set by parent pill class */
.site-pill.ok {
  background: rgba(var(--online-rgb),.10);
  border-color: rgba(var(--online-rgb),.3);
  color: var(--online);
}
.site-pill.ok .dot { background: var(--online); animation: pulse-green 2s infinite; animation-delay: -999s; }

.site-pill.warn {
  background: rgba(var(--warn-rgb),.10);
  border-color: rgba(var(--warn-rgb),.3);
  color: var(--warn);
}
.site-pill.warn .dot { background: var(--warn); }

.site-pill.error {
  background: rgba(var(--offline-rgb),.10);
  border-color: rgba(var(--offline-rgb),.3);
  color: var(--offline);
}
.site-pill.error .dot { background: var(--offline); }

/* ── Site column in device table ─────────────────────────── */
/* Hidden by default; visible only when table has .multi-site class */
.site-col { display: none; }
#device-table.multi-site .site-col { display: table-cell; }

.site-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 600;
  background: rgba(var(--site-rgb, 88, 166, 255), .15);
  color: var(--site-color, var(--accent));
  border: 1px solid rgba(var(--site-rgb, 88, 166, 255), .3);
  white-space: nowrap;
}

/* ── Site filter pills ────────────────────────────────────── */
/* Site filter buttons get individual colors via inline style */

/* ── Column chooser ──────────────────────────────────────── */
/* Inherits standard .dropdown-btn colours; only override width since
   the column chooser is icon-only and doesn't need the 130px min. */
/* Groups col chooser, export, and refresh to the right of the toolbar —
   margin-left: auto creates the gap between filters and action buttons */
.toolbar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

/* Export uses .btn which sizes by padding — force same height as col-chooser
   and refresh which both use height: var(--control-h) explicitly */
.toolbar-actions .btn { height: var(--control-h, 36px); }

/* Compact toolbar icon-dropdown buttons (Columns chooser + Share/Export).
   Shared so the Share button matches the Columns button sitting next to it:
   without it Share inherits .btn-sm's wider padding (5px 12px) + .dropdown-btn's
   8px gap and renders ~7px wider than Columns — the "Share button is too big"
   bug. Apply .toolbar-icon-btn to any icon+chevron toolbar dropdown. */
.col-chooser-btn,
.toolbar-icon-btn {
  min-width: auto !important;
  padding: 8px 10px !important;
  gap: 4px !important;
  /* Match surface2 bg used by export (.btn-secondary) and refresh (.refresh-btn) */
  background: var(--surface2) !important;
}

/* Hidden column support */
table .col-hidden { display: none !important; }

/* ── Utilities ────────────────────────────────────────────── */
.nav-link {
  text-decoration: none; display: inline-flex;
  align-items: center; gap: 5px; cursor: pointer;
}
.icon-muted { flex-shrink: 0; opacity: .6; }
.dropdown-panel-right { right: 0; left: auto; }
.logo-placeholder {
  display: block; margin: auto;
  filter: grayscale(1); opacity: .7;
}
.logo-placeholder-webex { width: 42px; height: auto; }
.logo-placeholder-unifi { height: 28px; width: auto; }

/* ── Footer ───────────────────────────────────────────────── */
footer {
  text-align: center;
  padding: 16px;
  color: var(--text-muted);
  font-size: 12px;
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
}
.footer-details { font-family: 'SF Mono','Fira Code',monospace; opacity: .5; }
/* Commit/PR links in the build-identity string: blend with the muted version text,
   reveal accent + underline on hover. Mirrors the .footer-brand a pattern below. */
.footer-details a { color: inherit; text-decoration: none; }
.footer-details a:hover { color: var(--accent); text-decoration: underline; }
.footer-brand a { color: var(--text-muted); text-decoration: none; opacity: .7; }
.footer-brand a:hover { color: var(--accent); opacity: 1; }

/* ══════════════════════════════════════════════════════════════
   Shared component styles — used by settings.html (and any
   future pages that need buttons, cards, modals, badges, etc.)
   Moved here from settings.html <style> block so all styles
   live in a single centralised stylesheet.
   ══════════════════════════════════════════════════════════════ */

/* ── Settings Tabs ───────────────────────────────────────────── */
/* Top tab bar for settings page — Integrations / Server / Security */
.settings-tabs {
  display: flex; gap: 4px;
  border-bottom: 1px solid var(--border);
  padding-bottom: 0;
  position: relative;
  /* 5 tabs (Settings/Security/Integrations/Alerts/Debug) don't fit a phone
     viewport. Scroll the row horizontally instead of letting it push the whole
     page wider than the screen — that overflow was cutting every card off on the
     left on mobile. Desktop has room in the 620px wrap so nothing scrolls there. */
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;        /* Firefox — hide the scrollbar; swipe still works */
}
.settings-tabs::-webkit-scrollbar { display: none; } /* Chrome/Safari */
.settings-tab {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 10px 16px;
  background: none; border: none; border-bottom: 2px solid transparent;
  color: var(--text-muted); font-size: 13px; font-weight: 600;
  cursor: pointer; transition: color .15s;
  margin-bottom: -1px; /* overlap the container border */
  flex-shrink: 0;            /* don't squish — let the row scroll instead */
  white-space: nowrap;       /* keep each label on one line */
}
.settings-tab:hover { color: var(--text); }
.settings-tab.active { color: var(--accent); }
/* Sliding indicator — positioned by JS in switchTab() */
.tab-indicator {
  position: absolute; bottom: 0; left: 0;
  height: 2px; background: var(--accent);
  transition: transform .25s ease, width .25s ease;
  pointer-events: none;
}
.settings-tab svg { flex-shrink: 0; }

/* Tab panels — only the active one is visible */
.tab-panel { display: none; flex-direction: column; gap: 16px; }
.tab-panel.active { display: flex; }

/* ── Form Cards ──────────────────────────────────────────────── */
.form-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.form-card-header {
  padding: 10px 16px;
  background: var(--surface2);
  border-bottom: 1px solid var(--border);
  font-size: 11px; font-weight: 700;
  color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .07em;
  display: flex; align-items: center; gap: 8px;
}
.card-header-label { flex: 1; }
/* Brand logos in integration card headers — small, muted to match text */
.card-header-logo     { height: 16px; width: auto; opacity: .7; filter: grayscale(.3); }
.card-header-logo-svg { flex-shrink: 0; color: var(--text-muted); }
.form-body { padding: 16px; display: flex; flex-direction: column; gap: 14px; }
/* Horizontal rule inside a card to separate sections (e.g. password vs IP allowlist) */
.card-divider { border: none; border-top: 1px solid var(--border); margin: 12px 0; }

/* Subsection headers inside a merged card (e.g. SSO / Password / IP in Access Control) */
.subsection-header {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; font-weight: 600; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .04em;
  margin-bottom: 12px;
}
.subsection-header svg { opacity: .6; flex-shrink: 0; }

/* Subheadings within form bodies (e.g. Quick Actions / Inspection Tools within merged card).
   Used to visually separate sections within a single form-card. Flex layout allows
   buttons to float right while the heading text stays left. */
.form-subheading {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 13px; font-weight: 600; color: var(--text);
  margin-top: 16px; margin-bottom: 8px;
}
.form-subheading:first-child { margin-top: 0; }

/* Hidden container for internal badge state (anti-pattern #16: setBadge()
   overwrites className, stripping .hidden — parent div stays hidden instead) */
.badge-store { display: none; }

/* SSO enforcement banner — shown in password subsection when SSO is active */
.sso-enforced-banner {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 10px 12px; margin-bottom: 12px; border-radius: var(--radius);
  background: rgba(var(--warn-rgb), .08);
  border: 1px solid rgba(var(--warn-rgb), .2);
  color: var(--warn); font-size: 13px; line-height: 1.4;
}
.sso-enforced-banner svg { flex-shrink: 0; margin-top: 1px; }

/* ── Form Fields ─────────────────────────────────────────────── */
.field { display: flex; flex-direction: column; gap: 5px; }
.field label { font-size: 12px; font-weight: 600; color: var(--text); }
.field-hint { font-size: 11px; color: var(--text-muted); }
.field-hint-warn { color: var(--warn); }

/* Config summary — shows live badge status mirroring each service card.
   Displayed in the Data Management card on the Settings page.
   Dots auto-update via setBadge() using BADGE_TO_DOT mapping. */
.config-summary {
  display: flex; flex-wrap: wrap; gap: 6px 16px;
  padding: 10px 12px; border-radius: var(--radius);
  background: var(--bg); border: 1px solid var(--border);
  font-size: 12px; color: var(--text-muted);
}
.config-summary-label {
  font-weight: 600; color: var(--text); width: 100%; margin-bottom: 2px;
}
.config-summary-item {
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
}

.field input, .field select {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text); font-size: 13px;
  padding: 8px 12px; outline: none;
  transition: border-color .15s; width: 100%;
  box-sizing: border-box;
}
.field input:focus, .field select:focus { border-color: var(--accent); }
.field input::placeholder { color: var(--text-muted); }
.input-mono { font-family: 'SF Mono','Fira Code',monospace; font-size: 12px; }
.input-readonly {
  background: var(--surface2) !important;
  border-color: transparent !important;
  color: var(--text-muted) !important;
  cursor: default;
  user-select: all;
  font-size: 13px;
}
/* Anchor that looks like a read-only field but deep-links the LIVE build's commit
   to GitHub. Pairs with .input-readonly + .input-mono; it must add the field-box
   geometry (padding/radius/width) itself because an <a> doesn't inherit the
   `.field input` rule. Accent colour + a trailing ↗ signal it's clickable. */
.input-readonly-link {
  display: block;
  box-sizing: border-box;
  width: 100%;
  padding: 8px 12px;
  border-radius: var(--radius);
  color: var(--accent) !important;
  text-decoration: none;
  cursor: pointer;
  user-select: text;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.input-readonly-link[href]::after { content: ' \2197'; opacity: .7; }
.input-readonly-link[href]:hover { text-decoration: underline; }
.field select {
  appearance: none; cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%238b949e' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 10px center;
  padding-right: 28px;
}

/* ── Masked field state ──────────────────────────────────────
   Applied to inputs showing masked saved values (e.g. ••••••••).
   Dimmed text + italic hint so users can tell it's not real input. */
.input-masked {
  color: var(--text-muted);
  font-style: italic;
}

/* Required field marker — red asterisk appended to label text */
.field-required { color: var(--offline); }

/* ── Password toggle (show/hide eye icon) ────────────────────
   Active when entering new credentials so user can verify input.
   Disabled (greyed out) when field shows masked saved values. */
.password-wrap { position: relative; }
.password-wrap input { padding-right: 40px; }
.toggle-pw {
  position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  padding: 3px; display: flex; align-items: center; transition: color .15s, opacity .15s;
}
.toggle-pw:hover:not(:disabled) { color: var(--text); }
.toggle-pw:disabled { opacity: 0.3; }
.toggle-pw svg { width: 14px; height: 14px; }

/* ── Slide toggle switch ───────────────────────────────────
   iOS-style on/off toggle. Usage:
   <label class="slide-toggle">
     <input type="checkbox" />
     <span class="slide-track"></span>
     Label text
   </label> */
.slide-toggle {
  display: inline-flex; align-items: center; gap: 8px;
  cursor: pointer; font-size: 12px; color: var(--text-muted);
  user-select: none;
}
.slide-toggle input { display: none; }
.slide-track {
  position: relative; width: 32px; height: 18px; flex-shrink: 0;
  background: var(--border); border-radius: 9px; transition: background .2s;
}
.slide-track::after {
  content: ''; position: absolute; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: 50%;
  background: var(--text-muted); transition: transform .2s, background .2s;
}
.slide-toggle input:checked + .slide-track { background: var(--accent); }
.slide-toggle input:checked + .slide-track::after {
  transform: translateX(14px); background: #fff;
}

/* ── Toggle row (Debug tab Inspection Tools) ──────────────────
   Vertically stacked rows where each toggle has a title + description
   beside the slider, instead of jamming everything into one flex line. */
.toggle-row {
  display: flex; align-items: flex-start; padding: 8px 0;
  border-top: 1px solid var(--border);
}
.toggle-row:first-of-type { border-top: none; padding-top: 0; }
.toggle-row .slide-toggle { align-items: flex-start; gap: 12px; width: 100%; cursor: pointer; }
.toggle-row .slide-track { margin-top: 2px; }
.toggle-label { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.toggle-title { color: var(--text); font-size: 13px; font-weight: 500; }
.toggle-desc { color: var(--text-muted); font-size: 11.5px; line-height: 1.4; }
.toggle-desc kbd {
  background: var(--surface2); border: 1px solid var(--border); border-radius: 4px;
  padding: 1px 6px; font-size: 10.5px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--text); margin-left: 2px;
}

/* ── Endpoint list collapsible summary (Debug tab) ────────────
   Reuses .scopes-details/.scopes-summary for the open/close
   chevron rotation; adds a label + endpoint-count + chevron row. */
.endpoint-summary-label { color: var(--text); font-size: 13px; font-weight: 500; }
.endpoint-summary-chevron { color: var(--text-muted); transition: transform .15s; flex-shrink: 0; }
.scopes-details[open] .endpoint-summary-chevron { transform: rotate(180deg); }
.scopes-summary { display: flex; align-items: center; gap: 8px; padding: 8px 0; }

/* ── Password strength indicator ────────────────────────────
   Shows a color-coded bar (red→orange→green) and label as the
   user types. Hidden when the field is empty or masked. */
/* Password strength gauge — bar + badge + crack-time estimate */
.pw-strength { margin-top: 8px; }
.pw-strength-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 6px;
}
.pw-strength-title { font-size: 11px; font-weight: 600; color: var(--text-muted); }
.pw-strength-badge {
  display: inline-block; padding: 2px 10px; border-radius: 12px;
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
}
.pw-strength-badge.weak   { background: rgba(var(--offline-rgb), .15); color: var(--offline); }
.pw-strength-badge.fair   { background: rgba(var(--warn-rgb), .15);    color: var(--warn); }
.pw-strength-badge.strong { background: rgba(var(--online-rgb), .15);  color: var(--online); }
.pw-strength-bar {
  width: 100%; height: 6px; border-radius: 3px;
  background: var(--surface2); overflow: hidden;
}
.pw-strength-fill {
  height: 100%; border-radius: 3px; width: 0;
  transition: width .3s ease, background .3s ease;
}
.pw-strength-fill.weak   { width: 33%; background: var(--offline); }
.pw-strength-fill.fair   { width: 66%; background: var(--warn); }
.pw-strength-fill.strong { width: 100%; background: var(--online); }
.pw-strength-crack {
  display: block; margin-top: 4px;
  font-size: 11px; color: var(--text-muted);
}
.pw-strength-crack strong { color: var(--text); }

/* Minimum-length hint next to label */
.pw-min-hint { font-size: 11px; color: var(--text-muted); font-weight: 400; }

/* Generate passphrase row */
.pw-generate-row { margin-top: 6px; }

/* ── Buttons ─────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 16px; border-radius: var(--radius);
  font-size: 13px; font-weight: 600; cursor: pointer;
  border: 1px solid transparent;
  transition: background .15s, border-color .15s, color .15s;
  white-space: nowrap; text-decoration: none;
}
.btn svg { width: 13px; height: 13px; flex-shrink: 0; }
.btn-primary  { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-primary:hover  { background: var(--accent-hover, #79b8ff); border-color: var(--accent-hover, #79b8ff); }
.btn-primary:disabled { opacity: .5; cursor: not-allowed; }
.btn-secondary { background: var(--surface2); color: var(--text); border-color: var(--border); }
.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
.btn-secondary:disabled { opacity: .4; cursor: not-allowed; }
/* Ghost buttons get a subtle border so they still read as clickable buttons */
.btn-ghost  { background: none; color: var(--text-muted); border-color: var(--border); }
.btn-ghost:hover { color: var(--text); border-color: var(--text-muted); }
/* Press effect — brief scale-down on click for tactile feedback */
.btn:active { transform: scale(0.95); transition: transform 0.05s; }
/* Spinning state for refresh buttons — works on both .refresh-btn and .btn */
.btn.spinning svg { animation: spin .6s linear infinite; }
.btn.spinning { pointer-events: none; opacity: 0.7; }
.btn-danger { background: rgba(var(--offline-rgb),.15); color: var(--offline); border-color: rgba(var(--offline-rgb),.4); }
.btn-danger:hover { background: rgba(var(--offline-rgb),.25); border-color: var(--offline); }
.btn-sm { padding: 5px 12px; font-size: 12px; }

/* ── API Status Badges (card headers) ────────────────────────── */
.api-badge {
  font-size: 10px; font-weight: 600; padding: 2px 8px;
  border-radius: 10px; text-transform: none; letter-spacing: 0;
  display: inline-flex; align-items: center; gap: 4px;
  border: 1px solid transparent;
  transition: background .2s, color .2s, border-color .2s;
}
/* Badge dot uses .dot .dot-xs .dot-inherit — inherits color from parent badge.
   Badge icon (.badge-icon) is an SVG alternative to the dot (e.g. lock for protected). */
.badge-icon { width: 11px; height: 11px; flex-shrink: 0; }
.badge-idle    { background: var(--surface); border-color: var(--border); color: var(--text-muted); }
.badge-saved   { background: rgba(var(--accent-rgb),.10); border-color: rgba(var(--accent-rgb),.3); color: var(--accent); }
.badge-ok      { background: rgba(var(--online-rgb),.12); border-color: rgba(var(--online-rgb),.35); color: var(--online); }
.badge-warn    { background: rgba(var(--warn-rgb),.10); border-color: rgba(var(--warn-rgb),.3); color: var(--warn); }
.badge-testing { background: rgba(var(--warn-rgb),.10); border-color: rgba(var(--warn-rgb),.3); color: var(--warn); }
.badge-error   { background: rgba(var(--offline-rgb),.12); border-color: rgba(var(--offline-rgb),.35); color: var(--offline); }
@keyframes pulse-dot { 0%,100%{opacity:1} 50%{opacity:.3} }
.badge-testing .dot { animation: pulse-dot .9s ease-in-out infinite; animation-delay: -999s; }

/* (Removed: animated trailing dots for badges — replaced with static text) */

/* ── Inline test result ──────────────────────────────────────── */

/* ── Action Row ──────────────────────────────────────────────── */
.action-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* Push an element to the right within a flex row (e.g. Test Connection button) */
.action-right { margin-left: auto; }

/* ── Toast Notifications ─────────────────────────────────────
   Fixed-position popup in bottom-right corner. Slides in from
   the right, auto-dismisses after 4s. No page scroll disruption.
   Used by showFeedback() in settings.html. */
.toast-container {
  position: fixed; bottom: 20px; right: 20px; z-index: 9999;
  display: flex; flex-direction: column-reverse; gap: 8px;
  pointer-events: none;
}
.feedback {
  border-radius: var(--radius); padding: 12px 16px;
  font-size: 13px; font-weight: 500;
  display: flex; align-items: center; gap: 8px;
  pointer-events: auto; min-width: 240px; max-width: 380px;
  box-shadow: var(--shadow);
  /* Slide-in animation from right */
  transform: translateX(calc(100% + 20px));
  opacity: 0;
  transition: transform .3s ease, opacity .3s ease;
}
.feedback.toast-visible {
  transform: translateX(0);
  opacity: 1;
}
/* Slide out before removal */
.feedback.toast-exit {
  transform: translateX(calc(100% + 20px));
  opacity: 0;
}
.feedback svg { width: 14px; height: 14px; flex-shrink: 0; margin-top:1px; }
.feedback-success { background:rgba(var(--online-rgb),.12); border:1px solid rgba(var(--online-rgb),.35); color:var(--online); }
.feedback-error   { background:rgba(var(--offline-rgb),.12); border:1px solid rgba(var(--offline-rgb),.35); color:var(--offline); }
.feedback-warn    { background:rgba(var(--warn-rgb),.12); border:1px solid rgba(var(--warn-rgb),.35); color:var(--warn); }

/* ── PWA install affordances (see InstallHelper in common.js) ─────────────────
   Install prompt — SAME toast family as .feedback (bottom-right, slides in from
   the right, matching radius/shadow), but sticky + actionable: it waits for an
   explicit Install/Dismiss instead of auto-dismissing after 4s. */
.install-toast {
  position: fixed; right: 20px; bottom: 20px; z-index: 9998;
  display: flex; align-items: flex-start; gap: 10px;
  width: 320px; max-width: calc(100vw - 40px);
  padding: 12px 14px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow);
  transform: translateX(calc(100% + 20px)); opacity: 0;
  transition: transform .3s ease, opacity .3s ease;
}
.install-toast.install-toast-visible { transform: translateX(0); opacity: 1; }
.install-toast-icon  { flex: 0 0 auto; color: var(--accent); margin-top: 1px; }
.install-toast-body  { flex: 1; min-width: 0; }
.install-toast-title { font-size: 14px; font-weight: 600; color: var(--text); }
.install-toast-text  { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
.install-toast-actions { display: flex; gap: 8px; margin-top: 10px; }
.install-toast-close {
  flex: 0 0 auto; background: none; border: none; cursor: pointer;
  color: var(--text-muted); font-size: 18px; line-height: 1; padding: 0 2px;
}
.install-toast-close:hover { color: var(--text); }
/* Numbered steps inside the self-built Safari/iOS instructions modal */
.install-steps { margin: 4px 0 0; padding-left: 20px; color: var(--text); }
.install-steps li { margin: 6px 0; font-size: 14px; }
/* Login-page CTA — deliberately quiet: a small, muted, centered text link (NOT a
   full-width button) so it reads as a secondary option beside Sign in. */
.install-cta-link {
  display: block; margin: 12px auto 0; padding: 4px 8px;
  background: none; border: none; cursor: pointer;
  font: inherit; font-size: 12.5px; color: var(--text-muted);
  transition: color .15s ease;
}
.install-cta-link:hover { color: var(--accent); }
.install-cta-link svg { margin-right: 5px; vertical-align: -2px; }
@media (max-width: 480px) {
  .install-toast { right: 8px; left: 8px; bottom: 8px; width: auto; }
}

/* ── Restart Banner ──────────────────────────────────────────── */
.restart-banner {
  background: rgba(var(--warn-rgb),.10); border: 1px solid rgba(var(--warn-rgb),.4);
  border-radius: var(--radius); padding: 12px 16px;
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.restart-banner svg { color: var(--warn); flex-shrink: 0; }
.restart-banner-msg { flex: 1; font-size: 13px; font-weight: 500; color: var(--warn); }

/* ── Modal ───────────────────────────────────────────────────── */
.modal-overlay {
  position: fixed; inset: 0; z-index: 2000;
  /* Darker, blurrier backdrop so the dialog separates clearly from the page
     behind it — the old .55 dim let card edges blend into the cards below. */
  background: rgba(0,0,0,.7); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity .15s;
  pointer-events: none;
}
.modal-overlay.open { opacity: 1; pointer-events: all; }
.modal-card {
  background: var(--surface); border: 1px solid var(--border);
  /* Deeper shadow + a faint accent ring give the card a crisp, high-contrast
     edge against the dark page without changing its (or its contents') bg. */
  border-radius: calc(var(--radius) * 1.5);
  box-shadow: 0 16px 48px rgba(0,0,0,.6), 0 0 0 1px rgba(var(--accent-rgb), .25);
  padding: 24px; width: 340px; max-width: calc(100vw - 40px);
  /* Cap height to viewport and let inner content scroll. Without this the
     debug stepper (6 rows × ~150px each = 900px) overflows below the fold
     and the action buttons become unreachable. */
  max-height: calc(100vh - 40px); overflow-y: auto;
  display: flex; flex-direction: column; gap: 12px;
  transform: scale(.95); transition: transform .15s;
}
.modal-overlay.open .modal-card { transform: scale(1); }

/* ── Shared JSON viewer (openJsonViewer, common.js) ────────────────────────
   Wide modifier on the shared .modal-card. Exists because new-tab JSON opens
   trap the installed PWA (no tabs/back button) — see openJsonViewer's comment.
   The <pre> is the ONLY scroll area so the action row stays reachable no
   matter how big the payload is.

   min-width: 0 on .jv-body is load-bearing, not decorative: .modal-card is a
   flex column, and flex children default to min-width:auto, which refuses to
   shrink below the content's intrinsic width. A pre containing one long
   unbroken line (e.g. a raw error string with no spaces) was poking PAST the
   card's edge instead of triggering its own overflow-x scrollbar — the box
   technically had overflow:auto but never got the chance to use it (real
   incident, 2026-07-03: user reported "can't scroll" on a long 404 body).
   pre-wrap + overflow-wrap:anywhere is the other half of the fix — wrapping
   long lines is more usable on a phone than horizontal scroll, so scrolling
   is now the fallback for tall payloads, not the primary way to read wide ones.
   Mobile gets a near-fullscreen card (below) since JSON payloads run long. */
.json-viewer-card { width: 720px; }
.jv-source {
  font-size: 11px; color: var(--text-muted);
  overflow-wrap: anywhere;               /* long query strings wrap, never overflow */
}
.jv-body {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 10px;
  margin: 0; font-size: 12px; line-height: 1.45;
  flex: 1 1 auto; min-height: 80px; max-height: 55vh; min-width: 0;
  overflow-y: auto; overflow-x: hidden;   /* wrapping (below) removes the need for x-scroll */
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.jv-actions { flex-wrap: wrap; }         /* 4 actions fit on phones by wrapping */
@media (max-width: 480px) {
  /* Phones: use nearly the whole screen instead of the standard 340px modal —
     JSON payloads are long, and the standard size leaves most of the phone
     unused. .modal-card's base max-width:calc(100vw - 40px) would otherwise
     still cap width at 350px on a 390px phone regardless of this override
     (max-width wins over width) — so max-width must be raised here too. */
  .json-viewer-card { width: calc(100vw - 16px); max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); }
  .jv-body { max-height: 70vh; }
  /* Same exemption pattern as .tenant-title-btn (line ~5849): the dashboard
     toolbar's global mobile rule (`.btn-label { display: none; }`, further
     down this file) is meant for Export/Refresh icon-only buttons — it isn't
     scoped to that toolbar, so it silently strips the "Open elsewhere" text
     off the Share button here too, leaving an unlabeled icon with no
     tooltip. The 2-class selector here outranks the 1-class global rule by
     specificity regardless of source order, restoring the label text. */
  .json-viewer-card .btn-label { display: inline; }
}

/* ── Design Capture HUD (Settings → UI Tools) ──────────────────────────────
   The panel reuses the shared .modal-card for ALL its chrome (surface, border,
   radius, shadow). .dc-panel is only a positioning/layout MODIFIER that pairs
   with .modal-card (like .login-btn-full pairs with .btn) so a draggable,
   non-blocking toolbar works — it deliberately does NOT use the blocking
   .modal-overlay, because Select Element must click elements on the page behind
   it. Header text uses .modal-title; buttons use .btn*; toasts use showFeedback().
   The remaining .dc-* are panel-internal layout + the page-overlay highlight
   (.dc-box / .dc-chip), which no shared component provides. */
.dc-panel {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 3000; min-width: 340px; width: auto;
  padding: 0; gap: 0;          /* rows own their spacing — override .modal-card */
  transition: none;            /* no transform animation while dragging */
  display: none; overflow: hidden;
}
.dc-panel.show { display: flex; }
.dc-row { display: flex; align-items: center; gap: 10px; padding: 10px 12px; }
.dc-row + .dc-row { border-top: 1px solid var(--border); }
.dc-head { cursor: move; user-select: none; background: var(--surface2); border-bottom: 1px solid var(--border); }
.dc-muted { color: var(--text-muted); }
.dc-spacer { flex: 1; }
.dc-x { cursor: pointer; color: var(--text-muted); font-size: 16px; line-height: 1; }
.dc-x:hover { color: var(--text); }
.dc-box {
  position: fixed; pointer-events: none; display: none; z-index: 2999;
  border: 2px solid var(--accent); border-radius: 4px; background: rgba(var(--accent-rgb), .08);
}
.dc-chip {
  position: fixed; pointer-events: none; display: none; z-index: 3001;
  background: var(--accent); color: #fff; font-size: 11px; padding: 2px 6px;
  border-radius: 3px; white-space: nowrap;
}

/* Debug step is data-dense (6 rows + JSON responses) — give it more
   horizontal room so the JSON doesn't wrap mid-base64-id. */
.modal-card[data-step="debug"] { width: 640px; }
.modal-title { font-size: 15px; font-weight: 700; color: var(--text); }
.modal-message { font-size: 13px; color: var(--text-muted); line-height: 1.5; }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
.modal-input {
  width: 100%; font-size: 13px; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface2); color: var(--text);
}

/* ── Location Validate Modal (multi-step) ────────────────────────────────
   Five display modes toggled via data-step on .modal-card:
     "select"    — initial picker (breakdown + destination pills + ext rows)
     "results"   — validate dry-run output
     "executing" — destructive move job in flight (status + Refresh button)
     "debug"     — manual step-through replacement for "executing"; admin
                   runs each Webex call individually + verifies in Control
                   Hub between steps. Triggered by the Debug Mode toggle
                   on the select step.
     "complete"  — terminal job state (per-user move results)
   Each step hides the others' top-level containers and the action-row buttons
   not relevant to that step. Reuses existing color variables — no new colors. */

/* Cadence stepper — Select Location / Validation / Result, matching Control
   Hub's own move-location wizard. Pure CSS: default state is "pending"
   (muted outline), then data-step attribute selectors below promote dots to
   "active" (accent fill) or "done" (online/green fill) — no JS involved, so
   there's no state to keep in sync with the existing data-step toggling. */
.loc-stepper { display: flex; align-items: center; margin-bottom: 16px; }
.loc-stepper-step { display: flex; flex-direction: column; align-items: center; gap: 5px; flex: 0 0 auto; }
.loc-stepper-dot {
  width: 20px; height: 20px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700;
  border: 2px solid var(--border);
  color: var(--text-muted); background: var(--surface2);
}
.loc-stepper-label { font-size: 10px; color: var(--text-muted); white-space: nowrap; }
.loc-stepper-line { flex: 1 1 auto; height: 2px; background: var(--border); margin: 0 6px 15px; }

.modal-card[data-step="select"]    .loc-stepper-step[data-stepper="1"] .loc-stepper-dot,
.modal-card[data-step="results"]   .loc-stepper-step[data-stepper="2"] .loc-stepper-dot,
.modal-card[data-step="executing"] .loc-stepper-step[data-stepper="3"] .loc-stepper-dot,
.modal-card[data-step="complete"]  .loc-stepper-step[data-stepper="3"] .loc-stepper-dot {
  background: var(--accent); border-color: var(--accent); color: #06202b;
}
.modal-card[data-step="select"]    .loc-stepper-step[data-stepper="1"] .loc-stepper-label,
.modal-card[data-step="results"]   .loc-stepper-step[data-stepper="2"] .loc-stepper-label,
.modal-card[data-step="executing"] .loc-stepper-step[data-stepper="3"] .loc-stepper-label,
.modal-card[data-step="complete"]  .loc-stepper-step[data-stepper="3"] .loc-stepper-label { color: var(--text); }

.modal-card[data-step="results"]   .loc-stepper-step[data-stepper="1"] .loc-stepper-dot,
.modal-card[data-step="results"]   .loc-stepper-line[data-stepper-line="1"],
.modal-card[data-step="executing"] .loc-stepper-step[data-stepper="1"] .loc-stepper-dot,
.modal-card[data-step="executing"] .loc-stepper-step[data-stepper="2"] .loc-stepper-dot,
.modal-card[data-step="executing"] .loc-stepper-line[data-stepper-line="1"],
.modal-card[data-step="executing"] .loc-stepper-line[data-stepper-line="2"],
.modal-card[data-step="complete"]  .loc-stepper-step[data-stepper="1"] .loc-stepper-dot,
.modal-card[data-step="complete"]  .loc-stepper-step[data-stepper="2"] .loc-stepper-dot,
.modal-card[data-step="complete"]  .loc-stepper-line[data-stepper-line="1"],
.modal-card[data-step="complete"]  .loc-stepper-line[data-stepper-line="2"] {
  background: var(--online); border-color: var(--online); color: #06240f;
}
.modal-card[data-step="results"]   .loc-stepper-step[data-stepper="1"] .loc-stepper-label,
.modal-card[data-step="executing"] .loc-stepper-step[data-stepper="1"] .loc-stepper-label,
.modal-card[data-step="executing"] .loc-stepper-step[data-stepper="2"] .loc-stepper-label,
.modal-card[data-step="complete"]  .loc-stepper-step[data-stepper="1"] .loc-stepper-label,
.modal-card[data-step="complete"]  .loc-stepper-step[data-stepper="2"] .loc-stepper-label { color: var(--text); }

/* Debug mode has its own 6-row manual stepper — hide the high-level one. */
.modal-card[data-step="debug"] .loc-stepper { display: none; }

.modal-card[data-step="select"]    .loc-step-results,
.modal-card[data-step="select"]    .loc-step-executing,
.modal-card[data-step="select"]    .loc-step-debug,
.modal-card[data-step="select"]    .loc-step-complete,
.modal-card[data-step="results"]   .loc-step-select,
.modal-card[data-step="results"]   .loc-step-executing,
.modal-card[data-step="results"]   .loc-step-debug,
.modal-card[data-step="results"]   .loc-step-complete,
.modal-card[data-step="executing"] .loc-step-select,
.modal-card[data-step="executing"] .loc-step-results,
.modal-card[data-step="executing"] .loc-step-debug,
.modal-card[data-step="executing"] .loc-step-complete,
.modal-card[data-step="debug"]     .loc-step-select,
.modal-card[data-step="debug"]     .loc-step-results,
.modal-card[data-step="debug"]     .loc-step-executing,
.modal-card[data-step="debug"]     .loc-step-complete,
.modal-card[data-step="complete"]  .loc-step-select,
.modal-card[data-step="complete"]  .loc-step-results,
.modal-card[data-step="complete"]  .loc-step-executing,
.modal-card[data-step="complete"]  .loc-step-debug { display: none; }

/* Action-row button visibility per step. Each step shows only the buttons
   that make sense — keeps the action bar uncluttered as the modal advances. */
.modal-card[data-step="select"]    #loc-change-close,
.modal-card[data-step="select"]    #loc-change-initiate,
.modal-card[data-step="select"]    #loc-change-refresh,
.modal-card[data-step="results"]   #loc-change-cancel,
.modal-card[data-step="results"]   #loc-change-validate,
.modal-card[data-step="results"]   #loc-change-refresh,
.modal-card[data-step="executing"] #loc-change-validate,
.modal-card[data-step="executing"] #loc-change-initiate,
.modal-card[data-step="executing"] #loc-change-close,
.modal-card[data-step="debug"]     #loc-change-validate,
.modal-card[data-step="debug"]     #loc-change-initiate,
.modal-card[data-step="debug"]     #loc-change-cancel,
.modal-card[data-step="debug"]     #loc-change-refresh,
.modal-card[data-step="complete"]  #loc-change-cancel,
.modal-card[data-step="complete"]  #loc-change-validate,
.modal-card[data-step="complete"]  #loc-change-initiate,
.modal-card[data-step="complete"]  #loc-change-refresh { display: none; }

.loc-breakdown {
  list-style: none; margin: 0 0 10px; padding: 8px 12px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: var(--radius); font-size: 12px; color: var(--text-muted);
  display: flex; flex-direction: column; gap: 4px;
}
.loc-breakdown li { display: flex; gap: 8px; align-items: baseline; }
.loc-breakdown li strong { color: var(--text); font-weight: 600; min-width: 90px; }
.loc-breakdown li.skipped { color: var(--text-muted); }

/* Person table: Name (read-only — see PR discussion on why it isn't
   editable yet) · Current location · Extension (editable). Destination is
   NOT repeated per row — it's the single shared dropdown above the table
   now, one choice applies to every selected device, so a per-row echo of it
   was redundant. Grid instead of flex: fixed side columns so every row's
   Ext input lines up under the header regardless of name/location length,
   with the name column absorbing all the remaining space (never crushed —
   the flex version's first cut let it collapse to "N." on longer names). */
.loc-person-row {
  display: grid;
  /* Fixed columns, not 1fr — the header and every data row are SEPARATE
     grid containers (independent divs, not one shared table), so a
     content-based track (1fr, or even max-content) sizes each row's Name
     column independently: the header's "NAME" label is short, a data row's
     name is longer, and their column boundaries drift apart instead of
     lining up — that was the actual bug ("big gap between NAME and CURRENT
     LOC", "so inconsistent"). Fixed px is the same number everywhere, so
     every row's columns land in the exact same place regardless of content
     — genuinely tight and aligned, not just usually close. 160px comfortably
     fits typical names; .loc-person-name's overflow-ellipsis + title=
     tooltip (below) still covers the rare longer one. */
  grid-template-columns: 160px 96px 72px;
  gap: 8px; align-items: center;
  padding: 8px 12px; font-size: 12px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: var(--radius); margin-bottom: 6px;
}
.loc-person-head {
  background: none; border: none; padding: 0 12px; margin-bottom: 4px;
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted);
}
.loc-person-name {
  color: var(--text); font-weight: 500; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.loc-person-loc {
  color: var(--text-muted); min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.loc-person-row .loc-ext-input { width: 100%; box-sizing: border-box; padding: 3px 6px; font-size: 12px; }

/* A touch wider than the default 340px modal-card on the select step only —
   the destination dropdown + person table need more breathing room than the
   simple confirm dialogs .modal-card's default width was sized for. */
.modal-card[data-step="select"] { width: 400px; }

/* Everything informational (tags/caller-ID/hunt-group impact + the Debug
   Mode power-tool) lives behind this native disclosure. The always-visible
   chip strip (in <summary>, so it shows even collapsed) gives a glanceable
   summary; "Details ▾" expands the full text. Native marker suppressed —
   the chips + toggle text ARE the affordance, a second browser triangle
   next to them would be redundant. */
.loc-details { margin: 10px 0 4px; font-size: 12px; }
.loc-details summary {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  list-style: none; cursor: pointer; user-select: none; padding: 4px 0;
}
.loc-details summary::-webkit-details-marker { display: none; }
.loc-chips { display: flex; flex-wrap: wrap; gap: 5px; }
.loc-chip {
  font-size: 10px; border-radius: 6px; padding: 3px 8px; white-space: nowrap;
  color: var(--text-muted); background: var(--surface2);
}
.loc-chip-ok   { color: var(--online); }
.loc-chip-warn { color: var(--warn); }
.loc-details-toggle { color: var(--accent); font-size: 11px; flex-shrink: 0; }
.loc-details summary:hover .loc-details-toggle { color: var(--accent-hover, #79b8ff); }

.loc-context-info {
  padding: 8px 12px;
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: var(--radius); font-size: 11px; color: var(--text-muted);
  line-height: 1.5;
}
.loc-context-info .loc-ctx-label { color: var(--text); font-weight: 600; margin-bottom: 4px; }
.loc-context-info .loc-ctx-row { display: flex; gap: 6px; align-items: baseline; }
.loc-ctx-preserved { color: var(--online); }
.loc-ctx-changed   { color: var(--warn); }

.loc-validate-results {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 4px;
  font-size: 12px;
}
.loc-validate-results li {
  display: flex; gap: 8px; align-items: baseline;
  padding: 6px 10px; border-radius: var(--radius);
  background: var(--surface2);
}
.loc-validate-results li.valid   { color: var(--online); }
.loc-validate-results li.invalid { color: var(--offline); }
.loc-validate-results li .loc-result-name   { color: var(--text); font-weight: 500; flex: 1; }
.loc-validate-results li .loc-result-reason { color: var(--text-muted); font-size: 11px; display: flex; flex-direction: column; gap: 2px; flex: 2; min-width: 0; }
.loc-result-detail { font-size: 11px; color: var(--text-muted); word-break: break-word; }

#loc-change-error-banner,
#loc-change-complete-banner {
  background: rgba(var(--offline-rgb), .12);
  border: 1px solid rgba(var(--offline-rgb), .3);
  color: var(--offline);
  padding: 8px 12px; border-radius: var(--radius);
  font-size: 12px;
}
/* Success variant — same banner shape, --online tokens instead of --offline.
   Toggled via JS (classList) when a validate/complete banner is reporting
   a clean pass rather than a blocker, matching Control Hub's green
   "Validation Success!" banner. */
#loc-change-error-banner.loc-banner-success,
#loc-change-complete-banner.loc-banner-success {
  background: rgba(var(--online-rgb), .12);
  border-color: rgba(var(--online-rgb), .3);
  color: var(--online);
}
.loc-csv-link {
  color: var(--accent); text-decoration: underline;
  margin-left: 4px;
}

/* Debug Mode toggle on the select step — small, muted, bottom-anchored. */
.loc-debug-toggle {
  display: flex; align-items: center; gap: 8px;
  font-size: 11px; color: var(--text-muted);
  margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--border);
}
.loc-debug-toggle input[type="checkbox"],
.loc-debug-verify input[type="checkbox"] { margin: 0; }

/* Debug step rows — one per Webex call. Vertical stack with a divider
   between rows. Each row owns: title + sub, status pill, Run button,
   collapsible response pre, and an optional "Verified in Control Hub"
   checkbox that gates the next step's button. */
.loc-debug-row {
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
}
.loc-debug-row:last-child { border-bottom: none; }
.loc-debug-row-head {
  display: flex; align-items: center; gap: 10px;
}
.loc-debug-row-title {
  flex: 1; display: flex; flex-direction: column; gap: 2px;
}
.loc-debug-row-title strong { color: var(--text); font-weight: 600; }
.loc-debug-row-sub {
  color: var(--text-muted); font-size: 11px;
}
.loc-debug-status {
  font-size: 10px; font-weight: 600; letter-spacing: 0.5px;
  padding: 3px 8px; border-radius: 10px;
  flex: 0 0 auto;
}
.badge-ready   { background: rgba(var(--accent-rgb, 0,164,228), 0); color: var(--text-muted); border: 1px solid var(--border); }
.badge-locked  { background: var(--surface2); color: var(--text-muted); opacity: 0.6; border: 1px solid var(--border); }
.badge-running { background: rgba(0,164,228,.15); color: var(--accent); border: 1px solid var(--accent); animation: pulse-dot 0.9s ease-in-out infinite; }
.badge-success { background: rgba(63,185,80,.15); color: var(--online); border: 1px solid var(--online); }
.badge-fail    { background: rgba(248,81,73,.15); color: var(--offline); border: 1px solid var(--offline); }

.loc-debug-response {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  margin-top: 8px;
  font-size: 11px;
  font-family: monospace;
  color: var(--text);
  white-space: pre-wrap; word-break: break-all;
  /* Lower default ceiling so all 6 rows fit comfortably in the modal
     viewport. Once a step is verified, _onDebugVerifyChange collapses
     the row's response to ~1.5 lines via .loc-debug-response-collapsed. */
  max-height: 120px; overflow-y: auto;
}
.loc-debug-response.loc-debug-response-collapsed {
  display: none;
}
.loc-debug-show-response {
  display: inline-block;
  margin-top: 4px;
  font-size: 10px;
  color: var(--accent);
  cursor: pointer;
  text-decoration: underline;
}

.loc-debug-verify {
  display: flex; align-items: center; gap: 8px;
  margin-top: 8px; font-size: 11px; color: var(--text-muted);
}

/* Spinner shown next to the "Move job in progress" status during the
   executing step. Reuses the existing @keyframes spin from the refresh-btn
   block — adopted via inline-block + border-radius for the dot ring. */
.loc-spinner {
  display: inline-block; width: 12px; height: 12px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  vertical-align: middle; margin-right: 6px;
  animation: spin .9s linear infinite;
}

/* ── Import/Export Modals ────────────────────────────────────────────────
   Drop zone inside the import modal and metadata preview panel. */

/* Drop zone: dashed bordered area the user clicks or drops a file onto.
   .drag-over is added via JS dragenter/dragleave to show active state. */
.drop-zone {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 6px; padding: 20px 16px; border: 2px dashed var(--border);
  border-radius: var(--radius); background: var(--bg);
  color: var(--text-muted); font-size: 12px; text-align: center;
  cursor: pointer; transition: border-color .15s, color .15s;
}
.drop-zone:hover, .drop-zone.drag-over {
  border-color: var(--accent); color: var(--text);
}
.drop-zone.has-file {
  border-color: var(--online); color: var(--online);
}

/* Metadata preview: shown after a valid file is loaded in the import modal. */
.import-meta-preview {
  font-size: 12px; padding: 8px 12px; border-radius: var(--radius);
  background: var(--bg); border: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 4px;
}
.import-meta-preview span { color: var(--text-muted); }
.import-meta-preview strong { color: var(--text); font-weight: 600; }

/* Service checklist — shared by export modal ("Export includes:") and
   import modal ("Backup includes:"). Uses Feather check-circle / x-circle
   icons driven by SERVICE_REGISTRY. */
.service-checklist {
  margin: 8px 0 4px; padding: 10px 12px; border-radius: var(--radius);
  background: var(--bg); border: 1px solid var(--border);
  display: flex; flex-wrap: wrap; gap: 4px 14px;
  font-size: 11px;
}
.service-checklist-label {
  font-weight: 600; color: var(--text); width: 100%; margin-bottom: 2px; font-size: 11px;
}
.service-check-item {
  display: inline-flex; align-items: center; gap: 4px; white-space: nowrap;
  cursor: pointer; position: relative; min-width: 120px;
}
.service-check-icon { width: 12px; height: 12px; flex-shrink: 0; }
.service-check-ok { color: var(--online); }
.service-check-none { color: var(--text-muted); opacity: .5; }

/* Interactive checkbox for selective import/export.
   Hidden native checkbox + styled span, following the adjacent-sibling pattern.
   Visual style matches .dropdown-check (accent fill when checked).
   position:relative on .service-check-item keeps the absolute checkbox contained. */
.service-check-item input[type="checkbox"] {
  position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none;
}
.service-checkbox {
  width: 14px; height: 14px; border-radius: 3px;
  border: 1.5px solid var(--border);
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; font-size: 10px; font-weight: 700;
  transition: background .15s, border-color .15s;
  color: transparent;
}
.service-check-item input[type="checkbox"]:checked + .service-checkbox {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.service-check-item.disabled {
  opacity: .4; pointer-events: none; cursor: default;
}
/* Locked items — mandatory selections that can't be toggled off.
   App-wide convention: normal text colour, not clickable, checkbox shows
   a muted accent fill to signal "always included".  Used in export checklist
   (General Settings, User Roles) and dashboard column selector (alwaysOn cols).
   Distinct from .disabled which dims items that have no data at all. */
.service-check-item.locked {
  pointer-events: none; cursor: default;
}
/* Locked checkbox: muted accent fill instead of full accent — signals "fixed on".
   Must override the :checked + .service-checkbox rule (specificity 0,2,2)
   so we match on .locked + :checked to reach 0,3,2. */
.service-check-item.locked input[type="checkbox"]:checked + .service-checkbox {
  background: rgba(var(--accent-rgb),.35); border-color: rgba(var(--accent-rgb),.5); color: #fff;
}
/* Header row: label left, toggle link right */
.service-checklist-header {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; margin-bottom: 2px;
}
.service-checklist-toggle {
  font-size: 10px; color: var(--accent); cursor: pointer;
  font-weight: 400; white-space: nowrap;
}
.service-checklist-toggle:hover { text-decoration: underline; }
/* Divider + "always included" note inside the checklist */
.service-checklist-divider {
  width: 100%; border: none; border-top: 1px solid var(--border);
  margin: 6px 0 4px;
}
.service-checklist-note {
  width: 100%; font-size: 10px; color: var(--text-muted); font-style: italic;
}
.service-checklist-count {
  width: 100%; font-size: 11px; color: var(--text); font-weight: 500;
  margin-top: 4px;
}

/* ── Modal Tabs ────────────────────────────────────────────────────────
   Tabs inside export/import modals. Reuses the visual style of .settings-tab
   but scoped to .modal-card so they don't conflict with page-level tabs. */
.modal-card-wide { width: 420px; }
.modal-tabs {
  display: flex; gap: 0; border-bottom: 1px solid var(--border);
}
.modal-tab {
  flex: 1; padding: 8px 0; background: none; border: none;
  border-bottom: 2px solid transparent;
  color: var(--text-muted); font-size: 12px; font-weight: 600;
  cursor: pointer; transition: color .15s, border-color .15s;
  margin-bottom: -1px; text-align: center;
}
.modal-tab:hover { color: var(--text); }
.modal-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.modal-tab-panel { display: none; }
.modal-tab-panel.active { display: block; }

/* Encrypt toggle — standalone checkbox label outside the service-checklist */
.export-encrypt-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--text); cursor: pointer;
  margin: 8px 0 4px; position: relative;
}
.export-encrypt-toggle input[type="checkbox"] {
  position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none;
}
.export-encrypt-toggle input[type="checkbox"]:checked + .service-checkbox {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
/* Tiny utility: 6px top margin for stacked password fields inside modals */
.mt-6 { margin-top: 6px; }
/* Log entry count label next to checkboxes (muted, small) */
.service-check-count { color: var(--text-muted); font-size: 10px; }

/* ── API Fields Explorer ─────────────────────────────────────────────────
   Button in header + wide modal with tabbed table showing raw/normalized
   Webex device fields. Helps admins discover available data. */


/* Wide modal variant for endpoint tables — overrides .modal-card 340px default.
   Used by both the per-integration endpoints modal (settings) and the API report modal (dashboard). */
.api-fields-card {
  width: 640px;
  max-width: calc(100vw - 40px);
  padding: 20px;
}

/* Scrollable body area */
.api-fields-body {
  overflow-y: auto; max-height: calc(100vh - 280px);
  min-height: 120px;
}

/* Hint text above the table */
.api-fields-hint {
  font-size: 12px; color: var(--text-muted); margin: 0 0 10px 0;
  line-height: 1.5;
}
.api-fields-hint code {
  background: var(--surface2); padding: 1px 5px; border-radius: 4px;
  font-size: 11px;
}
/* Link to official API documentation — shown at top of endpoints modal body */
.api-docs-link {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 12px; color: var(--accent);
  text-decoration: none; margin-bottom: 12px;
}
.api-docs-link:hover { text-decoration: underline; }
.api-docs-link svg { flex-shrink: 0; }

/* Table inside the modal */
.api-fields-table-wrap { overflow-x: auto; }
.api-fields-table {
  width: 100%; border-collapse: collapse; font-size: 12px;
}
.api-fields-table th {
  text-align: left; padding: 6px 10px; font-weight: 600;
  color: var(--text-muted); border-bottom: 1px solid var(--border);
  font-size: 11px; text-transform: uppercase; letter-spacing: .5px;
  position: sticky; top: 0; background: var(--surface);
}
.api-fields-table td {
  padding: 5px 10px; border-bottom: 1px solid rgba(var(--border-rgb, 48,54,61), .4);
  color: var(--text); vertical-align: top;
}
.api-fields-table tbody tr:hover { background: var(--surface2); }

/* Field name / endpoint URL column — monospace for easy copying.
   word-break allows long URLs to wrap instead of forcing horizontal scroll. */
.fields-name {
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px;
  color: var(--accent); word-break: break-all;
}

/* Source badges — small coloured pills */
.fields-source-badge {
  display: inline-block; padding: 1px 6px; border-radius: 10px;
  font-size: 10px; font-weight: 600; letter-spacing: .3px;
  vertical-align: middle;
}
.fields-source-webex { background: rgba(var(--accent-rgb), .15); color: var(--accent); }
.fields-source-unifi { background: rgba(var(--online-rgb), .15); color: var(--online); }
.fields-source-text { font-size: 11px; color: var(--text-muted); }

/* Action badges used in the API Write Log modal — reuse .fields-source-badge shape */
.wlog-action-add    { background: rgba(var(--online-rgb), .15);  color: var(--online);  }
.wlog-action-remove { background: rgba(var(--offline-rgb), .15); color: var(--offline); }
.wlog-action-modify { background: rgba(var(--accent-rgb),  .15); color: var(--accent);  }
/* Timestamp cell — prevent mid-value line breaks */
.wlog-ts { white-space: nowrap; }

/* ── Activity Log (Railway-style) ─────────────────────────────── */
/* Monospace log viewer with colored left borders per event type.
   Inspired by Railway.app's build log UI. */

/* ── Activity Log Toolbar ───────────────────────────────────── */
/* Two-row layout: Row 1 = filter dropdowns + clear + column chooser.
   Row 2 = search (full width) + refresh. */
.alog-toolbar {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 6px;
}
.alog-toolbar-row2 { margin-bottom: 10px; }
/* Search input reuses dashboard #search styles (shares .search-group parent) */
.alog-toolbar .search-group { flex: 1; min-width: 80px; }
.alog-toolbar .search-group input {
  width: 100%; height: var(--control-h, 36px); padding: 0 12px;
  background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius);
  color: var(--text); font-size: 12px;
  font-family: 'SF Mono', 'Fira Code', monospace;
}
.alog-toolbar .search-group input::placeholder { color: var(--text-muted); }
.alog-toolbar .search-group input:focus { outline: none; border-color: var(--accent); }

/* Container for log entries */
.activity-log-list {
  display: flex; flex-direction: column; gap: 0;
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px;
  max-height: 480px; overflow-y: auto;
  scrollbar-color: rgba(var(--text-muted-rgb), .35) transparent;
}

/* Single log entry row — left colored border, multi-column layout */
.activity-log-entry {
  display: flex; align-items: baseline; gap: 12px;
  padding: 6px 12px; border-left: 3px solid var(--border);
  transition: background .15s ease;
}
.activity-log-entry:hover { background: rgba(var(--text-muted-rgb), .06); }

/* Column sizing — shared between header and entry rows.
   .settings-wrap max-width:620px → 580px content → 516px usable after
   accordion-body + form-body padding. Columns must total <516px.
   Current: 100+60+95+80 = 335px + 48px gaps = 383px → ~133px for msg. */
/* overflow:hidden on fixed columns overrides min-width:auto (the flex
   default) so columns actually respect their flex-basis.  Without it,
   white-space:nowrap content expands the column beyond the basis width,
   stealing space from the flex-1 message column. */
.alog-col-ts    { flex: 0 0 100px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.alog-col-type  { flex: 0 0 60px;  overflow: hidden; }
.alog-col-msg   { flex: 1; min-width: 0; word-break: break-word; }
.alog-col-actor { flex: 0 0 95px;  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.alog-col-ip    { flex: 0 0 80px;  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 11px; }
/* Column visibility — mirrors dashboard .col-hidden */
.alog-col.alog-col-hidden { display: none !important; }

/* Timestamp column styling */
.activity-log-ts { color: var(--text-muted); font-size: 11px; }

/* Message column */
.activity-log-msg { color: var(--text); }

/* Actor column — muted */
.activity-log-actor { color: var(--text-muted); font-size: 11px; }

/* IP column — muted, monospace */
.activity-log-ip { color: var(--text-muted); font-size: 11px; }

/* Left border color classes — one per event category */
.activity-log-entry.log-login       { border-left-color: var(--online); }
.activity-log-entry.log-restart     { border-left-color: var(--text-muted); }
.activity-log-entry.log-redeploy    { border-left-color: var(--purple); } /* purple — matches Railway's accent */
.activity-log-entry.log-import      { border-left-color: var(--accent); }
.activity-log-entry.log-export      { border-left-color: var(--accent); }
.activity-log-entry.log-clear       { border-left-color: var(--warn); }
.activity-log-entry.log-password    { border-left-color: var(--warn); }
.activity-log-entry.log-ip          { border-left-color: var(--warn); }
.activity-log-entry.log-api-write   { border-left-color: var(--accent); }
/* Security events — failed logins, rate-limit lockouts, IP blocks, logout */
.activity-log-entry.log-security    { border-left-color: var(--offline); }
.activity-log-entry.log-logout      { border-left-color: var(--text-muted); }
.activity-log-entry.log-config-save { border-left-color: var(--accent); }

/* Type badge — small pill */
.activity-log-badge {
  display: inline-block; padding: 1px 6px; border-radius: 10px;
  font-size: 10px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .03em; flex-shrink: 0;
}
.log-badge-login      { background: rgba(var(--online-rgb), .15);  color: var(--online); }
.log-badge-admin      { background: rgba(var(--warn-rgb), .15);    color: var(--warn); }
.log-badge-api-write  { background: rgba(var(--accent-rgb), .15);  color: var(--accent); }
.log-badge-system     { background: rgba(var(--text-muted-rgb), .15); color: var(--text-muted); }
/* Security events — failed logins, rate-limit lockouts, IP blocks */
.log-badge-security   { background: rgba(var(--offline-rgb), .15); color: var(--offline); }

/* Low storage warning banner */
.activity-log-storage-warn {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px; margin-top: 8px;
  background: rgba(var(--warn-rgb), .1); border: 1px solid rgba(var(--warn-rgb), .3);
  border-radius: var(--radius); color: var(--warn); font-size: 12px;
}

/* Log retention setting row — inline layout below the log list */
.alog-retention-row {
  display: flex; align-items: center; gap: 8px;
  margin-top: 12px; padding-top: 12px;
  border-top: 1px solid var(--border);
  font-size: 12px;
}
.alog-retention-row label { color: var(--text-muted); white-space: nowrap; }
.alog-retention-row input[type="number"] {
  width: 60px; padding: 4px 6px; font-size: 12px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text);
}
.alog-retention-row .field-hint { flex: 1; min-width: 0; }

/* Header row — column labels above the log entries */
.activity-log-header {
  display: flex; align-items: center; gap: 12px;
  padding: 4px 12px 6px; border-bottom: 1px solid var(--border);
  font-size: 11px; font-weight: 600; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .05em;
}

/* Type badge for raw fields */
.fields-type-badge {
  display: inline-block; padding: 1px 6px; border-radius: 10px;
  font-size: 10px; font-weight: 600; background: var(--surface2);
  color: var(--text-muted);
}

/* Sample value column — truncate long values */
.fields-sample { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fields-desc { font-size: 11px; color: var(--text-muted); }

/* Loading and empty states inside modal */
.api-fields-loading {
  display: flex; align-items: center; gap: 10px; padding: 30px 0;
  justify-content: center; color: var(--text-muted); font-size: 13px;
}
.api-fields-empty {
  padding: 30px 0; text-align: center; color: var(--text-muted); font-size: 13px;
}

/* ── API Explorer (sec-card-explorer) ────────────────────────────
   Admin-only tool for probing upstream APIs with the system's
   already-configured credentials. Used as Pre-Work for new
   integrations (capture live response shapes without curl). */

.explorer-row {
  display: flex; flex-direction: column; gap: 6px;
  margin-bottom: 12px;
}
.explorer-row-tight {
  flex-direction: row; flex-wrap: wrap; gap: 8px; align-items: center;
}
.explorer-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
  color: var(--text-muted); font-weight: 600;
}
.explorer-segmented {
  display: flex; gap: 0; border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden; width: fit-content;
}
.explorer-seg {
  padding: 6px 14px; background: var(--surface);
  color: var(--text-muted); border: none; cursor: pointer;
  font-size: 12px; font-weight: 600;
  border-right: 1px solid var(--border);
  transition: background 0.1s, color 0.1s;
}
.explorer-seg:last-child { border-right: none; }
.explorer-seg:hover { background: var(--surface2); color: var(--text); }
.explorer-seg.active {
  background: var(--accent); color: #fff;
}
.explorer-scenarios {
  display: flex; flex-wrap: wrap; gap: 6px;
}
.explorer-scenario-group {
  flex: 0 0 100%;
  font-size: 10px; font-weight: 600; letter-spacing: 0.5px;
  color: var(--text-muted); text-transform: uppercase;
  padding: 8px 0 2px; border-bottom: 1px solid var(--border);
  margin-bottom: 2px;
}
.explorer-scenario-group:first-child { padding-top: 0; }
.explorer-scenario {
  padding: 5px 10px !important; font-size: 11px !important;
}
.explorer-scenario-danger {
  border-color: var(--offline) !important;
  color: var(--offline) !important;
}
.explorer-scenario-danger:hover {
  background: rgba(var(--offline-rgb), 0.1) !important;
}
.explorer-method {
  flex: 0 0 100px; height: var(--control-h);
}
.explorer-path {
  flex: 1 1 200px; min-width: 150px; height: var(--control-h);
  font-size: 12px; padding: 0 10px;
}
.explorer-body {
  width: 100%; min-height: 100px; resize: vertical;
  font-size: 12px; padding: 8px 10px;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.explorer-actions {
  display: flex; justify-content: flex-end; margin-bottom: 12px;
}
.explorer-response {
  margin-top: 8px; padding: 12px; background: var(--surface2);
  border: 1px solid var(--border); border-radius: var(--radius);
}
.explorer-response-meta {
  font-size: 11px; color: var(--text-muted); margin-bottom: 8px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.explorer-status-ok    { color: var(--online);  font-weight: 600; }
.explorer-status-warn  { color: var(--warn);    font-weight: 600; }
.explorer-status-error { color: var(--offline); font-weight: 600; }
.explorer-response-body {
  max-height: 400px; overflow: auto;
  margin: 0; padding: 10px;
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 11px; line-height: 1.4;
  white-space: pre-wrap; word-break: break-all;
}
.explorer-response-actions {
  display: flex; gap: 6px; margin-top: 8px; justify-content: flex-end;
}

/* ── Supply Chain Card (sec-card-deps) ───────────────────────────
   Live dependency-health view in the Security tab. Mirrors the
   Activity Log card pattern: header badge + toolbar + table.        */

.deps-summary {
  display: flex; gap: 8px; flex-wrap: wrap;
  margin-bottom: 14px;
}
.deps-stat {
  flex: 1 1 110px; min-width: 100px;
  padding: 10px 12px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
}
.deps-stat-label {
  font-size: 9px; font-weight: 700; letter-spacing: .5px;
  color: var(--text-muted); margin-bottom: 4px;
}
.deps-stat-value {
  font-size: 18px; font-weight: 700; color: var(--text);
  line-height: 1;
}
.deps-stat-sub {
  font-size: 10px; color: var(--text-muted); margin-top: 4px;
}
.deps-stat-high {
  background: rgba(var(--offline-rgb), .06);
  border-color: rgba(var(--offline-rgb), .35);
}
.deps-stat-high .deps-stat-label,
.deps-stat-high .deps-stat-value { color: var(--offline); }
.deps-stat-mod {
  background: rgba(var(--warn-rgb), .06);
  border-color: rgba(var(--warn-rgb), .35);
}
.deps-stat-mod .deps-stat-label,
.deps-stat-mod .deps-stat-value { color: var(--warn); }
.deps-stat-dep {
  background: rgba(var(--offline-rgb), .06);
  border-color: rgba(var(--offline-rgb), .35);
}
.deps-stat-dep .deps-stat-label,
.deps-stat-dep .deps-stat-value { color: var(--offline); }
.deps-stat-upd {
  background: rgba(var(--accent-rgb), .06);
  border-color: rgba(var(--accent-rgb), .35);
}
.deps-stat-upd .deps-stat-label,
.deps-stat-upd .deps-stat-value { color: var(--accent); }

.deps-toolbar {
  display: flex; align-items: center; gap: 8px;
  margin-bottom: 10px; flex-wrap: wrap;
}
.deps-toolbar .dropdown { min-width: 150px; }

/* Search input — matches dashboard #search styles via the shared
   .search-group wrapper. Same pattern as Activity Log toolbar. */
.deps-toolbar .search-group { flex: 1; min-width: 120px; }
.deps-toolbar .search-group input {
  width: 100%;
  height: var(--control-h, 36px);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0 12px;
  color: var(--text);
  font-size: 13px;
  font-family: inherit;
  outline: none;
  transition: border-color .15s;
}
.deps-toolbar .search-group input:focus { border-color: var(--accent); }
.deps-toolbar .search-group input::placeholder { color: var(--text-muted); }

/* Refresh button uses the dashboard .refresh-btn class — only override
   sizing here since the deps card's toolbar is tighter than dashboard. */
.deps-toolbar #deps-refresh-btn { padding: 0 12px; }
.deps-toolbar #deps-refresh-btn svg { width: 14px; height: 14px; }
.deps-last-checked {
  font-size: 11px; color: var(--text-muted);
  white-space: nowrap;
}
/* Spinner inherits from .refresh-btn.spinning rule (style.css:677) */

.deps-table-wrap {
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
  margin-bottom: 12px;
}
.deps-table-header,
.deps-row {
  display: flex; align-items: center;
  font-size: 12px;
}
.deps-table-header {
  background: var(--surface2);
  padding: 8px 10px;
  font-size: 9px; font-weight: 700; letter-spacing: .5px;
  color: var(--text-muted);
  border-bottom: 1px solid var(--border);
}
.deps-row {
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  color: var(--text);
}
.deps-row:last-child { border-bottom: none; }
.deps-row-expandable { cursor: pointer; }
.deps-row-expandable:hover { background: var(--surface2); }
.deps-row code,
.deps-row-detail code { font-size: 11px; color: var(--text); background: transparent; padding: 0; }
.deps-version-new { color: var(--online) !important; }

/* Tree-grouped layout: child rows are transitives nested under their
   nearest direct ancestor. Visual depth via indent + tree-prefix glyph. */
.deps-row-child {
  padding-left: 28px;
  background: rgba(0,0,0,.18);
  font-size: 11px;
}
.deps-row-child .deps-col-name { font-weight: 400; color: var(--text-muted); }
.deps-tree-prefix {
  display: inline-block;
  margin-right: 6px;
  color: var(--text-muted);
  font-family: var(--font-mono, monospace);
}
.deps-row-detail-child { margin-left: 28px; }

/* Direct dep that's currently expanded — subtle accent on left edge to
   visually tie the parent row to its children below. */
.deps-row-expanded { background: rgba(var(--accent-rgb), .04); border-left: 2px solid rgba(var(--accent-rgb), .35); padding-left: 8px; }

/* Pin spec next to direct dep names — small mono, muted. Only renders
   when p.pinned exists (i.e., direct deps from package.json). */
.deps-pin-spec {
  margin-left: 8px;
  font-family: var(--font-mono, monospace);
  font-size: 10px;
  font-weight: 400;
  color: var(--text-muted);
}

/* Multi-parent chip: "+N" badge on transitives that are required by
   multiple direct deps. Tooltip lists every parent. */
.deps-other-parents {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  background: rgba(var(--text-muted-rgb, 139,148,158), .15);
  border-radius: 8px;
  color: var(--text-muted);
  font-family: var(--font-mono, monospace);
  font-size: 9px;
  font-weight: 700;
  letter-spacing: .3px;
}

/* Spacer keeps non-expandable direct rows aligned with expandable ones */
.deps-row-expand-icon-spacer {
  display: inline-block;
  width: 12px;
  margin-right: 4px;
}

/* Column widths sized for the .settings-wrap container constraint: 620px
   max-width → ~516px usable inside accordion-body + form-body padding.
   Total fixed = 150 + 95 + 75 = 320px, leaving ~180px for PACKAGE name.
   Pinned + Installed + Required-by moved into the expand-detail row. */
.deps-col-name      { flex: 1 1 0; min-width: 0; font-weight: 600; word-break: break-word; padding-right: 8px; }
.deps-col-version   { flex: 0 0 150px; font-family: var(--font-mono, monospace); font-size: 11px; white-space: nowrap; }
.deps-col-status    { flex: 0 0 95px;  display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-start; }
.deps-col-actions   { flex: 0 0 75px;  text-align: center; }

.deps-row-expand-icon {
  display: inline-block; width: 12px;
  color: var(--text-muted);
  transition: transform .15s ease;
  margin-right: 4px;
}
.deps-row-expandable .deps-row-expand-icon { transform: rotate(0deg); }
.deps-row-expandable[data-expanded="true"] .deps-row-expand-icon { transform: rotate(90deg); }

.deps-badge {
  display: inline-block;
  padding: 2px 6px;
  border-radius: 10px;
  font-size: 9px; font-weight: 700; letter-spacing: .3px;
  white-space: nowrap;
}
.deps-badge-high  { background: rgba(var(--offline-rgb), .12); color: var(--offline); border: 1px solid rgba(var(--offline-rgb), .35); }
.deps-badge-mod   { background: rgba(var(--warn-rgb),    .12); color: var(--warn);    border: 1px solid rgba(var(--warn-rgb),    .35); }
.deps-badge-low   { background: rgba(var(--accent-rgb),  .12); color: var(--accent);  border: 1px solid rgba(var(--accent-rgb),  .35); }
.deps-badge-dep   { background: rgba(var(--offline-rgb), .12); color: var(--offline); border: 1px solid rgba(var(--offline-rgb), .35); }
.deps-badge-upd   { background: rgba(var(--accent-rgb),  .12); color: var(--accent);  border: 1px solid rgba(var(--accent-rgb),  .35); }
.deps-badge-clean { background: rgba(var(--online-rgb),  .12); color: var(--online);  border: 1px solid rgba(var(--online-rgb),  .35); }

.deps-row-detail {
  padding: 12px 14px 14px;
  background: rgba(0,0,0,.20);
  border-bottom: 1px solid var(--border);
  font-size: 12px;
}
.deps-row-detail.hidden { display: none; }
.deps-detail-meta {
  display: flex; flex-wrap: wrap; gap: 16px;
  font-size: 11px;
  margin-bottom: 10px;
  padding-bottom: 8px;
  border-bottom: 1px dashed var(--border);
}
.deps-detail-meta code {
  font-size: 11px; color: var(--text); background: transparent; padding: 0;
}
.deps-detail-deprecated {
  padding: 8px 10px;
  background: rgba(var(--offline-rgb), .08);
  border: 1px solid rgba(var(--offline-rgb), .35);
  border-radius: 4px;
  color: var(--text);
  margin-bottom: 8px;
}
.deps-vuln {
  padding: 8px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  margin-bottom: 6px;
}
.deps-vuln .deps-badge { margin-right: 8px; vertical-align: middle; }
.deps-vuln-title {
  display: inline; font-weight: 600; color: var(--text);
}
.deps-vuln-meta {
  margin-top: 4px;
  font-size: 11px; color: var(--text-muted);
}
.deps-vuln-meta a { color: var(--accent); text-decoration: none; }
.deps-vuln-meta a:hover { text-decoration: underline; }

.deps-fix {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px; margin-top: 8px;
  background: var(--surface);
  border: 1px dashed var(--border);
  border-radius: 4px;
  font-size: 11px;
  flex-wrap: wrap;
}
.deps-fix-label { color: var(--text-muted); font-size: 11px; }
.deps-fix-cmd {
  flex: 1; min-width: 200px;
  padding: 4px 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 3px;
  font-family: var(--font-mono, monospace);
  font-size: 11px;
  color: var(--text);
}
.deps-fix-note {
  margin-top: 8px; font-size: 11px; color: var(--text-muted);
}
.deps-fix-note code { color: var(--text); }

.deps-action-update {
  background: rgba(var(--online-rgb), .12);
  color: var(--online);
  border: 1px solid rgba(var(--online-rgb), .35);
  font-size: 10px; padding: 3px 10px;
}
.deps-action-update:hover {
  background: rgba(var(--online-rgb), .20);
}
.deps-clean-check { color: var(--online); font-weight: 700; }
.deps-parent { font-style: italic; }
.deps-issue-badge { white-space: nowrap; }

.deps-loading,
.deps-empty,
.deps-error {
  padding: 30px 14px;
  text-align: center;
  color: var(--text-muted);
  font-size: 13px;
}
.deps-error { color: var(--offline); }

.deps-howitworks {
  margin-top: 10px;
  padding: 10px 12px;
  background: rgba(var(--accent-rgb), .04);
  border: 1px dashed rgba(var(--accent-rgb), .30);
  border-radius: 4px;
  font-size: 11px; color: var(--text-muted);
}
.deps-howitworks summary {
  cursor: pointer; color: var(--accent); font-weight: 600;
  font-size: 11px;
}
.deps-howitworks ul { margin: 8px 0 0 16px; padding: 0; }
.deps-howitworks li { margin-bottom: 4px; }
.deps-howitworks code {
  font-size: 10px; padding: 1px 4px;
  background: var(--bg); border-radius: 2px;
  border: 1px solid var(--border);
}

/* Security tab variant of recovery-banner — orange/red trim
   indicating supply-chain findings instead of recovery-flag warning. */
.recovery-banner-supply {
  background: rgba(var(--offline-rgb), .08);
  border-color: rgba(var(--offline-rgb), .35);
  color: var(--offline);
}
.recovery-banner-supply .recovery-banner-text strong { color: var(--text); }
.recovery-banner-supply .recovery-banner-link {
  color: var(--accent); text-decoration: none; font-weight: 600;
}
.recovery-banner-supply .recovery-banner-link:hover { text-decoration: underline; }

/* Mobile: deps table collapses to two-line stacked rows.
   Line 1: package name + status badge (right-aligned)
   Line 2: version cell + action button (right-aligned) */
@media (max-width: 480px) {
  .deps-table-header { display: none; }
  .deps-row {
    flex-wrap: wrap;
    padding: 10px;
    row-gap: 4px;
  }
  .deps-col-name    { flex: 1 1 60% !important; min-width: 0; padding-right: 8px; font-size: 13px; }
  .deps-col-status  { flex: 0 0 auto !important; justify-content: flex-end; }
  .deps-col-version { flex: 1 1 60% !important; padding-right: 8px; }
  .deps-col-actions { flex: 0 0 auto !important; text-align: right; }
}

/* ── Connection Status Badge (header) ────────────────────────── */
.conn-status {
  display: flex; align-items: center; gap: 5px;
  font-size: 11px; color: var(--text-muted);
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 20px; padding: 3px 10px;
}
/* Connection status dot uses .dot .dot-sm base — state set by parent */
.conn-status.ok   { color: var(--online); border-color:rgba(var(--online-rgb),.3); background:rgba(var(--online-rgb),.08); }
.conn-status.ok   .dot { background: var(--online); }
.conn-status.fail { color: var(--offline); border-color:rgba(var(--offline-rgb),.3); background:rgba(var(--offline-rgb),.08); }
.conn-status.fail .dot { background: var(--offline); }

/* ── Layout Utilities ────────────────────────────────────────── */
.flex-row       { display: flex; gap: 16px; flex-wrap: wrap; }
.flex-row-tight { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.flex-col       { display: flex; flex-direction: column; gap: 8px; }
.flex-1         { flex: 1; }
/* ── createAccordionCard engine — shell (common.js) ──────────────────────────
   Generic collapsible card: header (grip · priority# · name/subtitle · collapse
   chevron · remove ✕) over a body. Styled ONCE here with generic .acc-* classes so
   any consumer reuses it — first (and currently only) consumer: Device Management's
   source/enrichment rows (createRowEditor renders one .re-row per card). DM-specific
   body styling is scoped under the `.dm-card` co-class further below (the engine's
   cardClass option adds it). See device-management.js renderSourceRow /
   renderEnrichmentRow and the engine registry in docs/MODULAR_ARCHITECTURE.md. */
.acc-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  overflow: hidden;
}
/* Accordion header — grip · priority# · name/subtitle · collapse chevron · remove.
   Clicking the header (off a control) toggles collapse; the grip is the drag handle. */
.acc-card-hd {
  display: flex; align-items: center; gap: 9px;
  padding: 10px 12px; cursor: pointer; user-select: none;
}
.acc-grip {
  flex: 0 0 auto; display: flex; align-items: center;
  color: var(--text-muted); opacity: .55;
  cursor: grab;
  touch-action: none;   /* the drag handler owns the gesture — no native scroll on the grip */
}
.acc-grip:hover { opacity: .9; color: var(--accent); }
.acc-prio {
  flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums;
  background: rgba(var(--accent-rgb), .15); color: var(--accent);
  border: 1px solid rgba(var(--accent-rgb), .4);
}
.acc-hd-main { flex: 1 1 auto; min-width: 0; }
.acc-card-name {
  display: block; color: var(--text); font-weight: 600; font-size: 14px;
  font-family: 'SF Mono','Fira Code',monospace;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.acc-card-sub { display: block; color: var(--text-muted); font-size: 11px; margin-top: 1px; }
.acc-collapse {
  flex: 0 0 auto; width: 26px; height: 26px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--text-muted); cursor: pointer;
}
.acc-collapse svg { transition: transform .15s; }
.acc-card:not(.collapsed) .acc-collapse svg { transform: rotate(180deg); }
.acc-card-body { padding: 0 12px 12px; }
.acc-card.collapsed .acc-card-body { display: none; }

/* createSortable (common.js) drag-to-reorder visuals — generic so any sortable
   list reuses them (first consumer: Device Management source/enrichment cards).
   Sibling rows slide only WHILE a drag is live (.sortable-active on the list), so
   the snap-back after drop is instant, not animated. */
.sortable-active .re-row { transition: transform .16s ease; }
.sortable-active, .sortable-active .acc-grip { cursor: grabbing; }
.sortable-dragging { opacity: .3; }                 /* placeholder left in the list */
.sortable-ghost {                                   /* floating clone following the finger */
  box-shadow: var(--shadow);
  border-radius: var(--radius);
  opacity: .97;
}
.sortable-ghost .acc-card { border-color: var(--accent); }
/* Corner remove (✕) — compact icon button, distinct from action .btn-s so it reads
   as "remove this card", not a primary action. Reddens on hover like other destructive. */
.acc-card-remove {
  width: 28px; height: 28px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 6px;
  background: transparent; color: var(--text-muted);
  font-size: 13px; line-height: 1; cursor: pointer;
  transition: color .15s, border-color .15s, background .15s, transform .1s;
}
.acc-card-remove:hover  { color: var(--offline); border-color: rgba(var(--offline-rgb), .5); background: rgba(var(--offline-rgb), .08); }
.acc-card-remove:active { transform: scale(0.95); }

/* Labeled field inside a card. The control (select / createFilterDropdown host)
   fills the card width. */
.dm-field { margin-bottom: 9px; }
.dm-field:last-child { margin-bottom: 0; }
/* .dm-card prefix so these beat the more-specific `.field label` (the card lives
   inside a .field on settings.html) — otherwise labels render bold/white 12px. */
.dm-card .dm-field-lbl { display: block; color: var(--text-muted); font-size: 11px; font-weight: 400; margin: 0 0 4px 2px; }
.dm-field > select.input-mono { width: 100%; }
/* Scope + Type (sources) / Scope + Match-by (enrichment) share a 2-up row. */
.dm-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-bottom: 9px; }
.dm-grid2 .dm-field { margin-bottom: 0; }

/* Every dropdown host in a DM card fills its field — createFilterDropdown (Scope/
   Type/Site + the device picker) AND createRadioDropdown (Provider/Tracking/
   Integration/Match-by). Each host carries .dropdown (added by buildFilterDD /
   createRadioDropdown), so one rule covers them all and any future card dropdown. */
.dm-card .dropdown-btn { width: 100%; min-width: 0; }
.dm-pick-dd { width: 100%; }
.dm-picker { margin-top: 9px; }

/* Count line: how many loaded devices match this row's full query (source + scope
   + type). Full-width clickable line (was a stranded pill) → opens that query's
   devices as JSON. Muted (.dm-count-zero) when nothing matches. */
.dm-count-line {
  display: flex; align-items: center; gap: 8px; width: 100%;
  margin-top: 11px; padding: 0;
  background: transparent; border: 0;
  color: var(--text-muted); font-size: 12px; text-align: left; cursor: pointer;
}
.dm-count-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--accent); border: 1px solid var(--accent); }
.dm-count-n { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
.dm-count-line:hover .dm-count-txt { color: var(--accent); }
/* Zero matches → muted dot + number so an empty query reads as "nothing here". */
.dm-count-zero .dm-count-dot { background: var(--surface2); border-color: var(--border); }
.dm-count-zero .dm-count-n   { color: var(--text-muted); }
.min-w-200      { min-width: 200px; }
.min-w-140      { min-width: 140px; }
.min-w-120      { min-width: 120px; }
.flex-fixed-100 { flex: 0 0 100px; }
/* CalVer strings (1.YYMMDD.HHMMSS, ~16 chars) overflow flex-fixed-100 and get
   clipped by the input's own width — pairs with .flex-fixed-100 per the
   modifier convention instead of widening it and affecting short fields
   (GitHub Commit short hash, Branch, Pull Request #) that fit fine at 100px. */
.flex-fixed-160 { flex: 0 0 160px; }
.flex-shrink-0  { flex-shrink: 0; }

/* ── Status Panel sub-components (used inside health panels) ── */
.status-panel-header { display: flex; align-items: center; gap: 8px; }
/* Title text is always primary (white) — only the dot carries status colour.
   Consistent across all integration health panels. */
.status-panel-header strong { color: var(--text); }
/* Small icon buttons in health panel headers (Logs + API).
   Standard: [dot] [title] ... [Logs] [API] — buttons grouped on the right.
   First button gets margin-left:auto to push both to the right edge. */
.health-endpoints-btn {
  cursor: pointer; background: none; border: 1px solid var(--border);
  border-radius: var(--radius); padding: 4px 6px; color: var(--text-muted);
  display: inline-flex; align-items: center; gap: 4px; font-size: 10px;
  font-family: inherit; transition: color .15s, border-color .15s;
}
/* First button in the header pushes the group to the right edge */
.status-panel-header .health-endpoints-btn:first-of-type { margin-left: auto; }
.health-endpoints-btn:hover { color: var(--accent); border-color: var(--accent); }
/* HTTP method badges inside the endpoints modal table */
.endpoint-method {
  display: inline-block; padding: 1px 6px; border-radius: 10px;
  font-size: 10px; font-weight: 600; letter-spacing: .3px;
  font-family: 'SF Mono', 'Fira Code', monospace;
}
.endpoint-method-get  { background: rgba(var(--online-rgb), .15); color: var(--online); }
.endpoint-method-post { background: rgba(var(--accent-rgb), .15); color: var(--accent); }

/* ── Endpoints Modal — Required + Observed sections (flex-based) ──
   Each row is a card-like flex column: main line (method + URL) on top,
   meta line (description + status badge) below. Long URLs wrap inside
   the row instead of squeezing other content. */

/* Section header — used standalone above each .ep-list group. */
.ep-section-divider {
  display: flex; align-items: center; gap: 8px;
  font-size: 11px; font-weight: 600; text-transform: uppercase;
  letter-spacing: .5px; color: var(--text-muted);
  margin: 16px 0 8px;
}
.ep-section-divider::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}
.ep-section-divider-standalone:first-child { margin-top: 4px; }

/* Row list container — vertical stack with consistent gap between rows. */
.ep-list {
  display: flex; flex-direction: column; gap: 6px;
}
.ep-row {
  padding: 8px 10px;
  border-radius: var(--radius);
  background: var(--surface);
  border: 1px solid var(--border);
  display: flex; flex-direction: column; gap: 4px;
  transition: background .12s, border-color .12s;
}
.ep-row:hover { background: var(--surface2); border-color: rgba(var(--accent-rgb), .35); }

/* Top line — method badge + URL. flex-wrap so a long URL drops below
   the badge on narrow widths instead of breaking layout. */
.ep-row-main {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
}
.ep-row-path {
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px;
  color: var(--accent); word-break: break-all; line-height: 1.4;
}

/* Bottom line — description + status badge. Indents to align under the URL
   on wider widths but wraps freely on narrow ones. */
.ep-row-meta {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 10px; font-size: 11px;
}
.ep-row-desc { color: var(--text-muted); }

/* Status badge: ✓ Active (observed since boot) or ⚠ Not observed yet.
   Uses existing online/warn variables — no new colors. */
.ep-status {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 6px; border-radius: 3px; font-weight: 500;
  white-space: nowrap;
}
.ep-status-active  { color: var(--online); background: rgba(var(--online-rgb), .12); }
.ep-status-pending { color: var(--warn);   background: rgba(var(--warn-rgb),   .15); }

.status-panel-meta   { display: flex; gap: 16px; flex-wrap: wrap; font-size: 11px; color: var(--text-muted); }
/* Tabular label for health panel metadata — fixed-width label so values align */
.hp-label            { display: inline-block; min-width: 120px; color: var(--text-muted); }
/* Checklist-style status rows (Railway-inspired) — green check + label */
.status-check      { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--text); }
.status-check-icon { flex-shrink: 0; }
.status-divider    { border: none; border-top: 1px solid var(--border); margin: 0; }
/* Collapsible scopes section within OAuth health panel */
.scopes-details    { padding: 0; margin-top: 2px; }
.scopes-summary    { cursor: pointer; list-style: none; user-select: none; }
.scopes-summary::-webkit-details-marker { display: none; } /* hide native marker */
/* Chevron rotates when <details> is open */
.scopes-chevron    { transition: transform .2s ease; color: var(--text-muted); }
.scopes-details[open] .scopes-chevron { transform: rotate(180deg); }
/* Individual scope items inside the expanded list — checkmarks the user likes */
.scopes-list       { display: flex; flex-direction: column; gap: 6px; padding: 8px 0 0 4px; }
.scopes-list-item  { font-size: 12px; color: var(--text-muted); }
/* Hint shown below a missing required scope */
.scope-missing-hint {
  font-size: 11px; color: var(--warn); margin-top: 4px; padding-left: 24px;
}

/* ── Impersonation Mode modal (header.js openImpersonationModal) ──────────────
   Pairs with the shared .modal-card (like .login-btn-full pairs with .btn): only
   widens the card. Role presets + a searchable user list. Colours via vars only. */
.imp-modal-card { width: 480px; }
.imp-section-label { font-size: 11px; font-weight: 700; letter-spacing: .5px; color: var(--text-muted); text-transform: uppercase; margin-top: 2px; }
.imp-role-row { display: flex; gap: 8px; }
.imp-role-option { flex: 1; display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 14px; cursor: pointer; text-align: left; transition: border-color .12s, background .12s; }
.imp-role-option:hover { border-color: var(--accent); background: rgba(var(--accent-rgb), .08); }
.imp-role-meta { margin-left: auto; font-size: 11px; color: var(--text-muted); }
.imp-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.imp-dot-editor { background: var(--accent); }
.imp-dot-viewer { background: var(--warn); }
.imp-user-scroll { max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 3px; border: 1px solid var(--border); border-radius: var(--radius); padding: 6px; background: var(--bg); }
.imp-user-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; background: transparent; border: none; border-radius: 6px; cursor: pointer; text-align: left; width: 100%; color: var(--text); }
.imp-user-row:hover { background: var(--surface2); }
.imp-user-avatar { width: 28px; height: 28px; border-radius: 50%; background: var(--surface2); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; color: var(--text-muted); flex: 0 0 auto; }
.imp-user-id { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.imp-user-name { font-size: 13px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.imp-user-email { font-size: 11px; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.imp-user-badge { font-size: 10.5px; font-weight: 600; padding: 2px 8px; border-radius: 10px; flex: 0 0 auto; }
.imp-badge-editor { background: rgba(var(--accent-rgb), .15); color: var(--accent); }
.imp-badge-viewer { background: rgba(var(--warn-rgb), .15); color: var(--warn); }
.imp-user-note { font-size: 11.5px; color: var(--text-muted); }
.imp-user-empty { padding: 12px; text-align: center; font-size: 12px; color: var(--text-muted); }

/* ── App Users card (Security tab) — health-card style box + grid rows ── */
/* Same visual treatment as .entra-health, .railway-health etc. — dark bg, border, rounded */
.appusers-health-box { padding: 10px 12px; border-radius: 6px; background: var(--bg); border: 1px solid var(--border); font-size: 12px; }

/* ── Roles & Permissions matrix (read-only role×capability grid, RBAC Phase 5) ──
   Each row: capability label (flex) + one fixed-width cell per role. Constrained
   to the .settings-wrap width like the Activity Log — 3 narrow role columns leave
   the rest for the label. ✓ = --online, — = --text-muted. No hardcoded colours. */
.roles-toolbar-hint { flex: 1; line-height: 1.4; }
.rp-grid { display: flex; flex-direction: column; }
.rp-row { display: flex; align-items: center; gap: 6px; padding: 5px 0; border-bottom: 1px solid var(--border); }
.rp-row:last-child { border-bottom: none; }
.rp-cap { flex: 1; min-width: 0; }
.rp-role { flex: 0 0 52px; text-align: center; }
.rp-head { font-weight: 600; color: var(--text-muted); border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); }
.rp-head .rp-role { font-size: 11px; text-transform: uppercase; letter-spacing: .03em; }
.rp-domain .rp-cap { font-size: 11px; text-transform: uppercase; letter-spacing: .03em; color: var(--accent); font-weight: 600; padding-top: 4px; }
.rp-domain { border-bottom: none; padding-bottom: 0; }
.rp-yes { color: var(--online); font-weight: 700; }
.rp-no { color: var(--text-muted); }
/* Editable matrix: toggle cells (editor/viewer), locked cells (admin + non-delegatable),
   and the Save/Reset action bar. Admin column & meta-caps never become toggles. */
.rp-locked { opacity: .55; }
.rp-toggle {
  display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border: 1px solid var(--border); border-radius: 4px;
  cursor: pointer; color: var(--online); font-weight: 700; font-size: 12px;
  background: var(--surface); user-select: none;
}
.rp-toggle:hover { border-color: var(--accent); }
.rp-toggle.on { background: rgba(var(--online-rgb), .15); border-color: var(--online); }
/* Cells whose value differs from the recommended (code) default — amber outline. */
.rp-changed { outline: 1.5px solid var(--warn); outline-offset: 2px; border-radius: 4px; }
/* Feather lock icon for non-delegatable cells (replaces the old 🔒 emoji). */
.rp-lock { display: inline-flex; align-items: center; justify-content: center; color: var(--text-muted); }
.rp-edit-actions {
  display: flex; justify-content: space-between; align-items: center; gap: 8px;
  flex-wrap: wrap; padding-top: 10px; margin-top: 6px; border-top: 1px solid var(--border);
}
.rp-reset-group, .rp-save-group { display: flex; gap: 6px; flex-wrap: wrap; }
.rp-legend { margin-top: 9px; font-size: 11px; color: var(--text-muted); display: flex; align-items: center; gap: 6px; }
.rp-legend-swatch { display: inline-block; width: 13px; height: 13px; border-radius: 4px; border: 1.5px solid var(--warn); flex-shrink: 0; }

/* Each row is a fixed-column grid so name/email, last-login, role, and the
   ⋮ menu line up across users instead of flex-wrapping unpredictably.
   Columns: [icon] [user (name + email stacked)] [last login] [role] [⋮].
   The user column flexes; everything else is a fixed pixel width. */
.appusers-row {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) 90px 110px 28px;
  align-items: center;
  column-gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
}
.appusers-row:last-child  { border-bottom: none; padding-bottom: 0; }
.appusers-row:first-child { padding-top: 0; }
/* Tiny header row that labels the columns. Same grid template as data rows. */
.appusers-header {
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-muted); opacity: .55;
  padding: 4px 0 6px; border-bottom: 1px solid var(--border);
}
.appusers-header span { white-space: nowrap; }
/* User column — name on top, email muted underneath */
.appusers-col-user {
  display: flex; flex-direction: column; gap: 1px; min-width: 0;
}
.appusers-name-line {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  min-width: 0;
}
.appusers-email {
  color: var(--text-muted); font-size: 11px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Inline status text shown next to the name (e.g. · Blocked / · Disabled in Entra) */
.appusers-status-label { font-size: 11px; white-space: nowrap; }
/* "(You)" tag — small, accent-coloured, no pill background */
.appusers-you-tag { font-size: 11px; font-weight: 500; }
.appusers-col-last { font-size: 11px; color: var(--text-muted); }
.appusers-col-role { display: flex; justify-content: flex-end; }
.appusers-row-icon { flex-shrink: 0; }

.appusers-role-select {
  font-size: 11px; padding: 3px 6px; width: 100%; max-width: 110px;
  background: var(--surface); border: 1px solid var(--border);
  color: var(--text); border-radius: 4px;
}
.appusers-role-select:disabled { opacity: 0.45; cursor: not-allowed; }
.appusers-manage-btn { padding: 2px 4px !important; min-height: auto !important; line-height: 1; }
/* Spacer keeps the ⋮ column width consistent for rows where the button is hidden (current user) */
.appusers-manage-spacer { display: inline-block; visibility: hidden; }

/* Two distinct dim levels — soft for app-blocked, harder for IDP-disabled */
.appusers-blocked      { opacity: 0.55; }
.appusers-idp-disabled { opacity: 0.4; }
/* Keep the ⋮ button readable even on dimmed rows so admins can still act on them */
.appusers-blocked .appusers-manage-btn,
.appusers-idp-disabled .appusers-manage-btn { opacity: 1; }

/* Mobile — collapse to a single column. Header row hidden; last-login and
   role drop below the name/email. */
@media (max-width: 480px) {
  .appusers-row {
    grid-template-columns: 18px minmax(0, 1fr) auto;
    grid-template-areas:
      "icon user manage"
      ".    last last"
      ".    role role";
    row-gap: 4px;
  }
  .appusers-row > .appusers-row-icon { grid-area: icon; }
  .appusers-row > .appusers-col-user { grid-area: user; }
  .appusers-row > .appusers-col-last { grid-area: last; }
  .appusers-row > .appusers-col-role { grid-area: role; justify-content: flex-start; }
  .appusers-row > .appusers-manage-btn,
  .appusers-row > .appusers-manage-spacer { grid-area: manage; }
  .appusers-header { display: none; }
}

/* Info-card user actions modal — replaces the old plain-text "Manage:" confirm.
   Renders inside #modal-message, which becomes a flex column when this content
   is present. */
/* Override .modal-message default muted color so values render in primary
   text colour. Sub-elements that need muted text re-state it explicitly. */
.appusers-modal { display: flex; flex-direction: column; gap: 12px; font-size: 12px; color: var(--text); text-align: left; }
.appusers-modal-subtitle { color: var(--text-muted); font-size: 11px; }
.appusers-modal-status {
  display: flex; flex-direction: column; gap: 6px;
  padding: 10px 12px; border-radius: 6px;
  background: var(--bg); border: 1px solid var(--border);
}
.appusers-modal-status-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.appusers-modal-status-label { color: var(--text-muted); font-size: 11px; }
.appusers-modal-status-value { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
.appusers-modal-meta {
  display: flex; flex-direction: column; gap: 4px;
  padding: 0 4px;
}
.appusers-modal-meta-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  font-size: 11px;
}
.appusers-modal-meta-label { color: var(--text-muted); }
.appusers-modal-note {
  display: flex; align-items: flex-start; gap: 6px;
  padding: 8px 10px; border-radius: 6px;
  background: rgba(var(--warn-rgb), .08);
  border: 1px solid rgba(var(--warn-rgb), .25);
  color: var(--text); font-size: 11px; line-height: 1.45;
}
.appusers-modal-note svg { flex-shrink: 0; color: var(--warn); margin-top: 1px; }

/* ── Permission audit display (Entra health panel + API modal) ── */
/* Section labels within the collapsible permissions list */
.perm-section-label { font-size: 11px; margin: 6px 0 2px; font-weight: 600; }
.perm-section-label:not(:first-child) { margin-top: 8px; }
/* Type badge pushed to the right in permission list items */
.perm-type-badge { margin-left: auto; font-size: 11px; }
/* Status icon in API modal permission table — centered in narrow first column */
.perm-status-cell { text-align: center; width: 24px; }
/* Inline SVG icons in permission rows need vertical centering */
.perm-status-icon { vertical-align: middle; }
/* Audit timestamp and notes use smaller text */
.perm-audit-note { font-size: 11px; }
.perm-audit-ts { font-size: 10px; }
/* Hint text shown when permission audit hasn't run yet */
.perm-audit-hint { font-size: 12px; padding: 4px 0; }
/* Dangerous section divider in API modal — uses offline color */
.ep-section-divider.ep-divider-danger { color: var(--offline); }
.ep-section-divider.ep-divider-warn { color: var(--warn); }
/* .status-dot removed — use .dot base class instead */
.deploy-list { font-size: 11px; color: var(--text-muted); display: flex; flex-direction: column; gap: 4px; }
.deploy-heading { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--text-muted); opacity: .6; margin-top: 4px; }
.deploy-row   { display: flex; align-items: center; gap: 6px; padding: 2px 0; }
.deploy-label { font-weight: 600; }
.deploy-id    { opacity: .5; }
.deploy-time  { margin-left: auto; opacity: .5; }
/* Deploy row reference links — Railway (the deploy that ran), Commit (the
   code that shipped, links to GitHub), PR (the change that introduced it).
   Three distinct peers, not one combined reference — each points somewhere
   different, so each is its own clickable element. */
.deploy-ref-link {
  display: inline-flex; align-items: center; gap: 3px;
  color: var(--text-muted); text-decoration: none;
}
.deploy-ref-link:hover { color: var(--accent); }
.deploy-commit { color: var(--accent); text-decoration: none; font-family: 'SF Mono','Fira Code',monospace; }
.deploy-commit:hover { text-decoration: underline; }
.deploy-pr     { color: var(--text-muted); opacity: .7; }

/* Deploy Timing comparison — TEMPORARY (see settings.html railway-deploy-timing-wrap
   comment). Two candidate deploy-duration metrics shown for comparison against
   Railway's own dashboard; removed once one is picked. */
.timing-test-row   { display: flex; align-items: baseline; gap: 8px; padding: 3px 0; }
.timing-test-label { font-size: 11px; color: var(--text-muted); min-width: 210px; }
.timing-test-value { font-size: 12px; font-weight: 700; }
.timing-test-value.is-muted { color: var(--text-muted); opacity: .7; font-weight: 400; }
.timing-test-note  { font-size: 10px; color: var(--text-muted); opacity: .65; margin-top: 2px; }

/* ── GraphQL operation detail sub-rows (Railway endpoints modal) ── */
/* Detail row sits directly below its parent row — no border, no hover,
   so it reads as a continuation rather than a separate entry. */
.ep-detail-row td { border-bottom: none; }
.ep-detail-row:hover { background: transparent; }
.ep-detail-cell {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 2px 0 6px;
}
/* Operation type badge — small pill matching .api-badge sizing */
.ep-op-badge {
  display: inline-block; padding: 1px 6px; border-radius: 4px;
  font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
}
.ep-op-badge-query    { background: rgba(var(--accent-rgb, 0,164,228), .15); color: var(--accent); }
.ep-op-badge-mutation { background: rgba(var(--warn-rgb, 210,153,34), .15);  color: var(--warn); }
/* Operation name — monospace to match endpoint URL column */
.ep-op-name {
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; color: var(--text);
}
/* Variables and auth info — muted secondary text */
.ep-op-meta { font-size: 10px; color: var(--text-muted); }
.ep-op-sep  { color: var(--text-muted); opacity: .4; }

/* ── Integration context row (e.g. Railway project/service/environment IDs) ── */
.ep-context {
  display: flex; flex-wrap: wrap; gap: 6px 16px;
  padding: 6px 0 2px; margin-bottom: 4px;
  border-bottom: 1px solid var(--border);
}
.ep-context-item { font-size: 11px; color: var(--text-muted); }
.ep-context-label { font-weight: 600; }
.ep-context-value {
  font-family: 'SF Mono', 'Fira Code', monospace; font-size: 10px;
  background: var(--surface2); padding: 1px 5px; border-radius: 3px;
  color: var(--text);
}

/* ── Section Labels & Tags ───────────────────────────────────── */
.section-label   { font-size: 12px; font-weight: 600; color: var(--text); }
.tag-optional {
  font-weight: 400; text-transform: none; letter-spacing: 0;
  opacity: .55; font-size: 11px;
}


/* ── Misc Reusable ───────────────────────────────────────────── */
/* Divider between field groups and connect/action sections — needs more
   breathing room than the default form-body gap provides */
.section-divider { border: none; border-top: 1px solid var(--border); margin: 8px 0; }
/* Subsection heading within a card — used for custom areas below common actions */
.subsection-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin: 4px 0 2px; }
/* Auth method toggle — segmented control for OAuth vs Manual Bearer Token */
.auth-method-toggle {
  display: flex; gap: 0; margin-bottom: 12px;
  border: 1px solid var(--border); border-radius: var(--radius);
  overflow: hidden;
}
.auth-method-btn {
  flex: 1; padding: 7px 12px;
  font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .03em;
  background: transparent; color: var(--text-muted);
  border: none; cursor: pointer; transition: background .15s, color .15s;
}
.auth-method-btn:hover { background: var(--surface2); color: var(--text); }
.auth-method-btn.selected {
  background: var(--surface2); color: var(--text);
  box-shadow: inset 0 -2px 0 var(--accent);
}

.font-bold    { font-weight: 600; }
.text-offline { color: var(--offline); }
.text-online  { color: var(--online); }
.text-warn    { color: var(--warn); }
.text-accent  { color: var(--accent); }
.mt-4         { margin-top: 4px; }
.mt-8         { margin-top: 8px; }
.mb-8         { margin-bottom: 8px; }
.mb-10        { margin-bottom: 10px; }
.mb-12        { margin-bottom: 12px; }
.m-0          { margin: 0; }

/* ── Save Section ────────────────────────────────────────────── */
.save-section  { display: flex; flex-direction: column; gap: 8px; }

/* ── Health Panels (all integrations) ───────────────────────── */
/* Shared compact card aesthetic: dot + bold title, detail lines  */
.unifi-health,
.oauth-health,
.token-health,
.railway-health,
.entra-health,
.teams-health,
.smtp2go-health {
  padding: 10px 12px; border-radius: 6px;
  background: var(--bg); border: 1px solid var(--border);
  font-size: 12px; line-height: 1.7;
}
.unifi-health-footer,
.oauth-health-footer,
.token-health-footer,
.railway-health-footer,
.teams-health-footer,
.smtp2go-health-footer { font-size: 11px; opacity: .45; margin-top: 4px; }
/* Token line: access + refresh side by side, wraps on small screens */
.oauth-health-tokens { font-size: 12px; color: var(--text-muted); display: flex; gap: 4px; flex-wrap: wrap; }
.oauth-health-sep    { color: var(--text-muted); opacity: .4; }
/* Individual controller rows in UniFi health panel */
.status-panel-hosts     { display: flex; flex-direction: column; gap: 4px; margin: 6px 0 2px; }
.status-panel-host-row  { display: flex; align-items: center; gap: 8px; font-size: 12px; }

/* ── Health Panel Section Dividers ─────────────────────────── */
/* Thin labeled dividers inside health panels to separate sections:
   Authentication, Details, Performance. Uses the same pattern as
   .ep-section-divider (endpoints modal) but sized for health panels. */
.hp-section {
  display: flex; align-items: center; gap: 8px;
  font-size: 9px; font-weight: 700; text-transform: uppercase;
  letter-spacing: .06em; color: var(--text-muted); opacity: .5;
  margin: 6px 0 2px;
}
.hp-section::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}

/* ── Health Panel Connect Animation ────────────────────────── */
/* Glow effect when a health panel first appears after connecting.
   Applied via JS, removed after animation completes. */
@keyframes hp-connect-glow {
  0%   { box-shadow: 0 0 0 0 rgba(var(--online-rgb), .4); }
  40%  { box-shadow: 0 0 12px 2px rgba(var(--online-rgb), .25); }
  100% { box-shadow: 0 0 0 0 rgba(var(--online-rgb), 0); }
}
.hp-glow {
  animation: hp-connect-glow 1.2s ease-out;
}

/* (Removed: .badge-xs — all badges now use the same standard size) */

/* ── Redirect URI row — copyable URL with inline copy button ── */
.redirect-uri-row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 10px; border-radius: 6px;
  background: var(--bg); border: 1px solid var(--border);
}
.redirect-uri-value {
  font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
  font-size: 12px; color: var(--text-muted); user-select: all;
}
/* Ghost-style copy button — transparent, inherits color, no border */
.btn-copy {
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer;
  color: var(--text-muted); padding: 4px; border-radius: 4px;
  margin-left: auto; /* push to right */
  transition: color .15s, background .15s;
}
.btn-copy:hover { color: var(--accent); background: rgba(0,164,228,.1); }
/* Spinning state for async copy actions (fetch-and-copy debug JSON).
   Mirrors the .btn.spinning rule (style.css:1597) so .btn-copy can share
   the same spinner via the .spinning class. */
.btn-copy.spinning svg { animation: spin .6s linear infinite; }
.btn-copy.spinning { pointer-events: none; opacity: 0.7; }

/* Pairing code display (Instance Mirror "Link Another Instance") — larger
   variant of .redirect-uri-value; sits inside the same .redirect-uri-row
   container. Bigger/bolder because it's read at a glance and hand-typed into
   another instance, unlike the small inline redirect URI. */
.mirror-code-value {
  font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
  font-size: 22px; font-weight: 700; letter-spacing: 2px; color: var(--text);
}

/* Local Backups list (Data Management card) — a stack of .redirect-uri-row
   entries, one per auto-saved pre-restore snapshot. Only new rule needed is
   the vertical stacking; each row itself reuses .redirect-uri-row unmodified. */
.local-backups-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.local-backups-empty { color: var(--text-muted); font-size: 12px; margin-top: 6px; }

/* Diff Confirm / Diff Result modals (Sync Data "Receive" + Local Backups
   Restore) — a per-store checklist of what's available to sync/restore,
   with counts, so an overwrite-everything action gets the same "what will
   actually change" transparency as e.g. the device Location Move flow. */
.diff-badges { display: flex; gap: 8px; margin: 4px 0 10px; }
.diff-badge { font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 999px; }
.diff-badge.changed   { background: rgba(var(--accent-rgb), .15); color: var(--accent); }
.diff-badge.unchanged { background: var(--surface2); color: var(--text-muted); }
.select-row { display: flex; justify-content: flex-end; align-items: center; margin-bottom: 10px; }
.select-links { display: flex; gap: 10px; font-size: 12px; }
.select-links a { color: var(--accent); cursor: pointer; text-decoration: none; }
.select-links a:hover { text-decoration: underline; }
.diff-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 6px; background: var(--surface2); margin-bottom: 6px; font-size: 13px; }
.diff-row .k { display: flex; align-items: center; gap: 8px; color: var(--text); flex: 1; cursor: pointer; }
.diff-row .v { color: var(--text-muted); font-size: 12px; white-space: nowrap; }
.diff-row.unchanged .k, .diff-row.unchanged .v { opacity: .6; }
.diff-row input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--accent); flex: 0 0 auto; }
.diff-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.diff-dot.changed   { background: var(--accent); }
.diff-dot.unchanged { background: var(--text-muted); }
.diff-warn-box { background: rgba(var(--warn-rgb), .1); border: 1px solid rgba(var(--warn-rgb), .3); border-radius: 8px; padding: 10px 12px; margin-top: 14px; font-size: 12px; color: var(--text-muted); }
.diff-warn-box div { margin-bottom: 4px; display: flex; gap: 6px; }
.diff-warn-box div:last-child { margin-bottom: 0; }
.diff-result-title { display: flex; align-items: center; gap: 8px; }
.diff-result-icon { width: 20px; height: 20px; color: var(--online); flex: 0 0 auto; }

/* ── Page Layout ─────────────────────────────────────────────── */
/* Centred content wrapper for single-column pages (settings, etc.) */
/* width:100% is required because body is display:flex and margin:auto
   overrides flex stretch — without it, the wrap shrinks to content width
   and cards change width when toggling between health panels and setup forms */
.settings-wrap {
  width: 100%;
  max-width: 620px;
  margin: 32px auto;
  padding: 0 20px 60px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* Recovery-flag warning banners — shown when one-time Railway Variables
   (CLEAR_APP_PASSWORD, CLEAR_ALLOWED_IPS) are still set after use. */
.recovery-banner {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--radius);
  background: rgba(var(--warn-rgb), .10);
  border: 1px solid rgba(var(--warn-rgb), .30);
  color: var(--warn);
  font-size: 13px;
  line-height: 1.45;
}
.recovery-banner-icon { flex-shrink: 0; margin-top: 1px; }
.recovery-banner-text { flex: 1; }
.recovery-banner-text strong { color: var(--text); }
.recovery-banner .btn-ghost {
  flex-shrink: 0;
  align-self: center;
  color: var(--text-muted);
}
/* Break-glass armed banner — full-width wrapper so the reused .recovery-banner
   sits with page padding when header.js injects it app-wide (outside .settings-wrap). */
.breakglass-bar { padding: 10px 16px 0; }

.page-title {
  display: flex; align-items: center; gap: 10px;
  font-size: 18px; font-weight: 700; color: var(--text);
}
.page-title svg { width: 18px; height: 18px; color: var(--accent); flex-shrink: 0; }

/* ── Login Page ──────────────────────────────────────────────── */
/* Centred card layout for the login/welcome screen */
.login-wrap {
  flex: 1; display: flex; align-items: center; justify-content: center;
  padding: 20px;
}
.login-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 32px; width: 100%;
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  position: relative; /* anchor for deploy overlay */
  overflow: hidden;    /* overlay respects border-radius */
}
/* When login logo is inside .logo-wrap (for random animations),
   override the default 64px sizing from logo-animations.css.
   border-radius: 50% kept for round logo appearance with glow. */
.login-card .logo-wrap { width: 80px; height: 80px; }
.login-card .logo-wrap img { width: 56px; height: 56px; }
.login-logo {
  width: 56px; height: 56px;
  border-radius: 50%;
  transition: filter .6s, box-shadow .6s;
}
/* Status glow — login page applies one of these based on /api/auth-status siteStatus.
   Logo stays at full opacity; only the drop-shadow glow breathes in/out.
   Color transitions happen at the breath trough (JS waits for animationiteration
   event before swapping classes) so the glow fades out as one color, fades in as another. */
.login-logo.glow-on {
  animation: glow-breathe-green 2s ease-in-out infinite;
}
.login-logo.glow-warn {
  animation: glow-breathe-warn 2s ease-in-out infinite;
}
.login-logo.glow-off {
  filter: grayscale(.6) drop-shadow(0 0 6px rgba(var(--text-muted-rgb), .25));
}
@keyframes glow-breathe-green {
  0%, 100% { filter: drop-shadow(0 0 4px var(--online)) drop-shadow(0 0 10px rgba(var(--online-rgb), .3)); }
  50%      { filter: drop-shadow(0 0 8px var(--online)) drop-shadow(0 0 18px rgba(var(--online-rgb), .5)); }
}
/* Warn glow uses a vivid orange (#f57c00 / 245,124,0) instead of the
   muted amber --warn (#d29922) — ensures clear contrast against green glow. */
@keyframes glow-breathe-warn {
  0%, 100% { filter: drop-shadow(0 0 4px #f57c00) drop-shadow(0 0 10px rgba(245, 124, 0, .35)); }
  50%      { filter: drop-shadow(0 0 8px #f57c00) drop-shadow(0 0 18px rgba(245, 124, 0, .55)); }
}
/* Deploy overlay — covers the login card when user tries to sign in during
   a Railway deploy. Shown via removing .hidden; fades in via opacity transition. */
.deploy-overlay {
  position: absolute; inset: 0;
  background: rgba(13, 17, 23, .92); /* matches --bg #0d1117 at 92% opacity */
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--radius);
  z-index: 10;
  opacity: 1;
  transition: opacity .3s ease;
}
.deploy-overlay.hidden { display: none; }
.deploy-overlay-content {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  text-align: center; padding: 24px;
}
/* Deploy logo wrapper — positions radar rings relative to the logo.
   Radar ping animation: sonar-style rings pulse outward while the app restarts. */
.deploy-logo-wrap {
  position: relative;
  width: 56px; height: 56px;
  display: flex; align-items: center; justify-content: center;
}
.deploy-logo {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  position: relative;
  z-index: 2;
}
/* Sonar rings — expand and fade from the logo center */
.deploy-radar-ring {
  position: absolute;
  width: 56px; height: 56px;
  border-radius: 50%;
  border: 2px solid var(--warn);
  animation: deploy-radar 2s ease-out infinite;
  pointer-events: none;
  z-index: 1;
}
.deploy-radar-ring:nth-child(2) { animation-delay: .66s; }
.deploy-radar-ring:nth-child(3) { animation-delay: 1.33s; }
@keyframes deploy-radar {
  0%   { transform: scale(1);   opacity: .7; }
  100% { transform: scale(2.5); opacity: 0; }
}
/* Lockout overlay — reuses deploy overlay layout with red rings for rate-limit lockout.
   Countdown timer auto-dismisses when the lockout window expires. */
.lockout-ring {
  border-color: var(--offline);
}
.deploy-overlay-title {
  font-size: 18px; font-weight: 700; color: var(--text);
}
.deploy-overlay-text {
  font-size: 13px; color: var(--text-muted); line-height: 1.5;
}
.login-title {
  font-size: 18px; font-weight: 700; color: var(--text); text-align: center;
}
.login-subtitle {
  font-size: 13px; color: var(--text-muted); text-align: center; margin-top: -8px;
}
.login-version {
  font-size: 11px; color: var(--text-muted); text-align: center; margin-top: -10px;
  font-family: 'SF Mono', 'Fira Code', monospace; opacity: .8;
}
.login-field {
  display: flex; flex-direction: column; gap: 5px; width: 100%;
}
.login-field label { font-size: 12px; font-weight: 600; color: var(--text); }
.login-field input {
  background: var(--bg); border: 1px solid var(--border);
  border-radius: var(--radius); color: var(--text);
  font-size: 14px; padding: 10px 14px; outline: none;
  transition: border-color .15s; width: 100%; box-sizing: border-box;
}
.login-field input:focus { border-color: var(--accent); }
/* Full-width override for login form — pairs with .btn .btn-primary so the
   login button uses the standard component system instead of a separate class.
   Only width and padding differ from the base .btn-primary styles. */
.login-btn-full {
  width: 100%; padding: 10px 16px; font-size: 14px;
  justify-content: center;
}
/* ── Disabled account page ── */
.disabled-icon    { text-align: center; margin: 16px 0 8px; }
.disabled-heading { text-align: center; font-size: 18px; font-weight: 700; color: var(--offline); margin-bottom: 6px; }
.disabled-message { text-align: center; font-size: 13px; color: var(--text-muted); margin-bottom: 20px; line-height: 1.5; }

/* Error banner — uses CSS variables instead of hardcoded rgba */
.login-error {
  font-size: 12px; color: var(--offline); width: 100%;
  background: rgba(var(--offline-rgb),.12); border: 1px solid rgba(var(--offline-rgb),.35);
  border-radius: var(--radius); padding: 8px 12px;
}
.login-error.login-success {
  color: var(--online);
  background: rgba(var(--online-rgb),.12);
  border-color: rgba(var(--online-rgb),.35);
}
/* "or" divider between password form and Microsoft button */
.login-divider {
  display: flex; align-items: center; gap: 12px;
  margin: 12px 0; width: 100%; color: var(--text-muted); font-size: 12px;
}
.login-divider::before, .login-divider::after {
  content: ''; flex: 1; height: 1px; background: var(--border);
}
/* Microsoft "Sign in" button — uses .btn .btn-secondary base plus login-specific sizing */
.login-entra-btn {
  display: inline-flex; align-items: center; gap: 8px;
  text-decoration: none; /* it's an <a> tag */
}
.login-entra-btn svg { flex-shrink: 0; }

/* Debug tab styles (.debug-table, .debug-label, .debug-value, .debug-ua,
   .debug-opt-text, .debug-opt-label, .debug-opt-path, .debug-endpoint-pick,
   .debug-pre) moved into module/mod_debug/public/debug-tab.css and loaded
   only on settings.html via a <link> in that file's head. */

/* ── Admin debug endpoint list (Settings → Troubleshooting card) ─────────
   One row per /api/debug/* link — friendly label · arrow · clickable URL.
   Reuses existing surface/border/text variables, .input-mono for the URL
   font. URL is the focal point so it grows; label is fixed-width so the
   arrows align in a column. */
.debug-endpoint-list {
  list-style: none; margin: 0; padding: 8px 12px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius);
  display: flex; flex-direction: column; gap: 2px;
  font-size: 12px;
}
.debug-endpoint-list li {
  display: flex; align-items: center; gap: 10px;
  padding: 4px 0;
}
.debug-endpoint-label {
  flex: 0 0 160px;
  color: var(--text);
}
.debug-endpoint-arrow {
  color: var(--text-muted);
  flex: 0 0 12px;
}
.debug-endpoint-url {
  flex: 1;
  color: var(--accent);
  font-size: 11px;
  text-decoration: none;
}
.debug-endpoint-url:hover { text-decoration: underline; }

/* ══════════════════════════════════════════════════════════════
   Mobile responsive — larger tap targets, stacked layouts,
   reduced padding for narrow screens.
   ══════════════════════════════════════════════════════════════ */
@media (max-width: 480px) {
  /* Reduce page padding on phones */
  .settings-wrap { padding: 0 12px 40px; margin: 16px auto; }
  .form-body { padding: 12px; }
  .form-card-header { padding: 8px 12px; }

  /* Stack side-by-side fields vertically */
  .flex-row { flex-direction: column; gap: 10px; }
  .min-w-200, .min-w-140, .min-w-120 { min-width: 0; }
  /* flex-fixed-* sets flex-BASIS for the horizontal (row) layout = a fixed WIDTH.
     Once .flex-row flips to column above, that basis becomes a fixed HEIGHT, so
     e.g. the About card's Version / Commit / Branch / PR fields render 100–160px
     tall with a big empty gap below the input. Reset to content height when stacked. */
  .flex-fixed-100, .flex-fixed-160 { flex: 0 0 auto; }

  /* Larger tap targets for mobile (44px accessibility minimum) */
  .btn { padding: 10px 16px; font-size: 14px; }
  .btn-sm { padding: 8px 14px; font-size: 13px; }
  /* 16px minimum prevents iOS Safari auto-zoom on focus */
  .field input, .field select, #search { font-size: 16px; }
  .field input, .field select { padding: 10px 12px; }

  /* Tighter tab labels on phones. The row holds 5 tabs and scrolls horizontally
     (see .settings-tabs overflow-x) — smaller padding/font fits more before a swipe. */
  .settings-tab { padding: 8px 10px; font-size: 12px; }

  /* Header + main content — tighter vertical gaps on mobile */
  .header-inner { padding: 0 12px; }
  main { padding: 8px 4px 4px; gap: 6px; }

  /* Brand title — truncate on narrow screens */
  #brand-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 120px; }

  /* Summary grid is always compact on mobile (forced by JS).
     Cards fill full width instead of shrinking to content. */
  .summary-grid.compact-left .card {
    flex: 1 1 0;
  }

  /* Controls bar — search full width on top, icon-only filters */
  .search-group { min-width: 100%; order: -1; }
  .dropdown-btn > span { display: none; }       /* hide text labels */
  .dropdown-btn .dropdown-chevron { display: none; } /* hide chevrons */
  .dropdown-btn { min-width: 36px; padding: 0; justify-content: center; }
  .btn-label { display: none; }                 /* hide Export/Refresh text labels */
  .refresh-countdown { display: none; }         /* hide countdown timer on mobile */
  .controls { gap: 6px; }
  /* All toolbar controls: uniform 36px square on mobile */
  .clear-filters-btn { padding: 0; width: 36px; height: 36px; min-width: 36px; justify-content: center; }
  .clear-filters-btn .btn-label { display: none; }
  .refresh-btn { padding: 0; width: 36px; min-width: 36px; justify-content: center; }
  .toolbar-actions { margin-left: auto; gap: 6px; }
  #export-dropdown .dropdown-btn { min-width: 36px; padding: 0; justify-content: center; }

  /* Device Management dropdowns (scope / type / enrich-scope / device-picker) are
     FORM FIELDS, not toolbar icons — exempt them from the global mobile collapse
     above (.dropdown-btn > span / .dropdown-chevron { display:none } + 36px icon-only),
     same rationale as .tenant-title-btn. Without this the scope/type dropdowns shrink
     to blank vertical slivers on phones (the reported bug). Keep them full-width + labeled. */
  .dm-card .dropdown-btn { min-width: 0; width: 100%; padding: 0 12px; justify-content: space-between; }
  .dm-card .dropdown-btn > span { display: inline; }
  .dm-card .dropdown-btn .dropdown-chevron { display: inline; }


  /* Data table — fill remaining viewport below sticky header/summary/controls.
     This prevents page-level scrolling on mobile: all scrolling happens inside
     the table-wrapper, so the sticky thead (top: 0) stays visible below the
     controls bar instead of sliding behind it.
     57px = header (56px + 1px border). --summary-sticky-h and --controls-sticky-h
     are maintained by layout.js ResizeObserver. 20px = main top padding (8px) + 2 flex gaps (6px × 2).
     Primary: --app-vh (set by layout.js from window.innerHeight — always correct on iOS).
     Fallback chain: dvh → vh for browsers where JS hasn't loaded yet. */
  .table-wrapper {
    max-height: calc(100vh - 57px - var(--summary-sticky-h) - var(--controls-sticky-h) - 20px);
    max-height: calc(100dvh - 57px - var(--summary-sticky-h) - var(--controls-sticky-h) - 20px);
    max-height: calc(var(--app-vh, 100dvh) - 57px - var(--summary-sticky-h) - var(--controls-sticky-h) - 20px);
  }

  /* Footer — hide on mobile to reclaim vertical space for the device table */
  footer { display: none; }
}

/* ── Accordion Layout ─────────────────────────────────────────────
   Layout-agnostic card accordion. Used by both Integrations and Security
   tabs. Card identity uses tab-card-sub convention (e.g. int-card-webex,
   sec-card-access). These classes are purely presentational — swappable
   without changing card IDs. */
.accordion { display: flex; flex-direction: column; gap: 6px; }
.accordion-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); overflow: hidden; transition: all .2s;
}
/* Unconfigured (muted) state — dimmed, [+] button is only bright element */
.accordion-card.card-muted {
  opacity: .55; border-color: rgba(var(--border-rgb, 48, 54, 61), .5);
}
.accordion-card.card-muted:hover { opacity: .75; }
/* Expanded or connected — full opacity, accent border */
.accordion-card.expanded { opacity: 1; border-color: var(--accent); }
.accordion-card.card-connected { opacity: 1; }
.accordion-card.card-connected.expanded { border-color: var(--accent); }
/* Header row */
.accordion-header {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; transition: background .15s; user-select: none;
}
.accordion-header:hover { background: var(--surface2); }
.accordion-header .card-logo {
  width: 24px; height: 24px; display: flex;
  align-items: center; justify-content: center; flex-shrink: 0;
}
.accordion-header .card-logo img {
  height: 20px; width: auto; filter: brightness(0) invert(1); opacity: .8;
}
.accordion-header .card-logo svg { width: 20px; height: 20px; }
.accordion-header .card-name { font-size: 13px; font-weight: 600; flex: 1; }
/* Status shown for connected services */
.accordion-header .card-status {
  display: flex; align-items: center; gap: 6px;
  font-size: 12px; color: var(--text-muted); margin-right: 8px;
}
/* [+] button for unconfigured cards — accent blue, the only bright element */
.accordion-add {
  width: 28px; height: 28px; border-radius: 6px;
  border: 1px solid var(--accent); background: transparent;
  color: var(--accent); cursor: pointer; display: flex;
  align-items: center; justify-content: center;
  transition: all .15s; flex-shrink: 0; opacity: 1;
  /* Override parent muted opacity so button stays bright */
  filter: none;
}
/* Undo muted opacity for the [+] button specifically */
.card-muted .accordion-add { opacity: calc(1 / .55); }
.card-muted:hover .accordion-add { opacity: calc(1 / .75); }
.accordion-add:hover { background: rgba(var(--accent-rgb), .15); }
/* Chevron for connected cards */
.accordion-chevron {
  width: 16px; height: 16px; color: var(--text-muted);
  transition: transform .2s; flex-shrink: 0; cursor: pointer;
}
.accordion-card.expanded .accordion-chevron { transform: rotate(90deg); }
/* Expandable body — form content lives directly in each card */
.accordion-body {
  display: none; border-top: 1px solid var(--border); padding: 16px;
}
.accordion-card.expanded .accordion-body { display: block; }

/* Accordion empty state */
.card-empty {
  text-align: center; padding: 24px;
  color: var(--text-muted); font-size: 13px;
}

/* Mobile adjustments for accordion layout */
@media (max-width: 480px) {
  .accordion-header .card-status span:not(.dot) { display: none; }
  .accordion-body { padding: 12px; }

  /* ── Activity Log mobile ──────────────────────────────────── */
  /* Toolbar: stack search full-width on top, filters below */
  .alog-toolbar { gap: 6px; }
  .alog-toolbar .search-group { min-width: 100%; order: -1; }
  .alog-toolbar .dropdown-btn > span { display: none; }
  .alog-toolbar .dropdown-btn .dropdown-chevron { display: none; }
  .alog-toolbar .dropdown-btn { min-width: 36px; justify-content: center; }

  /* Hide the column-label header row — stacked entries are self-describing */
  .activity-log-header { display: none; }

  /* Entries: stack vertically instead of fixed-width horizontal row */
  .activity-log-entry {
    flex-wrap: wrap; gap: 4px 8px; padding: 8px 10px;
  }
  /* Reset fixed column widths — let content size naturally */
  .alog-col-ts    { flex: 0 0 auto; }
  .alog-col-type  { flex: 0 0 auto; }
  .alog-col-msg   { flex: 1 1 100%; order: 1; }
  .alog-col-actor { flex: 0 0 auto; }
  .alog-col-ip    { flex: 0 0 auto; }

  /* Viewport-relative height instead of fixed 480px */
  .activity-log-list { max-height: 60vh; max-height: 60dvh; }
}

/* ── Alerts Manager ──────────────────────────────────────────── */

/* Empty state */
.alert-empty-state {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 32px 16px;
}
.alert-empty-title {
  color: var(--text-muted); font-size: 13px; font-weight: 600;
}
.alert-empty-hint {
  color: var(--text-muted); font-size: 12px; opacity: .7; margin-top: 4px;
}

/* 3-column wizard layout */
.alert-wizard {
  display: flex; gap: 16px;
}
.alert-wizard-section {
  flex: 1; min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
  padding: 0;
  overflow: hidden;
}
.alert-wizard-section-title {
  display: flex; align-items: center; gap: 6px;
  padding: 10px 14px;
  font-size: 12px; font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: .5px;
  background: var(--surface2);
  border-bottom: 1px solid var(--border);
}
.alert-wizard-body {
  padding: 14px;
}

/* Checkboxes and radios */
.alert-checkbox, .alert-radio {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 0; cursor: pointer;
  font-size: 13px; color: var(--text);
}
.alert-checkbox input, .alert-radio input {
  width: 15px; height: 15px;
}

/* Channel status indicator */
.alert-channel-status {
  font-size: 11px; margin-left: auto;
}
.alert-channel-ok { color: var(--online); }
.alert-channel-off { color: var(--text-muted); opacity: .6; }

/* Multi-select dropdowns */
.alert-multi-select {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  font-size: 12px;
  padding: 2px;
}
.alert-multi-select option {
  padding: 4px 8px;
}
.alert-multi-select option:checked {
  background: rgba(var(--accent-rgb), .2);
  color: var(--accent);
}

/* Day picker (schedule) */
.alert-day-picker {
  display: flex; gap: 4px;
}
.alert-day {
  width: 28px; height: 28px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text-muted);
  font-size: 11px; font-weight: 600;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .15s, color .15s, border-color .15s;
}
.alert-day:hover { border-color: var(--accent); color: var(--text); }
.alert-day.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Time inputs */
.alert-wizard input[type="time"] {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 4px;
  color: var(--text);
  padding: 4px 8px;
  font-size: 12px;
  width: 100%;
}

/* ── Alert Rule Cards ──────────────────────────────────────── */
#alert-rules-list { gap: 6px; }
.alert-rule-card {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg);
}
.alert-rule-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 12px 14px; gap: 12px;
}
.alert-rule-info { flex: 1; min-width: 0; }
.alert-rule-name {
  font-size: 13px; font-weight: 600;
  color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.alert-rule-meta {
  display: flex; flex-wrap: wrap; gap: 6px;
  align-items: center;
  margin-top: 4px;
  font-size: 11px; color: var(--text-muted);
}
.alert-rule-actions {
  display: flex; align-items: center; gap: 8px;
  flex-shrink: 0;
}
.alert-rule-stats {
  font-size: 11px; color: var(--text-muted);
  white-space: nowrap;
}

/* Tags (event/channel badges) */
.alert-tag {
  display: inline-flex;
  padding: 1px 6px;
  border-radius: 3px;
  font-size: 10px; font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .3px;
}
.alert-tag-offline {
  background: rgba(var(--offline-rgb), .15);
  color: var(--offline);
}
.alert-tag-online {
  background: rgba(var(--online-rgb), .15);
  color: var(--online);
}
.alert-tag-signin {
  background: rgba(var(--accent-rgb), .15);
  color: var(--accent);
}
.alert-tag-lockout {
  background: rgba(var(--warn-rgb), .15);
  color: var(--warn);
}
.alert-tag-channel {
  background: rgba(var(--accent-rgb), .12);
  color: var(--accent);
}

/* Toggle switch */
.alert-toggle-switch {
  position: relative;
  display: inline-block;
  width: 36px; height: 20px;
  cursor: pointer;
}
.alert-toggle-switch input {
  opacity: 0; width: 0; height: 0;
}
.alert-toggle-slider {
  position: absolute;
  inset: 0;
  background: var(--border);
  border-radius: 10px;
  transition: background .2s;
}
.alert-toggle-slider::before {
  content: '';
  position: absolute;
  width: 16px; height: 16px;
  left: 2px; bottom: 2px;
  background: var(--text);
  border-radius: 50%;
  transition: transform .2s;
}
.alert-toggle-switch input:checked + .alert-toggle-slider {
  background: var(--accent);
}
.alert-toggle-switch input:checked + .alert-toggle-slider::before {
  transform: translateX(16px);
}

/* ── Alert History ─────────────────────────────────────────── */
.alert-history-row {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  color: var(--text);
}
.alert-history-row:last-child { border-bottom: none; }
.alert-history-time {
  flex: 0 0 110px;
  color: var(--text-muted);
  font-size: 11px;
  white-space: nowrap;
}
.alert-history-dot { flex: 0 0 14px; text-align: center; }
.alert-history-device {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-weight: 500;
}
.alert-history-event {
  flex: 0 0 auto;
  font-size: 11px; color: var(--text-muted);
}
.alert-history-rule {
  flex: 0 0 auto;
  font-size: 11px; color: var(--text-muted);
  max-width: 100px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.alert-history-channel {
  flex: 0 0 auto;
  font-size: 11px; color: var(--accent);
}
.alert-history-status { flex: 0 0 16px; text-align: center; }

/* ── Alerts responsive ─────────────────────────────────────── */
@media (max-width: 768px) {
  .alert-wizard { flex-direction: column; }
}
@media (max-width: 480px) {
  .alert-rule-header { flex-direction: column; align-items: flex-start; }
  .alert-rule-actions { width: 100%; justify-content: flex-end; }
  .alert-history-row { flex-wrap: wrap; }
  .alert-history-time { flex: 0 0 auto; }
  .alert-history-device { flex: 1 1 100%; order: -1; }
}

/* ── Spreadsheet Mode — Easter Egg (Ctrl+Shift+S) ─────────── */
/* Transforms dashboard into a convincing Excel clone, then morphs back */

/* Transition enabler — only active during the return animation */
.spreadsheet-returning,
.spreadsheet-returning * {
  transition:
    background-color 2.5s ease,
    color 2.5s ease,
    border-color 2.5s ease,
    border-radius 2.5s ease,
    box-shadow 2.5s ease,
    opacity 2.5s ease,
    filter 2.5s ease !important;
}

/* ── Excel wrapper chrome ──────────────────────────────────── */

#excel-wrapper {
  display: none;
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 9999;
  font-family: 'Segoe UI', Tahoma, Geneva, sans-serif;
  font-size: 12px;
  user-select: none;
}
#excel-wrapper.xl-visible { display: block; }
#excel-wrapper.xl-fading { opacity: 0; transition: opacity 2.5s ease; }

/* Title bar */
.xl-titlebar {
  display: flex; align-items: center;
  background: #217346; color: #fff;
  height: 30px; padding: 0 8px;
  font-size: 12px;
}
.xl-titlebar-icon { margin-right: 8px; display: flex; align-items: center; }
.xl-titlebar-text { flex: 1; text-align: center; font-size: 12px; }
.xl-titlebar-btns { display: flex; gap: 2px; }
.xl-tbtn {
  width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; cursor: default;
}
.xl-tbtn:hover { background: rgba(255,255,255,.15); }
.xl-tbtn-close { cursor: pointer; }
.xl-tbtn-close:hover { background: #c42b1c; }

/* Ribbon tabs */
.xl-ribbon { background: #f3f3f3; border-bottom: 1px solid #d4d4d4; }
.xl-ribbon-tabs {
  display: flex; gap: 0; padding: 0 4px;
  border-bottom: 1px solid #d4d4d4;
}
.xl-rtab {
  padding: 6px 14px; font-size: 12px; color: #444; cursor: default;
}
.xl-rtab-active {
  background: #fff; border: 1px solid #d4d4d4;
  border-bottom: 1px solid #fff; margin-bottom: -1px;
  font-weight: 600; color: #217346;
}
.xl-ribbon-body {
  display: flex; align-items: center;
  padding: 6px 12px; gap: 8px; background: #fff;
  min-height: 32px;
}
.xl-ribbon-group { display: flex; gap: 6px; align-items: center; }
.xl-rb-label {
  padding: 2px 6px; cursor: default; color: #333;
  border: 1px solid transparent; border-radius: 2px;
}
.xl-rb-label:hover { background: #e5e5e5; border-color: #c4c4c4; }
.xl-ribbon-sep { width: 1px; height: 24px; background: #d4d4d4; }

/* Formula bar */
.xl-formula-bar {
  display: flex; align-items: center;
  background: #fff; border-bottom: 1px solid #d4d4d4;
  height: 26px; padding: 0 4px; gap: 4px;
}
.xl-cell-ref {
  background: #fff; border: 1px solid #d4d4d4;
  padding: 2px 8px; min-width: 50px; text-align: center;
  font-size: 11px; color: #333;
}
.xl-fx {
  color: #666; font-style: italic; font-size: 12px;
  padding: 0 4px; border-right: 1px solid #d4d4d4;
}
.xl-formula-input {
  flex: 1; border: none; background: transparent;
  font-size: 11px;
}

/* Column header bar (A, B, C...) */
.xl-col-headers {
  display: none; /* shown via JS when visible cols are counted */
  background: #e8e8e8; border-bottom: 1px solid #b0b0b0;
  height: 20px;
}
#excel-wrapper.xl-visible .xl-col-headers {
  display: flex; align-items: center;
}
.xl-col-hdr {
  flex: 1; text-align: center;
  font-size: 11px; color: #333; font-weight: 600;
  border-right: 1px solid #b0b0b0;
  line-height: 20px;
}
.xl-row-gutter {
  flex: 0 0 36px; background: #e8e8e8;
  border-right: 1px solid #b0b0b0;
}

/* Row numbers injected into table */
.xl-row-num {
  background: #e8e8e8 !important;
  color: #333 !important;
  font-size: 11px !important;
  font-weight: 600 !important;
  text-align: center !important;
  width: 36px !important;
  min-width: 36px !important;
  max-width: 36px !important;
  padding: 2px 4px !important;
  border: 1px solid #b0b0b0 !important;
  border-right: 2px solid #888 !important;
  font-family: 'Segoe UI', Tahoma, sans-serif !important;
}

/* ── Spreadsheet overrides (instant snap, no transitions) ──── */

body.spreadsheet-mode {
  background: #fff !important;
  color: #000 !important;
  font-family: 'Calibri', 'Segoe UI', sans-serif !important;
}

/* Hide dashboard chrome — header, summary cards, controls, footer, site strip */
body.spreadsheet-mode header,
body.spreadsheet-mode .summary-grid,
body.spreadsheet-mode .controls,
body.spreadsheet-mode .site-strip,
body.spreadsheet-mode footer,
body.spreadsheet-mode .error-banner {
  display: none !important;
}

/* Table section — flush to top, no padding, full width */
body.spreadsheet-mode main {
  padding: 0 !important;
  margin: 0 !important;
}
body.spreadsheet-mode .table-section {
  margin: 0 !important;
  padding: 0 !important;
}
body.spreadsheet-mode .table-wrapper {
  border: none !important;
  border-radius: 0 !important;
  box-shadow: none !important;
  max-height: none !important;
  overflow: visible !important;
}

/* Table — flat Excel grid */
body.spreadsheet-mode table {
  background: #fff !important;
  border-collapse: collapse !important;
}
body.spreadsheet-mode thead tr,
body.spreadsheet-mode th {
  background: #e8e8e8 !important;
  color: #333 !important;
  border: 1px solid #b0b0b0 !important;
  font-family: 'Calibri', 'Segoe UI', sans-serif !important;
  font-size: 11px !important;
  font-weight: 600 !important;
  padding: 2px 6px !important;
  text-transform: none !important;
  letter-spacing: normal !important;
}
body.spreadsheet-mode tbody tr {
  border-bottom: none !important;
  background: #fff !important;
}
body.spreadsheet-mode tbody tr:hover {
  background: #cce5ff !important;
}
body.spreadsheet-mode tbody tr.row-offline {
  background: #fff !important;
}
body.spreadsheet-mode td {
  color: #000 !important;
  border: 1px solid #d4d4d4 !important;
  font-family: 'Calibri', 'Segoe UI', sans-serif !important;
  font-size: 11px !important;
  padding: 1px 4px !important;
  line-height: 18px !important;
}

/* Flatten all cell decorations */
body.spreadsheet-mode .type-chip {
  background: transparent !important;
  border: none !important;
  border-radius: 0 !important;
  color: #000 !important;
  padding: 0 !important;
}
body.spreadsheet-mode .ip-text {
  color: #000 !important;
  font-family: 'Calibri', 'Segoe UI', sans-serif !important;
}
body.spreadsheet-mode .mac-text {
  color: #666 !important;
  font-family: 'Calibri', 'Segoe UI', sans-serif !important;
  font-size: 10px !important;
}
body.spreadsheet-mode .device-name {
  color: #000 !important;
}
body.spreadsheet-mode .owner-link:hover,
body.spreadsheet-mode .device-link:hover {
  color: #0563c1 !important;
  text-decoration: underline !important;
}
/* The Assignment cell's <input> carries its own color: var(--text) (light), which
   would be unreadable on the light spreadsheet background — force it dark too. */
body.spreadsheet-mode .cell-detail,
body.spreadsheet-mode .cell-detail .assignment-input {
  color: #000 !important;
}
body.spreadsheet-mode .cell-dash {
  color: #999 !important;
}
body.spreadsheet-mode .last-seen-text {
  color: #000 !important;
}

/* Status dots — flatten to simple colored squares (like cell bg) */
body.spreadsheet-mode .dot-on,
body.spreadsheet-mode .dot-on-static {
  background: #548235 !important;
  box-shadow: none !important;
  animation: none !important;
  border-radius: 0 !important;
}
body.spreadsheet-mode .dot-off {
  background: #c00000 !important;
  opacity: 1 !important;
  border-radius: 0 !important;
}

/* Sort indicators */
body.spreadsheet-mode th.sort-asc::after,
body.spreadsheet-mode th.sort-desc::after {
  color: #333 !important;
}

/* Hide logos in column headers — Excel wouldn't have images */
body.spreadsheet-mode .logo-placeholder {
  display: none !important;
}

/* Active cell selection highlight (the blue border Excel shows) */
body.spreadsheet-mode tbody tr:first-child td:first-of-type:not(.xl-row-num) {
  outline: 2px solid #217346 !important;
  outline-offset: -2px;
}

/* ── Designer Page (Filerobot-as-page) ─────────────────────────
   Used only on /designer.html (renamed from /branding.html — see
   docs/DESIGNER.md). The page is a two-zone grid: editor on the
   left (Filerobot mounts directly into a div, no modal), library
   sidebar on the right. Below ~1100px viewport width the grid
   collapses to a single column. */

/* Custom checkbox styling — used in the deploy modal's "Apply
   Changes" row. The rest of the page uses Filerobot's own UI. */
.branding-checkbox {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text);
  cursor: pointer;
  user-select: none;
  font-weight: 600;
}
.branding-checkbox input[type="checkbox"] {
  width: 14px;
  height: 14px;
  accent-color: var(--accent);
  cursor: pointer;
  margin: 0;
}

/* Library sidebar — saved-design thumbnails.
   Visual rules:
     • Tiles sit on --surface2 to "lift" them from the card's --surface bg
     • Hover/:focus-visible swap the idle --border for --accent — no
       browser-default yellow focus ring (anti-pattern: jarring colour
       outside the project palette)
     • Whole tile is the click target (tabindex=0 in the JS) — image is
       not the only hit zone, and keyboard nav works
     • Filename splits into stem + .ext so long names don't ellipsis-
       truncate mid-word; ext is dimmed so the eye focuses on the name */
.branding-library-status {
  font-size: 11px;
  color: var(--text-muted);
  padding: 4px 0 8px;
}
.branding-library-item {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 6px;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  transition: border-color .12s ease, box-shadow .12s ease;
}
.branding-library-item:hover {
  border-color: var(--accent);
}
.branding-library-item:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 2px rgba(0, 164, 228, 0.28);
}
.branding-library-thumb {
  width: 100%;
  aspect-ratio: 5 / 3;
  object-fit: cover;
  background: var(--surface);
  border-radius: 4px;
  display: block;
  pointer-events: none;
}
.branding-library-name {
  font-family: 'SF Mono', 'Fira Code', monospace;
  font-size: 10px;
  line-height: 1.3;
  color: var(--text);
  text-align: center;
  overflow: hidden;
  word-break: break-all;
  padding: 0 2px;
}
.branding-library-name-ext {
  color: var(--text-muted);
  font-size: 9px;
}

/* ── Two-zone Designer grid: editor + library sidebar ───────
   Editor takes most of the width; sidebar is fixed-width on the
   right. Collapses to single-column at ≤1100px. */
.designer-grid {
  display: grid;
  /* tools | editor | library — narrow rail on the left for designer-specific
     utilities (Safe Zone, future Grid, etc.). Width matches Filerobot's own
     left rail so the two visually align. */
  grid-template-columns: 84px minmax(0, 1fr) minmax(220px, 280px);
  gap: 12px;
  /* stretch → TOOLS / EDITOR / MY DESIGNS become equal-length columns.
     EDITOR anchors the height (its body is a fixed calc below); TOOLS and
     MY DESIGNS fill to match via flex:1 1 0 (basis 0 so their content
     doesn't drive the row taller than the editor). */
  align-items: stretch;
  width: 100%;
}
.designer-col-tools,
.designer-col-editor,
.designer-col-right {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}
/* Uniform header height across the three columns — MY DESIGNS carries
   header buttons (Calibration / Webex Library) so its header is naturally
   taller; pin a shared min-height so all three line up. */
.designer-tools-card > .form-card-header,
.designer-fr-card > .form-card-header,
.designer-library-card > .form-card-header {
  min-height: 46px;
}

/* Tools rail — designer-specific utility buttons. Card matches the Library
   card style so the page reads as three peer columns. */
.designer-tools-card {
  /* Fill the column to the same full length as EDITOR / MY DESIGNS.
     Buttons sit at the top; the rest is empty card body. flex-basis 0 so
     the short button list doesn't drive the row height. */
  flex: 1 1 0;
  min-height: 0;
}
.designer-tools-body {
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.designer-tools-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  padding: 8px 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}
.designer-tools-btn:hover {
  border-color: var(--accent);
  background: var(--surface2);
}
.designer-tools-btn:active { transform: scale(0.97); }
.designer-tools-btn svg { width: 22px; height: 22px; color: var(--text-muted); }
.designer-tools-btn:hover svg { color: var(--accent); }
.designer-tools-btn-label {
  font-size: 10px;
  color: var(--text-muted);
  text-align: center;
}
.designer-tools-btn-sub {
  font-size: 9px;
  color: var(--text-muted);
  opacity: 0.6;
  text-align: center;
}
.designer-tools-btn:hover .designer-tools-btn-label { color: var(--text); }
/* Pressed state — accent fill when the toggle is ON (guides visible). */
.designer-tools-btn[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--surface2);
}
.designer-tools-btn[aria-pressed="true"] svg,
.designer-tools-btn[aria-pressed="true"] .designer-tools-btn-label {
  color: var(--accent);
}
/* Disabled (e.g. Remove button before zones are visible). Greyed out
   so the affordance is obvious without dimming the rest of the rail. */
.designer-tools-btn[disabled],
.designer-tools-btn[disabled]:hover {
  background: var(--surface);
  border-color: var(--border);
  cursor: not-allowed;
  opacity: 0.45;
}
.designer-tools-btn[disabled]:hover svg,
.designer-tools-btn[disabled]:hover .designer-tools-btn-label {
  color: var(--text-muted);
}
.designer-tools-btn[disabled]:active { transform: none; }

/* Opacity slider — vertical-stacked card matching the button width.
   Range input is styled dark to match the rest of the rail. */
.designer-tools-slider-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 4px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.designer-tools-slider-label {
  font-size: 10px;
  color: var(--text-muted);
  text-align: center;
}
.designer-tools-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  outline: none;
  cursor: pointer;
  margin: 4px 0;
}
.designer-tools-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
  cursor: pointer;
  transition: transform .1s ease;
}
.designer-tools-slider::-webkit-slider-thumb:hover { transform: scale(1.15); }
.designer-tools-slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface);
  cursor: pointer;
}
.designer-tools-slider:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}
.designer-tools-slider:disabled::-webkit-slider-thumb {
  background: var(--text-muted);
  cursor: not-allowed;
}
.designer-tools-slider:disabled::-moz-range-thumb {
  background: var(--text-muted);
  cursor: not-allowed;
}
.designer-tools-slider-pct {
  font-size: 11px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  min-width: 32px;
  text-align: center;
}
.designer-tools-slider:disabled + .designer-tools-slider-pct,
.designer-tools-slider-wrap:has(.designer-tools-slider:disabled) .designer-tools-slider-pct {
  color: var(--text-muted);
}
.designer-tools-divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 2px 0;
}

.designer-wm-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
  gap: 8px;
  padding: 8px 0;
  max-height: 320px;
  overflow-y: auto;
}
.designer-wm-item {
  position: relative;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  text-align: center;
}
.designer-wm-item img {
  width: 100%;
  aspect-ratio: 1;
  object-fit: contain;
  display: block;
  padding: 4px;
}
.designer-wm-item-name {
  font-size: 10px;
  color: var(--text-muted);
  padding: 0 4px 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.thumb-delete-btn {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  border: none;
  background: rgba(0, 0, 0, .65);
  backdrop-filter: blur(4px);
  color: var(--offline);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity .15s, background .12s;
  padding: 0;
}
.thumb-delete-btn:hover {
  background: var(--offline);
  color: #fff;
}
.designer-wm-item:hover .thumb-delete-btn,
.branding-library-item:hover .thumb-delete-btn { opacity: 1; }
.designer-wm-modal-card { max-width: 420px; }
.designer-wm-empty {
  color: var(--text-muted);
  text-align: center;
  padding: 16px 0;
}

/* Library sidebar card — sticky to top of viewport while user
   interacts with the editor on the left. */
.designer-library-card {
  /* Same full length as EDITOR / TOOLS: fill the column (flex-basis 0 so
     13+ designs never drive the row taller than the editor) and scroll the
     grid inside the body instead of stretching the page — no page scroll. */
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.designer-library-body {
  flex: 1 1 auto;
  overflow-y: auto;
  min-height: 0;
}
/* 2-column grid by default — fits ~10 designs in the visible viewport
   on a typical 1080p screen (~120 px per thumbnail card). Falls back to
   single-column at very narrow column widths via auto-fill. */
.designer-library-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(85px, 1fr));
  gap: 6px;
  padding: 4px 0;
}
/* Tighter padding + smaller filename for 2-col layout so the thumbnail
   image itself stays the dominant visual. */
.designer-library-grid .branding-library-item {
  padding: 4px;
  gap: 4px;
}
.designer-library-grid .branding-library-name {
  font-size: 9px;
  line-height: 1.2;
}
.designer-library-grid .branding-library-name-ext {
  font-size: 8px;
}

/* ── Library lightbox ───────────────────────────────────────
   Larger than the standard prompt modal so the 800×480 image is
   readable without scrolling. */
.designer-lightbox-card { width: min(880px, 96vw); }
.designer-lightbox-img {
  width: 100%;
  max-height: 60vh;
  object-fit: contain;
  background: var(--surface2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  display: block;
  margin: 8px 0 16px;
}

/* ── Filerobot editor — page-level mount ───────────────────
   Filerobot mounts directly into #bd-fr-main. We give it a fixed
   tall viewport so its internal tabs/toolbar/canvas all have room
   without scrolling the host page. The loading placeholder sits
   absolutely positioned on top until mountEditor() flips it
   hidden. */
.designer-fr-card {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.designer-fr-body {
  padding: 0;
  position: relative;
  min-height: 600px;
  height: calc(100vh - 180px); /* leave room for page header + footer */
  display: flex;
  flex-direction: column;
}
.designer-fr-main {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  background: var(--surface2);
}
.designer-fr-loading {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
  color: var(--text-muted);
  background: var(--surface2);
  pointer-events: none;
  z-index: 2;
}
.designer-fr-loading.hidden { display: none; }

/* Mobile / narrow viewport — collapse to single column. The tools
   rail stacks above the editor as a horizontal row of buttons; library
   drops below the editor and loses its sticky positioning so it
   doesn't dominate small screens. */
@media (max-width: 1100px) {
  .designer-grid { grid-template-columns: 1fr; }
  /* Content-height cards when stacked — override the desktop flex:1 1 0 so
     they don't collapse to zero in the single-column (auto-height) layout. */
  .designer-tools-card { flex: 0 0 auto; }
  .designer-tools-body { flex-direction: row; flex-wrap: wrap; align-items: stretch; }
  .designer-tools-btn { flex: 0 0 auto; min-width: 84px; }
  /* Slider gets its own row width on narrow viewports so the thumb
     has enough travel for fine-grain percentage control. */
  .designer-tools-slider-wrap { flex: 1 1 200px; flex-direction: row; gap: 8px; padding: 8px 12px; }
  .designer-tools-slider { flex: 1 1 auto; margin: 0; }
  .designer-tools-slider-label { flex: 0 0 auto; }
  .designer-tools-slider-pct { flex: 0 0 auto; }
  .designer-library-card { flex: 0 0 auto; max-height: none; }
  .designer-library-body { overflow: visible; }
  .designer-fr-body { height: 80vh; }
}

/* Designer desktop: fit everything in one viewport — NO page scroll.
   The body is a fixed-height (100dvh) flex column with overflow hidden, so
   header + main + footer exactly fill the screen and the page cannot scroll.
   main and the grid flex to fill the space; the grid's single row is 1fr so
   the three columns stretch to equal length; the EDITOR body fills (instead
   of a fixed calc), and MY DESIGNS scrolls inside its own card. Desktop only
   (>=1101px) — mobile keeps the normal stacked, scrollable layout above. */
@media (min-width: 1101px) {
  body.designer-page { height: 100dvh; overflow: hidden; }
  body.designer-page > main {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  body.designer-page .designer-grid {
    flex: 1 1 auto;
    min-height: 0;
    grid-template-rows: minmax(0, 1fr);
  }
  /* EDITOR card fills its column to the same length as TOOLS / MY DESIGNS
     (those already flex:1 1 0). Without this the editor collapses and leaves
     a gap below it now that its body is flex-filled instead of a fixed calc. */
  body.designer-page .designer-fr-card {
    flex: 1 1 0;
    min-height: 0;
  }
  body.designer-page .designer-fr-body {
    height: auto;
    min-height: 0;
    flex: 1 1 auto;
  }
}

/* ── Productions (Tenant) page ─────────────────────────────────
   Client-facing card view of phones grouped by tag. Reuses the shared
   .summary-grid/.card components for counts and .dropdown(-searchable) for
   the production picker. The .device-card* component below is rendered by
   renderDeviceCards() in common.js — see that engine for the consumer list. */
.tenant-header {
  max-width: var(--tenant-width);
  margin: 0 auto;
  padding: 20px 16px 4px;
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}

/* Summary badges on the Account/Portal pages. The .summary-grid component is
   shared with the dashboard (where it spans <main>'s full 1400px), so we DON'T
   touch the base class — this modifier just caps + centers the badges to the
   same --tenant-width as the banner/toolbar/table so all four rows are edge-
   aligned. width:100% lets it shrink with the viewport below that cap. */
.summary-grid.tenant-summary {
  max-width: var(--tenant-width);
  width: 100%;
  margin-left: auto;
  margin-right: auto;
}
.tenant-badge {
  width: 48px; height: 48px;
  border-radius: var(--radius);
  background: rgba(var(--accent-rgb), .14);
  color: var(--accent);
  display: grid; place-items: center;
  font-weight: 700; font-size: 16px; letter-spacing: .5px;
  flex: 0 0 auto;
}
.tenant-titles { flex: 1 1 auto; min-width: 180px; }
.tenant-titles h1 { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -.2px; }
.tenant-titles p { margin: 2px 0 0; font-size: 13px; color: var(--text-muted); }
.tenant-updated { font-size: 12px; color: var(--text-muted); white-space: nowrap; }

/* Account picker rendered AS the banner title — keeps the <h1> look (big, bold)
   with a subtle chevron, instead of a toolbar filter pill. Reuses the shared
   .dropdown engine + .dropdown-panel; only the trigger is restyled from a pill
   into a heading. */
.tenant-title-dd { position: relative; display: inline-block; max-width: 100%; }
.tenant-title-btn {
  display: inline-flex; align-items: center; gap: 8px;
  background: transparent; border: none; padding: 0; height: auto; width: auto;
  cursor: pointer; max-width: 100%;
}
.tenant-title-name {
  font-size: 19px; font-weight: 700; letter-spacing: -.2px; line-height: 1.2;
  color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tenant-title-chevron {
  width: 16px; height: 16px; flex: 0 0 auto;
  color: var(--text-muted);
  transition: color .12s ease, transform .15s ease;
}
.tenant-title-btn:hover .tenant-title-name,
.tenant-title-btn:hover .tenant-title-chevron { color: var(--accent); }
/* Chevron flips while the panel is open (engine toggles .open on the trigger). */
.tenant-title-btn.open .tenant-title-chevron { transform: rotate(180deg); }

.tenant-controls {
  max-width: var(--tenant-width);
  margin: 8px auto 0;
  padding: 0 16px;
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
/* No per-dropdown min-width here — filter dropdowns size to their content and
   pack tight, exactly like the dashboard's .controls toolbar. (A blanket
   min-width:220px used to live here from when the account picker was the only
   control; it made every filter a fat 220px pill once the full toolbar landed.) */

.tenant-grid {
  max-width: var(--tenant-width);
  margin: 0 auto;
  padding: 16px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 14px;
}

/* device-card — shared component rendered by renderDeviceCards() (common.js) */
.device-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 14px;
  display: flex; flex-direction: column; gap: 12px;
  transition: transform .12s ease, border-color .12s ease;
}
.device-card:hover { transform: translateY(-2px); border-color: rgba(var(--accent-rgb), .5); }
.device-card-top { display: flex; align-items: flex-start; gap: 11px; }
.device-card-ico {
  width: 38px; height: 38px; border-radius: 9px;
  background: rgba(var(--accent-rgb), .14); color: var(--accent);
  display: grid; place-items: center; flex: 0 0 auto;
}
.device-card-name { flex: 1 1 auto; min-width: 0; }
.device-card-nm {
  font-size: 14px; font-weight: 600; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.device-card-status {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 12px; font-weight: 600;
  padding: 3px 9px; border-radius: 999px; white-space: nowrap;
}
.device-card-status .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.device-card-status-up      { color: var(--online);     background: rgba(var(--online-rgb), .15); }
.device-card-status-down    { color: var(--offline);    background: rgba(var(--offline-rgb), .14); }
.device-card-status-unknown { color: var(--text-muted); background: rgba(var(--text-muted-rgb), .15); }
.device-card-meta { display: flex; flex-direction: column; gap: 7px; }
.device-card-row { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.device-card-row svg { width: 15px; height: 15px; color: var(--text-muted); flex: 0 0 auto; }
.device-card-row .k { color: var(--text-muted); }
.device-card-row .v {
  color: var(--text); margin-left: auto; font-weight: 500; text-align: right;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* DID · Ext share one row (.device-card-row-dual): each is an inline label+value
   pair; the second pair pins right. Override the single-row .v auto-push so both
   pairs sit on the same line instead of each grabbing all the free space. */
.device-card-row-dual .dcr-pair { display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; }
.device-card-row-dual .dcr-pair-right { margin-left: auto; }
.device-card-row-dual .dcr-pair .v { margin-left: 0; }

/* ── Phone List PDF (print-to-PDF) ──────────────────────────────────────────
   Built by buildPhoneListPrintHtml()/printPhoneList() (common.js) into a hidden
   #phone-list-print container; @media print hides the app and shows ONLY the
   document. Literal black/white/grey are intentional — this is a printed paper
   document for sharing, not the dark UI (same exception as spreadsheet-mode). */
.phone-list-print { display: none; }
@media print {
  body.printing-phone-list { background: #fff; }
  /* Hide every direct child of <body> except the print document. */
  body.printing-phone-list > *:not(.phone-list-print) { display: none !important; }
  body.printing-phone-list .phone-list-print { display: block !important; }

  .pl-doc { color: #000; font-family: Arial, Helvetica, sans-serif; }
  .pl-head { margin-bottom: 14px; border-bottom: 2px solid #000; padding-bottom: 8px; }
  .pl-title { font-size: 20px; margin: 0 0 3px; font-weight: 700; }
  .pl-sub { font-size: 11px; color: #555; }

  .pl-table { width: 100%; border-collapse: collapse; font-size: 12px; }
  .pl-table th {
    text-align: left; padding: 6px 8px; border-bottom: 1.5px solid #000;
    font-size: 10px; text-transform: uppercase; letter-spacing: .04em; color: #000;
  }
  .pl-table td { padding: 6px 8px; border-bottom: 1px solid #ccc; vertical-align: top; }
  .pl-table tr:nth-child(even) td { background: #f4f4f4; }   /* zebra for scanability */
  .pl-table .pl-name { font-weight: 600; }
  .pl-table .pl-ext, .pl-table .pl-did { white-space: nowrap; font-variant-numeric: tabular-nums; }
  .pl-dash { color: #999; }
  .pl-empty { text-align: center; color: #777; padding: 16px; }
  .pl-foot { margin-top: 14px; font-size: 10px; color: #777; }

  /* Repeat the header row on every printed page; don't split a row across pages. */
  .pl-table thead { display: table-header-group; }
  .pl-table tr { page-break-inside: avoid; }
  @page { margin: 14mm; }
}
.device-card-empty {
  grid-column: 1 / -1;
  text-align: center; color: var(--text-muted);
  padding: 40px 16px; font-size: 14px;
}

/* View toggle — segmented List | Card control. Lives in the toolbar's
   .toolbar-actions cluster (which owns right-alignment), so the toggle itself
   needs no margin. Mirrors the debug tab's .rh-window/.rh-win-btn segmented
   pattern (active = accent fill), redefined here because the Tenant page
   doesn't load debug-tab.css. Rendered + persisted by createViewToggle(). */
.view-toggle {
  position: relative;            /* anchor for the absolutely-positioned slider */
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  height: var(--control-h);
  flex: 0 0 auto;
  background: var(--surface);
}
/* The accent pill that SLIDES under the active segment (createViewToggle sets
   its width = 100/N% and transform = index×100%). Sits behind the buttons. */
.view-toggle-slider {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 50%;
  background: var(--accent);
  z-index: 0;
  transition: transform .22s cubic-bezier(.4, 0, .2, 1), width .22s ease;
}
.view-toggle-slider-init { transition: none; }   /* no slide-in on first paint */
.view-toggle-btn {
  position: relative; z-index: 1;   /* above the slider */
  flex: 1 1 0;                       /* equal-width segments so the pill aligns */
  background: transparent; color: var(--text-muted);
  border: none;
  padding: 0 14px; font-size: 12px; font-weight: 500; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  cursor: pointer; white-space: nowrap;
  transition: color .15s ease;
}
.view-toggle-btn svg { width: 14px; height: 14px; flex: 0 0 auto; }
.view-toggle-btn:hover { color: var(--text); }
.view-toggle-btn.active,
.view-toggle-btn.active:hover { color: #fff; }   /* #fff allowed: text on accent fill */

/* Phone directory (list view) — rendered by renderDeviceList() (common.js).
   Reuses the .device-card surface/border tokens; rows and the header share one
   grid template so columns line up. Status pill reuses .device-card-status*. */
.tenant-list { max-width: var(--tenant-width); margin: 0 auto; padding: 16px; }
.device-list {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
/* Head + rows share one grid template, supplied per-render as the --dl-cols CSS
   var on .device-list (set by renderDeviceList from the visible column widths),
   so the column chooser can hide/reorder columns and the two stay aligned. The
   fallback keeps all five columns if the var is ever absent. */
.device-list-head,
.device-row {
  display: grid;
  grid-template-columns: var(--dl-cols, minmax(0, 2.2fr) minmax(64px, .9fr) minmax(72px, 1fr) minmax(0, 2.4fr) auto);
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
}
.device-list-head {
  font-size: 11px; font-weight: 600; letter-spacing: .4px; text-transform: uppercase;
  color: var(--text-muted);
  background: var(--surface2);
  border-bottom: 1px solid var(--border);
}
.dl-h { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Click-to-sort (createColumnSort, common.js) — same sort-asc/sort-desc
   classes and arrow convention as the dashboard's th.sortable above, so the
   two implementations share one visual language. */
.dl-h.sortable { cursor: pointer; }
.dl-h.sortable:hover { color: var(--text); }
.dl-h.sort-asc::after { content: ' ↑'; color: var(--accent); }
.dl-h.sort-desc::after { content: ' ↓'; color: var(--accent); }
/* The Name cell is icon (24px) + gap (9px) + text (.device-row-name in
   common.js) — without this inset the "NAME" header sits above the icon
   instead of above the actual name text, reading as misaligned. */
.dl-h-name { padding-left: 33px; }
.device-row {
  font-size: 13px;
  border-bottom: 1px solid var(--border);
  transition: background .12s ease;
}
.device-row:last-child { border-bottom: none; }
.device-row:hover { background: var(--surface2); }
.device-row-name { display: flex; align-items: center; gap: 9px; min-width: 0; }
.device-row-ico {
  width: 24px; height: 24px; border-radius: 6px; flex: 0 0 auto;
  background: rgba(var(--accent-rgb), .14); color: var(--accent);
  display: grid; place-items: center;
}
.device-row-ico svg { width: 13px; height: 13px; }
.device-row-nm { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.device-row-type,
.device-row-model { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.device-row-loc { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Status left-aligns like every other column (was justify-self:end, written
   when Status was the last/rightmost column by default; the column chooser
   lets it move anywhere now, so a fixed right-alignment no longer makes
   sense and reads as inconsistent wherever it lands). */

@media (max-width: 480px) {
  .tenant-grid { grid-template-columns: 1fr; gap: 10px; padding: 10px; }
  .device-card-row .v { max-width: 60%; }

  .tenant-list { padding: 10px; }
  /* Directory rows stack on phones. Flex + order/basis (not grid-areas) so the
     stacking is independent of which columns the chooser left visible and their
     order: Name + Status on the first line, the rest each on their own line. */
  .device-list-head { display: none; }
  .device-row {
    display: flex; flex-wrap: wrap; align-items: center;
    column-gap: 10px; row-gap: 2px;
  }
  .device-row-name { flex: 1 1 60%; order: 1; }
  .device-row .device-card-status { order: 2; margin-left: auto; }
  .device-row-type,
  .device-row-model,
  .device-row-loc { flex: 1 1 100%; order: 3; }

  /* Account-title dropdown is EXEMPT from the global mobile collapse above
     (.dropdown-btn > span/.dropdown-chevron { display:none } + 36px icon-only).
     That rule is for icon+text filter buttons; the title has NO leading icon —
     its span IS the account name — so the collapse blanks the title on phones.
     Keep the name + chevron and let the title size to its content. The higher-
     specificity selectors (and later source order) override the global rule. */
  .tenant-title-btn { min-width: 0; padding: 0; justify-content: flex-start; }
  .tenant-title-btn .tenant-title-name { display: inline; }
  .tenant-title-btn .tenant-title-chevron { display: inline; }
}

/* ── Assignment field — inline, LOCAL-ONLY editable input + suggestions popover ─
   Rendered by assignmentInputHtml() (common.js) inside the card row
   (.device-card-row-assign) and the list column (.device-row-assign); made live by
   wireAssignmentInputs(). The popover reuses .dropdown-panel chrome (fixed
   position/bg/border/shadow/scroll). All colours are CSS variables. The value is
   reference-only and never syncs to Webex (counterpart to device tags). */
.assignment-input {
  width: 100%; min-width: 0;
  font: inherit; font-size: 13px;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 3px 7px;
  transition: background .12s ease, border-color .12s ease;
}
.assignment-input::placeholder { color: var(--text-muted); opacity: .85; }
.assignment-input:hover { background: var(--surface2); }
.assignment-input:focus {
  outline: none;
  background: var(--surface);
  border-color: var(--accent);
}
/* List cell wrapper — let the input fill the column track. */
.device-row-assign { min-width: 0; }
/* Card row — the .v normally right-aligns plain text; for the editable field let
   it grow and hold a full-width, left-aligned input instead (incl. on mobile,
   where the generic .device-card-row .v caps width at 60%). */
.device-card-row-assign .v {
  flex: 1 1 auto; margin-left: 0; text-align: left;
  max-width: none; overflow: visible; white-space: normal;
}
/* Suggestions popover — chrome inherited from .dropdown-panel; JS sets position. */
.assignment-pop { padding: 4px; }
.assignment-pop-opt {
  padding: 7px 10px; font-size: 13px; color: var(--text);
  border-radius: 6px; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.assignment-pop-opt:hover,
.assignment-pop-opt.active { background: var(--surface2); }

/* ── Account Setup page (account-setup.html / .js) ──────────────────────────
   Manage accounts + device assignments. Reuses .form-card / .btn / .action-row
   / .input-mono; the acct-* classes below add only the page-specific layout. */
.acct-page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; margin-bottom: 16px;
}
.acct-titles h1 { margin: 0; font-size: 19px; font-weight: 700; letter-spacing: -.2px; }
.acct-titles p  { margin: 2px 0 0; font-size: 13px; color: var(--text-muted); }
.acct-list { display: flex; flex-direction: column; gap: 12px; }
.acct-empty {
  padding: 18px; text-align: center; font-size: 13px; color: var(--text-muted);
  border: 1px dashed var(--border); border-radius: var(--radius);
}
.acct-card .form-card-header { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.acct-name  { font-weight: 700; }
.acct-header-actions { display: flex; align-items: center; gap: 8px; }
.acct-bookmark-btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 4px;
  color: var(--text-muted); cursor: pointer;
  transition: color .15s ease;
}
.acct-bookmark-btn:hover { color: var(--accent); }
.acct-bookmark-btn:active { transform: scale(0.95); }
.acct-count {
  font-size: 12px; font-weight: 600; color: var(--text-muted);
  background: var(--surface2); border: 1px solid var(--border);
  padding: 2px 9px; border-radius: 999px; white-space: nowrap;
}
.acct-card-archived { opacity: .72; }
.acct-deeplink-highlight {
  animation: acct-highlight 3s ease-out;
}
@keyframes acct-highlight {
  0% { box-shadow: 0 0 0 3px var(--accent); background-color: rgba(var(--accent-rgb), .05); }
  100% { box-shadow: 0 0 0 0 var(--accent); background-color: transparent; }
}
.acct-tag {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px; color: var(--text-muted); margin-bottom: 12px;
}
/* Assign (Manage Devices) panel — revealed inline under a card */
.acct-assign { margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px; }
.acct-assign-head {
  font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px;
  color: var(--text-muted); margin-bottom: 8px;
  display: flex; flex-direction: column; gap: 2px;
}
.acct-assign-head .field-hint { text-transform: none; letter-spacing: 0; font-weight: 400; }
.acct-search { width: 100%; margin-bottom: 8px; }
.acct-devices {
  display: flex; flex-direction: column;
  max-height: 280px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: var(--radius);
}
.acct-device {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 11px; font-size: 13px; cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.acct-device:last-child { border-bottom: 0; }
.acct-device:hover { background: var(--surface2); }
.acct-device input { flex: 0 0 auto; accent-color: var(--accent); }
.acct-device-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-device-meta { flex: 0 0 auto; font-size: 12px; color: var(--text-muted); }
/* Archived disclosure toggle */
.acct-archived-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  margin-top: 14px; padding: 6px 4px;
  background: none; border: 0; cursor: pointer;
  font-size: 13px; color: var(--text-muted);
}
.acct-archived-toggle:hover { color: var(--text); }
.acct-archived-toggle .dropdown-chevron { width: 14px; height: 14px; transition: transform .15s ease; }
.acct-archived-toggle.open .dropdown-chevron { transform: rotate(180deg); }
#acct-archived-list { margin-top: 10px; }

/* ── Magic-link grants (Account Setup → Magic Links panel) ──────────────────── */
.acct-links-list { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.acct-link-row {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 10px 12px;
}
.acct-link-main { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.acct-link-label { font-weight: 600; color: var(--text); }
.acct-link-meta { color: var(--text-muted); font-size: 12px; margin-top: 4px; }
.acct-link-actions { margin-top: 8px; }
/* Status pill — three states, all driven by CSS vars (no hardcoded colours). */
.acct-link-status {
  font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .03em;
  padding: 2px 8px; border-radius: 999px; border: 1px solid transparent;
}
.acct-link-status.is-active  { background: rgba(var(--online-rgb),.12);  border-color: rgba(var(--online-rgb),.35);  color: var(--online); }
.acct-link-status.is-expired { background: rgba(var(--warn-rgb),.12);    border-color: rgba(var(--warn-rgb),.35);    color: var(--warn); }
.acct-link-status.is-revoked { background: rgba(var(--offline-rgb),.12); border-color: rgba(var(--offline-rgb),.35); color: var(--offline); }
/* The generated URL, shown once in the create-link modal. */
.acct-link-url {
  display: block; font-family: monospace; font-size: 12px;
  word-break: break-all; color: var(--text);
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 8px 10px; margin-top: 4px;
}

/* ── Account Users panel (Phase 3) ─────────────────────────────────────────── */
.acct-users-list { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
.acct-user-row {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 10px 12px;
}
.acct-user-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.acct-user-name { font-weight: 600; color: var(--text); }
.acct-user-actions { margin-left: auto; display: flex; gap: 4px; }
.acct-user-contact { color: var(--text-muted); font-size: 12px; margin-top: 4px; }
.acct-user-nocontact { font-style: italic; }
.acct-user-access { color: var(--text-muted); font-size: 12px; margin-top: 6px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.acct-user-access .acct-link-actions { margin-top: 0; margin-left: auto; display: flex; gap: 4px; }
.acct-mono { font-family: monospace; color: var(--text); }
/* Access-mode pill — three modes, all CSS-var driven (no hardcoded colours). */
.acct-mode-pill {
  font-size: 11px; font-weight: 600; letter-spacing: .02em;
  padding: 2px 8px; border-radius: 999px; border: 1px solid transparent; white-space: nowrap;
}
.acct-mode-pill.mode-link      { background: rgba(var(--accent-rgb),.12);  border-color: rgba(var(--accent-rgb),.35);  color: var(--accent); }
.acct-mode-pill.mode-otp-2fa   { background: rgba(var(--online-rgb),.12);  border-color: rgba(var(--online-rgb),.35);  color: var(--online); }
.acct-mode-pill.mode-otp-login { background: rgba(var(--warn-rgb),.12);    border-color: rgba(var(--warn-rgb),.35);    color: var(--warn); }

/* ── Form-modal radio group (createFormModal) ──────────────────────────────── */
.form-radio-group { display: flex; flex-direction: column; gap: 6px; }
.form-radio-option {
  display: flex; align-items: flex-start; gap: 8px; cursor: pointer;
  padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface);
}
.form-radio-option:hover { background: var(--surface2); }
/* width:auto + flex:none undo the global `.field input{width:100%}` (and the
   mobile `.field input{padding:10px 12px}` override) that otherwise stretch the
   radio to full width — that bloated dot squeezed the label into a one-word-wide
   column that wrapped vertically. Keep the radio at its intrinsic size and let
   .form-radio-text (flex:1; min-width:0) absorb the remaining width. */
.form-radio-option input { margin-top: 2px; accent-color: var(--accent); width: auto; flex: 0 0 auto; padding: 0; }
.form-radio-text { display: flex; flex-direction: column; flex: 1; min-width: 0; color: var(--text); font-size: 13px; }
.form-radio-hint { color: var(--text-muted); font-size: 12px; margin-top: 1px; }

/* ── Public magic-link portal — denied/expired landing ─────────────────────── */
.portal-denied { max-width: 480px; margin: 80px auto; text-align: center; padding: 0 20px; }
.portal-denied-logo { width: 56px; height: 56px; opacity: .85; margin-bottom: 18px; }
.portal-denied h1 { font-size: 20px; color: var(--text); margin-bottom: 12px; }
.portal-denied p { color: var(--text-muted); line-height: 1.5; }

/* ── Public portal OTP verify + OTP-login pages (Phase 3) ───────────────────── */
.portal-auth { max-width: 400px; margin: 72px auto; padding: 0 20px; }
.portal-auth-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: 28px 24px; box-shadow: var(--shadow);
  text-align: center;
}
.portal-auth-logo { width: 48px; height: 48px; opacity: .9; margin-bottom: 14px; }
.portal-auth-card h1 { font-size: 19px; color: var(--text); margin-bottom: 6px; }
.portal-auth-sub { color: var(--text-muted); font-size: 13px; line-height: 1.5; margin-bottom: 18px; }
.portal-auth-card .field { text-align: left; }
.portal-otp-input { width: 100%; text-align: center; letter-spacing: 8px; font-size: 24px; }
.portal-auth-actions { margin-top: 16px; display: flex; flex-direction: column; gap: 10px; }
.portal-auth-resend { background: none; border: none; color: var(--accent); font-size: 13px; cursor: pointer; padding: 4px; }
.portal-auth-resend:disabled { color: var(--text-muted); cursor: default; }
.portal-channel-choice { display: flex; gap: 8px; justify-content: center; margin-bottom: 14px; flex-wrap: wrap; }
.portal-auth-error { color: var(--offline); font-size: 13px; min-height: 18px; margin-top: 4px; }
.portal-auth-hidden { display: none !important; }

/* ── Webex background library manager (Designer modal) ───────────────
   Manages the org 100-slot background store; opened from the MY DESIGNS
   header (designer.js openWebexLibrary / loadWebexBgLibrary). Meter fill
   width comes from the --wxlib-fill custom property set in JS (allowed
   exception to no-inline-style). */
.wxlib-modal-card { width: min(560px, 96vw); }
.wxlib-usage-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; margin-top: 12px; }
.wxlib-usage-label { color: var(--text-muted); font-size: 13px; }
.wxlib-usage-count { color: var(--text); font-size: 13px; font-variant-numeric: tabular-nums; }
.wxlib-meter { height: 10px; border-radius: 5px; background: var(--surface2); overflow: hidden; }
.wxlib-meter-fill { height: 100%; width: var(--wxlib-fill, 0%); background: var(--online); transition: width .3s ease, background .3s ease; }
.wxlib-meter-fill.is-warn { background: var(--warn); }
.wxlib-meter-fill.is-full { background: var(--offline); }
.wxlib-warn { margin-top: 10px; color: var(--warn); font-size: 13px; line-height: 1.4; }
.wxlib-stats { display: flex; gap: 16px; margin-top: 10px; }
.wxlib-stat { color: var(--text-muted); font-size: 12px; }
.wxlib-stat b { color: var(--text); }
.wxlib-list { margin-top: 12px; max-height: 260px; overflow-y: auto; display: flex; flex-direction: column; }
.wxlib-row { display: flex; align-items: center; gap: 8px; padding: 6px 2px; border-bottom: 1px solid var(--border); }
.wxlib-row-name { flex: 1; min-width: 0; font-family: monospace; font-size: 12px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.wxlib-tag { flex: 0 0 auto; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 10px; border: 1px solid transparent; }
.wxlib-tag.dup { color: var(--offline); border-color: var(--offline); }
.wxlib-tag.keep { color: var(--accent); border-color: var(--accent); }
.wxlib-tag.inuse { color: var(--online); border-color: var(--online); }
.wxlib-modal-list { margin-top: 10px; max-height: 220px; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.wxlib-progress { margin-top: 8px; }
.wxlib-progress .wxlib-usage-row { margin-top: 0; }
