/* ============================================================
   pages.css — per-page layouts.
   Phase 2 dashboard grid + Phase 3 page styles.
   ============================================================ */

/* ---- Dashboard grid (Phase 2) ---------------------------- */
.dash-row {
  display: grid;
  gap: 14px;
  margin-bottom: 14px;
}
.dash-row.r-1 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.dash-row.r-2 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.1fr); }
.dash-row.r-3 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.dash-row .panel       { min-height: 220px; }
.dash-row.r-2 .panel   { min-height: 260px; }

/* GH #289: pages that append bare .panel sections directly (not inside a
   .dash-row grid, which already provides its own `gap`) had no spacing
   between them at all -- only each panel's own 1px border kept adjacent
   panels (e.g. RTLS Tracking's "Gateways" and "Equipment Availability")
   from visually touching. Scoped to direct children of #view-mount so it
   doesn't stack on top of .dash-row's own grid `gap`. */
#view-mount > .panel + .panel { margin-top: 16px; }

/* GH #323: Reader Status stacks bare .panel sections (GPIO Monitor & Control,
   Packet Parser) around a .dual-col row (Identity & Network + Antennas). The
   rule above only matches .panel-to-.panel siblings, so the panel-to-.dual-col
   and .dual-col-to-panel edges had zero gap -- panels read as visually fused
   to the two-column row above/below them. Same fix as GH #289, extended to
   the .dual-col boundary. */
#view-mount > .panel + .dual-col,
#view-mount > .dual-col + .panel { margin-top: 16px; }

/* GH #322: MQTT Monitoring's status cards live in a bare #mq-status div
   (not a .panel, since it's re-painted independently by the polling tick),
   sandwiched directly between the "Reader" panel and the "Broker
   Configuration" panel. Neither of the rules above matches a bare div, so
   it had zero top margin and visually touched/overlapped the Reader panel's
   bottom edge. .stat-grid's own margin-bottom already spaces it from the
   panel below. */
#mq-status { margin-top: 16px; }

/* GH #325: Analytics' summary stat cards live in a bare #an-cards div (not
   a .panel, since it's repainted independently after each filter change),
   sandwiched directly between the "Reader Filter" panel and the charts
   below. Same bug class as GH #322/#mq-status: neither of the .panel-based
   rules above matches a bare div, so it had zero top margin and visually
   overlapped the Reader Filter panel's bottom edge. */
#an-cards { margin-top: 16px; }

@media (max-width: 1100px) {
  .dash-row.r-1, .dash-row.r-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash-row.r-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash-row.r-2 .panel.span-2 { grid-column: 1 / -1; }
}
@media (max-width: 640px) {
  .dash-row.r-1, .dash-row.r-2, .dash-row.r-3 { grid-template-columns: 1fr; }
  .dash-row .panel { min-height: 0; }
}

/* ---- About page (Phase 2) -------------------------------- */
.about-card { max-width: 760px; margin: 12px auto 0; }
.about-card .panel { padding: 28px 32px; }
.about-version {
  font-size: 28px; font-weight: 700; color: var(--text-1);
  letter-spacing: -0.02em; margin: 0 0 4px;
}
.about-tagline { color: var(--accent-cyan); font-size: 13px; margin: 0 0 18px; }
.about-kv .kv-label { min-width: 160px; }
.about-links { margin-top: 18px; display: flex; gap: 10px; flex-wrap: wrap; }

/* ---- Stub / Phase-4-pending pages ------------------------ */
.stub-card { max-width: 600px; margin: 60px auto 0; text-align: center; }
.stub-icon {
  width: 64px; height: 64px; margin: 0 auto 18px;
  display: grid; place-items: center; border-radius: 50%;
  background: var(--accent-cyan-dim); color: var(--accent-cyan); font-size: 26px;
}
.stub-title { font-size: 20px; color: var(--text-1); margin: 0 0 8px; font-weight: 600; }
.stub-sub   { color: var(--text-3); font-size: 13.5px; max-width: 440px; margin: 0 auto; }
.stub-meta {
  margin-top: 24px; display: inline-block;
  padding: 6px 14px; border-radius: 999px;
  background: var(--accent-cyan-dim); color: var(--accent-cyan);
  font-size: 11.5px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
}

/* ---- Inventory control & alerts (Phase 2) --------------- */
.inv-rows .kv:last-of-type {
  margin-bottom: 12px; padding-bottom: 12px;
  border-bottom: 1px solid var(--border-1);
}
.inv-error { font-size: 12.5px; color: var(--accent-red); margin: 6px 0 12px; min-height: 18px; }
.inv-error.is-empty { color: var(--text-4); }
.alerts-empty {
  flex: 1; display: flex; flex-direction: column;
  align-items: center; justify-content: center; padding: 14px;
}
.alerts-empty .empty-icon {
  background: var(--accent-green-dim); color: var(--accent-green);
  font-size: 22px; width: 48px; height: 48px;
}
.alerts-empty .empty-title { color: var(--text-1); font-weight: 600; font-size: 14px; margin-top: 4px; }
.alerts-empty .empty-sub   { font-size: 12px; }
.alerts-list { display: flex; flex-direction: column; gap: 8px; }
.alerts-list .alert-item {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 8px 10px; border-radius: var(--radius-1);
  background: var(--bg-3);
  border-left: 3px solid var(--accent-red);
  font-size: 12.5px;
}
.alerts-list .alert-item.is-warn { border-left-color: var(--accent-amber); }
.alerts-list .alert-item.is-info { border-left-color: var(--accent-cyan); }
.alert-time { color: var(--text-3); font-family: var(--font-mono); font-size: 11.5px; flex-shrink: 0; }
.alert-msg  { color: var(--text-1); min-width: 0; word-break: break-word; }

/* ---- Loading shimmer (Phase 2) -------------------------- */
.is-loading .kv-value, .is-loading .kv-hero-value,
.is-loading .kv-num, .is-loading .strip-strong {
  position: relative; color: transparent !important;
}
.is-loading .kv-value::after, .is-loading .kv-hero-value::after,
.is-loading .kv-num::after, .is-loading .strip-strong::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%;
  height: 0.7em; margin-top: -0.35em;
  background: linear-gradient(90deg, var(--bg-3) 0%, var(--border-1) 50%, var(--bg-3) 100%);
  background-size: 200% 100%; border-radius: 3px;
  animation: shimmer 1.6s ease infinite;
}
@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.view-enter { opacity: 0; transform: translateY(4px); animation: viewEnter var(--t-med) ease forwards; }
@keyframes viewEnter { to { opacity: 1; transform: translateY(0); } }


/* ============================================================
   Phase 3 — shared page constructs
   ============================================================ */

/* ---- Page header (title + actions row) ------------------- */
.page-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; margin: 6px 0 18px;
  flex-wrap: wrap;
}
.page-header-main { display: flex; align-items: center; gap: 14px; min-width: 0; }
.page-icon {
  width: 38px; height: 38px;
  display: grid; place-items: center;
  border-radius: var(--radius-2);
  font-size: 17px;
}
.page-icon.acc-cyan   { background: var(--accent-cyan-dim);   color: var(--accent-cyan); }
.page-icon.acc-violet { background: var(--accent-violet-dim); color: var(--accent-violet); }
.page-icon.acc-amber  { background: var(--accent-amber-dim);  color: var(--accent-amber); }
.page-icon.acc-green  { background: var(--accent-green-dim);  color: var(--accent-green); }
.page-icon.acc-red    { background: var(--accent-red-dim);    color: var(--accent-red); }
.page-icon.acc-blue   { background: var(--accent-blue-dim);   color: var(--accent-blue); }

.page-titles { min-width: 0; }
.page-title {
  margin: 0; font-size: 20px; font-weight: 700; color: var(--text-1);
  letter-spacing: -0.01em;
}
.page-subtitle { margin: 3px 0 0; font-size: 12.5px; color: var(--text-3); }
.page-actions  { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- Stat tiles ----------------------------------------- */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 12px;
  margin-bottom: 18px;
}
.stat-tile {
  background: var(--bg-2);
  border: 1px solid var(--border-1);
  border-radius: var(--radius-2);
  padding: 14px 16px;
  box-shadow: var(--shadow-panel);
  border-left: 3px solid var(--accent-cyan);
}
/* GH #47: interactive analytics — floating tooltip + clickable affordances */
.chart-tip {
  position: fixed;
  z-index: 3000;
  pointer-events: none;
  background: var(--bg-1, #0b1220);
  color: var(--text-1, #e5e7eb);
  border: 1px solid var(--border-1);
  border-radius: 6px;
  padding: 4px 8px;
  font-size: 12px;
  line-height: 1.3;
  box-shadow: 0 4px 14px rgba(0,0,0,0.4);
  max-width: 260px;
}
.chart-hit { cursor: default; }
.chart-clickable { cursor: pointer; }
.ant-bar-clickable { cursor: pointer; border-radius: 6px; }
.ant-bar-clickable:hover { background: rgba(255,255,255,0.04); }
.stat-clickable { cursor: pointer; }
.stat-clickable:hover { border-color: var(--accent-green); }
.stat-tile.acc-violet { border-left-color: var(--accent-violet); }
.stat-tile.acc-amber  { border-left-color: var(--accent-amber); }
.stat-tile.acc-green  { border-left-color: var(--accent-green); }
.stat-tile.acc-red    { border-left-color: var(--accent-red); }
.stat-tile.acc-blue   { border-left-color: var(--accent-blue); }
.stat-value {
  font-size: 22px; font-weight: 700; color: var(--text-1);
  font-variant-numeric: tabular-nums; line-height: 1.1;
}
.stat-label {
  font-size: 11.5px; color: var(--text-3);
  text-transform: uppercase; letter-spacing: 0.05em;
  margin-top: 6px;
}

/* ---- Filter bar (above tables) -------------------------- */
.filter-bar {
  display: flex; flex-wrap: wrap;
  align-items: flex-end; gap: 12px;
  margin-bottom: 12px;
}
.filter-label {
  display: flex; flex-direction: column; gap: 4px;
  font-size: 11px; color: var(--text-3);
  text-transform: uppercase; letter-spacing: 0.04em;
}
.filter-search, .filter-select {
  padding: 7px 11px;
  border-radius: var(--radius-1);
  background: var(--bg-2);
  border: 1px solid var(--border-2);
  color: var(--text-1);
  font-size: 13px;
  min-width: 180px;
  font-family: inherit;
}
.filter-search:focus, .filter-select:focus {
  outline: none;
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px rgba(6,182,212,0.15);
}
.filter-actions { margin-left: auto; display: flex; gap: 8px; }

/* GH #606: "Showing X-Y of Z" readout -- top-left, above the table, below
   the filter/toolbar row (matches org.js/users.js's existing `.user-count`
   placement) so every grid on the shared filterableTable() component shows
   it, opposite the "Per Page" select (top-right, via .pager-size-label
   above) and the First/Prev/Page-X-of-Y/Next/Last nav (bottom-center). */
.filtertbl-count { margin: -4px 0 8px; }

.filtertbl { display: flex; flex-direction: column; }
.pager {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 0 4px;
  font-size: 12px; color: var(--text-3);
}
.pager-info { margin: 0 8px; font-variant-numeric: tabular-nums; }

/* GH #585: standardized First/Prev/Next/Last + Rows Per Page pager, shared
   by every data grid via components.js's renderPager().
   GH #606: pagination controls centered at the bottom of the grid (was
   effectively left-aligned on desktop -- justify-content:space-between has
   nothing to space out once GH #604 moved the Rows Per Page select out of
   this row and into the top filter bar, leaving .pager-nav as the sole
   child). */
.pager-full { flex-wrap: wrap; justify-content: center; width: 100%; }
.pager-nav { display: flex; align-items: center; gap: 4px; }
.pager-btn { padding: 4px 10px; min-width: 32px; }
.pager-btn:disabled { opacity: 0.4; cursor: default; }
.pager-size-label { margin-left: auto; display: flex; align-items: center; gap: 6px; }
@media (max-width: 720px) {
  .pager-full { justify-content: center; }
  .pager-size-label { margin-left: 0; }
}

/* ---- Form fields ---------------------------------------- */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 14px;
}
.field { display: flex; flex-direction: column; gap: 6px; }
.field-label {
  font-size: 11.5px; color: var(--text-3);
  text-transform: uppercase; letter-spacing: 0.04em;
}
/* GH #281: `.input` is used across the RTLS Tracking page's Search Filters,
   Asset Manager and Floor Plan Configuration forms, but no rule ever defined
   it -- those inputs/selects/textareas rendered as unstyled native controls
   (no border, background, padding or consistent height) next to fields using
   the app's real `.field-*` classes. Fold `.input` into the same shared rule
   so both class names produce identical, consistent styling. */
.field-input, .field-select, .field-textarea, .input {
  padding: 8px 11px;
  border-radius: var(--radius-1);
  background: var(--bg-2);
  border: 1px solid var(--border-2);
  color: var(--text-1);
  font-size: 13px;
  font-family: inherit;
  line-height: 1.3;
}
.field-textarea, textarea.input { resize: vertical; min-height: 80px; font-family: var(--font-mono); font-size: 12.5px; }
.field-input:focus, .field-select:focus, .field-textarea:focus, .input:focus {
  outline: none;
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 3px rgba(6,182,212,0.15);
}
.field-hint { font-size: 11.5px; color: var(--text-4); }

/* GH #372: reusable toggle switch (e.g. "Enable Account Lockout"). A real
   checkbox underneath for accessibility/keyboard support, styled as a
   sliding pill via its sibling <span>. */
.toggle-switch { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
.toggle-switch input[type="checkbox"] { position: absolute; opacity: 0; width: 1px; height: 1px; }
.toggle-switch .toggle-track {
  position: relative; width: 40px; height: 22px; border-radius: 999px;
  background: var(--bg-3, var(--border-2)); border: 1px solid var(--border-2);
  transition: background-color 0.15s ease;
  flex: 0 0 auto;
}
.toggle-switch .toggle-track::after {
  content: ""; position: absolute; top: 1px; left: 1px;
  width: 18px; height: 18px; border-radius: 50%;
  background: var(--text-1, #fff);
  transition: transform 0.15s ease;
}
.toggle-switch input[type="checkbox"]:checked + .toggle-track {
  background: var(--accent-green); border-color: var(--accent-green);
}
.toggle-switch input[type="checkbox"]:checked + .toggle-track::after { transform: translateX(18px); }
.toggle-switch input[type="checkbox"]:focus-visible + .toggle-track { box-shadow: 0 0 0 3px rgba(6,182,212,0.25); }
.toggle-switch input[type="checkbox"]:disabled + .toggle-track { opacity: 0.5; cursor: not-allowed; }
.toggle-switch .toggle-label { font-size: 13px; color: var(--text-1); }

/* Required-field marker (GH #251) — the ONE mechanism for flagging a
   required CRUD-form field across the app. Apply to the <span>/<label>
   text node that holds the field's label; the trailing asterisk is drawn
   via ::after so it never becomes part of the label's actual textContent
   (tests assert on the [data-required] attribute set alongside this class
   instead — see lib/form_validation.js). */
.field-required { position: relative; }
.field-required::after { content: " *"; color: var(--accent-red); font-weight: 700; }

/* ---- Phase-4-pending callout ----------------------------- */
.phase4-pending {
  display: flex; align-items: center; gap: 10px;
  padding: 12px 16px;
  margin: 16px 0;
  border-radius: var(--radius-1);
  background: var(--accent-amber-dim);
  border-left: 3px solid var(--accent-amber);
  color: var(--text-2);
  font-size: 12.5px;
}
.phase4-icon { font-size: 17px; }
.phase4-text { color: var(--text-2); }

/* ---- Two-column "main + side" layout used by detail pages */
.dual-col {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}
@media (max-width: 1100px) { .dual-col { grid-template-columns: 1fr; } }

/* ---- Antenna grid (Reader Status / Inventory) ----------- */
.ant-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
}
.ant-card {
  background: var(--bg-3);
  border: 1px solid var(--border-1);
  border-radius: var(--radius-1);
  padding: 12px;
  display: flex; flex-direction: column; gap: 4px;
}
.ant-card-title { font-size: 11.5px; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
.ant-card-value { font-size: 16px; font-weight: 700; color: var(--text-1); }

/* ---- Logs page - full-height streaming console ---------- */
.logs-page .log-console {
  max-height: none;
  height: calc(100vh - 360px);
  min-height: 320px;
  font-size: 12.5px;
}
.logs-toolbar {
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.logs-counter {
  margin-left: auto;
  font-size: 11.5px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* ---- API Docs iframe ------------------------------------ */
.api-docs-frame {
  width: 100%;
  height: calc(100vh - 280px);
  min-height: 500px;
  border: 1px solid var(--border-1);
  border-radius: var(--radius-2);
  background: #fff; /* Swagger renders light */
}

/* ---- Mobile tweaks for Phase 3 pages -------------------- */
@media (max-width: 820px) {
  .page-header { margin-top: 4px; }
  .page-title { font-size: 17px; }
  .stat-grid { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; }
  .stat-value { font-size: 18px; }
  .filter-bar { gap: 8px; }
  .filter-search, .filter-select { min-width: 0; width: 100%; }
  .filter-label { flex: 1 1 100%; }
  .filter-actions { width: 100%; }
  .dual-col { gap: 12px; }
  .logs-page .log-console { height: calc(100vh - 320px); }
  .api-docs-frame { height: calc(100vh - 200px); min-height: 400px; }
}

@media (max-width: 480px) {
  .page-header-main { gap: 10px; }
  .page-icon { width: 32px; height: 32px; }
  .page-title { font-size: 16px; }
  .page-subtitle { font-size: 11px; }
  .stat-tile { padding: 11px 12px; }
  .stat-value { font-size: 17px; }
}

/* ============================================================
 * Phase 4 styles: login, user menu, RBAC, audit, health, backup,
 * config editor, network ping.
 *
 * All variables come from base.css; nothing hard-coded.
 * ============================================================ */

/* ---- Hide the app frame when on the login screen ---- */
body.on-login .app-sidebar,
body.on-login .sidebar,
body.on-login .sidebar-toggle,
body.on-login .app-header { display: none !important; }
/* QA #7: the reader status strip and restart banner live OUTSIDE the header,
   so hide them explicitly before authentication, and prevent a page scrollbar
   so the sign-in card is cleanly centered in the viewport. */
body.on-login .reader-strip,
body.on-login #restart-banner { display: none !important; }
/* GH #460: the global app footer (RFID Monitoring System | ... | version |
   copyright) is a sibling of .app-main inside .app-frame, same as the
   sidebar/header above -- it was never added to this hide list, so it stayed
   visible on the Sign In page even though the page has its own copyright
   line inside .login-card. The Sign In page should show only login-related
   content. */
body.on-login .app-footer { display: none !important; }
body.on-login { overflow: hidden; }
/* QA #7: reset the shell offset (left/right/top nav) so the full-screen sign-in
   split fills the viewport with no horizontal overflow / stray scrollbar. */
body.on-login { margin-left: 0 !important; margin-right: 0 !important; margin-top: 0 !important; }
body.on-login .app-frame  { grid-column: 1 / -1; }
/* GH #461: `.app-frame` itself (not just `body`, reset above) carries its own
   `margin-left: var(--sidebar-w)` (layout.css) -- or `margin-right` under
   `html[data-nav-layout="right"]` -- to make room for the sidebar. That
   margin survived even after the sidebar itself was hidden above, pushing
   the entire `.login-split` ~230px off-center and leaving a large dead
   strip on one side with no matching space on the other. Reset both
   directions unconditionally so Sign In is centered regardless of which
   side the (hidden) sidebar would normally occupy. */
body.on-login .app-frame { margin-left: 0 !important; margin-right: 0 !important; }
/* GH #390: this was `overflow: hidden`, which -- combined with
   `.login-split`'s `min-height: 100vh` demanding a full viewport inside an
   `.app-main` that's actually shrunk by the still-visible `.app-footer`'s
   height (see .app-frame, layout.css) -- silently clipped the bottom of the
   Sign In card on any viewport short enough for the two to overlap, instead
   of ever showing the clipped content. Let this container scroll like every
   other `.app-main` region does, so the full card is always reachable. */
body.on-login .app-main   { padding: 0; background: var(--bg-app); overflow-y: auto; overflow-x: hidden; }

/* ---- Login page ---- */
.login-wrapper {
  display: grid;
  place-items: center;
  min-height: 100vh;
  background:
    radial-gradient(ellipse at top, rgba(20, 184, 166, 0.12), transparent 60%),
    radial-gradient(ellipse at bottom right, rgba(99, 102, 241, 0.10), transparent 60%),
    var(--bg-app);
  padding: 2rem 1rem;
}
.login-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 16px;
  /* GH #428: 2.5rem top/bottom plus every section's own margins (brand,
     remember/forgot row, help details) routinely pushed the card past a
     standard 900px-tall viewport, forcing .app-main's scrollbar to engage
     even though nothing below the fold actually needed it. Trimmed padding
     and the brand's bottom margin (below) claw back ~1.5rem of height. */
  padding: 1.75rem 2rem;
  width: 100%;
  max-width: 420px;
  box-shadow: 0 24px 64px -16px rgba(0, 0, 0, 0.5);
}
.login-brand {
  text-align: center;
  margin-bottom: 1.25rem;
}
.login-brand img { width: 64px; height: 64px; margin: 0 auto 0.75rem; display: block; }
.login-brand h1 { font-size: 1.25rem; margin: 0; }
.login-form { display: grid; gap: 1rem; }
.login-form label { display: grid; gap: 0.35rem; }
.login-form label > span { font-size: 0.85rem; color: var(--text-muted); }
.login-form input {
  background: var(--bg-input, rgba(255,255,255,0.04));
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.65rem 0.85rem;
  color: var(--text);
  font-size: 0.95rem;
  width: 100%;
  box-sizing: border-box;
}
.login-form input:focus {
  outline: 2px solid var(--accent-cyan);
  outline-offset: -1px;
  border-color: transparent;
}
/* GH #434: Chromium-on-Windows/Edge paint a built-in clear ("x") and
   reveal-password icon inside text/password inputs unless explicitly
   suppressed. It sits inside the same box as our own focus outline/padding
   above, so it visually collides with the field. We render our own eye
   toggle for the password field (GH #430) and don't want a browser-native
   "x" on either field, so hide both native icons everywhere on this form.
   IMPORTANT: ::-ms-* and ::-webkit-* pseudo-elements must stay in SEPARATE
   comma-separated rules — an engine that doesn't recognize one selector in
   a comma list (e.g. Chromium seeing ::-ms-clear) drops the WHOLE rule, not
   just that branch, which would have silently no-opped the ::-webkit-*
   half too. */
.login-form input[type="text"]::-ms-clear,
.login-form input[type="text"]::-ms-reveal,
.login-form input[type="password"]::-ms-clear,
.login-form input[type="password"]::-ms-reveal,
.login-help input[type="text"]::-ms-clear,
.login-help input[type="text"]::-ms-reveal {
  display: none;
  width: 0;
  height: 0;
}
.login-form input[type="text"]::-webkit-clear-button,
.login-form input[type="text"]::-webkit-search-cancel-button,
.login-form input[type="password"]::-webkit-clear-button,
.login-form input[type="password"]::-webkit-search-cancel-button,
.login-help input[type="text"]::-webkit-clear-button,
.login-help input[type="text"]::-webkit-search-cancel-button {
  display: none;
  width: 0;
  height: 0;
}
/* GH #430: password visibility toggle — the input gets right-side padding
   so its own text never renders underneath the button. */
.login-pw-wrap { position: relative; }
/* GH #443: the field now also hosts a clear button alongside the eye
   toggle, so it needs room for both (see .login-clear-btn below). */
.login-pw-wrap input { padding-right: 4.3rem; }
.login-pw-toggle {
  position: absolute;
  top: 50%;
  right: 0.35rem;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0.35rem;
  line-height: 1;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 6px;
}
.login-pw-toggle:hover { color: var(--text); }
.login-pw-toggle:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 1px; }
/* GH #469: standard eye / eye-slash SVG pair -- only one is unhidden at a
   time (see login.js), both share this box so the button never reflows
   when the icon swaps. */
.login-pw-icon { display: block; }
.login-pw-icon[hidden] { display: none; }
/* GH #443: clear (✕) button for the Username and Password fields, shown
   only once the field has a value. `.login-field-wrap` is the Username
   field's own wrapper (mirrors `.login-pw-wrap`, minus the eye toggle);
   `.login-pw-wrap .login-clear-btn` shifts it left of the eye toggle so
   the two buttons never overlap. */
.login-field-wrap { position: relative; }
.login-field-wrap input { padding-right: 2.4rem; }
.login-clear-btn {
  position: absolute;
  top: 50%;
  right: 0.35rem;
  transform: translateY(-50%);
  width: 1.6rem;
  height: 1.6rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 0;
  padding: 0;
  font-size: 0.8rem;
  line-height: 1;
  color: var(--text-muted);
  cursor: pointer;
  border-radius: 6px;
}
.login-clear-btn:hover { color: var(--text); background: rgba(255, 255, 255, 0.06); }
.login-clear-btn:focus-visible { outline: 2px solid var(--accent-cyan); outline-offset: 1px; }
.login-clear-btn[hidden] { display: none; }
/* GH #443 follow-up: 2.05rem left only ~2px of clearance from the eye
   toggle's actual rendered left edge (0.35rem right offset + ~1.82rem of
   padding+icon width), so the two buttons' hit areas overlapped by a couple
   of pixels at default zoom -- caught live-browser by
   tests/e2e/gh443_444_e2e.py's 443-password-buttons-no-overlap check.
   2.35rem leaves a real gap. */
.login-pw-wrap .login-clear-btn { right: 2.35rem; }
/* GH #39: the login form validates in JS (the <form> is novalidate), so
   the browser must not paint its native "required but empty" red border
   on first paint before the user has typed anything. Keep the neutral
   border for :invalid / :-moz-ui-invalid until the field is focused. */
.login-form input:invalid,
.login-form input:-moz-ui-invalid {
  border-color: var(--border);
  box-shadow: none;
}
/* GH #40: Remember-me row — checkbox sits inline with its label, not
   stacked like the text inputs above it. */
.login-remember {
  display: flex !important;
  flex-direction: row !important;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.9rem;
  color: var(--text-muted);
  cursor: pointer;
}
.login-remember input[type="checkbox"] {
  width: auto;
  margin: 0;
  cursor: pointer;
}
/* GH #392: Forgot password? — sits directly under Remember me, right-
   aligned so it doesn't compete with the primary Sign in action below it. */
.login-forgot {
  display: block;
  text-align: right;
  font-size: 0.85rem;
  margin: 0.35rem 0 0.75rem;
}
.login-error {
  /* GH #238: was a hardcoded light-pink (#fca5a5) meant for a dark
     background. In Light theme the page background is near-white, so pale
     pink text on a pale pink tint failed contrast entirely. `--accent-red`
     is theme-aware (bright red on dark themes, a darker readable red in
     Light) and already used for error text elsewhere (`.txt-error`). */
  background: var(--accent-red-dim, rgba(220, 38, 38, 0.12));
  border: 1px solid rgba(220, 38, 38, 0.35);
  color: var(--accent-red, #dc2626);
  padding: 0.6rem 0.85rem;
  border-radius: 8px;
  font-size: 0.9rem;
}
/* GH #392: success banner for the Forgot/Reset password pages — same shape
   as .login-error, green instead of red. */
.login-success {
  background: var(--accent-green-dim, rgba(34, 197, 94, 0.12));
  border: 1px solid rgba(34, 197, 94, 0.35);
  color: var(--accent-green, #16a34a);
  padding: 0.6rem 0.85rem;
  border-radius: 8px;
  font-size: 0.9rem;
}
/* GH #49: demo-credentials panel (only rendered on demo builds) */
.login-demo {
  margin-top: 1.25rem;
  padding: 0.9rem 1rem;
  border: 1px dashed var(--border);
  border-radius: var(--radius-1, 8px);
  background: rgba(56, 189, 248, 0.06);
}
.login-demo-title {
  font-weight: 600;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin-bottom: 0.25rem;
}
.login-demo-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.6rem;
}
/* GH #462: was `flex: 1 1 auto` -- with the old "Fill Admin Credentials" /
   "Fill Operator Credentials" labels, each button's own content was wide
   enough that `flex-grow` immediately claimed a full row for itself, so the
   "horizontal row" only ever showed once wrapped, i.e. never. `flex-basis: 0`
   (rather than `auto`) makes every button split the row equally regardless
   of label length -- now short ("Fill Admin"/"Fill Operator"/"Fill Viewer")
   -- and `min-width` keeps them from being crushed unreadably thin, so on a
   narrow viewport (mobile) the flex-wrap above still kicks in and wraps
   gracefully instead of squeezing three buttons onto one unreadable row. */
.login-demo-fill { flex: 1 1 0; min-width: 108px; }

.login-help {
  /* GH #428: was 1.5rem/1rem; two of these stack (first-boot help + legacy
     API key), so trimming both saves real height even though each <details>
     is collapsed by default. */
  margin-top: 1rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
}
.login-help summary { cursor: pointer; color: var(--text-muted); }
.login-help summary:hover { color: var(--text); }
.login-help pre {
  background: var(--bg-input, rgba(255,255,255,0.04));
  padding: 0.5rem;
  border-radius: 6px;
  font-size: 0.78rem;
  overflow-x: auto;
  margin: 0.5rem 0;
}
.login-help input[type="text"] {
  width: 100%;
  margin-top: 0.5rem;
  padding: 0.45rem 0.6rem;
  background: var(--bg-input, rgba(255,255,255,0.04));
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-size: 0.85rem;
  font-family: var(--mono);
  box-sizing: border-box;
}

/* ---- Buttons used on login + Phase 4 surfaces ---- */
.btn-primary, .btn-secondary {
  cursor: pointer;
  font-size: 0.9rem;
  padding: 0.55rem 1.1rem;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: rgba(255,255,255,0.04);
  color: var(--text);
  transition: background 0.1s, border-color 0.1s;
}
.btn-primary {
  background: var(--accent-cyan);
  color: #000;
  border-color: transparent;
  font-weight: 600;
}
.btn-primary:hover:not(:disabled) { filter: brightness(1.1); }
.btn-secondary:hover:not(:disabled) { background: rgba(255,255,255,0.08); }
.btn-primary:disabled, .btn-secondary:disabled { opacity: 0.5; cursor: not-allowed; }

/* GH #256: system-wide "Export ▾" dropdown (used by Recent Reads, Tag Events,
   Audit, Analytics). One button, five formats. */
.export-menu { position: relative; display: inline-block; }
.export-menu-trigger { display: inline-flex; align-items: center; gap: 6px; }
.export-menu-caret { font-size: 0.7em; opacity: 0.8; }
.export-menu-list {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 60;
  min-width: 168px;
  padding: 4px;
  background: var(--bg-2, #141414);
  border: 1px solid var(--border-1, var(--border, #5a5a5a));
  border-radius: var(--radius-1, 8px);
  box-shadow: var(--shadow-panel, 0 8px 24px rgba(0,0,0,0.35));
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.export-menu-list[hidden] { display: none; }
.export-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 7px 10px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text-1, var(--text, #eee));
  font-size: 13px;
  font-family: var(--font-sans, inherit);
  text-align: left;
  cursor: pointer;
}
.export-menu-item:hover,
.export-menu-item:focus-visible {
  background: var(--bg-3, rgba(255,255,255,0.08));
  outline: none;
}
.export-menu-item-icon {
  width: 1.4em;
  text-align: center;
  opacity: 0.75;
  font-size: 12px;
}
.btn-link {
  background: none; border: none; cursor: pointer;
  color: var(--accent-cyan); padding: 0.2rem 0.4rem;
  font: inherit; text-decoration: none;
}
.btn-link:hover { text-decoration: underline; }
.btn-link.danger { color: #f87171; }

/* ---- Page header (used by Phase 4 pages) ---- */
.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: 1.5rem;
  flex-wrap: wrap;
}
.page-header h2 { margin: 0; }
.page-header p { margin: 0.25rem 0 0; }
.page-header-actions {
  display: flex; gap: 0.5rem; align-items: flex-end;
  flex-wrap: wrap;
}

/* ---- Callouts ---- */
.callout {
  padding: 0.75rem 1rem;
  border-radius: 8px;
  margin: 0.75rem 0;
  font-size: 0.9rem;
}
.callout.error   { background: rgba(220, 38, 38, 0.12);  color: #fca5a5; border: 1px solid rgba(220, 38, 38, 0.3); }
.callout.warn    { background: rgba(245, 158, 11, 0.12); color: #fcd34d; border: 1px solid rgba(245, 158, 11, 0.3); }
.callout.success { background: rgba(16, 185, 129, 0.12); color: #6ee7b7; border: 1px solid rgba(16, 185, 129, 0.3); }
/* GH #350: the Notification Rule Test result (pages/notifications.js
   testRule()) sets className to "callout info" while the dry-run is in
   flight, and to "callout ok" for a not-tripped result -- neither modifier
   had a rule here (only .error/.warn/.success did), so the base .callout
   alone applied: padding and a border-radius with no background, border,
   or color, i.e. plain text. Adding "info" (mirrors the cyan used for
   informational chrome elsewhere, e.g. .alert-item.is-info) covers the
   loading state; "ok" is aliased to .success's own styling rather than
   duplicated, since it means the same "good" result. */
.callout.info    { background: var(--accent-cyan-dim); color: var(--accent-cyan); border: 1px solid rgba(6, 182, 212, 0.35); }
.callout.ok      { background: rgba(16, 185, 129, 0.12); color: #6ee7b7; border: 1px solid rgba(16, 185, 129, 0.3); }
.callout summary { cursor: pointer; }
/* GH #291 residual: expandable field list inside the Configuration page's
   own inline save callout (the global restart banner's equivalent is
   already fixed in PR #294). Capped height so even a large field list can't
   take over the screen -- it scrolls internally. */
.callout-details { margin-top: 6px; }
.callout-details summary { font-size: 0.85rem; color: inherit; opacity: 0.9; }
.callout-details ul {
  margin: 6px 0 0; padding-left: 1.1rem; max-height: 160px; overflow-y: auto;
  font-size: 0.85rem;
}
.callout-details li { margin: 2px 0; }

/* ---- Data tables (users, audit, backup) ---- */
.data-table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.data-table th, .data-table td {
  padding: 0.6rem 0.75rem;
  text-align: left;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}
/* GH #575: the symmetric per-cell padding above left every table's first
   column visibly offset from the table's own left boundary (excessive
   whitespace before the first header/cell), while every other column kept
   its normal breathing room. Zero just the first column's left inset --
   matching the left-flush convention `.tbl` (panels.css, used by Recent
   Reads/filterableTable) already applies to every column -- without
   touching the consistent inter-column spacing everywhere else. */
.data-table th:first-child, .data-table td:first-child { padding-left: 0; }
/* Settings → Table Density = Compact: tighter rows + slightly smaller text.
   Set on the document root by prefs.js (data-density="compact"). */
:root[data-density="compact"] .data-table { font-size: 0.84rem; }
:root[data-density="compact"] .data-table th,
:root[data-density="compact"] .data-table td { padding: 0.3rem 0.55rem; }
:root[data-density="compact"] .data-table th:first-child,
:root[data-density="compact"] .data-table td:first-child { padding-left: 0; }
.data-table th {
  font-weight: 600;
  font-size: 0.78rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.data-table tbody tr:hover { background: rgba(255,255,255,0.02); }
/* GH #45: keep header labels on a single line so they stay aligned above their
   data column, and centre-align cell content so rows read as an even grid.
   th shares the same vertical-align as td so header and body cells agree
   (previously th sat at "top" while td sat at "middle"). */
.data-table th { white-space: nowrap; vertical-align: middle; }
.data-table td { vertical-align: middle; }

/* GH #45: row action buttons must stay on ONE line. In a dense grid (Reader
   Management) a wrapping Actions column stacked its 4 buttons vertically, which
   forced very tall, unbalanced rows. Keep them inline; the table scrolls
   horizontally instead.
   GH #553: right-align the buttons within the Actions cell instead of
   clustering them at its left edge. Actions' column weight (reader_mgmt.js
   COLUMNS) was trimmed from 32.5% to a realistic 15% of the grid, which
   already removes most of the old dead space after the buttons; this
   flex-end closes the rest so they land flush against the grid's real right
   edge, matching every other column's action pattern in the app. */
.row-actions { display: flex; gap: 0.2rem; flex-wrap: nowrap; white-space: nowrap; justify-content: flex-end; }
.row-actions button {
  background: none; border: 1px solid var(--border);
  color: var(--text); padding: 0.22rem 0.4rem;
  border-radius: 6px; font-size: 0.74rem; cursor: pointer;
}
.row-actions button:hover { background: rgba(255,255,255,0.06); }
.row-actions button.danger { color: #f87171; border-color: rgba(220, 38, 38, 0.3); }

/* GH #45 (round 2): Reader Management is a wide 15-column enterprise grid.
   The first fix (a bare `min-width: 1180px` on `table-layout: auto`) only
   patched the symptoms it was looking at (wrapping headers/Actions) — it left
   the browser's auto-layout heuristic in charge of column widths (uneven
   columns) and a hard pixel floor wider than the content area on common
   1366-1440px laptop screens (forced horizontal scroll = "container width
   not used"). Fixed properly: `table-layout: fixed` + `width: 100%` with an
   explicit <colgroup> (in reader_mgmt.js) giving every column a fixed,
   proportional share of the *actual* container width, so the grid always
   fills its container and columns are evenly, deterministically sized
   instead of guessed by the browser. `min-width` is now a much smaller
   tablet-width floor — a true fallback for genuinely narrow viewports, not
   the thing governing desktop rendering. Overflowing text cells truncate
   with an ellipsis; the flex-based Actions cell is exempted so its 4 buttons
   stay fully visible rather than being clipped.

   GH #45 (round 120): the previous fix passed every automated alignment
   check (0px header spread, no forced scroll) but a genuine fresh-install
   Playwright pass measured real seeded data against the shipped colgroup
   and found two columns actively too narrow for their own content —
   Status needed 102px in a 57.7px column, IP/Serial needed 146px in an
   80.8px column — with NO visible ellipsis, because `text-overflow:
   ellipsis` on the <td> does not reliably render through a nested
   `.badge`/`.status-pill` <span>; the pill just hard-clips flush at the
   column edge (this is very likely why QA kept reporting "still
   unbalanced" through three rounds of alignment-only fixes: the columns
   were aligned, but illegible). Fixed two ways: (1) badges/pills now
   ellipsis-truncate themselves so a clipped value at least shows "…";
   (2) every data cell also gets a `title` attribute (added in
   reader_mgmt.js) with the untruncated value, so hovering always reveals
   the full text regardless of column width. */
/* GH #45 (round 154): OS/browser "text-only" size boosting (an accessibility
   setting, e.g. Chromium's Settings > Appearance > Font size, distinct from
   page zoom) scales the root font-size while the viewport stays the same CSS
   px. Every column here is a % of #rmTable's own (fixed, viewport-derived)
   rendered width, decoupled from font-size — so a bigger font just had to
   cram into the same pixel budget per column. Measured live at a 2x root
   font boost: almost every header/data cell collapsed to 1-3 characters
   ("CO...", "U..", "192.168...."), far more severe than any previously-fixed
   viewport-width case. min-width was a flat 900px (a pure narrow-viewport
   fallback); adding a `62.5em` floor (900px at the table's default 14.4px
   font) makes the floor grow with the table's own font-size, so a large
   enough boost pushes #rmTable past its container width and the existing
   `.table-wrap { overflow-x: auto }` (built for narrow viewports, round 84)
   takes over — legible content behind a scrollbar instead of silent,
   near-total truncation. */
#rmTable { table-layout: fixed; width: 100%; min-width: max(900px, 62.5em); }
#rmTable th, #rmTable td {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding-left: 0.55rem;
  padding-right: 0.55rem;
}
/* GH #45 (round 123): a genuine fresh-install pass with realistic seeded
   data found 8 of the 15 header labels (Company/Site/Location/Reader
   Type/Group/Brand/Model/Protocol) hard-clipped to 1-3 letters
   ("CO...", "SI...", "R...") with NO title= fallback at all — unlike data
   cells (round 120), a header that clips has no way to recover the full
   label. Two-word headers (Reader Type, Assigned User, Last Heartbeat)
   only need their WIDEST SINGLE WORD once wrapped onto 2 lines, which is
   30-45% narrower than the unwrapped phrase — so headers, unlike data
   rows, are allowed to wrap. This does not reintroduce the round-84
   tall-row bug: all <th> cells share one <tr>, so the header row is
   uniformly (not unevenly) taller, and no row's Actions buttons are
   affected. A slightly smaller/tighter header typeface (scoped to this
   table only) buys back a little more room across every column.

   GH #45 (round 125): round 124's own commit measured (and shipped anyway,
   documented as a known gap) that Protocol/Last Heartbeat still fall short
   of their header text by 17-22px at the two most common QA laptop
   resolutions (1440x900, 1366x768). Because `overflow: visible` was set
   above so wrapped two-word headers wouldn't clip, a single unbreakable
   word (e.g. "PROTOCOL") that doesn't fit its column has nowhere to wrap
   to — it overflows horizontally and visibly bleeds into the *next*
   column's header text ("PROTOCOL" over "IP / SERIAL"), which is exactly
   what a human tester keeps flagging as "headers not aligned / unbalanced"
   even though every automated alignment check (0px header-height spread,
   no forced scroll) kept passing. Fixed at the root: `overflow-wrap:
   anywhere` lets a too-wide single word break mid-word onto another line
   within its own cell instead of overflowing sideways — the header row
   grows uniformly (same non-issue round 123 already reasoned through), and
   `overflow: hidden` is restored as a second line of defence so any
   sub-pixel rounding residue clips invisibly instead of bleeding into a
   neighboring header. Belt-and-suspenders: this holds regardless of future
   column/weight tuning, so a future rebalance can't reintroduce this exact
   failure mode.

   GH #45 (round 127): a genuine fresh-install pass at 1280x800 (a narrow-
   laptop viewport no prior round on this issue had tried) and at an
   emulated 125% browser-zoom effective viewport (1152x720 -- 1440x900
   divided by 1.25, a very common default Windows laptop scale factor)
   found round 125's own fix, taken to its logical conclusion at a narrower
   width, doesn't just wrap a header onto 2 lines -- with no bound on how
   many lines `overflow-wrap: anywhere` may use, columns like "Protocol"
   and "Last Heartbeat" fragmented into 3-5 lines of near-meaningless
   letter clusters ("PRO"/"TOC"/"OL", "LAS"/"T"/"HEA"/"RTB"/"EAT"),
   measured live: the header row grew from 59px (1440x900) to 72px
   (1366x768/1280x800) to 85px (1152x720 effective-zoom) -- unbounded
   growth in the exact direction the round-84 tall-row bug came from,
   and exactly the kind of illegible/unbalanced-looking header a human
   QA pass would flag again. Fixed by capping headers to a hard 2-line
   clamp with an ellipsis (matching how data cells already truncate,
   round 120) instead of letting `overflow-wrap: anywhere` consume
   unlimited vertical space one broken syllable at a time; the existing
   `title=` attribute (round 123) remains the full-text fallback on
   hover/tap regardless of how a label clips.

   Implementation note: `-webkit-line-clamp` requires `display:-webkit-box`
   on the clamped element, but applying that directly to the <th> (a table
   cell) overrides its table-cell display type and breaks
   table-layout:fixed's column-width distribution entirely (every header
   collapsed to the same intrinsic width -- verified live, this made
   headers overlap 100%, far worse than the bug being fixed). So the box
   model lives on an inner `.rm-th-label` <span> (see reader_mgmt.js)
   instead of the <th> itself, which stays a normal table cell.

   GH #45 (round 146): testing an angle no prior round on this issue tried --
   `dir="rtl"` (a real browser/OS RTL-locale setting, e.g. Arabic/Hebrew) --
   found the 2-line clamp on `.rm-th-label` renders illegibly under RTL. The
   column headers are fixed, hardcoded English UI strings (COLUMNS[].label in
   reader_mgmt.js; this app has no i18n/translation), never user data, so
   they must always read left-to-right regardless of the *document's* base
   direction. Without an explicit `direction`, the label's bidi/ellipsis
   direction silently inherits from the ancestor <html dir="rtl">, so the
   line-clamp truncates from the wrong edge -- measured live: "Last
   Heartbeat" renders "LAST" / "HEA…" in LTR (a legible truncation) but
   "LAST" / "…EAR" in RTL (an illegible fragment, ellipsis mid-word
   nowhere near a recognizable prefix). Data cells are deliberately NOT
   touched here -- row content (company/site/location names, IP/serial,
   etc.) is genuine user-entered data that could itself be legitimate RTL
   text in an Arabic/Hebrew-locale deployment, so forcing `direction: ltr`
   there would be wrong; this fix is scoped to the fixed-label header only. */
#rmTable th {
  white-space: normal;
  overflow: hidden;
  overflow-wrap: anywhere;
  word-break: break-word;
  text-overflow: clip;
  line-height: 1.15;
  font-size: 0.72rem;
  letter-spacing: 0.01em;
}
#rmTable th .rm-th-label {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: anywhere;
  word-break: break-word;
  text-overflow: ellipsis;
  max-height: 2.6em;
  direction: ltr;
  text-align: left;
  unicode-bidi: plaintext;
}
#rmTable td.row-actions { overflow: visible; text-overflow: clip; }
/* GH #553: Actions header label mirrors the right-aligned row buttons below
   it. Overrides `.rm-th-label`'s hardcoded `text-align: left` (kept there
   for every other, genuinely left-to-right-only header label -- see the RTL
   note above) for this one column only. */
#rmTable th.col-actions-th .rm-th-label { text-align: right; }
#rmTable td .badge, #rmTable td .status-pill, #rmTable td .tbl-muted {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: middle;
}

/* GH #45 (round 124): the real fix the round-123 finding flagged — no
   rebalance of 15 fixed columns fits every header at 1440px. Letting
   low-value columns (Group/Brand/Model/Assigned User/Last Read — usually
   "—" in practice) be hidden gives the columns that remain (Protocol,
   Last Heartbeat, ...) their width back, without losing the data: it's
   still one click away via this picker. */
.rm-col-menu-wrap { position: relative; display: inline-block; }
.rm-col-menu {
  position: absolute; top: 110%; left: 0; z-index: 80; width: 240px;
  background: var(--bg-2); border: 1px solid var(--border-2); border-radius: 10px;
  box-shadow: var(--shadow-panel); padding: 0.5rem 0.6rem;
}
.rm-col-menu-list { display: flex; flex-direction: column; gap: 0.3rem; max-height: 320px; overflow-y: auto; }
.rm-col-menu-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.82rem; color: var(--text-2); cursor: pointer; }
.rm-col-menu-item input[disabled] { cursor: not-allowed; }
.rm-col-menu-actions { display: flex; justify-content: space-between; margin-top: 0.5rem; padding-top: 0.4rem; border-top: 1px solid var(--border-1); }

/* ---- Role badges & status pills ---- */
.role-badge {
  display: inline-block; font-size: 0.72rem; padding: 0.15rem 0.5rem;
  border-radius: 999px; margin-right: 0.25rem;
  background: rgba(255,255,255,0.05); border: 1px solid var(--border);
}
.role-badge.role-admin    { background: rgba(220, 38, 38, 0.15);  color: #fca5a5; border-color: rgba(220, 38, 38, 0.35); }
.role-badge.role-operator { background: rgba(245, 158, 11, 0.15); color: #fcd34d; border-color: rgba(245, 158, 11, 0.35); }
.role-badge.role-viewer   { background: rgba(99, 102, 241, 0.15); color: #c4b5fd; border-color: rgba(99, 102, 241, 0.35); }

.status-pill {
  display: inline-block; font-size: 0.72rem; padding: 0.15rem 0.5rem;
  border-radius: 999px;
  background: rgba(255,255,255,0.05); border: 1px solid var(--border);
}
.status-pill.ok   { background: rgba(16, 185, 129, 0.15); color: #6ee7b7; border-color: rgba(16, 185, 129, 0.35); }
.status-pill.warn { background: rgba(245, 158, 11, 0.15); color: #fcd34d; border-color: rgba(245, 158, 11, 0.35); }
/* round 177: routes/pages.js emits `.status-pill.danger` (Maintenance page's DB
   integrity-check FAIL / scheduled-op FAIL) and `.status-pill.info` (Maintenance
   page's mid-range freelist-ratio badge) but neither modifier had a CSS rule,
   so both silently fell back to the plain neutral `.status-pill` above --
   visually identical to an inert "Disabled"/"No" badge, with no color
   distinction from a passing (green) or warning (amber) state at all. Matches
   the existing `.role-badge.role-admin` red and `--accent-cyan` info palette
   used elsewhere in this file. */
.status-pill.danger { background: rgba(220, 38, 38, 0.15); color: #fca5a5; border-color: rgba(220, 38, 38, 0.35); }
.status-pill.info   { background: var(--accent-cyan-dim); color: var(--accent-cyan); border-color: rgba(6, 182, 212, 0.35); }

/* ---- Modal ---- */
.modal {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.7);
  display: grid; place-items: center;
  z-index: 1000;
  padding: 1rem;
}
.modal[hidden] { display: none; }
.modal-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  width: 100%; max-width: 500px;
  max-height: 90vh; overflow-y: auto;
}
.modal-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
}
.modal-header h3 { margin: 0; }
.modal-close {
  background: none; border: none; color: var(--text-muted);
  font-size: 1.5rem; cursor: pointer; padding: 0;
}
.modal-body { padding: 1rem 1.25rem; }
.modal-body label { display: block; margin-bottom: 0.85rem; }
.modal-body label > span { display: block; font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.25rem; }
/* GH #300: `input[type="text"]` only matches an <input> that carries a
   literal type="text" attribute — it does NOT match an <input> with no
   `type` attribute at all, even though the browser treats an untyped input
   as text. roleName/roleDescription (New Role dialog) and
   userUsername/userFullName (New User dialog) all omit `type`, so they fell
   through this rule entirely and rendered with bare unstyled UA borders.
   `:not([type])` closes that gap without touching checkbox/file/hidden
   inputs, which all set an explicit `type`. */
.modal-body input[type="text"], .modal-body input[type="email"], .modal-body input[type="password"],
.modal-body input:not([type]) {
  width: 100%; box-sizing: border-box;
  padding: 0.5rem 0.7rem;
  background: var(--bg-input, rgba(255,255,255,0.04));
  border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); font-size: 0.9rem;
}
.modal-body label.checkbox { display: flex; align-items: center; gap: 0.5rem; }
.modal-body label.checkbox > span { margin: 0; }
.modal-footer {
  display: flex; justify-content: flex-end; gap: 0.5rem;
  padding: 1rem 1.25rem; border-top: 1px solid var(--border);
}
.role-pills { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.role-pill-input { display: flex; align-items: center; gap: 0.35rem; cursor: pointer; margin: 0 !important; }
.role-pill-input input { margin: 0; }
.password-field small { display: block; margin-top: 0.25rem; }

/* ---- Health page ---- */
.health-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 1rem;
  margin-bottom: 1.5rem;
}
.health-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.1rem 1.25rem;
}
.health-card header {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 0.65rem;
}
.health-label { font-size: 0.8rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.health-val { font-size: 1rem; font-weight: 600; }
.health-card footer { margin-top: 0.5rem; }
.health-bar {
  height: 8px;
  background: rgba(255,255,255,0.05);
  border-radius: 999px;
  overflow: hidden;
}
.health-bar-fill {
  height: 100%;
  background: linear-gradient(90deg, var(--accent-cyan), #2dd4bf);
  width: 0%;
  transition: width 0.4s ease, background 0.4s ease;
  border-radius: 999px;
}
.health-card.warn .health-bar-fill   { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
.health-card.danger .health-bar-fill { background: linear-gradient(90deg, #dc2626, #f87171); }

.health-secondary { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
.health-tile {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem;
}
.health-tile-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.5rem; }
.health-tile-value { font-size: 1.5rem; font-weight: 700; }

/* ---- Audit log table ---- */
.audit-row.sev-danger { background: rgba(220, 38, 38, 0.06); }
.audit-row.sev-warn   { background: rgba(245, 158, 11, 0.05); }
.status-ok      { color: #6ee7b7; font-weight: 600; }
.status-warn    { color: #fcd34d; font-weight: 600; }
.status-danger  { color: #f87171; font-weight: 600; }
.status-unknown { color: var(--text-muted); }

/* ---- Backup page ---- */
.backup-create.card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.25rem;
  margin-bottom: 1.5rem;
}
.backup-create label { display: block; margin: 0.5rem 0; }
.backup-create label > span { display: block; font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.25rem; }
.backup-create input {
  width: 100%; box-sizing: border-box;
  padding: 0.5rem 0.7rem;
  background: var(--bg-input, rgba(255,255,255,0.04));
  border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); font-size: 0.9rem;
}

/* ---- Config editor ---- */
/* Line-numbered YAML editor: a gutter div scroll-synced to the textarea. */
.yaml-editor-wrap {
  display: flex;
  align-items: stretch;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: var(--bg-input, rgba(0,0,0,0.2));
}
.yaml-gutter {
  flex: 0 0 auto;
  min-width: 3ch;
  box-sizing: border-box;
  padding: 0.85rem 0.5rem 0.85rem 0.75rem;
  margin: 0;
  text-align: right;
  color: var(--text-3, #6b7280);
  background: var(--bg-2, rgba(255,255,255,0.03));
  border-right: 1px solid var(--border);
  font-family: var(--mono, ui-monospace, Menlo, monospace);
  font-size: 0.85rem;
  line-height: 1.55;
  white-space: pre;
  overflow: hidden;
  user-select: none;
}
.cfg-meta { display: flex; align-items: center; gap: 10px; margin: 6px 0; flex-wrap: wrap; }

/* GH #288: field-based configuration form now uses the same `.panel` card +
   `.settings-subnav` menu-wise layout as Settings (see settings.js /
   panels.css) instead of stacked `<details>` collapsibles — `.form-grid` /
   `.field` / `.field-label` / `.field-input` are the shared field styles
   every other form in the app already uses (users.js, settings.js). */
.cfg-field-bool { flex-direction: row; align-items: center; gap: 8px; }
.cfg-field-bool input[type="checkbox"] { width: auto; flex-shrink: 0; }
.cfg-hot-badge { font-size: 0.65rem; padding: 1px 6px; }
.cfg-form-savebar { margin-top: 18px; }
.yaml-editor {
  flex: 1 1 auto;
  width: 100%;
  min-height: 480px;
  box-sizing: border-box;
  background: transparent;
  border: 0;
  border-radius: 0;
  color: var(--text);
  font-family: var(--mono, ui-monospace, Menlo, monospace);
  font-size: 0.85rem;
  line-height: 1.55;
  padding: 0.85rem 1rem;
  resize: vertical;
  white-space: pre;
  tab-size: 2;
}
.yaml-editor:focus { outline: none; }

/* ---- Ping form ---- */
.net-ping.card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.25rem;
  margin-bottom: 1.5rem;
}
.ping-form {
  display: grid;
  grid-template-columns: 1fr 100px auto;
  gap: 0.75rem;
  align-items: end;
}
.ping-form label { display: block; }
.ping-form label > span { display: block; font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.25rem; }
.ping-form input, .ping-form select {
  width: 100%; box-sizing: border-box;
  padding: 0.5rem 0.7rem;
  background: var(--bg-input, rgba(255,255,255,0.04));
  border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); font-size: 0.9rem;
}
.ping-presets { margin-top: 0.85rem; display: flex; gap: 0.25rem; flex-wrap: wrap; align-items: center; }
.ping-summary { display: grid; gap: 0.5rem; margin: 0.75rem 0; font-size: 0.92rem; }
.ping-raw {
  background: var(--bg-input, rgba(0,0,0,0.2));
  padding: 0.65rem;
  border-radius: 6px;
  font-size: 0.78rem;
  overflow-x: auto;
}
.card-danger { border-color: rgba(220, 38, 38, 0.4); }
.text-ok     { color: #6ee7b7; }
.text-danger { color: #f87171; }
.muted.small { font-size: 0.78rem; }

@media (max-width: 700px) {
  .ping-form { grid-template-columns: 1fr; }
  .page-header { flex-direction: column; align-items: stretch; }
  .health-tile-value { font-size: 1.15rem; }
}

/* ============================================================
 * Phase 5 styles: notifications page, sparklines, tabs, subheaders
 * ============================================================ */

/* Tabs — used by the Notifications page */
.tabs {
  display: flex;
  gap: 0.25rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1.25rem;
}
.tab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--text-muted);
  padding: 0.6rem 1rem;
  cursor: pointer;
  font: inherit;
  font-size: 0.92rem;
  transition: color 0.15s, border-color 0.15s;
}
.tab:hover { color: var(--text); }
.tab.is-active {
  color: var(--accent-cyan);
  border-bottom-color: var(--accent-cyan);
}
.tab-pane { animation: tab-fade-in 0.2s ease; }
@keyframes tab-fade-in {
  from { opacity: 0; transform: translateY(2px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Subheader inside a tab pane */
.page-subheader {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin: 0.5rem 0 1rem;
  flex-wrap: wrap;
}
.page-subheader h3 { margin: 0; }
/* GH #604: groups a Rows Per Page select with the tab/section's other
   top-right controls (e.g. Refresh) on subheaders that aren't a full
   filter-bar/filterableTable(). */
.page-subheader-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }

/* Sparkline container under health bars */
.health-spark {
  margin-top: 0.5rem;
  height: 36px;
}
.sparkline {
  display: block;
  width: 100%;
  height: 100%;
}
.sparkline-empty {
  display: block;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg,
    rgba(255,255,255,0.03) 25%,
    rgba(255,255,255,0.06) 50%,
    rgba(255,255,255,0.03) 75%);
  background-size: 200% 100%;
  border-radius: 4px;
}

/* Wider modal for the rule editor — it has more fields than the
   Phase 4 user/channel modals */
#ruleModal .modal-card { max-width: 560px; }

/* GH #363: the Test Connector result is a single status pill + one line of
   text + an OK button -- the default 560px modal-card leaves a large empty
   area around it. Narrow it to a content-fitting size, same override
   pattern as #ruleModal's widening above. */
#connTestModal .modal-card { max-width: 380px; }

/* GH #359: reusable compact size for the shared confirmModal() (confirm.js)
   -- a short one-line question + two buttons doesn't need the 560px
   default width either. */
.modal-card.modal-card-sm { max-width: 380px; }

/* GH #375: warning icon shown in the shared confirmModal() header for
   danger (destructive) confirmations -- title row wraps icon + h3 so
   .modal-header's space-between still lays out as a single left-aligned
   group instead of splitting them to opposite edges. */
.confirm-modal-title-row { display: flex; align-items: center; gap: 10px; }
.confirm-modal-icon { display: flex; flex-shrink: 0; color: var(--accent-amber); }

/* GH #361 / GH #505: read-only "Record Information" block (components.js
   recordInfoBlock()) shown above the Cancel/Save footer of every Edit
   dialog -- compact 3 rows: Created By, Created On, Accessed By. */
.record-info {
  margin-top: 4px;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--bg-3);
  border: 1px solid var(--border-1);
}
.record-info-title {
  margin: 0 0 6px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-3);
}
.record-info-row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  font-size: 12.5px;
  padding: 2px 0;
  color: var(--text-2);
}
.record-info-label { color: var(--text-3); flex-shrink: 0; }
.record-info-row > span:last-child { text-align: right; }

/* Notifications log row severity tints (re-uses the audit-row classes) */
.tab-pane .data-table { font-size: 0.88rem; }

/* "Export ALL (server)" buttons share the ghost-style of the existing
   per-page export buttons; nothing extra needed here. */

/* ============================================================
 * Phase 6 styles: account page, 2FA wizard, recovery codes,
 * audit histogram
 * ============================================================ */

/* Account page */
.account-section.card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.5rem;
  margin-bottom: 1.5rem;
}
.account-section.card h3 { margin-top: 0; }

/* 2FA wizard modal */
.modal-card.twofa-wizard { max-width: 560px; }

.twofa-qr {
  display: grid;
  place-items: center;
  padding: 1rem;
  background: rgba(255, 255, 255, 0.95);   /* QR scanners want white */
  border-radius: 8px;
  margin: 1rem 0;
}
.twofa-qr svg {
  /* Force dark modules to ALWAYS be black on the QR specifically — the
     theme's CSS vars can't influence scannability. */
  --text: #000;
}

.twofa-secret {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  background: var(--bg-input, rgba(0, 0, 0, 0.2));
  border: 1px solid var(--border);
  border-radius: 6px;
  margin: 0.75rem 0;
  flex-wrap: wrap;
}
.twofa-secret code {
  font: 0.95rem/1.5 var(--mono, ui-monospace, Menlo, monospace);
  letter-spacing: 0.05em;
  flex: 1;
  min-width: 0;
  word-break: break-all;
}

/* Recovery codes grid */
.recovery-codes {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem;
  margin: 1rem 0;
  padding: 1rem;
  background: var(--bg-input, rgba(0, 0, 0, 0.2));
  border: 1px solid var(--border);
  border-radius: 8px;
}
.recovery-codes code {
  display: block;
  padding: 0.5rem 0.75rem;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 4px;
  font: 0.95rem/1 var(--mono, ui-monospace, Menlo, monospace);
  letter-spacing: 0.05em;
  text-align: center;
  user-select: all;
}

/* Audit histogram on the alerts page */
.audit-histogram-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem 1.25rem;
  margin-bottom: 1.5rem;
}
.audit-histogram-card header {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 0.65rem;
}
.audit-histogram-card #auditHistogram {
  height: 60px;
}
.audit-histogram-card footer { margin-top: 0.5rem; }

/* SMS role badge — pick a distinct hue */
.role-badge.role-admin {
  /* Already defined in Phase 4 (red). Reuse for SMS to draw the eye. */
}

@media (max-width: 600px) {
  .recovery-codes { grid-template-columns: 1fr; }
  .twofa-qr svg { width: 200px !important; height: 200px !important; }
}

/* ============================================================
 * Phase 7 styles: DB maintenance page
 * ============================================================ */

.mt-card {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1.25rem 1.5rem;
  margin-bottom: 1.25rem;
}
.mt-card h3 { margin-top: 0; }

.mt-stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 0.75rem;
  margin-bottom: 1rem;
}
/* Scoped under .mt-stats-grid: this page's .stat-tile has no `accent`
   border-left and different padding than the shared components.js
   statTile()/statGrid() used by every other page. Unscoped, these rules
   shared the exact same class names (.stat-tile/.stat-label/.stat-value)
   as the global ones in the "Stat tiles" block above, and — because two
   classes (`.stat-tile .stat-label`) beat one (`.stat-label`) on
   specificity regardless of source order — silently overrode the shared
   component's label color/size on every other page (GH #305 follow-up). */
.mt-stats-grid .stat-tile {
  background: var(--bg-input, rgba(0, 0, 0, 0.2));
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.85rem 1rem;
}
.mt-stats-grid .stat-tile .stat-label {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted, #8a96a8);
}
.mt-stats-grid .stat-tile .stat-value {
  font-size: 1.4rem;
  font-weight: 600;
  margin: 0.25rem 0;
  line-height: 1.2;
}
.mt-stats-grid .stat-tile .stat-sub {
  font-size: 0.8rem;
  color: var(--text-muted, #8a96a8);
}
.mt-stat-wide { grid-column: span 2; }

.mt-sched-summary {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.6rem;
}

.mt-compact td, .mt-compact th { padding: 0.4rem 0.65rem; }
.mt-row-counts summary {
  cursor: pointer;
  padding: 0.5rem 0;
  user-select: none;
}
.mt-row-counts table { margin-top: 0.5rem; }
.num { text-align: right; font-variant-numeric: tabular-nums; }

.mt-archive-form {
  margin-bottom: 1rem;
}
.mt-archive-form summary {
  cursor: pointer;
  display: inline-block;
  padding: 0.5rem 0.85rem;
  border-radius: 4px;
  border: none;
  list-style: none;
}
.mt-archive-form summary::-webkit-details-marker { display: none; }
.mt-archive-form[open] summary { margin-bottom: 0.75rem; }
.mt-archive-form-body {
  padding: 1rem;
  background: var(--bg-input, rgba(0, 0, 0, 0.2));
  border: 1px solid var(--border);
  border-radius: 8px;
  display: grid;
  gap: 0.5rem;
}

/* Op result callout pre */
#mtOpResult pre {
  margin: 0.5rem 0 0 0;
  padding: 0.5rem;
  background: var(--bg-input, rgba(0, 0, 0, 0.2));
  border-radius: 4px;
  font-size: 0.8rem;
  max-height: 240px;
  overflow: auto;
}

@media (max-width: 700px) {
  .mt-stat-wide { grid-column: span 1; }
  .mt-sched-summary { flex-direction: column; align-items: flex-start; gap: 0.5rem; }
}

/* ============================================================
 * Phase 7.1: User Management toolbar / pagination / sortable,
 * reader discovery list, and the first-time setup wizard.
 * ============================================================ */

.users-toolbar {
  display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap;
  margin: 0.75rem 0 1rem;
}
.users-search { flex: 1 1 280px; }
.users-search input {
  width: 100%; padding: 0.5rem 0.75rem; border-radius: 6px;
  border: 1px solid var(--border); background: var(--bg-input, rgba(0,0,0,0.2));
  color: inherit; font-size: 0.9rem;
}
.users-filter { display: flex; gap: 0.85rem; }
.inline-field { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.75rem; }
.inline-field span { text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted, #8a96a8); }
.inline-field select {
  /* Round 287 (GH #285): match .btn-primary/.btn-secondary's vertical padding
     (0.55rem) so a filter dropdown sitting next to an action button in
     .page-header-actions is the same height, not just bottom-aligned. */
  padding: 0.55rem 0.6rem; border-radius: 6px; border: 1px solid var(--border);
  background: var(--bg-input, rgba(0,0,0,0.2)); color: inherit;
}
.user-count { font-size: 0.85rem; color: var(--text-muted, #8a96a8); }

/* GH #454: Users grid multi-select bulk delete — narrow checkbox column
   (header "select all" + one per row), doesn't wrap/steal width from the
   real data columns. */
.data-table .col-check { width: 2rem; text-align: center; padding-left: 0.5rem; padding-right: 0.25rem; }
.data-table .col-check input[type="checkbox"] { cursor: pointer; }
.data-table .col-check input[type="checkbox"]:disabled { cursor: not-allowed; opacity: 0.4; }

/* GH #425: profile photos — grid thumbnail + modal upload field */
.avatar-thumb {
  display: inline-grid; place-items: center;
  width: 24px; height: 24px; border-radius: 50%;
  background: linear-gradient(135deg, var(--accent-cyan), var(--accent-indigo, #6366f1));
  color: #000; font-size: 0.65rem; font-weight: 700;
  vertical-align: middle; margin-right: 0.5rem;
}
.avatar-thumb.has-photo {
  background-color: var(--surface-2, #1c2430);
  background-size: cover; background-position: center;
}
.avatar-field { margin-bottom: 1rem; }
.avatar-field-row { display: flex; align-items: center; gap: 0.85rem; margin-top: 0.3rem; }
.avatar-preview {
  display: grid; place-items: center;
  width: 56px; height: 56px; border-radius: 50%; flex: none;
  background: linear-gradient(135deg, var(--accent-cyan), var(--accent-indigo, #6366f1));
  color: #000; font-size: 1.1rem; font-weight: 700;
}
.avatar-preview.has-photo {
  background-color: var(--surface-2, #1c2430);
  background-size: cover; background-position: center;
}
.avatar-field-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }

.users-pagination {
  display: flex; align-items: center; justify-content: center; gap: 1rem;
  margin-top: 1rem;
}
.users-pagination button[disabled] { opacity: 0.4; cursor: not-allowed; }

th.sortable { cursor: pointer; user-select: none; position: relative; }
th.sortable:hover { color: var(--accent-cyan, #38bdf8); }
/* GH #433: sorting worked on every one of these columns already, but with
   no glyph shown before the first click there was nothing telling a user a
   header was interactive. Show a neutral "sort available" indicator on
   every sortable header by default, then let the more-specific sort-asc/
   sort-desc rules (same element, two classes -> higher specificity) override
   it with the active direction once a column is actually sorted. */
th.sortable::after       { content: " ⇅"; font-size: 0.7em; opacity: 0.45; }
th.sortable.sort-asc::after  { content: " ▲"; font-size: 0.7em; opacity: 0.8; }
th.sortable.sort-desc::after { content: " ▼"; font-size: 0.7em; opacity: 0.8; }

/* #18: make action controls look like real buttons */
.btn-row {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.32rem 0.7rem; border-radius: 6px;
  border: 1px solid var(--border);
  background: var(--bg-input, rgba(255,255,255,0.04));
  color: inherit; font-size: 0.8rem; font-weight: 500; cursor: pointer;
  transition: background 0.12s, border-color 0.12s;
}
.btn-row:hover { background: rgba(56, 189, 248, 0.12); border-color: var(--accent-cyan, #38bdf8); }
.btn-row.danger { color: #f87171; border-color: rgba(220, 38, 38, 0.35); }
.btn-row.danger:hover { background: rgba(220, 38, 38, 0.14); }

.discover-list { margin: 0.4rem 0 0; padding-left: 1.1rem; }
.discover-list li { font-family: var(--mono, monospace); font-size: 0.85rem; padding: 0.1rem 0; }

/* ---- First-time setup wizard ---- */
.setup-wizard {
  min-height: 80vh; display: flex; align-items: center; justify-content: center;
  padding: 2rem 1rem;
}
.setup-card {
  width: 100%; max-width: 520px;
  background: var(--bg-panel, #121823); border: 1px solid var(--border);
  border-radius: 14px; padding: 2rem; box-shadow: 0 12px 40px rgba(0,0,0,0.4);
}
.setup-head { text-align: center; margin-bottom: 1.5rem; }
.setup-logo { font-size: 1.2rem; font-weight: 700; letter-spacing: 0.05em; color: var(--accent-cyan, #38bdf8); }
.setup-sub { font-size: 0.85rem; color: var(--text-muted, #8a96a8); margin-top: 0.25rem; }
.setup-progress { display: flex; gap: 0.5rem; justify-content: center; margin-top: 1rem; }
.setup-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--border); }
.setup-dot.active { background: var(--accent-cyan, #38bdf8); }
.setup-dot.done { background: #34d399; }
.setup-body h2 { margin: 0 0 0.5rem; }
.setup-field { display: flex; flex-direction: column; gap: 0.3rem; margin: 0.9rem 0; }
.setup-field span { font-size: 0.8rem; color: var(--text-muted, #8a96a8); }
.setup-field input {
  padding: 0.55rem 0.75rem; border-radius: 6px; border: 1px solid var(--border);
  background: var(--bg-input, rgba(0,0,0,0.2)); color: inherit; font-size: 0.95rem;
}
.setup-actions { display: flex; justify-content: space-between; gap: 1rem; margin-top: 1.5rem; }
.setup-actions .btn-primary { margin-left: auto; }
.setup-review { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.setup-review td { padding: 0.4rem 0.6rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
.setup-review td:first-child { color: var(--text-muted, #8a96a8); width: 40%; }
.setup-hint { margin-top: 0.5rem; }

/* ============================================================
 * Phase 8: theme picker, debug console, notification bell,
 * dropdown-contrast fix (#48), connection-control buttons (#49).
 * ============================================================ */

/* ---- #48: make native select + option text visible on dark themes ---- */
select, .field-select, .inline-field select, .users-filter select {
  background: var(--bg-2);
  color: var(--text-1);
  border: 1px solid var(--border-1);
}
select option, .field-select option {
  background: var(--bg-2);
  color: var(--text-1);
}
/* Light theme: options follow the light surface */
html[data-theme="light"] select option {
  background: #ffffff;
  color: #0f1726;
}

/* ---- Theme picker grid ---- */
.theme-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.6rem;
  margin-bottom: 0.5rem;
}
.theme-swatch {
  display: flex; align-items: center; gap: 0.6rem;
  padding: 0.6rem 0.7rem; border-radius: 8px; cursor: pointer;
  border: 1px solid var(--border-1); background: var(--bg-2);
  color: var(--text-1); text-align: left; transition: border-color .12s, background .12s;
}
.theme-swatch:hover { border-color: var(--accent-cyan); background: var(--bg-3); }
.theme-swatch.selected { border-color: var(--accent-cyan); box-shadow: 0 0 0 1px var(--accent-cyan) inset; }
.theme-swatch-chip {
  width: 22px; height: 22px; border-radius: 50%; flex: 0 0 auto;
  border: 1px solid rgba(255,255,255,0.2);
}
.theme-swatch-name { font-size: 0.85rem; }

.checkbox-row { display: flex; flex-wrap: wrap; gap: 0.8rem; margin-top: 0.3rem; }
.checkbox-row .checkbox { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.85rem; }

/* ---- Connection-control buttons (#49) ---- */
.conn-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0.3rem 0 0.6rem; }
.btn-conn {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0.45rem 0.9rem; border-radius: 7px; font-size: 0.85rem; font-weight: 600;
  border: 1px solid var(--border-2); background: var(--bg-3); color: var(--text-1);
  cursor: pointer; transition: background .12s, border-color .12s;
}
.btn-conn:hover { border-color: var(--accent-cyan); background: var(--accent-cyan-dim); }
.btn-conn.btn-primary { background: var(--accent-cyan); border-color: var(--accent-cyan); color: #04121a; }
.btn-conn.btn-primary:hover { filter: brightness(1.08); }

/* ---- Debug console ---- */
.debug-autorefresh { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.82rem; color: var(--text-3); }
.debug-feed {
  border: 1px solid var(--border-1); border-radius: 8px;
  background: var(--bg-log); max-height: 70vh; overflow-y: auto;
  font-family: var(--font-mono); font-size: 0.8rem;
}
.debug-row {
  display: grid; grid-template-columns: 72px 70px 64px 1fr;
  gap: 0.5rem; padding: 0.35rem 0.7rem; border-bottom: 1px solid var(--border-soft);
  align-items: baseline;
}
.debug-row:last-child { border-bottom: 0; }
.debug-ts { color: var(--text-4); }
.debug-cat { font-weight: 700; }
.debug-cat.cat-reader { color: var(--accent-cyan); }
.debug-cat.cat-api    { color: var(--accent-blue); }
.debug-cat.cat-mqtt   { color: var(--accent-violet); }
.debug-cat.cat-system { color: var(--text-3); }
.debug-cat.cat-error  { color: var(--accent-red); }
.debug-sev { color: var(--text-4); text-transform: uppercase; font-size: 0.7rem; }
.debug-msg { color: var(--text-2); word-break: break-word; }
.debug-src { color: var(--text-4); }
.debug-detail { color: var(--text-4); font-size: 0.72rem; margin-top: 0.2rem; }
.debug-row.sev-warn .debug-msg { color: var(--accent-amber); }
.debug-row.sev-error { background: var(--accent-red-dim); }
.debug-row.sev-error .debug-msg { color: var(--accent-red); }
.debug-row.live-row { grid-template-columns: 72px 150px 1fr; }
.live-row .debug-cat { color: var(--accent-cyan); }

/* ---- Notification bell ---- */
.notif-bell-block { position: relative; display: flex; align-items: center; }
.notif-bell {
  position: relative; background: none; border: 0; cursor: pointer;
  font-size: 1.15rem; padding: 0.3rem; line-height: 1; color: var(--text-2);
}
.notif-bell:hover { color: var(--text-1); }
.notif-bell-badge {
  position: absolute; top: -2px; right: -2px; min-width: 16px; height: 16px;
  padding: 0 4px; border-radius: 8px; background: var(--accent-red); color: #fff;
  font-size: 0.62rem; font-weight: 700; display: flex; align-items: center; justify-content: center;
}
.notif-dropdown {
  position: absolute; top: 110%; right: 0; width: 340px; z-index: 80;
  background: var(--bg-2); border: 1px solid var(--border-2); border-radius: 10px;
  box-shadow: var(--shadow-panel); overflow: hidden;
}
.notif-dropdown-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0.6rem 0.8rem; border-bottom: 1px solid var(--border-1); font-size: 0.85rem;
}
.notif-dropdown-link { color: var(--accent-cyan); font-size: 0.78rem; }
.notif-dropdown-list { max-height: 360px; overflow-y: auto; }
.notif-item {
  display: flex; gap: 0.5rem; padding: 0.5rem 0.8rem;
  border-bottom: 1px solid var(--border-soft); font-size: 0.8rem;
}
.notif-item-time { color: var(--text-4); flex: 0 0 auto; font-family: var(--font-mono); font-size: 0.72rem; }
.notif-item-msg { color: var(--text-2); }
.notif-item.notif-error .notif-item-msg { color: var(--accent-red); }
.notif-item.notif-warn .notif-item-msg { color: var(--accent-amber); }
.notif-empty { padding: 1rem 0.8rem; color: var(--text-4); font-size: 0.82rem; text-align: center; }

/* ---- Header tools: search / favorites / theme (GH #555) ---- */
/* Shares the notification bell's own button/dropdown idiom directly above
   (position:relative host + absolute-positioned panel, opened/closed by
   header_tools.js's shared togglePanel()/closeAllPanels()) so all header
   quick-access controls look and behave like one family. */
.header-tool-block { position: relative; display: flex; align-items: center; }
/* GH #574: these controls (Favorites, + Create New, Theme) previously had no
   visible chrome at all until hovered (transparent background + transparent
   border) -- next to real buttons elsewhere in the app they read as bare
   text/icons rather than buttons. Give them the same always-visible
   button surface `.btn-ghost` uses (panels.css), so they look like
   first-class header controls at rest, not just on hover. */
.header-tool-btn {
  display: flex; align-items: center; gap: 0.35rem;
  background: var(--bg-2); border: 1px solid var(--border-2); cursor: pointer;
  font-size: 0.95rem; padding: 0.3rem 0.5rem; line-height: 1;
  color: var(--text-2); border-radius: var(--radius-1);
}
.header-tool-btn:hover { color: var(--text-1); background: var(--bg-3); border-color: var(--accent-cyan); }
.header-tool-btn[aria-expanded="true"] { color: var(--accent-cyan); background: var(--bg-3); border-color: var(--accent-cyan); }
.header-tool-dropdown {
  position: absolute; top: 110%; right: 0; z-index: 85;
  background: var(--bg-2); border: 1px solid var(--border-2); border-radius: 10px;
  box-shadow: var(--shadow-panel); overflow: hidden;
}

/* Global search -- GH #555 (reopened): a permanently-visible bar with the
   icon embedded inside the input itself, not an icon that must be clicked
   to reveal a hidden input. Only the RESULTS panel below it opens/closes. */
.header-search-bar {
  display: flex; align-items: center; gap: 0.4rem;
  background: var(--bg-1); border: 1px solid var(--border-2); border-radius: var(--radius-1);
  padding: 0.3rem 0.6rem; min-width: 220px;
}
.header-search-bar:focus-within { border-color: var(--accent-cyan); }
.header-search-bar-icon { color: var(--text-3); font-size: 0.85rem; flex: 0 0 auto; line-height: 1; }
.header-search-bar-input {
  flex: 1; min-width: 0; border: 0; background: none; outline: none;
  color: var(--text-1); font-size: 0.85rem;
}
.header-search-bar-input::placeholder { color: var(--text-4); }
.header-search-panel { width: 320px; padding: 0.5rem; top: 115%; }
.header-search-results { max-height: 320px; overflow-y: auto; }
.header-search-hint { padding: 0.6rem 0.4rem; color: var(--text-4); font-size: 0.8rem; text-align: center; }
.header-search-result {
  display: flex; align-items: center; gap: 0.5rem; width: 100%;
  padding: 0.4rem 0.5rem; background: none; border: 0; border-radius: 6px;
  color: var(--text-2); text-align: left; cursor: pointer; font-size: 0.84rem;
}
.header-search-result:hover { background: var(--bg-3); color: var(--text-1); }
.header-search-result-icon { flex: 0 0 auto; }
.header-search-result-text { display: flex; flex-direction: column; min-width: 0; }
.header-search-result-label { color: var(--text-1); }
.header-search-result-section { font-size: 0.7rem; color: var(--text-4); }

/* Favorites */
.header-tool-btn.is-active-fav { color: var(--accent-amber, #f59e0b); }
.header-fav-panel { width: 300px; padding: 0.4rem; }
.header-fav-toggle {
  display: block; width: 100%; box-sizing: border-box; text-align: left;
  padding: 0.5rem 0.6rem; margin-bottom: 0.3rem; border-radius: 6px;
  background: var(--bg-1); border: 1px solid var(--border-1); color: var(--text-1);
  font-size: 0.82rem; cursor: pointer;
}
.header-fav-toggle:hover { background: var(--bg-3); }
.header-fav-list { max-height: 300px; overflow-y: auto; }
.header-fav-empty { padding: 0.8rem 0.5rem; color: var(--text-4); font-size: 0.8rem; text-align: center; }
.header-fav-item { display: flex; align-items: center; gap: 0.2rem; }
.header-fav-item-open {
  flex: 1; display: flex; align-items: center; gap: 0.5rem; min-width: 0;
  padding: 0.4rem 0.5rem; background: none; border: 0; border-radius: 6px;
  color: var(--text-2); text-align: left; cursor: pointer; font-size: 0.84rem;
}
.header-fav-item-open:hover { background: var(--bg-3); color: var(--text-1); }
.header-fav-item-remove {
  flex: 0 0 auto; background: none; border: 0; color: var(--text-4);
  cursor: pointer; font-size: 1rem; padding: 0.2rem 0.5rem; line-height: 1;
}
.header-fav-item-remove:hover { color: var(--accent-red); }

/* "+ Create New" (GH #555 reopened follow-up) */
.header-create-btn { white-space: nowrap; font-weight: 600; }
.header-create-panel { width: 240px; padding: 0.4rem; }
.header-create-list { max-height: 340px; overflow-y: auto; }
.header-create-option {
  display: flex; align-items: center; gap: 0.55rem; width: 100%;
  padding: 0.4rem 0.5rem; background: none; border: 0; border-radius: 6px;
  color: var(--text-2); text-align: left; cursor: pointer; font-size: 0.84rem;
}
.header-create-option:hover { background: var(--bg-3); color: var(--text-1); }

/* Theme quick-switcher */
.header-theme-swatch {
  display: inline-block; width: 12px; height: 12px; border-radius: 50%;
  border: 1px solid var(--border-2); flex: 0 0 auto;
}
.header-theme-panel { width: 220px; padding: 0.4rem; }
.header-theme-list { max-height: 340px; overflow-y: auto; }
.header-theme-group-label {
  padding: 0.35rem 0.5rem 0.15rem; font-size: 0.68rem; color: var(--text-4);
  text-transform: uppercase; letter-spacing: 0.05em;
}
.header-theme-option {
  display: flex; align-items: center; gap: 0.55rem; width: 100%;
  padding: 0.4rem 0.5rem; background: none; border: 0; border-radius: 6px;
  color: var(--text-2); text-align: left; cursor: pointer; font-size: 0.84rem;
}
.header-theme-option:hover { background: var(--bg-3); color: var(--text-1); }
.header-theme-option.is-active { color: var(--text-1); font-weight: 600; }
.header-theme-check { margin-left: auto; color: var(--accent-cyan); }

/* User menu (GH #630) -- avatar-triggered dropdown of account actions,
   moved out of the sidebar footer. Reuses .btn-signout/.btn-signout-ico
   as-is (below) so Sign Out looks identical to before the move. */
.header-user-btn { padding: 0.25rem 0.6rem 0.25rem 0.3rem; }
.header-user-avatar {
  width: 24px; height: 24px; border-radius: 50%; flex: 0 0 auto;
  background: linear-gradient(135deg, var(--accent-cyan), var(--accent-indigo, #6366f1));
  color: #000; font-size: 0.65rem; font-weight: 700;
  display: grid; place-items: center;
}
.header-user-avatar.has-photo { background-color: var(--surface-2, #1c2430); background-size: cover; background-position: center; }
.header-user-name { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.header-user-panel { width: 220px; padding: 0.4rem; }
.header-user-links { display: flex; flex-direction: column; }
.header-user-link {
  display: flex; align-items: center; gap: 0.55rem; width: 100%;
  padding: 0.4rem 0.5rem; background: none; border: 0; border-radius: 6px;
  color: var(--text-2); text-align: left; cursor: pointer; font-size: 0.84rem;
}
.header-user-link:hover { background: var(--bg-3); color: var(--text-1); }
/* GH #630: the Sign Out button keeps its `.btn-signout` id/class/icon as-is
   (below, unchanged since GH #582) for behavior and icon rendering, but as
   one item among several in a compact dropdown -- rather than standing
   alone in the sidebar -- its old solid-red-pill treatment reads as
   visually louder than the menu around it. This compound selector
   (specificity 0,0,2,0) intentionally outranks the plain `.btn-signout`
   rule regardless of file order, so it doesn't need to live after it. */
.header-user-panel .btn-signout {
  width: 100%; justify-content: flex-start; margin-top: 0.4rem;
  padding: 0.55rem 0.5rem 0.4rem; border-radius: 0;
  border: 0; border-top: 1px solid var(--border-1);
  background: none; color: var(--accent-red, #f87171); font-weight: 500;
}
.header-user-panel .btn-signout:hover { background: var(--bg-3); color: var(--accent-red, #f87171); }

@media (max-width: 640px) {
  .header-search-panel, .header-fav-panel, .header-theme-panel, .header-user-panel { width: min(88vw, 320px); }
  .header-user-name { display: none; }
}

/* ============================================================
 * Phase 9: connectors suggestion grid + branding copyright lines.
 * ============================================================ */

.suggest-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.6rem;
  margin-top: 0.5rem;
}
.suggest-card {
  display: flex; flex-direction: column; gap: 0.2rem;
  padding: 0.7rem 0.8rem; border-radius: 8px;
  border: 1px solid var(--border-1); background: var(--bg-2);
}
.suggest-card strong { font-size: 0.9rem; color: var(--text-1); }

/* Branding copyright lines (login + about) */
.login-copy, .about-copy {
  text-align: center;
  opacity: 0.75;
}
.login-copy { margin-top: 1rem; }
.about-logo { display: block; }

/* ============================================================
 * Phase 10: professional empty states (replaces phase-N placeholders),
 * toast/snackbar, polished modal shell, premium login split layout.
 * ============================================================ */

.empty-state {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.5rem; text-align: center; padding: 2.4rem 1.5rem;
  border: 1px dashed var(--border-2); border-radius: 12px;
  background: var(--bg-2); color: var(--text-3); min-height: 140px;
}
.empty-state-icon { font-size: 2rem; opacity: 0.7; }
.empty-state-title { font-size: 1rem; font-weight: 700; color: var(--text-2); }
.empty-state-text { font-size: 0.86rem; max-width: 520px; line-height: 1.5; }

/* ---- Toast / snackbar ---- */
#toast-host {
  position: fixed; right: 18px; bottom: 18px; z-index: 9999;
  display: flex; flex-direction: column; gap: 10px; max-width: 380px;
}
.toast {
  display: flex; align-items: flex-start; gap: 10px; padding: 12px 14px;
  border-radius: 10px; background: var(--bg-2); border: 1px solid var(--border-2);
  border-left: 4px solid var(--accent-cyan); box-shadow: var(--shadow-panel);
  color: var(--text-1); font-size: 0.88rem; animation: toast-in .18s ease;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.toast.toast-success { border-left-color: var(--accent-green); }
.toast.toast-error   { border-left-color: var(--accent-red); }
.toast.toast-warn    { border-left-color: var(--accent-amber); }
.toast.toast-info    { border-left-color: var(--accent-blue); }
.toast-icon { font-size: 1rem; line-height: 1.3; }
.toast-body { flex: 1; }
.toast-title { font-weight: 700; margin-bottom: 2px; }
.toast-msg { color: var(--text-2); font-size: 0.82rem; }
.toast-close { background: none; border: 0; color: var(--text-3); cursor: pointer; font-size: 1rem; }

/* ---- Premium login split layout (desktop) ---- */
/* GH #390: was `min-height: 100vh`, sized against the viewport rather than
   the actual space `.app-main` has available. (At the time this was written
   the footer was still a visible sibling on the login screen -- GH #460
   later hid it entirely via `body.on-login .app-footer` above -- but
   `.app-main` can still be shorter than the viewport for other reasons, e.g.
   a mobile browser's dynamic address-bar chrome, so `100%` remains the
   correct basis.) `100%` matches the parent's real, already-shrunk height so
   the card only scrolls when it genuinely needs more room than that. */
.login-split { display: flex; min-height: 100%; width: 100%; }
.login-aside {
  flex: 1 1 46%; display: flex; flex-direction: column; justify-content: center;
  padding: 3rem 3.5rem; gap: 1rem;
  background: linear-gradient(150deg, var(--bg-2) 0%, var(--bg-0) 70%);
  border-right: 1px solid var(--border-1);
}
.login-aside .la-logo { width: 84px; height: 84px; border-radius: 16px; }
.login-aside h2 { font-size: 1.7rem; margin: 0.4rem 0 0; color: var(--text-1); }
.login-aside .la-sub { color: var(--text-3); max-width: 420px; line-height: 1.55; }
.login-aside .la-features { display: flex; flex-direction: column; gap: 0.55rem; margin-top: 1rem; }
.login-aside .la-feature { display: flex; align-items: center; gap: 0.6rem; color: var(--text-2); font-size: 0.9rem; }
.login-aside .la-feature .lf-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--accent-cyan); flex: 0 0 auto; }
.login-main {
  flex: 1 1 54%; display: flex; align-items: center; justify-content: center; padding: 1.5rem;
}
.login-main .login-card {
  width: 100%; max-width: 460px; min-width: 0;
  /* GH #428: safety net for short viewports — the card itself scrolls
     internally as a last resort instead of forcing .app-main (its parent)
     to grow the outer scrollbar on the rare viewport where even the
     trimmed padding above isn't enough. */
  max-height: calc(100vh - 3rem);
  overflow-y: auto;
}
@media (max-width: 860px) {
  .login-aside { display: none; }
  .login-main { flex-basis: 100%; }
}

/* ---- Polished modal shell ---- */
.modal { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center;
  justify-content: center; background: rgba(3, 7, 15, 0.66); padding: 1.5rem; }
.modal[hidden] { display: none; }
.modal-card {
  width: 100%; max-width: 560px; max-height: 86vh; display: flex; flex-direction: column;
  background: var(--bg-2); border: 1px solid var(--border-2); border-radius: 14px;
  box-shadow: 0 18px 50px rgba(0,0,0,0.5); overflow: hidden;
}
.modal-header { display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px; border-bottom: 1px solid var(--border-1); }
.modal-header h3 { margin: 0; font-size: 1.05rem; }
.modal-close { background: none; border: 0; color: var(--text-3); font-size: 1.5rem;
  line-height: 1; cursor: pointer; padding: 0 4px; }
.modal-close:hover { color: var(--text-1); }
/* GH #356/#357: a flex child's default min-height:auto keeps it from
   shrinking below its content's height even though .modal-card caps itself
   at max-height:86vh with overflow:hidden -- so on short viewports the
   content doesn't shrink to fit and whatever falls outside the box (here,
   the footer's Cancel/Save buttons) gets clipped instead of the body
   scrolling. flex:1 1 auto + min-height:0 let the body actually shrink and
   scroll internally so the header/footer stay fixed and visible. */
.modal-body { padding: 18px 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px;
  flex: 1 1 auto; min-height: 0; }
/* GH #395: flex children never collapse adjoining margins the way normal-
   flow block siblings do, so a bare, unstyled `<p>` (its UA default is
   `margin: 1em 0`) stacks its own top/bottom margins on top of this
   container's `gap: 12px` instead of being absorbed by it -- e.g. the
   Connector/Notification-channel "Test" result modal (badge `<p>` +
   message `<p>`, nothing else) ends up with roughly margin-bottom + gap +
   margin-top (~40px) between them and margin + padding (~34px) above/below,
   several times the intended spacing, for a modal whose whole point is to
   be a compact status readout. `gap` is this container's one spacing
   mechanism; zero out the default paragraph margin so it's the only one. */
.modal-body p { margin: 0; }
.modal-footer { display: flex; justify-content: flex-end; gap: 10px;
  padding: 14px 20px; border-top: 1px solid var(--border-1); }

/* ============================================================
 * Phase 11: analytics charts, antenna bars, live connection dot,
 * field-check, textarea fields.
 * ============================================================ */
.chart { display: block; max-width: 100%; }
.ant-bars { display: flex; flex-direction: column; gap: 10px; }
.ant-bar-row { display: flex; align-items: center; gap: 12px; }
.ant-bar-label { width: 96px; flex: 0 0 auto; color: var(--text-2); font-size: 0.85rem; }
.ant-bar-track { flex: 1; height: 16px; background: var(--bg-3); border-radius: 8px; overflow: hidden; }
.ant-bar-fill { height: 100%; background: linear-gradient(90deg, var(--accent-violet), var(--accent-cyan)); border-radius: 8px; }
.ant-bar-val { width: 120px; flex: 0 0 auto; text-align: right; color: var(--text-3); font-size: 0.8rem; }
textarea.field-input { resize: vertical; font-family: var(--font-mono); }
.field-check { display: flex; align-items: center; gap: 6px; margin-top: 8px; color: var(--text-2); }
.dot.dot-ok   { background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green); }
.dot.dot-warn { background: var(--accent-amber); box-shadow: 0 0 6px var(--accent-amber); }
.dot.dot-err  { background: var(--accent-red); }

/* Issue #39: action links styled as buttons (DB Maintenance archive actions) */
a.btn-row { text-decoration: none; display: inline-flex; align-items: center; }
a.btn-row:hover { text-decoration: none; }

/* ============================================================
   Manual-QA affordance pass (Poonam #8/#27/#39, UIUX-15)
   Make row/table action controls unmistakably look clickable:
   visible fill + border, weight, hover/focus-visible/active/disabled.
   ============================================================ */
.btn-row,
.row-actions button,
.row-actions a.btn-row {
  background: var(--btn-row-bg, rgba(56, 189, 248, 0.10));
  border: 1px solid var(--btn-row-border, rgba(56, 189, 248, 0.45));
  color: var(--text);
  padding: 0.34rem 0.72rem;
  border-radius: 6px;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.1px;
  cursor: pointer;
  display: inline-flex; align-items: center; gap: 0.32rem;
  line-height: 1.1;
  transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
}
.btn-row:hover,
.row-actions button:hover { background: rgba(56, 189, 248, 0.20); border-color: var(--accent-cyan, #38bdf8); }
.btn-row:active,
.row-actions button:active { transform: translateY(0.5px); background: rgba(56, 189, 248, 0.28); }
.btn-row:focus-visible,
.row-actions button:focus-visible {
  outline: 2px solid var(--accent-cyan, #38bdf8);
  outline-offset: 1px;
}
.btn-row:disabled, .btn-row[disabled],
.row-actions button:disabled, .row-actions button[disabled] {
  opacity: 0.5; cursor: not-allowed; box-shadow: none;
}
.btn-row.danger, .row-actions button.danger {
  background: rgba(220, 38, 38, 0.12); border-color: rgba(220, 38, 38, 0.5); color: #fca5a5;
}
.btn-row.danger:hover, .row-actions button.danger:hover { background: rgba(220, 38, 38, 0.22); }
.btn-row.warn { background: rgba(245, 158, 11, 0.12); border-color: rgba(245, 158, 11, 0.5); }

/* ===== Multi-Reader Overview panel (dashboard) ===== */
#mr-overview .panel-head { margin-bottom:10px; }
.mr-cards { display:grid; grid-template-columns: repeat(4, 1fr); gap:12px; margin-bottom:14px; }
.mr-card { background: rgba(56,189,248,0.07); border:1px solid rgba(56,189,248,0.25); border-radius:10px; padding:12px 14px; }
.mr-card-value { font-size:1.6rem; font-weight:700; color: var(--text); }
.mr-card-label { font-size:0.78rem; color: var(--text-muted,#94a3b8); margin-top:2px; }
.mr-grids { display:grid; grid-template-columns: repeat(2, 1fr); gap:14px; }
.mr-grid { background: var(--bg-input, rgba(255,255,255,0.03)); border:1px solid var(--border); border-radius:10px; padding:10px 12px; }
.mr-grid h4 { margin:0 0 8px; font-size:0.85rem; color: var(--text); }
.mr-row { display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px dashed var(--border); font-size:0.82rem; }
.mr-row:last-child { border-bottom:none; }
.mr-row-label { color: var(--text); }
.mr-row-value { color: var(--accent-cyan,#38bdf8); font-weight:600; }
@media (max-width: 900px){ .mr-cards{grid-template-columns:repeat(2,1fr)} .mr-grids{grid-template-columns:1fr} }

/* ===== Reader Management ===== */
/* GH #603: without align-items:flex-end, flex's default `stretch` makes the
   Register/Cancel buttons in #rpAddForm as tall as their sibling <label>s
   (span + input stacked), which are taller than a lone <button> -- bottom-
   aligning everything on this row keeps the buttons the same height as the
   input fields instead. */
.rm-filters { display:flex; flex-wrap:wrap; align-items:flex-end; gap:12px; margin:10px 0 14px; }
/* GH #603: align-items:flex-end above stops the buttons from *stretching* to
   the label column's height, but .btn-primary/.btn-secondary's own padding/
   font-size (pages.css ~750) is still taller than .field-input's -- match
   the input's box model exactly so Register/Cancel render at the same
   height as the fields beside them. */
.rm-filters > .btn-primary, .rm-filters > .btn-secondary {
  padding: 8px 11px; font-size: 13px; line-height: 1.3;
}
.rm-filters label { display:flex; flex-direction:column; font-size:0.78rem; color:var(--text-muted,#94a3b8); gap:3px; }
.rm-filters select { min-width:150px; padding:5px 8px; border-radius:6px; border:1px solid var(--border); background:var(--bg-input,rgba(255,255,255,0.04)); color:var(--text); }
/* Round 324: .rm-filters is reused by #rpAddForm (the toggleable "+ Add reader"
   quick-form, which sets/clears the `hidden` attribute via JS) as well as the
   always-visible #rmFilters filter row. Author CSS beats the UA's unadorned
   `[hidden] { display:none }` rule regardless of specificity, so this class's
   own `display:flex` silently defeated `hidden` and left the add-reader form
   (and its Cancel button) permanently visible and interactive on every load. */
.rm-filters[hidden] { display:none; }
.app-modal-overlay { position:fixed; inset:0; background:rgba(2,6,23,0.62); display:flex; align-items:center; justify-content:center; z-index:1000; }
.app-modal { background:var(--bg-panel,#0f172a); border:1px solid var(--border); border-radius:12px; width:min(720px,94vw); max-height:88vh; overflow:auto; box-shadow:0 18px 48px rgba(0,0,0,0.5); }
.app-modal-head { display:flex; justify-content:space-between; align-items:center; padding:14px 18px; border-bottom:1px solid var(--border); }
.app-modal-head h3 { margin:0; font-size:1rem; }
.app-modal-close { background:none; border:none; color:var(--text-muted,#94a3b8); font-size:1.3rem; cursor:pointer; line-height:1; }
.app-modal-body { padding:16px 18px; }
.app-modal-foot { display:flex; justify-content:flex-end; gap:10px; padding:12px 18px; border-top:1px solid var(--border); }
/* GH #503/#504: align-items:start keeps every label cell pinned to the top
   of its grid row instead of CSS Grid's default stretch -- without it, a
   field with helper/hint text (e.g. Name, GPS lat/lng) is taller than its
   row siblings, and grid stretch then vertically centers/stretches the
   shorter siblings' inputs to match, so inputs in the same row land at
   different heights/vertical positions and leave uneven empty space below
   the shorter fields. */
.rm-form { display:grid; grid-template-columns:repeat(3,1fr); gap:14px 12px; align-items:start; }
.rm-form label { display:flex; flex-direction:column; gap:4px; font-size:0.78rem; color:var(--text-muted,#94a3b8); }
/* GH #503/#504: explicit box-sizing + height normalizes <select> vs
   <input type="text"> vs <input type="number"> to the identical rendered
   height -- browsers apply different intrinsic line-height/UA padding to
   each control type by default, which is what made dropdowns/textboxes/
   numeric fields look inconsistent within the same form/row. */
.rm-form input:not([type="checkbox"]), .rm-form select {
  box-sizing: border-box; height: 34px; width: 100%;
  padding:6px 9px; border-radius:6px; border:1px solid var(--border);
  background:var(--bg-input,rgba(255,255,255,0.04)); color:var(--text);
  font-size: 0.85rem; line-height: 1.3; font-family: inherit;
}
/* GH #511: the rule above sizes every .rm-form <input> to a uniform 34px-tall,
   full-width text-field box -- browsers scale a checkbox's native appearance to
   whatever width/height it's given, so the Active checkbox in the Company/Site/
   Location editors (the only .rm-form dialogs with a checkbox field) inherited
   that same 34px/100% box and rendered as an oversized square instead of a
   normal checkbox. Excluded from the rule above and sized like every other
   checkbox in the app instead. */
.rm-form input[type="checkbox"] { box-sizing: border-box; width: 16px; height: 16px; flex-shrink: 0; }
/* #258: Reader Management > Actions > Assign User (and Assign Location) drop a
   lone <button> straight into .rm-form as a grid item -- every OTHER .rm-form
   usage (Add/Edit reader, org.js company/site/location/group editors) keeps
   its Save/Cancel buttons in the modal's own footer, outside this grid, so
   this stretch behavior was never exercised before. With no override, CSS
   Grid's default stretch alignment fills the button to its whole 1fr column
   width and to the row's full height (set by the taller label+select block
   beside it), rendering the "Save assignment" button oversized and out of
   line with the dropdown it sits next to. Buttons keep their natural size
   and sit flush with the bottom of the row, level with the field beside them.
*/
.rm-form > button { align-self: end; justify-self: start; }
/* GH #578: .btn-primary sets `color: #000` (black text, tuned for the light
   cyan/green/amber accent backgrounds its OTHER uses have); combined with
   `.danger`'s solid red background that black text drops to ~4:1 contrast
   against this app's red accent across its 10 themes (below WCAG AA's 4.5:1
   for normal text) -- worst case a near-unreadable black-on-red "Revoke…"
   button (License, and every other destructive action across the app that
   reuses this exact class combo: Backup, Organization, Reader Management,
   Schedules). White text against the same red backgrounds measures 4.8-6.6:1
   across every theme's --accent-red, comfortably passing WCAG AA. */
.btn-primary.danger { background:rgba(220,38,38,0.85); border-color:rgba(220,38,38,0.9); color: #fff; }
@media (max-width:820px){ .rm-form{grid-template-columns:1fr 1fr} }
/* GH #503/#504: a single mid-size breakpoint alone still left 2 columns of
   ~34px-tall controls cramped on real phone widths (375-428px); collapse to
   a single column so labels/inputs/hints keep equal width and don't wrap
   or overlap the modal's fixed 720px-max card on narrow viewports. */
@media (max-width:480px){ .rm-form{grid-template-columns:1fr} }

/* ===== Table numeric alignment (Tag Events / Recent Reads) ===== */
.data-table td, .data-table th { font-variant-numeric: tabular-nums; }
.data-table th[align="right"], .data-table td[align="right"],
.data-table .align-right, .data-table .col-num { text-align: right; }
.tbl-mono { font-family: var(--font-mono, Consolas, monospace); font-variant-numeric: tabular-nums; }

/* ===== Reader Management CSV import ===== */
/* GH #426: CSV template download moved from the toolbar into this dialog */
.rm-import-tpl-row { margin:6px 0 10px; }
.rm-import-report { margin-top:10px; max-height:300px; overflow:auto; }
.rm-import-report .imp-ok td { color: var(--text); }
.rm-import-report .imp-warn td { color: var(--accent-amber,#e6b800); }
.rm-import-report .imp-err td { color: var(--accent-red,#ff6b6b); }
/* GH #30: import summary count badges */
.imp-counts { display:flex; flex-wrap:wrap; gap:8px; margin:10px 0; }
.imp-badge { font-size:12px; padding:3px 9px; border-radius:999px;
  border:1px solid var(--border,#2a3340); background: var(--surface-2,#1c2430); }
.imp-badge.imp-ok { color: var(--accent-green,#22c55e); border-color: rgba(34,197,94,0.4); }
.imp-badge.imp-warn { color: var(--accent-amber,#e6b800); border-color: rgba(230,184,0,0.4); }
.imp-badge.imp-err { color: var(--accent-red,#ff6b6b); border-color: rgba(255,107,107,0.4); }
.imp-badge.muted { color: var(--text-muted,#9aa4b2); }
/* #34: CSV import progress indicator (readers + users) */
.rm-import-progress { margin-top:12px; }
.rm-import-progress-label { font-size:13px; color: var(--text-muted,#9aa4b2); margin-bottom:6px; }
.rm-import-bar { height:10px; border-radius:6px; background: var(--surface-2,#1c2430);
  overflow:hidden; border:1px solid var(--border,#2a3340); }
.rm-import-bar-fill { height:100%; width:0%; border-radius:6px;
  background: linear-gradient(90deg, var(--accent,#c9a227), #e6c04d);
  transition: width .15s ease; }
.callout.success { background: rgba(34,197,94,0.12); border:1px solid rgba(34,197,94,0.4); color: var(--text); padding:8px 12px; border-radius:8px; }

/* ===== Reader Management discrete actions (Track 4) ===== */
.rm-action-bar { display:flex; flex-wrap:wrap; gap:6px; margin:6px 0; }
.rm-action-bar .btn-secondary { font-size:12px; padding:5px 9px; }
.rm-action-result { margin-top:10px; max-height:340px; overflow:auto; }
.rm-action-result .data-table { width:100%; }
.callout.warn { background: rgba(245,158,11,0.12); border:1px solid rgba(245,158,11,0.4); color: var(--text); padding:8px 12px; border-radius:8px; }

/* ===== Alerts & Audit Trail pagination (QA #11) ===== */
.audit-pager { display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-top:8px; }
.audit-pager-controls { display:flex; align-items:center; gap:6px; }
.audit-pager-controls .btn-row { padding:4px 10px; font-size:12px; }
.audit-pager-controls .btn-row:disabled { opacity:.45; cursor:default; }
#auditPageInfo { min-width:96px; text-align:center; }

/* GH #421: per-column search/filter row under the Audit Log header */
.audit-filter-row th { padding:4px 8px 8px; background: var(--surface-1, transparent); }
.audit-col-filter {
  width:100%; box-sizing:border-box; font-size:12px; padding:4px 6px;
  border-radius:6px; border:1px solid var(--border,#2a3340);
  background: var(--surface-2,#1c2430); color: var(--text);
}
.audit-col-filter::placeholder { color: var(--text-muted,#9aa4b2); }
/* GH #560: Time column filter is a From/To date-range pair instead of a
   free-text box matched against the raw ISO ts column (which never matched
   what the cell visibly displays, formatted via fmtDateTime()). */
.audit-filter-time { display: flex; gap: 4px; }
.audit-col-filter-date {
  width: 100%; min-width: 0; box-sizing: border-box; font-size: 11px; padding: 4px 4px;
  border-radius: 6px; border: 1px solid var(--border,#2a3340);
  background: var(--surface-2,#1c2430); color: var(--text);
}
/* GH #560: Module/Status filter-row dropdowns match the row's text inputs'
   width/sizing instead of each select's own intrinsic content width. */
.audit-filter-row #auditModuleFilter, .audit-filter-row #auditStatusFilter {
  width: 100%; box-sizing: border-box; font-size: 12px; padding: 4px 6px;
  border-radius: 6px; border: 1px solid var(--border,#2a3340);
  background: var(--surface-2,#1c2430); color: var(--text);
}

/* GH #530 slice 1: HTTP Method/API Path/raw Action/Duration/IP moved off the
   default grid into this collapsed panel (same server-side f_* filters as
   before, see routes_audit.py) plus a per-row expandable technical-detail
   row (below). */
.audit-advanced-filters { margin: 10px 0; }
.audit-advanced-filters summary {
  cursor: pointer; font-size: 12.5px; color: var(--text-3, var(--text-muted,#9aa4b2));
  padding: 6px 2px;
}
.audit-advanced-filters summary:hover { color: var(--text-1, var(--text)); }
.audit-advanced-filters-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px; padding: 10px 2px 4px;
}
.audit-advanced-filter-field { display: flex; flex-direction: column; gap: 4px; }
.audit-advanced-filter-field input { width: 100%; box-sizing: border-box; }

.audit-row-toggle {
  border: none; background: none; cursor: pointer; color: var(--text-3, var(--text-muted,#9aa4b2));
  font-size: 12px; width: 22px; height: 22px; border-radius: 4px; padding: 0;
}
.audit-row-toggle:hover, .audit-row-toggle:focus-visible { color: var(--text-1, var(--text)); background: var(--bg-3, rgba(255,255,255,0.06)); }
.audit-detail-row td { background: var(--surface-1, rgba(255,255,255,0.02)); padding: 8px 12px; }
.audit-detail-row[hidden] { display: none; }
.audit-detail-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 8px 16px; font-size: 12.5px;
}

/* GH #530 slice 2: field-level Old Value/New Value change history, shown
   under the technical-details grid when a row has one (create/update/
   delete on Users or Settings so far -- see routes_audit.py's
   _field_changes()). */
.audit-field-changes {
  margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border-1, rgba(255,255,255,.08));
  display: flex; flex-direction: column; gap: 6px;
}
.audit-field-change {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px;
  font-size: 12.5px; padding: 2px 0;
}
.audit-field-change-name { font-weight: 600; min-width: 140px; }
.audit-field-change-values { display: flex; flex-wrap: wrap; gap: 4px 16px; }
.audit-field-change-old, .audit-field-change-new { white-space: pre-wrap; word-break: break-word; }

/* ===== QA #3: Settings sub-navigation (sticky rail) + two-column layout ===== */
/* QA #1: the Settings sections are navigated by horizontal TABS across the top of
   the settings content — not a nested vertical sidebar. The global app-navigation
   layout (Left/Right/Top) is controlled from Appearance → Navigation Layout. */
.settings-layout { display: block; }
.settings-subnav {
  position: static; display: flex; flex-direction: row; flex-wrap: wrap; gap: 6px;
  background: transparent; border: 0; border-bottom: 1px solid var(--border-1, rgba(255,255,255,.08));
  border-radius: 0; padding: 0 0 10px; margin-bottom: 16px;
}
.settings-subnav-title { display: none; }
.settings-subnav-item {
  text-align: center; background: var(--bg-2); border: 1px solid var(--border-1, rgba(255,255,255,.08));
  color: var(--text-2); padding: 8px 16px; border-radius: 999px; cursor: pointer; font-size: 13px;
  border-left: 1px solid var(--border-1, rgba(255,255,255,.08));
  transition: background var(--t-fast, .15s) ease, color var(--t-fast,.15s) ease;
}
.settings-subnav-item:hover { background: rgba(255,255,255,.06); color: var(--text-1); }
.settings-subnav-item.active { background: var(--accent-cyan, #06b6d4); color: #08131b; border-color: transparent; font-weight: 600; }
.settings-body { display: flex; flex-direction: column; gap: 16px; }
@media (max-width: 900px) {
  .settings-subnav { flex-wrap: wrap; }
}

/* GH#19/#29: reader dialog uses the shared modal; wider card for the 3-col form */
.modal-card.modal-card-wide { max-width: 720px; }
.rm-hint { font-size: 0.7rem; color: var(--text-3, #94a3b8); margin-top: 2px; }
.rm-form input:invalid { border-color: var(--accent-red, #ef4444); }

/* GH#32: Reader Management pagination bar */
.rm-pagination {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 10px; margin-top: 12px; padding-top: 10px;
  border-top: 1px solid var(--border-1, rgba(255,255,255,.08));
}
.rm-page-info { font-size: 12.5px; color: var(--text-2, #94a3b8); }
.rm-page-controls { display: flex; gap: 6px; flex-wrap: wrap; }
.rm-page-btn { min-width: 64px; }
.rm-page-btn:disabled { opacity: .45; cursor: default; }
.rm-page-size-label { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--text-2, #94a3b8); }
/* GH #604: Rows Per Page moved into the top filter bar -- push it to the
   bar's far right, on the same line as the other filter controls. */
#rmPageSizeMount { margin-left: auto; }

/* GH#31: prominent, labelled Sign Out button in the user menu */
.btn-signout {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: 8px;
  background: var(--accent-red, #ef4444); color: #fff;
  border: 1px solid transparent; font-size: 13px; font-weight: 600; cursor: pointer;
  transition: background var(--t-fast, .15s) ease, transform var(--t-fast,.15s) ease;
}
.btn-signout:hover { background: #dc2626; }
.btn-signout:active { transform: translateY(1px); }
.btn-signout:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
/* GH #582: inline SVG (not a Unicode glyph) so the icon renders identically
   across platforms instead of depending on OS font glyph coverage. */
.btn-signout .btn-signout-ico { display: inline-flex; line-height: 1; }
.btn-signout .btn-signout-ico svg { display: block; width: 14px; height: 14px; }

/* GH#33: high-visibility login error */
/* GH #238: this rule comes later in the cascade than the earlier
   `.login-error` block above and was the one actually winning — same
   hardcoded-light-pink-on-pale-tint contrast bug, just a second copy of it.
   `--accent-red` is theme-aware (see the earlier block's comment). */
.login-error {
  display: flex; align-items: center; gap: 8px;
  background: var(--accent-red-dim, rgba(220, 38, 38, 0.18));
  border: 1px solid rgba(248, 113, 113, 0.6);
  border-left: 4px solid var(--accent-red, #ef4444);
  color: var(--accent-red, #dc2626); font-weight: 600;
  box-shadow: 0 2px 10px rgba(220,38,38,.18);
}
/* GH #238: Light theme's --accent-red (#dc2626) still measures under WCAG
   AA (4.5:1) once composited against the actual rendered background here —
   the .login-aside/.login-main/.login-card chain plus this box's own
   translucent red tint land on a pale pink (~rgb(238,220,225)), and
   #dc2626 on THAT specific pink only reaches ~3.7:1, not 4.5:1. A single
   shared --accent-red is used everywhere (buttons, badges, dots) so it
   can't just be darkened globally without a wider re-audit; scope this to
   the login error banner specifically with a darker red (#b91c1c,
   ~4.9:1 against the same background) that's still clearly "red/error".  */
html[data-theme="light"] .login-error { color: #b91c1c; }
/* GH #39: this `.login-error { display: flex }` rule (added for the error
   icon layout) has the same specificity as the UA `[hidden] { display:none }`
   and comes later in the cascade, so it was overriding `hidden` and painting
   an EMPTY red error box below the password field before any interaction.
   Explicitly hide the box while the `hidden` attribute is set. */
.login-error[hidden] { display: none !important; }
.login-error-ico { font-size: 16px; line-height: 1; flex: 0 0 auto; }
.login-error.shake { animation: login-err-shake .4s ease; }
@keyframes login-err-shake {
  0%,100% { transform: translateX(0); }
  20% { transform: translateX(-6px); } 40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); } 80% { transform: translateX(4px); }
}

/* ===== RTLS Tracking Map (pages/rtls_map.js) ============================== */
.rtm-toolbar {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px;
  margin: 8px 0 12px; padding: 10px 12px;
  background: var(--bg-2); border: 1px solid var(--border-1); border-radius: 10px;
}
.rtm-field { display: flex; flex-direction: column; gap: 3px; }
.rtm-field .kv-label { font-size: 11px; }
.rtm-live {
  display: inline-flex; align-items: center; gap: 4px; font-size: 12px;
  color: var(--accent-green); letter-spacing: .02em;
}
.rtm-live::first-letter { animation: rtm-pulse 1.6s ease-in-out infinite; }

.rtm-stage {
  position: relative; display: block; width: 100%;
  border: 1px solid var(--border-2); border-radius: 10px; overflow: hidden;
  background: var(--bg-1); line-height: 0;
}

/* Round 189: zoom/pan viewport clips the transform-scaled stage. Transforms are
   visual-only, so the viewport keeps its zoom-1 height and clips the overflow.
   Round 287 (GH #284): block + width:100% (was inline-block, which shrink-wraps
   to the floor-plan image's intrinsic pixel size) so the map fills the
   available page width instead of rendering compressed regardless of viewport. */
.rtm-viewport {
  position: relative; display: block; width: 100%; max-width: 100%;
  overflow: hidden; border-radius: 10px; touch-action: none;
  /* GH #299: normal (non-fullscreen) view now has a fixed, standard-size
     window instead of letting `width:100%` + the stage img's `height:auto`
     (GH #284) grow the rendered map's HEIGHT without limit -- on a wide
     viewport, a floor plan's own aspect ratio could stretch the map to well
     over 1500px tall, forcing the whole page (toolbar/legend/footer) to
     scroll along with it. Capping height only (not width, so #284's "fill
     the available width" still holds for small plan images) reuses the SAME
     clipping window the viewport already provides for zoomed-in views:
     `overflow: hidden` above simply clips the now-taller stage, and the
     existing zoom/drag-to-pan controls (unchanged) reach the rest, exactly
     like zooming in already does. Full Screen's own rule below
     (`.rtm-fs:fullscreen .rtm-viewport`) overrides this to fill the
     available screen space instead; exiting Full Screen restores this base
     rule automatically since nothing about it is stateful.

     Round 311 (GH #299 reopened by QA after Round 307): the 62vh cap from
     Round 291 was never a raw overflow bug -- a fresh headless check at
     QA's own reported viewport (1919x943) found zero page-level scroll and
     a viewport correctly clipped to the cap, in both normal and an extreme
     1:4-portrait-plan case. But `.app-main` (layout.css) scrolls
     *internally*, and 62vh -- combined with the toolbar above and legend
     below -- pushed the legend past `.app-main`'s own visible fold at that
     exact viewport, needing an internal scroll to see it. The ordinary
     Dashboard fits fully on one screen with no scroll at the same
     viewport, so the Tracking Map page visibly looked like it was
     "occupying more space than intended" relative to the rest of the app,
     even though nothing was clipped or overflowing at the document level.
     That is almost certainly what QA's repeated, non-technical "map is too
     big / doesn't match the standard dashboard layout" reports were
     describing. Dropping the cap from 62vh to 50vh (measured against the
     toolbar+legend chrome height at 1919x943) restores a full,
     scroll-free view of toolbar + map + legend together at that viewport --
     matching how every other dashboard page already behaves there -- while
     still giving the map the clearly-dominant, map-first size the page is
     for. (At smaller/shorter viewports, e.g. 1366x768, the Dashboard itself
     already needs a little scroll to show its lowest row of cards; the Map
     page needing the same there is parity, not a regression.)

     Round 425 (GH #525): that "needs an internal scroll" call was wrong in
     practice -- a flat 50vh takes no account of the toolbar/title chrome
     ABOVE the viewport, so on shorter viewports the map+legend can outgrow
     `.app-main`'s visible fold by more than a "little scroll": measured live
     with a floor plan loaded at 1366x768, the legend rendered a full ~88px
     past `.app-main`'s bottom edge, with no visible scrollbar affordance
     hinting it was still there -- reading exactly as QA's "legend is
     clipped / cut off / overlaps the footer" report, not a parity case.
     `min()` against a `calc(100vh - 500px)` budget (toolbar + title chrome +
     legend + `.app-main` padding, measured at 1366x768/1920x1080) only
     shrinks the map on viewports short enough to actually need it -- at
     1920x1080 and above the budget already exceeds 50vh, so the cap is
     unchanged there; a `max()` floor keeps the map from vanishing on very
     short/landscape-mobile heights. */
  max-height: max(200px, min(50vh, calc(100vh - 500px)));
}
.rtm-viewport.is-zoomed { cursor: grab; }
.rtm-viewport.is-panning { cursor: grabbing; }
.rtm-viewport.is-drawing { cursor: crosshair; }
.rtm-stage { transition: transform .08s ease-out; }
.rtm-viewport.is-panning .rtm-stage { transition: none; }

/* Round 189: wall-drawing layer — the in-progress rubber-band + pending walls +
   clicked-point pins. */
.rtm-draw-wall { stroke: #f59e0b; stroke-width: 5; stroke-linecap: round; opacity: .95; }
.rtm-draw-pending { stroke: #ef4444; stroke-width: 5; stroke-linecap: round; opacity: .7; stroke-dasharray: 8 5; }
.rtm-draw-pin { fill: #f59e0b; stroke: #fff; stroke-width: 2; }

/* Round 191: polygon geofence zones — translucent fills; keep_out ("Restricted",
   exclusion) red, keep_in ("Allowed", containment) blue. The in-progress draw
   polygon is dashed.
   Round 294 (GH #282): no_go ("No-Go", elevated-severity exclusion) gets its
   own violet treatment, distinct from both -- same alert-on-enter rule as
   keep_out but styled to stand out as the higher-severity zone kind. */
.rtm-gf-keep_out { fill: rgba(239,68,68,.16); stroke: #ef4444; stroke-width: 2.5; }
.rtm-gf-keep_in  { fill: rgba(59,130,246,.14); stroke: #3b82f6; stroke-width: 2.5; }
.rtm-gf-no_go    { fill: rgba(168,85,247,.18); stroke: #a855f7; stroke-width: 2.5; }
.rtm-gf-label { fill: var(--text-1); font-size: 13px; font-weight: 600; paint-order: stroke; stroke: rgba(10,17,32,.85); stroke-width: 3px; }
.rtm-gf-draw { fill: rgba(245,158,11,.12); stroke: #f59e0b; stroke-width: 2.5; stroke-dasharray: 7 5; }

/* Round 194: manage saved walls / geofence zones — a chip list with per-item
   delete, so an operator can remove a specific saved item (not just Undo the
   ones added this session). */
.rtm-item-list { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.rtm-item-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 2px 4px 2px 8px;
  font-size: 11px; line-height: 1.6; color: var(--text-1);
  background: rgba(255,255,255,.06); border: 1px solid var(--border-2); border-radius: 12px;
}
.rtm-item-chip.rtm-item-keepout { border-color: #ef4444; }
.rtm-item-chip.rtm-item-keepin  { border-color: #3b82f6; }
.rtm-item-chip.rtm-item-nogo    { border-color: #a855f7; }
.rtm-item-x {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; padding: 0; border: none; border-radius: 50%;
  font-size: 11px; line-height: 1; cursor: pointer;
  background: rgba(239,68,68,.85); color: #fff;
}
.rtm-item-x:hover { background: #ef4444; }
/* A tag currently breaching a geofence zone pulses amber. */
.rtm-tag.is-geofence .rtm-tag-dot {
  box-shadow: 0 0 0 3px rgba(245,158,11,.9), 0 0 10px rgba(245,158,11,.9);
  animation: rtm-pulse 1s ease-in-out infinite;
}
.rtm-key-gf { background: rgba(239,68,68,.35); border: 1px solid #ef4444; border-radius: 2px; }
/* Round 287 (GH #284): fill the full stage width (was inline-style
   max-width:100%, which shrink-wrapped to the floor plan's intrinsic pixel
   size instead of scaling up to use the available page space). height:auto
   preserves aspect ratio. Fullscreen overrides this back to width:auto +
   max-height below, so the plan is height-constrained instead there. */
.rtm-stage img { display: block; width: 100%; height: auto; border-radius: 10px; }
.rtm-grid-layer, .rtm-overlay {
  position: absolute; inset: 0; pointer-events: none;
}
.rtm-grid-svg { width: 100%; height: 100%; display: block; }
.rtm-grid-minor { stroke: rgba(6,182,212,0.14); stroke-width: 1; }
.rtm-grid-major { stroke: rgba(6,182,212,0.30); stroke-width: 1.25; }

/* Round 187: walls / no-go barriers — thick amber-red segments the travel
   path routes around instead of crossing. */
.rtm-wall {
  stroke: #ef4444; stroke-width: 5; stroke-linecap: round;
  opacity: 0.85; filter: drop-shadow(0 0 2px rgba(0,0,0,.6));
}

/* Gateway anchor markers */
.rtm-gw { position: absolute; transform: translate(-50%,-50%); text-align: center; }
.rtm-gw-dot {
  display: block; width: 14px; height: 14px; margin: 0 auto;
  background: var(--accent-amber); border: 2px solid #fff; border-radius: 3px;
  transform: rotate(45deg); box-shadow: 0 0 6px rgba(0,0,0,.55);
}
.rtm-gw.is-offline .rtm-gw-dot { background: var(--text-4); }
.rtm-gw-label {
  display: inline-block; margin-top: 3px; padding: 1px 5px; font-size: 10px;
  line-height: 1.4; color: var(--text-1); white-space: nowrap;
  background: rgba(10,17,32,0.78); border-radius: 4px;
}

/* Live tag markers — CSS transition gives the "gliding" motion between polls */
.rtm-tag {
  position: absolute; transform: translate(-50%,-50%); text-align: center;
  transition: left .6s ease, top .6s ease;
}
.rtm-tag-dot {
  display: block; width: 12px; height: 12px; margin: 0 auto;
  background: var(--accent-cyan); border: 2px solid #fff; border-radius: 50%;
  box-shadow: 0 0 6px rgba(6,182,212,.7);
}
.rtm-tag.is-alert .rtm-tag-dot {
  background: var(--accent-red); box-shadow: 0 0 8px rgba(239,68,68,.85);
  animation: rtm-pulse 1.2s ease-in-out infinite;
}
.rtm-tag-label {
  display: inline-block; margin-top: 3px; padding: 1px 5px; font-size: 10px;
  line-height: 1.4; color: var(--text-1); white-space: nowrap;
  background: rgba(10,17,32,0.78); border-radius: 4px;
}

/* Round 185: markers accept hover so the rich popover ("mouse over data menu")
   and browser tooltip work even though the overlay layer ignores pointer events. */
.rtm-tag, .rtm-gw { pointer-events: auto; cursor: default; }

/* Per-tag icon glyph centred on the marker dot for at-a-glance identification. */
.rtm-tag-icon {
  position: absolute; left: 50%; top: 0; transform: translate(-50%,-50%);
  font-size: 15px; line-height: 1; pointer-events: none;
  text-shadow: 0 0 3px rgba(0,0,0,.95), 0 0 3px rgba(0,0,0,.95);
}

/* Rich hover popover with tag + tracking details. Positioned above the marker. */
.rtm-popover {
  position: absolute; z-index: 40; min-width: 180px; max-width: 260px;
  padding: 8px 10px; font-size: 12px; line-height: 1.5; color: var(--text-1);
  background: rgba(10,17,32,0.97); border: 1px solid var(--border-2);
  border-radius: 8px; box-shadow: 0 6px 20px rgba(0,0,0,.5);
  pointer-events: none; transform: translate(-50%, calc(-100% - 16px));
  white-space: normal;
}
.rtm-popover-title { display: flex; align-items: center; gap: 6px; font-weight: 600; margin-bottom: 5px; }
.rtm-popover-swatch { width: 11px; height: 11px; border-radius: 50%; border: 1px solid rgba(255,255,255,.6); flex: 0 0 auto; }
.rtm-popover-row { display: flex; justify-content: space-between; gap: 14px; color: var(--text-3); }
.rtm-popover-row b { color: var(--text-1); font-weight: 600; }

.rtm-legend {
  display: flex; flex-wrap: wrap; gap: 16px; margin: 10px 0 14px;
  font-size: 12px; color: var(--text-3);
}
.rtm-legend > span { display: inline-flex; align-items: center; gap: 6px; }
.rtm-key { display: inline-block; width: 12px; height: 12px; border: 2px solid #fff; }
.rtm-key-gw  { background: var(--accent-amber); border-radius: 3px; transform: rotate(45deg); }
.rtm-key-tag { background: var(--accent-cyan); border-radius: 50%; }
.rtm-key-alert { background: var(--accent-red); border-radius: 50%; }
.rtm-key-wall { background: #ef4444; border-radius: 1px; width: 16px; height: 4px; border: none; }

/* Fullscreen: fill the screen, centre the map, let it grow */
.rtm-fs:fullscreen {
  background: var(--bg-0); padding: 16px; overflow: auto;
  display: flex; flex-direction: column;
}
.rtm-fs:fullscreen .rtm-viewport {
  margin: auto; max-width: 100%; max-height: calc(100vh - 140px);
}
.rtm-fs:fullscreen .rtm-stage { margin: auto; max-width: 100%; max-height: calc(100vh - 140px); }
.rtm-fs:fullscreen .rtm-stage img { max-height: calc(100vh - 140px); width: auto; }

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

/* ===== RTLS Tracking Map — History mode + playback (pages/rtls_map.js) ==== */
.rtm-seg { display: inline-flex; gap: 0; }
.rtm-seg .btn { border-radius: 0; }
.rtm-seg .btn:first-child { border-top-left-radius: 8px; border-bottom-left-radius: 8px; }
.rtm-seg .btn:last-child { border-top-right-radius: 8px; border-bottom-right-radius: 8px; }

.rtm-hist-row {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: 10px;
  margin: 0 0 12px; padding: 10px 12px;
  background: var(--bg-2); border: 1px solid var(--border-1); border-radius: 10px;
}
.rtm-tags {
  display: flex; flex-wrap: wrap; gap: 6px 12px; max-width: 480px;
  max-height: 74px; overflow-y: auto; padding: 4px 2px;
}
.rtm-tagck { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; color: var(--text-2); }
.rtm-tagck input { accent-color: var(--accent-cyan); }

.rtm-path { pointer-events: none; }

.rtm-playbar {
  display: flex; align-items: center; gap: 12px; margin-top: 10px;
  padding: 10px 14px; background: var(--bg-2);
  border: 1px solid var(--border-1); border-radius: 10px;
}
.rtm-scrub { flex: 1 1 auto; accent-color: var(--accent-cyan); cursor: pointer; }
.rtm-clock {
  font-variant-numeric: tabular-nums; font-size: 12px; color: var(--text-2);
  white-space: nowrap; min-width: 180px;
}
.rtm-fs:fullscreen .rtm-playbar { margin-top: auto; }

/* ---- Round 206: Roles & Permissions reference (Users page) --------------- */
.perm-ref {
  margin-top: 1.2rem; border: 1px solid var(--border, rgba(148,163,184,0.25));
  border-radius: 10px; padding: 0.75rem 1rem; background: rgba(148,163,184,0.04);
}
.perm-ref > summary {
  cursor: pointer; font-weight: 600; list-style: revert;
  color: var(--text, #e2e8f0); user-select: none;
}
.perm-ref > summary:hover { color: var(--accent-cyan, #67e8f9); }
.perm-roles {
  /* GH #287: was a bare auto-fill grid with no rhythm between cards of very
     different heights (a system role with 12 permission chips next to a
     fresh custom role with none) — align items to the grid row's top edge so
     ragged card heights don't read as "unorganized", and widen the minimum
     so a role name + tags never wraps awkwardly.

     Round 425 (GH #523): switched from a multi-column card grid to a single
     full-width column. The old flat perm-chip-row fit fine in a narrow
     ~280px card; the new module x action permission matrix (7 action
     columns) does not -- in the grid layout its table was silently clamped
     to the card's column width with no visible scrollbar affordance,
     cutting columns off exactly like GH #525's map legend. A full-width
     stack gives every role's matrix the room a table actually needs; each
     role's card keeps its own internal `.perm-matrix-wrap` horizontal
     scroll as a fallback for narrow/mobile viewports. */
  display: flex; flex-direction: column; gap: 1rem; margin-bottom: 1rem;
}
.perm-role-card {
  border: 1px solid var(--border, rgba(148,163,184,0.25)); border-radius: 10px;
  padding: 0.85rem 1rem; background: rgba(15,23,42,0.35);
  display: flex; flex-direction: column; gap: 0.55rem;
  transition: border-color var(--t-fast, .15s) ease, box-shadow var(--t-fast, .15s) ease;
}
.perm-role-card:hover {
  border-color: rgba(99,102,241,0.4);
  box-shadow: 0 2px 10px rgba(15,23,42,0.25);
}
.perm-role-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.perm-role-head .role-badge { font-size: 0.85rem; padding: 0.2rem 0.6rem; }
.perm-role-head .muted.small { margin-left: auto; }
.perm-sys-tag {
  font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em;
  padding: 0.05rem 0.35rem; border-radius: 4px; color: #94a3b8;
  border: 1px solid rgba(148,163,184,0.35);
}
.perm-role-desc { margin: 0; line-height: 1.4; }

/* ---- Round 425 (GH #523): module-wise permission matrix per role card,
   replacing the old flat perm-chip-row tag list. ---- */
.perm-matrix-wrap { overflow-x: auto; }
.perm-role-matrix { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
.perm-role-matrix th, .perm-role-matrix td {
  padding: 0.3rem 0.55rem; text-align: center; border: 1px solid var(--border, rgba(148,163,184,0.18));
  white-space: nowrap;
}
.perm-role-matrix thead th {
  color: var(--text-3); font-weight: 600; font-size: 0.7rem;
  text-transform: uppercase; letter-spacing: 0.04em; background: rgba(148,163,184,0.06);
}
.perm-matrix-module {
  text-align: left; font-weight: 600; color: var(--text-1); white-space: normal;
}
.perm-matrix-cell { color: var(--text-3); }
.perm-matrix-cell.perm-matrix-yes { color: var(--accent-green, #34d399); font-weight: 700; background: rgba(16,185,129,0.08); }
.perm-matrix-na { color: var(--text-4, #475569); }
.perm-cat-title { margin: 0.6rem 0 0.5rem; font-size: 0.95rem; }
.perm-cat-wrap { overflow-x: auto; }
#permCatalog td.mono, #permCatalog .mono {
  font-family: var(--mono, ui-monospace, monospace); font-size: 0.8rem; white-space: nowrap;
}

/* GH #222: dynamic role management additions. */
.perm-custom-tag { color: #6ee7b7; border-color: rgba(16,185,129,0.35); }
.perm-disabled-tag { color: #fca5a5; border-color: rgba(220,38,38,0.35); }
.perm-role-card.perm-role-disabled { opacity: 0.6; }
.perm-role-actions { gap: 0.4rem; }

/* ---- GH #286: New Role form — permission grid + field hierarchy --------- */
.perm-matrix {
  display: flex; flex-direction: column; gap: 0.9rem;
  max-height: 340px; overflow-y: auto; padding: 0.9rem;
  border: 1px solid var(--border, rgba(148,163,184,0.25)); border-radius: 10px;
  background: rgba(15,23,42,0.25);
}
.perm-matrix-group {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 0.5rem 0.7rem;
}
.perm-matrix-group + .perm-matrix-group {
  padding-top: 0.9rem; border-top: 1px solid var(--border, rgba(148,163,184,0.15));
}
.perm-matrix-group-title {
  font-size: 0.72rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--accent-cyan, #67e8f9); margin-bottom: 0.15rem; grid-column: 1 / -1;
}
.perm-matrix-item {
  display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem;
  padding: 0.3rem 0.5rem; border-radius: 6px;
  border: 1px solid transparent; transition: background var(--t-fast, .15s) ease;
}
.perm-matrix-item:hover { background: rgba(148,163,184,0.08); }
.perm-matrix-item:has(input:checked) {
  background: rgba(99,102,241,0.12); border-color: rgba(99,102,241,0.3);
}

/* ---- GH #287: individual user permission overrides modal ---------------- */
.user-perm-matrix {
  display: flex; flex-direction: column; gap: 1rem;
  max-height: 55vh; overflow-y: auto; padding: 0.2rem 0.1rem;
}
.user-perm-group { display: flex; flex-direction: column; gap: 0.4rem; }
.user-perm-group + .user-perm-group {
  padding-top: 0.9rem; border-top: 1px solid var(--border, rgba(148,163,184,0.15));
}
.user-perm-row {
  display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.4rem 0.6rem; border-radius: 8px;
}
.user-perm-row:hover { background: rgba(148,163,184,0.06); }
.user-perm-info { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.user-perm-name { font-size: 0.88rem; font-weight: 600; text-transform: capitalize; }
.user-perm-role-hint { font-size: 0.72rem; color: #94a3b8; }
.user-perm-role-hint.is-granted { color: #6ee7b7; }
.seg-toggle {
  display: inline-flex; flex-shrink: 0; border: 1px solid var(--border, rgba(148,163,184,0.3));
  border-radius: 7px; overflow: hidden;
}
.seg-btn {
  border: none; background: transparent; color: var(--text-muted, #94a3b8);
  font-size: 0.75rem; padding: 0.32rem 0.65rem; cursor: pointer;
  border-right: 1px solid var(--border, rgba(148,163,184,0.3));
  transition: background var(--t-fast, .15s) ease, color var(--t-fast, .15s) ease;
}
.seg-btn:last-child { border-right: none; }
.seg-btn:hover { background: rgba(148,163,184,0.1); }
.seg-btn.active { color: #fff; background: rgba(99,102,241,0.55); }
.seg-btn.seg-grant.active { background: rgba(16,185,129,0.55); }
.seg-btn.seg-deny.active { background: rgba(220,38,38,0.55); }
