/* "Enterprise slate" theme (chosen over dark-console/light-SaaS/dark-neon
   alternatives when this was last revisited) - a light content area with a
   dark, fixed sidebar (the Azure Portal/ServiceNow split), one confident
   indigo accent for every primary action/link/active-state, and status
   colours dark/saturated enough to read as text as well as fills. Replaced
   the previous ad hoc GitHub-dark-mode palette (bg #0d1117 etc.) wholesale -
   see git history if that's ever needed again. */
:root {
  /* Base surfaces (light content area) */
  --bg: #f1f5f9;
  --panel: #ffffff;
  --panel-alt: #f8fafc;
  --text: #0f172a;
  --text-secondary: #334155;
  --muted: #64748b;
  --border: #e2e8f0;
  --border-strong: #cbd5e1;
  --row-hover: rgba(15, 23, 42, 0.035);
  --track-color: #94a3b8;

  /* Sidebar - deliberately dark against the otherwise light app, so
     navigation reads as a distinct, permanent structural element rather
     than just another light panel (see #app-sidebar below). */
  --sidebar-bg: #0f172a;
  --sidebar-bg-hover: #1e293b;
  --sidebar-border: #1e293b;
  --sidebar-text: #94a3b8;
  --sidebar-text-active: #ffffff;

  /* Accent - one indigo used for every primary button, link, active nav
     state, and focus/selection highlight. Two shades: the darker one reads
     as text/fills on the light content area, the lighter one is for the
     dark sidebar (a flat AA-contrast indigo would be nearly invisible
     against --sidebar-bg). */
  --accent: #4338ca;
  --accent-hover: #3730a3;
  --accent-soft: #e0e7ff;
  --accent-on-dark: #818cf8;

  /* Status semantics - kept dark/saturated enough to double as readable
     text, not just fills. Mirrored by hand in STATUS_COLORS/GAUGE_COLORS
     (public/js/graph.js draws these straight into SVG attributes, so it
     can't read a CSS variable) - keep both in sync if these ever change. */
  --status-fresh: #15803d;
  --status-warning: #b45309;
  --status-stale: #b91c1c;
  --status-dead: #64748b;
  --status-error: #7f1d1d;
  --status-unknown: #475569;

  /* Soft tints of the above, for badge/banner backgrounds on a light panel
     (the old theme could get away with a translucent rgba() over a dark
     panel for this; that reads as barely-there on white, so these are flat
     colours instead). */
  --status-fresh-soft: #dcfce7;
  --status-warning-soft: #fef3c7;
  --status-stale-soft: #fee2e2;
  --status-dead-soft: #f1f5f9;
  --status-error-soft: #fee2e2;
  --status-unknown-soft: #f1f5f9;

  --shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08);
  --shadow-md: 0 4px 14px rgba(15, 23, 42, 0.10);
  --shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.14);

  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;

  /* Inter via Google Fonts CDN (each page's own <head> carries the <link>,
     same convention as the existing per-page stylesheet links - this app
     has no shared templating). Falls back to the previous system-font stack
     if the CDN is unreachable. */
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

* { box-sizing: border-box; }

/* A true global utility, unlike the many scoped `#some-id.hidden` rules
   already scattered through this file (one per element that happens to
   need hiding) - this project never had a single catch-all `.hidden` rule
   before, which meant every newly-hidden element needed its own explicit
   selector added here or it would silently keep rendering despite the
   class being applied. Purely additive - identical `display: none` to
   every scoped rule above, so it can never conflict with one - just
   covers anything that doesn't have (or hasn't yet needed) its own. */
.hidden { display: none; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

/* App-wide left sidebar - persistent across the app's top-level pages
   (public/index.html "Customer Pipelines", public/data-quality.html "Data
   Quality Control", public/users.html "Manage Users"), giving the app room
   to grow into further tools without any one page having to carry multiple
   unrelated purposes (that's what pushed the name-mismatch report out of
   index.html onto its own page - see README). Deliberately NOT present on
   public/customers.html - that's a sub-page reached FROM Customer Pipelines'
   own header link, not a top-level destination in its own right (unlike
   Manage Users, which - despite also being admin-only - gets a sidebar
   entry since it isn't tied to any one other page the way customer admin
   is). Each page just hardcodes its own copy of this markup (this app has
   no shared templating), with `.active` set on whichever link matches that
   page. */
#app-shell {
  display: flex;
  min-height: 100vh;
  /* Breathing room between the dark sidebar and the light content column -
     without this the two surfaces' hard colour contrast reads as if they're
     jammed against each other even though there's technically a 1px border
     between them. The page's own background (var(--bg)) shows through this
     gap, so it reads as intentional whitespace rather than a gap-shaped
     hole. */
  gap: 20px;
  background: var(--bg);
}
#app-sidebar {
  width: 208px;
  flex-shrink: 0;
  background: var(--sidebar-bg);
  padding: 16px 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
  box-shadow: var(--shadow-md);
}
#app-sidebar-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--sidebar-text-active);
  padding: 0 20px 16px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--sidebar-border);
}
#app-sidebar-brand svg { flex-shrink: 0; color: var(--accent-on-dark); }
.app-nav-link {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 20px;
  color: var(--sidebar-text);
  text-decoration: none;
  font-size: 13.5px;
  border-left: 3px solid transparent;
  transition: color 0.12s ease, background-color 0.12s ease;
}
.app-nav-link svg { flex-shrink: 0; opacity: 0.85; }
.app-nav-link:hover { color: var(--sidebar-text-active); background: var(--sidebar-bg-hover); }
.app-nav-link.active {
  color: var(--sidebar-text-active);
  background: var(--sidebar-bg-hover);
  border-left-color: var(--accent-on-dark);
  font-weight: 600;
}
.app-nav-link.active svg { opacity: 1; color: var(--accent-on-dark); }
/* Sticks to the bottom of the sidebar (margin-top: auto, inside the
   sidebar's own column flex) regardless of how many nav links are above it
   - a placeholder for now (see README), not yet wired to package.json. */
#app-version {
  padding: 4px 20px 0;
  font-size: 11px;
  color: var(--sidebar-text);
}
#app-content {
  flex: 1;
  min-width: 0;
}

/* "Signed in as X (role)" + Sign out - lives at the bottom of the sidebar,
   just above #app-version (public/js/auth-widget.js populates this once its
   own GET /api/auth/me resolves). margin-top: auto pushes this - and
   #app-version right after it - down to the bottom of the sidebar's column
   flex regardless of how many nav links are above; #app-version has no
   margin-top of its own so it just follows immediately below this rather
   than also fighting for the same auto space. Stacked vertically here since
   the sidebar is only 200px wide - too narrow for the horizontal layout this
   same widget still uses in #admin-main's header (see admin.css), which is
   why the vertical-stack rules below are scoped to #app-sidebar rather than
   changing #user-widget's own base rule. */
#app-sidebar #user-widget {
  margin-top: auto;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  padding: 12px 20px;
  border-top: 1px solid var(--sidebar-border);
  font-size: 12px;
  color: var(--sidebar-text);
}
#app-sidebar #user-widget .role-badge {
  border-color: var(--sidebar-border);
  color: var(--sidebar-text);
}
#app-sidebar #sign-out-btn {
  width: 100%;
  padding: 5px 10px;
  font-size: 12px;
  background: transparent;
  color: var(--sidebar-text);
  border-color: var(--sidebar-border);
}
#app-sidebar #sign-out-btn:hover { background: var(--sidebar-bg-hover); border-color: var(--sidebar-border); color: var(--sidebar-text-active); }

#user-widget {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--muted);
  font-size: 13px;
}
#user-widget .role-badge {
  text-transform: uppercase;
  font-size: 10px;
  letter-spacing: 0.04em;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 6px;
  color: var(--muted);
}
#sign-out-btn {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 12px;
  font-size: 13px;
  cursor: pointer;
}
#sign-out-btn:hover { border-color: var(--border-strong); }

/* Login page (public/login.html) - deliberately has no #app-shell/sidebar
   of its own, since there's nothing to navigate to yet while signed out. */
#login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
}
#login-form {
  width: 320px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  box-shadow: var(--shadow-md);
}
#login-form h1 {
  font-size: 18px;
  margin: 0;
}
#login-form label {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 12px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
#login-form input {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 14px;
  text-transform: none;
  letter-spacing: normal;
}
#login-form button {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 6px;
  padding: 10px;
  font-size: 14px;
  cursor: pointer;
  transition: background-color 0.12s ease;
}
#login-form button:hover { background: var(--accent-hover); }
#login-error {
  color: var(--status-stale);
  font-size: 12.5px;
}
#login-error.hidden { display: none; }

/* "Sign in with Microsoft" - the primary path when SSO is configured (see
   public/js/login.js's initMicrosoftSignIn()), styled as a real button even
   though it's a plain <a> (a full-page navigation to
   /api/auth/microsoft/login, not a fetch() - the whole point is leaving this
   app for Microsoft's own sign-in page). Hidden entirely, along with the
   divider below it, when SSO isn't configured - the password form beneath is
   then the only thing on the page, exactly as it was before SSO existed. */
#microsoft-sign-in-link {
  display: block;
  text-align: center;
  background: var(--text);
  color: #fff;
  border: 1px solid var(--text);
  border-radius: 6px;
  padding: 10px;
  font-size: 14px;
  text-decoration: none;
  transition: background-color 0.12s ease;
}
#microsoft-sign-in-link:hover { background: var(--text-secondary); border-color: var(--text-secondary); }
#login-password-divider {
  text-align: center;
  color: var(--muted);
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}

header h1 {
  font-size: 18px;
  font-weight: 600;
  margin: 0;
  letter-spacing: 0.01em;
  color: var(--text);
}

#meta {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--muted);
  font-size: 13px;
}

#notifications-wrap { position: relative; }

#notifications-bell {
  position: relative;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 14px;
  cursor: pointer;
  line-height: 1;
}
#notifications-bell:hover { border-color: var(--border-strong); }

#notifications-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  background: var(--status-stale);
  color: #fff;
  border-radius: 999px;
  font-size: 10px;
  font-weight: 700;
  line-height: 1;
  padding: 3px 5px;
  min-width: 16px;
  text-align: center;
}
#notifications-badge.hidden { display: none; }
/* Shown instead of a normal count when GET /api/faults itself failed (see
   refreshFaults()/renderNotifications() in graph.js) - deliberately a
   different colour and glyph ("!" not a number) from the ordinary badge, so
   "couldn't check for faults" is never visually confused with "checked, and
   there are none". */
#notifications-badge.notifications-badge-error { background: var(--status-error); }

#notifications-dropdown {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 6px;
  width: 320px;
  max-height: 360px;
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: var(--shadow-lg);
  z-index: 20;
  text-align: left;
}
#notifications-dropdown.hidden { display: none; }

.notification-item {
  padding: 8px 12px;
  font-size: 12.5px;
  color: var(--text);
  cursor: pointer;
  border-bottom: 1px solid var(--border);
}
.notification-item:last-child { border-bottom: none; }
.notification-item:hover { background: var(--row-hover); }
.notification-text { color: var(--text); }
.notification-detail { color: var(--muted); font-size: 11px; margin-top: 2px; }

.notifications-empty, .notifications-more {
  padding: 10px 12px;
  font-size: 12px;
  color: var(--muted);
  text-align: center;
  font-style: italic;
}

.notifications-error {
  padding: 8px 12px;
  font-size: 11.5px;
  color: var(--status-error);
  text-align: center;
  border-bottom: 1px solid var(--border);
}

/* Brief highlight when a gateway fault in the notifications dropdown is
   clicked (see jumpToGatewayFault() in graph.js) - draws the eye to the
   exact row after scrollIntoView, since the table itself has no other way
   to show "this is the one you just clicked". */
@keyframes row-flash-fade {
  from { background: rgba(180, 83, 9, 0.18); }
  to { background: transparent; }
}
.row-flash { animation: row-flash-fade 2s ease-out; }

#refresh-btn, #refresh-customer-btn, #clear-selection-btn {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 12px;
  font-size: 13px;
  cursor: pointer;
}
#refresh-btn:not(:disabled):hover, #refresh-customer-btn:not(:disabled):hover, #clear-selection-btn:hover { border-color: var(--border-strong); }
#refresh-btn:disabled, #refresh-customer-btn:disabled { opacity: 0.5; cursor: not-allowed; }
#clear-selection-btn.hidden { display: none; }

#breadcrumb-nav {
  display: flex;
  align-items: flex-end;
  gap: 16px;
  padding: 12px 24px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

#breadcrumb-nav label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: relative;
}

#breadcrumb-nav select,
#breadcrumb-nav input {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
  min-width: 160px;
}

#device-search-wrap { position: relative; }

#device-search-results {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 4px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  min-width: 220px;
  max-height: 260px;
  overflow-y: auto;
  z-index: 20;
  box-shadow: var(--shadow-lg);
}
#device-search-results.hidden { display: none; }
#device-search-results .result-item {
  padding: 8px 10px;
  font-size: 12.5px;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text);
  cursor: pointer;
  border-bottom: 1px solid var(--border);
}
#device-search-results .result-item:last-child { border-bottom: none; }
#device-search-results .result-item:hover { background: var(--row-hover); }
#device-search-results .result-item .muted { color: var(--muted); font-size: 11px; }

#source-banner {
  padding: 10px 24px;
  font-size: 12.5px;
  background: var(--status-error-soft);
  border-bottom: 1px solid var(--status-error);
  color: var(--status-error);
}
#source-banner.hidden { display: none; }
#source-banner .source-line { margin: 2px 0; }

#legend {
  display: flex;
  gap: 18px;
  padding: 10px 24px;
  font-size: 12px;
  color: var(--muted);
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
#legend .legend-item { display: flex; align-items: center; gap: 6px; }
#legend .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
#legend .legend-divider { width: 1px; align-self: stretch; background: var(--border); }
#legend .legend-heading {
  display: flex;
  align-items: center;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
}

#map-container {
  padding: 24px;
  overflow: auto;
}

#map { width: 100%; min-width: 700px; height: auto; display: block; }

/* Split into two side-by-side panes (#gateway-pane / #integrations-pane)
   rather than stacking Gateways above Integrations - both are "what's live
   right now for this organisation" summaries meant to be scanned together,
   and stacking them would push Integrations below the fold on anything but
   a tall window. #gateway-section itself stays the single show/hide unit
   driven by org selection (see gatewaySection in graph.js) - only its
   internal layout changes here. */
#gateway-section {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: start;
  padding: 8px 24px 32px;
  border-top: 1px solid var(--border);
}
#gateway-section.hidden { display: none; }
#gateway-section h2 {
  font-size: 14px;
  margin: 16px 0 12px;
  color: var(--text);
}
#gateway-pane { min-width: 0; padding-right: 20px; }
#integrations-pane {
  min-width: 0;
  padding-left: 20px;
  border-left: 1px solid var(--border);
}

#gateway-filters:empty { display: none; }
#gateway-filters {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  padding: 12px 20px;
  align-items: flex-end;
}
#gateway-filters .panel-filter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  flex: 1;
  min-width: 140px;
}
#gateway-filters .panel-filter select,
#gateway-filters .panel-filter input {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
}

#gateway-table-wrap { overflow-x: auto; }
#gateway-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
#gateway-table.hidden { display: none; }
#gateway-table th {
  text-align: left;
  padding: 8px 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
#gateway-table th:hover { color: var(--text); }
/* Sort direction indicator - only one header carries either class at a
   time (see updateGatewaySortHeaderUI() in graph.js), a plain unicode arrow
   rather than an image/icon font so it needs no extra asset. */
#gateway-table th.sort-asc::after { content: ' \25B2'; font-size: 9px; }
#gateway-table th.sort-desc::after { content: ' \25BC'; font-size: 9px; }
#gateway-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
#gateway-table tbody tr:last-child td { border-bottom: none; }
#gateway-table tbody tr:hover { background: var(--row-hover); }
#gateway-table .muted { color: var(--muted); font-size: 11px; }

.gateway-group-row td {
  padding: 10px 12px 4px;
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  border-bottom: none;
  background: none;
}
.gateway-group-row:hover { background: none; }

/* Checkbox column shared by the gateway table and (task #233) the device
   panel list - narrow and non-sortable, so it's excluded from the
   pointer/hover-color treatment the rest of #gateway-table th gets. */
.select-col { width: 32px; cursor: default !important; }
.select-col:hover { color: var(--muted) !important; }
.gateway-group-row .select-col { padding: 10px 12px 4px; }

/* Inline "under maintenance" indicator next to a gateway/device name - a
   plain emoji (no extra asset) with a tooltip carrying the note, distinct
   from .station-node-maintenance-badge which sits on the SVG map itself. */
.maintenance-badge { font-size: 12px; cursor: help; }

/* Multi-select toolbar above the gateway table (renderGatewayToolbar() in
   graph.js) - reuses .panel-toolbar-btn/-count/-status from the device
   panel toolbar so both look and behave the same way. */
#gateway-toolbar.hidden { display: none; }
#gateway-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 0;
  flex-wrap: wrap;
}

.system-badge {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid;
  border-radius: 10px;
  font-size: 11px;
  line-height: 1.6;
}

#gateway-empty {
  padding: 16px 12px;
  color: var(--muted);
  font-size: 12.5px;
}
#gateway-empty.hidden { display: none; }

/* Integrations pane (public/index.html, driven by graph.js's
   refreshIntegrations()/renderIntegrationsTable()) - mirrors #gateway-filters/
   #gateway-table/#gateway-empty above almost rule-for-rule rather than
   sharing selectors, same convention this stylesheet already uses for each
   independent section (see the Data Quality Control comment below). No
   toolbar/select-column equivalent - integrations are read-only here (managed
   from /customers.html instead), so there's nothing to bulk-act on. Status
   cells reuse .webhook-status-cell/-ok/-error (below, shared with the
   standalone Webhooks page) rather than inventing a second colour pairing
   for the same two-value status. */
#integrations-filters:empty { display: none; }
#integrations-filters {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  padding: 12px 20px;
  align-items: flex-end;
}
#integrations-filters .panel-filter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  flex: 1;
  min-width: 140px;
}
#integrations-filters .panel-filter select {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
}

#integrations-table-wrap { overflow-x: auto; }
#integrations-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
#integrations-table.hidden { display: none; }
#integrations-table th {
  text-align: left;
  padding: 8px 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
#integrations-table th:hover { color: var(--text); }
#integrations-table th.sort-asc::after { content: ' \25B2'; font-size: 9px; }
#integrations-table th.sort-desc::after { content: ' \25BC'; font-size: 9px; }
#integrations-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
#integrations-table tbody tr:last-child td { border-bottom: none; }
#integrations-table tbody tr:hover { background: var(--row-hover); }
#integrations-table .muted { color: var(--muted); font-size: 11px; }

#integrations-empty {
  padding: 16px 12px;
  color: var(--muted);
  font-size: 12.5px;
}
#integrations-empty.hidden { display: none; }

/* Data Quality Control page (public/data-quality.html) - mirrors
   #gateway-section/table styling above rather than sharing selectors, same
   convention this stylesheet already uses for each independent section.
   Used to sit below the map/gateways on index.html; now the sole content of
   its own page (reached via #app-sidebar), so `.hidden` is never actually
   applied there - kept only in case something needs it in future. */
#data-quality-section {
  padding: 8px 24px 32px;
  border-top: 1px solid var(--border);
}
#data-quality-section.hidden { display: none; }
#data-quality-section h2 {
  font-size: 14px;
  margin: 16px 0 4px;
  color: var(--text);
}
.section-subtitle {
  font-size: 12px;
  color: var(--muted);
  margin: 0 0 12px;
}
#data-quality-controls {
  display: flex;
  gap: 16px;
  align-items: flex-end;
  margin-bottom: 12px;
}
#data-quality-controls .panel-filter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  min-width: 200px;
}
#data-quality-controls .panel-filter select {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
}
#data-quality-download-btn {
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 12px;
  font-size: 13px;
  cursor: pointer;
}
#data-quality-download-btn:hover { border-color: var(--border-strong); }
#data-quality-table-wrap { overflow-x: auto; }
#data-quality-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
#data-quality-table th {
  text-align: left;
  padding: 8px 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
#data-quality-table th:hover { color: var(--text); }
#data-quality-table th.sort-asc::after { content: ' \25B2'; font-size: 9px; }
#data-quality-table th.sort-desc::after { content: ' \25BC'; font-size: 9px; }
#data-quality-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
#data-quality-table tbody tr:last-child td { border-bottom: none; }
#data-quality-table tbody tr:hover { background: var(--row-hover); }
#audit-log-controls {
  display: flex;
  gap: 16px;
  align-items: flex-end;
  margin-bottom: 12px;
}
#audit-log-controls .panel-filter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  min-width: 200px;
}
#audit-log-controls .panel-filter select,
#audit-log-controls .panel-filter input {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
}
#audit-log-table-wrap { overflow-x: auto; }
#audit-log-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
#audit-log-table th {
  text-align: left;
  padding: 8px 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
#audit-log-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
#audit-log-table tbody tr:last-child td { border-bottom: none; }
#audit-log-table tbody tr:hover { background: var(--row-hover); }
#audit-log-table .muted { color: var(--muted); font-size: 11px; }
#audit-log-table.hidden { display: none; }
#audit-log-empty {
  padding: 16px 12px;
  color: var(--muted);
  font-size: 12.5px;
}
#audit-log-load-more-btn {
  margin-top: 12px;
  background: var(--panel);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 12px;
  font-size: 13px;
  cursor: pointer;
}
#audit-log-load-more-btn:hover { border-color: var(--border-strong); }
#audit-log-load-more-btn.hidden { display: none; }
#data-quality-empty {
  padding: 16px 12px;
  color: var(--muted);
  font-size: 12.5px;
}

#uptime-controls {
  display: flex;
  gap: 16px;
  align-items: flex-end;
  margin-bottom: 12px;
}
#uptime-controls .panel-filter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  min-width: 180px;
}
#uptime-controls .panel-filter select {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
}
#uptime-table-wrap { overflow-x: auto; }
#uptime-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
#uptime-table th {
  text-align: left;
  padding: 8px 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
#uptime-table th:hover { color: var(--text); }
#uptime-table th.sort-asc::after { content: ' \25B2'; font-size: 9px; }
#uptime-table th.sort-desc::after { content: ' \25BC'; font-size: 9px; }
#uptime-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
#uptime-table tbody tr:last-child td { border-bottom: none; }
#uptime-table tbody tr:hover { background: var(--row-hover); }
#uptime-table .muted { color: var(--muted); font-size: 11px; }
#uptime-table.hidden { display: none; }
.uptime-cell { font-weight: 600; }
.uptime-cell.uptime-good { color: var(--status-fresh); }
.uptime-cell.uptime-warn { color: var(--status-warning); }
.uptime-cell.uptime-bad { color: var(--status-stale); }
.uptime-cell.uptime-none { color: var(--muted); font-weight: 400; }

/* Webhooks page (public/webhooks.html) - same table/filter shell as the
   Uptime page above, just under its own #webhooks-* ids so neither page's
   CSS accidentally leaks into the other if one changes later. */
#webhooks-controls {
  display: flex;
  gap: 16px;
  align-items: flex-end;
  margin-bottom: 12px;
  flex-wrap: wrap;
}
#webhooks-controls .panel-filter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  min-width: 180px;
}
#webhooks-controls .panel-filter select,
#webhooks-controls .panel-filter input {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
}
#webhooks-table-wrap { overflow-x: auto; }
#webhooks-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12.5px;
}
#webhooks-table th {
  text-align: left;
  padding: 8px 12px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  white-space: nowrap;
  border-bottom: 1px solid var(--border);
}
#webhooks-table th[data-sort] { cursor: pointer; user-select: none; }
#webhooks-table th[data-sort]:hover { color: var(--text); }
#webhooks-table th.sort-asc::after { content: ' \25B2'; font-size: 9px; }
#webhooks-table th.sort-desc::after { content: ' \25BC'; font-size: 9px; }
#webhooks-table td {
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
#webhooks-table tbody tr:last-child td { border-bottom: none; }
#webhooks-table tbody tr:hover { background: var(--row-hover); }
#webhooks-table .muted { color: var(--muted); font-size: 11px; }
#webhooks-table.hidden { display: none; }
.webhook-status-cell { font-weight: 600; }
.webhook-status-cell.webhook-status-ok { color: var(--status-fresh); }
.webhook-status-cell.webhook-status-error { color: var(--status-error); }
#uptime-empty {
  padding: 16px 12px;
  color: var(--muted);
  font-size: 12.5px;
}

#fleet-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 14px;
}
.fleet-tile {
  display: block;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 4px solid var(--status-unknown);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  text-decoration: none;
  color: var(--text);
  box-shadow: var(--shadow-sm);
  transition: border-color 0.12s ease, transform 0.12s ease, box-shadow 0.12s ease;
}
.fleet-tile:hover { transform: translateY(-2px); border-color: var(--border-strong); box-shadow: var(--shadow-md); }
.fleet-tile-fresh { border-left-color: var(--status-fresh); }
.fleet-tile-warning { border-left-color: var(--status-warning); }
.fleet-tile-stale { border-left-color: var(--status-stale); }
.fleet-tile-dead { border-left-color: var(--status-dead); }
.fleet-tile-error { border-left-color: var(--status-error); }
.fleet-tile-unknown { border-left-color: var(--status-unknown); }
.fleet-tile-header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 6px;
}
.fleet-tile-name { font-size: 14px; font-weight: 600; }
.fleet-tile-status-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
.fleet-tile-count { font-size: 12px; color: var(--muted); margin-bottom: 8px; }
.fleet-tile-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; }
.fleet-tile-chip {
  font-size: 11px;
  padding: 2px 7px;
  border-radius: 10px;
  background: var(--bg);
  color: var(--text);
}
.fleet-tile-chip-fresh { color: var(--status-fresh); }
.fleet-tile-chip-warning { color: var(--status-warning); }
.fleet-tile-chip-stale { color: var(--status-stale); }
.fleet-tile-chip-dead { color: var(--status-dead); }
.fleet-tile-chip-error { color: var(--status-error); }
.fleet-tile-chip-unknown { color: var(--muted); }
.fleet-tile-faults { font-size: 12px; color: var(--status-warning); font-weight: 600; }
.fleet-tile-faults-none { color: var(--muted); font-weight: 400; }
#fleet-empty {
  padding: 16px 12px;
  color: var(--muted);
  font-size: 12.5px;
}
#data-quality-empty.hidden { display: none; }

.station-label {
  fill: var(--text);
  font-size: 12px;
  font-weight: 600;
  text-anchor: middle;
}

/* Optional per-customer alias (Mobiusflow's "Alias" field today - see
   src/systemTypes.js/stationAliases in graph.js's render()) - sits directly
   above the station name, smaller and muted so the real station name still
   reads as the primary label. */
.station-alias-label {
  fill: var(--muted);
  font-size: 10px;
  font-style: italic;
  text-anchor: middle;
}

.station-timestamp-label {
  fill: var(--muted);
  font-size: 9.5px;
  text-anchor: middle;
  pointer-events: none;
}

.track {
  fill: none;
  stroke: var(--track-color);
  cursor: default;
}
.track.highlighted {
  stroke: var(--accent);
}
/* Same-column "link" edges (Datacake<->Mobiusflow etc - see isLink in
   pipelineEngine.js/computeColumns() above) - dashed rather than solid so
   this reads as "these two are connected" (a tube-map interchange) rather
   than the directional "feeds into" a normal solid track implies. */
.track.track-link {
  stroke-dasharray: 4 5;
}

.station-node-group { cursor: pointer; }
.station-node-group.dimmed { opacity: 0.25; }
.station-node-wedge {
  stroke: var(--bg);
  stroke-width: 1.5;
  transition: stroke-width 0.15s;
}
.station-node-group:hover .station-node-wedge { stroke-width: 3; }
.station-node-hit { fill: transparent; }

/* Battery/service-status ring segments - visible arcs stay pointer-events:
   none (each has its own invisible, wider .station-node-ring-segment-hit
   sibling on top - see graph.js's render() - which is what actually reacts
   to the mouse). stroke-width transitions here so the JS-toggled
   .ring-segment-hover class (added/removed by that sibling's mouseenter/
   mouseleave) reads as the hovered segment visibly "popping" outward from
   the rest of the ring, rather than an instant snap. */
.station-node-battery-ring,
.station-node-service-ring {
  pointer-events: none;
}
.station-node-ring-segment {
  transition: stroke-width 0.12s ease;
}
.station-node-ring-segment.ring-segment-hover {
  stroke-width: 11;
}
.station-node-ring-segment-hit {
  cursor: pointer;
}
.station-node-maintenance-badge {
  font-size: 15px;
  cursor: default;
  user-select: none;
}

/* Illustrative (no-adapter) custom stations - see graph.js's buildGraph()
   `node.illustrative` branch. Deliberately plain and unclickable: dashed
   outline + neutral fill instead of a colour that would otherwise read as a
   real status, and the default cursor rather than the pointer every real
   station gets, since there's nothing to click through to. */
.station-node-illustrative { cursor: default; }
.station-node-illustrative-circle {
  fill: var(--bg);
  stroke: var(--muted);
  stroke-width: 1.5;
  stroke-dasharray: 4 3;
}

.tooltip {
  position: fixed;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text);
  pointer-events: none;
  max-width: 300px;
  box-shadow: var(--shadow-lg);
  z-index: 10;
}
.tooltip.hidden { display: none; }
.tooltip b { color: var(--text); }
.tooltip .muted { color: var(--muted); }

#device-panel {
  position: fixed;
  top: 0;
  right: 0;
  width: 380px;
  height: 100%;
  background: var(--panel);
  border-left: 1px solid var(--border);
  box-shadow: -12px 0 32px rgba(15, 23, 42, 0.18);
  z-index: 30;
  display: flex;
  flex-direction: column;
}
#device-panel.hidden { display: none; }

#device-panel-header {
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
#device-panel-header .close-btn {
  cursor: pointer;
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
  background: none;
  border: none;
}
#device-panel-header .close-btn:hover { color: var(--text); }

#device-panel-maintenance-banner {
  padding: 8px 20px;
  font-size: 12.5px;
  color: var(--status-warning);
  background: var(--status-warning-soft);
  border-bottom: 1px solid var(--border);
}
#device-panel-maintenance-banner.hidden { display: none; }

#device-panel-filters:empty { display: none; }
#device-panel-filters {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  padding: 12px 20px;
  border-bottom: 1px solid var(--border);
}
#device-panel-filters .panel-filter {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  flex: 1;
  min-width: 120px;
}
#device-panel-filters .panel-filter select,
#device-panel-filters .panel-filter input {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 8px;
  font-size: 13px;
  text-transform: none;
  letter-spacing: normal;
}

#device-panel-toolbar:empty { display: none; }
#device-panel-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 20px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
.panel-toolbar-btn {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 12px;
  cursor: pointer;
}
.panel-toolbar-btn:hover { border-color: var(--border-strong); }
.panel-toolbar-count { font-size: 11.5px; color: var(--muted); margin-left: auto; }
.panel-toolbar-status { font-size: 11.5px; color: var(--status-fresh); width: 100%; }

#device-panel-body {
  padding: 12px 20px;
  overflow-y: auto;
  font-size: 12.5px;
}

.device-panel-row {
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}
.device-panel-row:last-child { border-bottom: none; }
.device-panel-row .row-title { font-weight: 600; display: flex; align-items: center; gap: 6px; }
.device-panel-row .row-title span { flex: 1; }
.device-panel-row .row-sub { color: var(--muted); font-size: 11.5px; margin-top: 2px; }
.row-select { display: flex; align-items: center; flex-shrink: 0; cursor: pointer; }

/* Sub-row of Trigger/End-Maintenance controls, appended into
   #device-panel-toolbar (see renderDevicePanelMaintenanceToolbar() in
   graph.js) - only occupies space once something's actually selected,
   otherwise collapses to nothing rather than leaving an empty gap. */
#device-panel-maintenance-toolbar:empty { display: none; }
#device-panel-maintenance-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  width: 100%;
  padding-top: 8px;
  margin-top: 4px;
  border-top: 1px solid var(--border);
}
.row-view-btn {
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 3px 8px;
  font-size: 11px;
  font-weight: 400;
  cursor: pointer;
  flex-shrink: 0;
}
.row-view-btn:hover { border-color: var(--border-strong); }
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }

/* See src/dataQuality.js - a device matched across systems by a real
   devEui/serial identity where those systems disagree on its name. Shown
   inline wherever a device's name already appears (station panel rows,
   search results, the device panel header) - see nameMismatchBadge() in
   graph.js. */
.name-mismatch-badge {
  font-size: 11px;
  color: var(--status-warning);
  border: 1px solid var(--status-warning);
  border-radius: 4px;
  padding: 1px 5px;
  margin-left: 4px;
  white-space: nowrap;
  cursor: help;
}

.signal-meta { font-size: 11px; margin-top: 6px; }
.signal-errors { color: var(--status-error); font-size: 11px; margin-top: 4px; }

.device-panel-section { padding: 12px 0 4px; border-top: 1px solid var(--border); margin-top: 8px; }
.device-panel-section-title { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 10px; }

.signal-gauges-row { display: flex; flex-wrap: wrap; gap: 10px 6px; justify-content: space-between; }
.signal-gauges-loading { color: var(--muted); font-size: 12px; padding: 4px 0; }

.signal-gauge { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 58px; }
.gauge-ring { position: relative; width: 52px; height: 52px; }
.gauge-ring svg { width: 100%; height: 100%; }
.gauge-value {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1.15;
  white-space: pre-line;
  padding: 0 2px;
}
.gauge-label { font-size: 9.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.02em; text-align: center; }

.station-list-item {
  display: flex;
  justify-content: space-between;
  padding: 6px 0;
  border-bottom: 1px dashed var(--border);
  font-size: 12px;
}
.station-list-item:last-child { border-bottom: none; }
