:root {
  --bg: #0f1115;
  --panel: #171a21;
  --panel-border: #262b36;
  --text: #e6e9ef;
  --text-dim: #8b93a3;
  --accent: #4f8cff;
  --green: #35d07f;
  --amber: #f5b942;
  --gray: #5a6172;
  --red: #ef5350;
  --radius: 10px;
}

@media (prefers-color-scheme: light) {
  :root {
    --bg: #f4f5f7;
    --panel: #ffffff;
    --panel-border: #dde1e8;
    --text: #1a1d24;
    --text-dim: #666e7d;
  }
}

/* Manual theme toggle (common.js) overrides the OS preference above in
   either direction once the user picks a side explicitly. */
:root[data-theme="dark"] {
  --bg: #0f1115;
  --panel: #171a21;
  --panel-border: #262b36;
  --text: #e6e9ef;
  --text-dim: #8b93a3;
}
:root[data-theme="light"] {
  --bg: #f4f5f7;
  --panel: #ffffff;
  --panel-border: #dde1e8;
  --text: #1a1d24;
  --text-dim: #666e7d;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
}

/* Plain content links (repo URLs, device/hardware cross-links, etc.) had no
   color rule anywhere, so they fell back to the browser's default link blue
   (~#0000EE) - low contrast and hard to read against the dark theme's
   near-black background. --accent (#4f8cff) is already tuned for both
   themes elsewhere on this page (badges, borders), so reuse it here too.
   More specific rules (header a, a.btn-nav, .linklike, ...) already set
   their own color and win over this on specificity. */
a { color: var(--accent); }
a:visited { color: var(--accent); }

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

/* Shared top navigation (injected by renderNav in common.js on every page) -
   one consistent bar so you always know where you are and where you can go. */
.topnav {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 12px 24px;
  border-bottom: 1px solid var(--panel-border);
  flex-wrap: wrap;
}
.topnav .brand {
  font-weight: 700;
  font-size: 15px;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}
.topnav .navlinks { display: flex; gap: 4px; flex: 1; flex-wrap: wrap; }
.topnav .navlink {
  padding: 6px 12px;
  border-radius: 8px;
  font-size: 14px;
  color: var(--text-dim);
  text-decoration: none;
  border: 1px solid transparent;
}
.topnav .navlink:hover { color: var(--text); background: var(--panel); }
.topnav .navlink.active {
  color: var(--text);
  background: var(--panel);
  border-color: var(--panel-border);
  font-weight: 600;
}
.topnav .navstatus { display: flex; align-items: center; gap: 14px; }

.page-title { font-size: 20px; font-weight: 700; margin: 0 0 16px; }
/* The hub module map's visible half: which named module this screen belongs
   to (injected by hubKicker in common.js — never hand-written per page). */
.hub-kicker { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-dim); margin: 0 0 2px; }

/* Path input + Browse button, and the folder-picker modal it opens. */
.path-row { display: flex; gap: 8px; align-items: center; }
.path-row .form-input { flex: 1; }
.path-row .cmd-btn { white-space: nowrap; }
.dirpick-overlay {
  position: fixed; inset: 0; z-index: 200;
  background: rgba(0, 0, 0, 0.5);
  display: flex; align-items: center; justify-content: center; padding: 20px;
}
.dirpick {
  width: min(560px, 100%); max-height: 80vh; display: flex; flex-direction: column;
  background: var(--panel); border: 1px solid var(--panel-border); border-radius: 12px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.dirpick-head { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--panel-border); }
.dirpick-path { padding: 8px 16px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--text-dim); word-break: break-all; border-bottom: 1px solid var(--panel-border); }
.dirpick-list { overflow-y: auto; padding: 6px; flex: 1; }
.dirpick-item {
  display: block; width: 100%; text-align: left; background: none; border: none;
  color: var(--text); padding: 8px 10px; border-radius: 8px; cursor: pointer; font-size: 14px;
}
.dirpick-item:hover { background: var(--bg); }
.dirpick-foot { display: flex; gap: 10px; align-items: center; padding: 12px 16px; border-top: 1px solid var(--panel-border); }
.dirpick-newrow { display: flex; gap: 8px; padding: 10px 16px; border-top: 1px solid var(--panel-border); }
/* Long-text popup editor (openTextEditorModal) - reuses the dirpick shell. */
.dirpick.texted { width: min(860px, 94vw); height: min(640px, 88vh); display: flex; flex-direction: column; }
.texted-area { flex: 1; margin: 12px 16px 0; resize: none; font-size: 14px; line-height: 1.5; }
.texted-tabs { display: flex; gap: 6px; margin-left: auto; margin-right: 10px; }
/* The head's cmd-btns render filled - the active tab must invert, not tint
   (accent-on-accent text was invisible, caught in the first screenshot). */
.texted-tabs .cmd-btn { background: transparent; color: var(--text-dim); border: 1px solid var(--panel-border); }
.texted-tabs .cmd-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.texted-render { flex: 1; overflow-y: auto; margin: 12px 16px 0; padding: 4px 8px; }
.texted-render[hidden] { display: none; }
/* Rendered-markdown typography - kept minimal, inherits the app's colors. */
.md-body { font-size: 14px; line-height: 1.55; }
.md-body h1 { font-size: 20px; margin: 14px 0 8px; }
.md-body h2 { font-size: 17px; margin: 14px 0 6px; }
.md-body h3 { font-size: 15px; margin: 12px 0 4px; }
.md-body p { margin: 8px 0; }
.md-body ul, .md-body ol { margin: 8px 0; padding-left: 22px; }
.md-body li { margin: 3px 0; }
.md-body code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; background: rgba(139,147,163,0.14); padding: 1px 5px; border-radius: 4px; }
.md-body pre { background: var(--bg); border: 1px solid var(--panel-border); border-radius: 8px; padding: 10px 12px; overflow-x: auto; }
.md-body pre code { background: none; padding: 0; }
.md-body table { border-collapse: collapse; margin: 10px 0; }
.md-body th, .md-body td { border: 1px solid var(--panel-border); padding: 5px 10px; font-size: 13px; }
.md-body th { background: rgba(139,147,163,0.10); }
.md-body blockquote { border-left: 3px solid var(--panel-border); margin: 8px 0; padding: 2px 12px; color: var(--text-dim); }
/* Natural-size diagrams (useMaxWidth:false) - scroll when wider than the
   modal, zoom-in cursor because clicking opens the lightbox. */
.md-body .mermaid { display: block; text-align: center; margin: 12px 0; overflow-x: auto; cursor: zoom-in; }
.diagram-lightbox {
  position: fixed; inset: 0; z-index: 400;
  background: rgba(10, 12, 16, 0.94);
  display: flex; align-items: center; justify-content: center;
  cursor: zoom-out;
}
.diagram-lightbox svg { display: block; }
/* Inline description preview + open button in the project form. */
.desc-preview-row { display: flex; gap: 8px; align-items: center; }
.desc-preview { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; color: var(--text-dim); border: 1px solid var(--panel-border); border-radius: 8px; padding: 7px 10px; background: var(--bg); }
.desc-preview.empty { font-style: italic; }
.dirpick-newrow[hidden] { display: none; } /* display:flex would otherwise beat the hidden attribute (same class of bug as .card[hidden]) */
.dirpick-newrow .form-input { flex: 1; }

/* Product-photo thumbnails (hardware inventory + device detail). object-fit
   keeps varied vendor images from stretching; a light pad + border frames
   transparent PNGs on either theme. */
.hw-thumb,
.hw-preview {
  object-fit: contain;
  border-radius: 8px;
  background: #fff;
  border: 1px solid var(--panel-border);
  padding: 3px;
  cursor: zoom-in;
  transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.hw-thumb { width: 64px; height: 64px; }
.hw-preview { width: 76px; height: 76px; }

/* Hover to enlarge so you can actually inspect the item. In-place scale is
   only safe for a LONE image (device detail card): a scaled image sits on
   top of its neighbors and steals their mouse events, which is exactly what
   broke the multi-photo gallery (Peter, 2026-07-27). Gallery previews
   (.hw-preview) instead use hardware.html's floating #imgPreview box -
   pointer-events:none, viewport-clamped - plus click-to-fullscreen. */
.hw-thumb:hover {
  transform: scale(4);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  border-color: var(--accent);
  position: relative;
  z-index: 60;
  transform-origin: top right;
}
.hw-preview:hover { border-color: var(--accent); }

header h1 {
  font-size: 18px;
  margin: 0;
  font-weight: 600;
}

header a {
  color: var(--text);
  text-decoration: none;
}

.conn-indicator {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-dim);
}

.dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--gray);
  flex-shrink: 0;
}
.dot.live { background: var(--green); }
.dot.stale { background: var(--amber); }
.dot.never { background: var(--gray); }
.dot.offline { background: var(--red); }

main {
  max-width: 720px;
  margin: 0 auto;
  padding: 24px;
}
/* .wide is for pages that are wider than reading-width but aren't a raw
   data table - dashboard cards (index.html), a project's detail/edit form
   (project.html). 980px reads fine there; going further just stretches
   single-line text inputs across the whole window for no benefit. */
main.wide { max-width: 980px; }
/* .table-wide is for the actual list/table pages (devices, projects,
   hardware, purchases, log) - these should grow with the browser instead of
   being stuck at a fixed width regardless of window size. A soft ceiling
   keeps rows from stretching absurdly thin-and-huge on an ultrawide/4K
   display; drop it entirely if that turns out not to matter in practice. */
main.table-wide { max-width: 1800px; }

/* A page that fills the window. For the Overview, which Peter runs on an
   office monitor to see the state of everything at a glance — a 980px column
   in the middle of a 32-inch screen wastes the entire point of having it
   there. Padding rather than a max-width, so it scales with whatever it is
   shown on. */
main.full { max-width: none; padding-left: clamp(16px, 2vw, 40px); padding-right: clamp(16px, 2vw, 40px); }

/* Wall mode: the nav and chrome go away, type scales up, and nothing
   scrolls. Toggled per browser and remembered, because the office display
   wants it permanently while a laptop does not. */
body.wall #topnav { display: none; }
body.wall main.full { padding-top: 14px; }
body.wall .page-title { font-size: 22px; }
body.wall { overflow: hidden; }              /* glanceable means no scrolling */
body.wall .kpi .n { font-size: clamp(30px, 4.2vw, 68px); }
body.wall .kpi .k { font-size: clamp(11px, 0.9vw, 16px); }
body.wall .ov-panel h2 { font-size: clamp(14px, 1.1vw, 20px); }
body.wall .site-row, body.wall .attn-row { font-size: clamp(12px, 0.95vw, 18px); }

/* Fill the HEIGHT too. Fitting the window is only half of it — a dashboard
   that occupies the top third of a 32-inch monitor and leaves the rest black
   wastes exactly the space it was put there to use. The panels grow, and the
   row cap below is computed from what actually fits. */
body.wall main.full { height: 100vh; display: flex; flex-direction: column; box-sizing: border-box; }
body.wall .ov-cols { flex: 1 1 auto; min-height: 0; }
body.wall .ov-panel { display: flex; flex-direction: column; min-height: 0; }
body.wall .ov-panel .attn { flex: 1 1 auto; min-height: 0; }
body.wall .tiles { flex: 0 0 auto; }

/* Touch: anything tappable gets a real target. 44px is the size a fingertip
   actually hits reliably; a 22px icon button on a wall display is a game. */
@media (pointer: coarse), (min-width: 1600px) {
  .cmd-btn, .btn-save, .icon-btn, .tile, .segmented button, .view-toggle button {
    min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
  }
  .tile { min-height: 92px; }
}

.card {
  display: block;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin-bottom: 12px;
  text-decoration: none;
  color: var(--text);
  transition: border-color 0.15s ease;
}
.card:hover { border-color: var(--accent); }

.group-section { margin-bottom: 8px; }

.group-header {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  margin: 20px 0 8px;
}
.group-section:first-child .group-header { margin-top: 0; }

.card-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.card-title {
  font-size: 15px;
  font-weight: 600;
}

.card-meta {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 4px;
}

.status-label {
  font-size: 12px;
  color: var(--text-dim);
  display: flex;
  align-items: center;
  gap: 6px;
}

.empty-state {
  text-align: center;
  color: var(--text-dim);
  padding: 48px 24px;
  border: 1px dashed var(--panel-border);
  border-radius: var(--radius);
}

.back-link {
  display: inline-block;
  margin-bottom: 16px;
  color: var(--text-dim);
  text-decoration: none;
  font-size: 13px;
}
.back-link:hover { color: var(--text); }

.panel-header {
  margin-bottom: 20px;
}

.panel-header h2 {
  margin: 0 0 4px 0;
  font-size: 20px;
}

.panel-header .source-path {
  font-size: 12px;
  color: var(--text-dim);
  font-family: ui-monospace, SFMono-Regular, monospace;
}

.field-list {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  overflow: hidden;
  margin-bottom: 20px;
}

.field-row {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 16px;
  border-bottom: 1px solid var(--panel-border);
  font-size: 14px;
}
.field-row:last-child { border-bottom: none; }

.field-key {
  color: var(--text-dim);
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 13px;
}

.field-value {
  font-weight: 500;
  text-align: right;
  word-break: break-word;
}

.pill {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
}
.pill.on { background: rgba(53, 208, 127, 0.15); color: var(--green); }
.pill.off { background: rgba(90, 97, 114, 0.2); color: var(--text-dim); }
/* Fields like fault/error/alarm are inverted - their true state is bad and
   should read as alarming, not the same reassuring green as e.g. "Device
   Powered: ON" (see renderFieldValue's NEGATIVE_BOOL_FIELDS). */
.pill.bad { background: rgba(239, 83, 80, 0.15); color: var(--red); }
.pill.warn { background: rgba(245, 185, 66, 0.15); color: var(--amber); }

.stage-pill {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  background: rgba(79, 140, 255, 0.15);
  color: var(--accent);
  margin-left: 4px;
}

/* Track: which world the project belongs to (Home proving ground vs CRINT
   outside world) - same pill shape as stage, its own colors. */
.track-badge {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  margin-left: 4px;
  white-space: nowrap;
}
.track-badge.track-home { background: rgba(53, 208, 127, 0.15); color: var(--green); }
.track-badge.track-home-to-crint { background: rgba(245, 185, 66, 0.15); color: var(--amber); }
.track-badge.track-crint { background: rgba(79, 140, 255, 0.15); color: var(--accent); }
.track-badge.track-shared { background: rgba(90, 97, 114, 0.2); color: var(--text-dim); }

/* Kind tabs on the Projects page (All / Hardware / Software / Mixed) */
.kind-tabs { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; }
.kind-tab {
  padding: 6px 14px;
  border-radius: 999px;
  border: 1px solid var(--panel-border);
  background: var(--panel);
  color: var(--text-dim);
  font-size: 13px;
  cursor: pointer;
}
.kind-tab:hover { color: var(--text); }
.kind-tab.active { color: var(--text); border-color: var(--accent); background: rgba(79, 140, 255, 0.12); font-weight: 600; }

.commands {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 20px;
}

button.cmd-btn,
a.cmd-btn {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: var(--radius);
  padding: 10px 18px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
button.cmd-btn:disabled { opacity: 0.5; cursor: default; }
button.cmd-btn:not(:disabled):hover,
a.cmd-btn:hover { filter: brightness(1.1); }

/* Parameterized command: its number inputs + send button travel as one
   visual unit inside the .commands row. */
.cmd-param-group {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.cmd-param {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-dim);
  white-space: nowrap;
}
.cmd-param-input { width: 70px; }

/* Resizable table columns - see makeColumnsResizable() in common.js. Shared
   here (not per-page) since every list page's table gets the same handle. */
th.col-resizable { position: relative; overflow: hidden; text-overflow: ellipsis; }
.col-resize-handle {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 8px;
  cursor: col-resize;
  -webkit-user-select: none;
  user-select: none;
  touch-action: none;
}
.col-resize-handle:hover,
table.col-resizing .col-resize-handle { background: var(--accent); opacity: 0.35; }
table.col-resizing { cursor: col-resize; -webkit-user-select: none; user-select: none; }

/* .linklike - a button styled as an inline text link, for low-weight row
   actions (remove/reopen) that shouldn't compete visually with .cmd-btn/
   .btn-save. Shared here since more than one page's inline row-actions use it. */
.linklike { background: none; border: none; padding: 0; color: var(--accent); cursor: pointer; font: inherit; text-decoration: underline; }

/* ── Button hierarchy ──────────────────────────────────────────
   .cmd-btn (above) is the primary/solid style, reserved for actions with a
   real-world effect: a command sent to a device, or a real network scan.
   .btn-save is for form saves - deliberate but reversible, no device/network
   side effect - lower visual weight than a command but still a clear
   call-to-action. .btn-nav is for safe navigation (opening another page/app)
   - least weight, styled like a bordered link rather than a button. Without
   this distinction every action on a panel looked equally consequential. */
button.btn-save {
  background: transparent;
  color: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: 9px 17px;
  font-size: 14px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}
button.btn-save:hover { background: rgba(79, 140, 255, 0.1); }
button.btn-save:disabled { opacity: 0.5; cursor: default; }

a.btn-nav {
  background: none;
  color: var(--text-dim);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 9px 17px;
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  display: inline-block;
}
a.btn-nav:hover { border-color: var(--accent); color: var(--text); }

.raw-block {
  font-family: ui-monospace, SFMono-Regular, monospace;
  font-size: 12px;
  color: var(--text-dim);
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 12px 16px;
  white-space: pre-wrap;
  word-break: break-all;
}

.last-updated {
  font-size: 12px;
  color: var(--text-dim);
  margin-top: 6px;
}

/* ── Header controls (theme toggle, scan-now) ─────────────────── */
.header-actions {
  display: flex;
  align-items: center;
  gap: 14px;
}

.icon-btn {
  background: none;
  border: 1px solid var(--panel-border);
  border-radius: 8px;
  width: 32px;
  height: 32px;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: center;
}
.icon-btn:hover { border-color: var(--accent); }

/* ── Health summary strip ─────────────────────────────────────── */
.health-summary {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  font-size: 13px;
  color: var(--text-dim);
  padding: 12px 0 4px;
  margin-bottom: 8px;
  border-bottom: 1px solid var(--panel-border);
}
.health-summary strong { color: var(--text); font-weight: 600; }

.scan-now-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 16px;
}
.scan-now-row .last-scanned {
  font-size: 12px;
  color: var(--text-dim);
}

/* ── Card type accents (distinguish MQTT installation vs. discovered
   device vs. a linked/merged card at a glance in the grouped list) ─── */
.card { border-left: 3px solid transparent; }
.card.card-installation { border-left-color: var(--accent); }
.card.card-installation.card-linked { border-left-color: var(--green); }
.card.card-device { border-left-color: var(--gray); }

.card-type-badge {
  display: inline-block;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 1px 6px;
  border-radius: 4px;
  margin-left: 6px;
  vertical-align: middle;
}
.card-type-badge.mqtt { background: rgba(79, 140, 255, 0.15); color: var(--accent); }
.card-type-badge.linked { background: rgba(53, 208, 127, 0.15); color: var(--green); }
.card-type-badge.device { background: rgba(90, 97, 114, 0.2); color: var(--text-dim); }
.card-type-badge.fw-outdated { background: rgba(239, 83, 80, 0.15); color: var(--red); }

/* ── Toasts ────────────────────────────────────────────────────── */
.toast-container {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 1000;
  align-items: center;
  width: 100%;
  pointer-events: none;
}

.toast {
  pointer-events: auto;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  color: var(--text);
  padding: 10px 16px;
  border-radius: 8px;
  font-size: 13px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.2s ease, transform 0.2s ease;
  max-width: 90vw;
  display: flex;
  align-items: center;
  gap: 10px;
}
.toast.toast-in { opacity: 1; transform: translateY(0); }
.toast.toast-error { border-color: var(--red); }
.toast.toast-success { border-color: var(--green); }
.toast.toast-confirm { border-color: var(--amber); }

.toast-cancel-btn {
  background: none;
  border: 1px solid var(--panel-border);
  color: var(--text);
  border-radius: 6px;
  padding: 3px 9px;
  font-size: 12px;
  cursor: pointer;
}
.toast-cancel-btn:hover { border-color: var(--accent); }

/* ── Unsaved-changes indicator (notes/group editors) ──────────── */
.unsaved-indicator {
  font-size: 12px;
  color: var(--amber);
  margin-left: 8px;
}

/* ── Manage-groups panel (index.html, behind the ⚙ button) ────── */
.group-filter-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.filter-arrow { color: var(--text-dim); font-size: 16px; }
.manage-groups-panel {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 16px;
}
.manage-group-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}
.manage-group-row:last-child { margin-bottom: 0; }
.manage-group-row .mg-name { flex: 0 0 200px; max-width: 200px; }
.manage-group-row .mg-notes { flex: 1 1 auto; min-width: 120px; }
.manage-groups-panel > .group-usage {
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 10px;
}
button.btn-danger {
  background: none;
  border: 1px solid var(--panel-border);
  color: var(--red);
  border-radius: var(--radius);
  padding: 7px 13px;
  font-size: 13px;
  font-family: inherit;
  cursor: pointer;
}
button.btn-danger:hover { border-color: var(--red); }
button.btn-danger:disabled { opacity: 0.4; cursor: default; }

/* ── Inline warning (re-purposed board, etc.) ─────────────────── */
.warning-text {
  font-size: 13px;
  color: var(--amber);
  border: 1px solid var(--amber);
  border-radius: var(--radius);
  padding: 8px 12px;
  margin: 8px 0 16px;
}

/* ── Tabs + group dropdown ─────────────────────────────────────── */
.filter-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.tabs {
  display: flex;
  gap: 4px;
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 3px;
}

.tab-btn {
  background: none;
  border: none;
  border-radius: 7px;
  padding: 6px 14px;
  font-size: 13px;
  font-family: inherit;
  font-weight: 500;
  color: var(--text-dim);
  cursor: pointer;
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { background: var(--accent); color: #fff; }

.group-select {
  font-family: inherit;
  font-size: 14px;
  padding: 8px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--panel-border);
  background: var(--panel);
  color: var(--text);
  min-width: 160px;
}
.group-select:focus { outline: none; border-color: var(--accent); }

/* ── Search/filter box ─────────────────────────────────────────── */
.search-row {
  margin-bottom: 16px;
}
.search-row input {
  width: 100%;
  font-family: inherit;
  font-size: 14px;
  padding: 9px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--panel-border);
  background: var(--panel);
  color: var(--text);
}
.search-row input:focus {
  outline: none;
  border-color: var(--accent);
}

/* ── Collapsible group headers ─────────────────────────────────── */
.group-header {
  cursor: pointer;
  -webkit-user-select: none;
  user-select: none;
  display: flex;
  align-items: center;
  gap: 6px;
}
.group-header .collapse-arrow {
  display: inline-block;
  transition: transform 0.15s ease;
  font-size: 10px;
}
.group-section.collapsed .collapse-arrow { transform: rotate(-90deg); }
.group-section.collapsed .card { display: none; }

/* ── Loading skeleton ──────────────────────────────────────────── */
.skeleton-card { pointer-events: none; }
.skeleton-line {
  height: 12px;
  border-radius: 4px;
  margin: 6px 0;
  background: linear-gradient(90deg, var(--panel-border) 25%, var(--bg) 50%, var(--panel-border) 75%);
  background-size: 200% 100%;
  animation: skeleton-shimmer 1.4s ease-in-out infinite;
}
@keyframes skeleton-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* ── WebSocket connection indicator ────────────────────────────── */
.ws-indicator {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--text-dim);
}

/* ── Command history ───────────────────────────────────────────── */
.command-history {
  margin: 4px 0 20px;
  font-size: 12px;
  color: var(--text-dim);
}
.command-history-item {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 4px 0;
  border-bottom: 1px solid var(--panel-border);
}
.command-history-item:last-child { border-bottom: none; }

/* ── Edit form (group/notes/project fields) ─────────────────────
   Wraps the editable fields in a bordered card matching .field-list above
   it, instead of bare label+input pairs floating on the page background -
   these two sections used to look like different UIs on the same page. */
.edit-form {
  background: var(--panel);
  border: 1px solid var(--panel-border);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin-bottom: 20px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.form-field { width: 100%; }
.form-field.narrow { max-width: 280px; }
.form-field.tiny { max-width: 200px; }
/* Grouped form layout (project settings): labeled sections, and rows that
   put related narrow fields side by side instead of one long column. */
.form-section { border-top: 1px solid var(--panel-border); padding-top: 12px; display: flex; flex-direction: column; gap: 12px; }
.form-section:first-child { border-top: none; padding-top: 0; }
.form-section-title { font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-dim); }
.form-row { display: flex; gap: 14px; flex-wrap: wrap; align-items: flex-end; }
.form-row .form-field { flex: 1 1 220px; width: auto; }
.form-row .form-field.narrow { flex: 0 1 240px; }
.form-row .form-field.tiny { flex: 0 1 160px; }
.form-label {
  font-size: 12px;
  color: var(--text-dim);
  display: block;
  margin-bottom: 4px;
}
.form-input,
.form-textarea {
  width: 100%;
  font-family: inherit;
  font-size: 14px;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--panel-border);
  background: var(--bg);
  color: var(--text);
}
.form-input:focus,
.form-textarea:focus { outline: none; border-color: var(--accent); }
.form-textarea { resize: vertical; }
.form-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

/* ── Sparkline (inline, next to a numeric field value) ─────────── */
.sparkline {
  color: var(--accent);
  vertical-align: middle;
  margin-left: 8px;
}

/* "N apps down" in the health strip - a watched program (computer agent)
   not running on an otherwise reachable computer. */
.health-summary .apps-down-alert { color: var(--red); }
.health-summary .apps-down-alert strong { color: var(--red); }

/* Danger confirm button inside a confirm toast (restart/shutdown/flash) -
   the deliberate "yes, do it" action, visually distinct from Cancel. */
.toast-danger-btn {
  background: var(--red);
  color: #fff;
  border: none;
  border-radius: 6px;
  padding: 3px 11px;
  font-size: 12px;
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
}
.toast-danger-btn:hover { filter: brightness(1.1); }

/* A stale IP is shown, not offered: an offline device's address was true at
   last contact and will not load now. Dimmed and unlinked, so it still
   identifies the device without inviting a click that hangs. */
.dev-ip-stale { color: var(--text-dim); }

/* A paused project's device: deliberately quiet, not alarming. It reads as a
   scheduled absence, which is what it is. */
.dev-paused { color: var(--text-dim); }
/* A pause about to lapse. Deliberately quieter than an alert: it is a reminder
   to go and switch the thing back on, and if it is ignored the ordinary offline
   alert takes over by itself the day after. */
.dev-paused-soon { font-size: 11px; opacity: .75; white-space: nowrap; }

/* The device pause control (panel.html). A row rather than stacked fields:
   "off since X until Y" is one sentence and reads as one. */
.pause-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.pause-row .form-input { width: auto; }
.pause-sep { color: var(--text-dim); font-size: 12px; }
.form-hint { color: var(--text-dim); font-size: 12px; margin: 2px 0 6px; line-height: 1.4; }

/* The per-row pause editor (devices.html). A full-width row under the device
   it belongs to, rather than a modal: the row stays visible, so it is obvious
   which device is being paused. */
.pause-editor { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 6px 0; }
.pause-editor .form-input { width: auto; }
.pause-editor-row { background: var(--bg-alt, rgba(127,127,127,.06)); cursor: default; }
.pause-hint { color: var(--text-dim); font-size: 11px; flex: 1 1 220px; line-height: 1.35; }
/* Actions sit side by side. They used to be two inline-blocks in a narrow
   column, which wrapped onto two lines and doubled the height of EVERY row in
   the table the moment a second action existed - a density regression paid by
   rows that were not even paused. */
.dev-actions-row { display: flex; align-items: center; gap: 6px; justify-content: flex-end; flex-wrap: nowrap; }

/* Pausing is a bookkeeping action, not a primary one: it should be reachable
   without competing with the row's own content for attention. Outlined rather
   than filled, and quieter still once a pause is set - at that point the row
   already says "paused" in its status cell, so the button is just the way back
   in. */
/* button.cmd-btn (above) is one specificity point higher than a bare class, so
   these have to match the element+class shape or they silently lose - which is
   exactly what happened on the first attempt: the rule was correct and had no
   effect. nowrap because the icon and label were breaking onto two lines in
   this narrow column, undoing the height saved above. */
button.row-pause-btn { background: transparent; color: var(--text-dim); border: 1px solid var(--panel-border); white-space: nowrap; }
button.row-pause-btn:hover { color: var(--text); }
button.row-pause-btn.on { background: transparent; color: var(--text-dim); border-style: dashed; }
a.row-log-btn { white-space: nowrap; }

/* Clear is the undo, not the commit - Save is the filled one. */
button.pause-clear { background: transparent; color: var(--text-dim); border: 1px solid var(--panel-border); }

/* Origin chips (account pages, both hubs): provenance is a first-class
   column, never a tooltip — user-management.md. Filled = this record is
   central's; outline+glyph = it belongs to a venue. Same vocabulary on
   both hubs so an operator who has seen one page has seen both. */
.origin-chip { display: inline-block; font-size: 10px; font-weight: 600; letter-spacing: .4px;
  padding: 1px 7px; border-radius: 9px; vertical-align: 1px; }
.origin-chip.central { background: var(--accent); color: #fff; }
.origin-chip.site { border: 1px solid var(--accent); color: var(--accent); background: transparent; }
.sync-toggle { font-size: 11px; color: var(--text-dim); margin-left: 8px; cursor: pointer; }
.sync-toggle input { vertical-align: -2px; }
.venue-users-head { font-size: 13px; margin: 14px 0 4px; }
/* Venue-local clocks (worldwide fleet): quiet, monospace-adjacent, with a
   moon during the venue's night hours. */
.site-clock { font-size: 11px; color: var(--text-dim); white-space: nowrap; margin-left: 6px; cursor: pointer; }
.site-clock:hover, .hub-clock:hover { color: var(--text); }
.hub-clock { cursor: pointer; }
.site-clock-unset { opacity: .55; }

/* Time-zone popover: hover = where does this clock come from, click = the
   automatic/manual editor. One shared element, positioned at the chip. */
.tz-popover { position: absolute; z-index: 300; max-width: 320px; background: var(--panel);
  border: 1px solid var(--panel-border); border-radius: var(--radius); padding: 10px 12px;
  font-size: 12px; color: var(--text); box-shadow: 0 6px 24px rgba(0,0,0,.35); }
.tz-pop-title { font-weight: 650; font-size: 12.5px; margin-bottom: 6px; }
.tz-pop-line { margin: 4px 0; line-height: 1.45; }
.tz-pop-warn { margin: 6px 0; line-height: 1.45; color: var(--amber); }
.tz-pop-hint { margin-top: 6px; font-size: 11px; color: var(--text-dim); line-height: 1.4; }
.tz-mode { display: block; margin: 4px 0; cursor: pointer; }
.tz-mode input { vertical-align: -2px; margin-right: 4px; }
.tz-dim { color: var(--text-dim); }
.tz-manual { margin: 6px 0 2px 20px; }
.tz-manual select { width: 100%; max-width: 260px; font-size: 12px; background: var(--bg);
  color: var(--text); border: 1px solid var(--panel-border); border-radius: 6px; padding: 4px 6px; }
.tz-dst { display: block; margin-top: 6px; font-size: 11.5px; cursor: pointer; }
.tz-dst input { vertical-align: -2px; margin-right: 4px; }
.tz-actions { margin-top: 8px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tz-err { font-size: 11px; color: var(--red); }
.site-clock-warn { color: var(--amber); }
/* The device page while its machine is mid-update: banner + greyed acts. */
.updating-banner { border-left: 3px solid var(--amber); background: rgba(245,185,66,.09);
  border-radius: 0 10px 10px 0; padding: 10px 14px; margin: 10px 0; font-size: 13px;
  animation: updpulse 1.6s ease-in-out infinite; }
@keyframes updpulse { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
.cmds-locked { opacity: .45; pointer-events: none; }
.tz-fix-armed { background: var(--amber); color: #1a1d24; }
.tz-fix-ok { color: var(--green); }
/* Brand block, 2026-08-26 layout pass: two lines behind a colored role
   edge. Line 1 = logo + one-word state chip; line 2 = the hub's clock
   with host · build as a dim tail. The edge repeats the chip's role in a
   form legible from across the room — blue active central, DASHED amber
   standby (the state that must be impossible to miss), gray venue. */
.brand-block { display: flex; flex-direction: column; gap: 3px; min-width: 0;
  border-left: 3px solid transparent; padding-left: 9px; }
.brand-block.role-active { border-left-color: var(--accent); }
.brand-block.role-standby { border-left-color: #d69e2e; border-left-style: dashed; }
.brand-block.role-site { border-left-color: var(--gray); }
.brand-line { display: flex; align-items: center; gap: 8px; white-space: nowrap; }
.brand-sub { line-height: 1; white-space: nowrap; }
.hub-version { color: inherit; letter-spacing: .3px; }
.hub-clock { font-size: 11px; color: var(--text); font-variant-numeric: tabular-nums; }
.hub-clock .hc-tail { font-size: 9.5px; color: var(--text-dim); }
.origin-chip.nav-hub { font-size: 9px; letter-spacing: .6px; vertical-align: middle; }
/* STANDBY: unmistakably not the writing hub — amber outline, no fill. */
.origin-chip.standby { border: 1.5px dashed #d69e2e; color: #d69e2e; background: transparent; }

/* The card view's pause action sits among text links, so it wears their
   weight rather than a button's - the card already has enough boxes. */
button.card-pause-btn { border: none; background: transparent; padding: 0; font: inherit; cursor: pointer; color: var(--accent); }
button.card-pause-btn:hover { text-decoration: underline; }
button.card-pause-btn.on { color: var(--text-dim); }
.card-pause-editor { grid-column: 1 / -1; padding: 8px 12px 12px; border-top: 1px solid var(--panel-border); }

/* A paused project is parked, not gone: dimmed enough to skip while scanning,
   still fully readable and still draggable. */
.board-card.is-paused { opacity: 0.55; }
.board-card.is-paused:hover { opacity: 1; }
.paused-badge {
  font-size: 11px; color: var(--text-dim);
  border: 1px solid var(--panel-border); border-radius: 999px; padding: 1px 7px;
}

/* Nav rearrange mode. Deliberately unobtrusive until entered: the handle is a
   dim glyph at the end of the bar, and only in edit mode do the tabs look
   grabbable. */
.nav-edit {
  background: none; border: 1px solid transparent; color: var(--text-dim);
  border-radius: 7px; cursor: pointer; font-size: 13px; padding: 4px 8px; margin-left: 2px;
}
.nav-edit:hover { color: var(--text); border-color: var(--panel-border); }
.nav-edit.on { color: var(--accent); border-color: var(--accent); }
.navlinks.nav-editing .navlink {
  cursor: grab; border: 1px dashed var(--panel-border); border-radius: 7px;
}
.navlinks.nav-editing .navlink.active { border-style: solid; }
.navlink.nav-dragging { opacity: 0.4; }

/* Overview location picker. Inline in the banner so setting your location is
   where you read it, not buried in a settings page. */
.ov-sitepick {
  background: var(--panel); color: var(--text); border: 1px solid var(--panel-border);
  border-radius: 7px; padding: 3px 8px; font-size: 13px; margin-left: 6px;
}
.ov-setby { color: var(--text-dim); font-size: 12px; }
/* The override and the evidence disagreeing: shown, never silently resolved. */
.ov-conflict { color: var(--amber, #d29922); font-size: 13px; }
.ov-banner-unknown { border-color: rgba(210,153,34,0.45); }

/* Where a unit is physically installed — the answer to "which of these five
   identical boards am I looking at". */
.dev-where { color: var(--accent, #58a6ff); }

/* Breadcrumbs — see breadcrumbHtml() in common.js. Small and quiet: they are
   orientation, not navigation furniture competing with the page title. */
.crumbs { font-size: 12px; margin: 0 0 6px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.crumbs a { color: var(--text-dim); text-decoration: none; }
.crumbs a:hover { color: var(--accent); text-decoration: underline; }
.crumb-sep { color: var(--text-dim); opacity: .55; }
.crumb-here { color: var(--text-dim); }

/* Utility classes replacing static inline styles (webhint no-inline-styles).
   The JS-generated inline styles elsewhere are not lint-visible and stay. */
.lede        { margin: -8px 0 14px; }   /* description line under a page title */
.lede-tight  { margin: -8px 0 12px; }   /* same, slightly tighter */
.filter-label  { font-size: 13px; color: var(--text-dim); }
.limit-select  { width: auto; display: inline-block; padding: 4px 8px; }
.filter-banner { margin: 10px 0; padding: 8px 12px; border: 1px solid var(--panel-border); border-radius: 8px; background: var(--panel); font-size: 13px; }

/* Row action menu (⋯) — the shared per-row actions component (rowMenu in
   common.js). Row content stays visible; action buttons live in here. */
.row-menu { position: relative; display: inline-block; }
.row-menu-btn { background: none; border: 1px solid transparent; border-radius: 7px; color: var(--text-dim);
                font-size: 16px; line-height: 1; padding: 2px 9px 6px; cursor: pointer; }
.row-menu-btn:hover, .row-menu.open .row-menu-btn { border-color: var(--panel-border); color: var(--text); background: var(--panel); }
.row-menu-list { display: none; position: absolute; right: 0; top: calc(100% + 2px); z-index: 60; min-width: 180px;
                 background: var(--panel); border: 1px solid var(--panel-border); border-radius: 10px;
                 padding: 4px; box-shadow: 0 8px 24px rgba(0,0,0,0.35); }
.row-menu.open .row-menu-list { display: block; }
.row-menu-item { display: block; width: 100%; text-align: left; background: none; border: none; border-radius: 7px;
                 color: var(--text); font-size: 13px; padding: 7px 10px; cursor: pointer; }
.row-menu-item:hover { background: rgba(139,147,163,0.12); }
.row-menu-item.danger { color: var(--red); }
.row-menu-item.danger:hover { background: rgba(239,83,80,0.12); }
.row-menu-item:disabled { color: var(--text-dim); cursor: default; }
.row-menu-item:disabled:hover { background: none; }
.row-menu-sep { border-top: 1px solid var(--panel-border); margin: 4px 6px; }

/* Which hub you are in, top-left next to the brand — pairs with the
   hub-scoped nav: the tabs show only this hub, this label names it. */
.brand-hub { font-size: 12px; font-weight: 700; color: var(--accent); margin-left: 7px; white-space: nowrap; }

/* Status dot for identity rows: quiet when fine, a word only when not. */
.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--green); }
.status-dot.bad { background: var(--red); }

/* ── Progressive disclosure + fleet table (2026-08-31) ─────────────────────
   Collapsible page sections (native <details>) with remembered state, the
   project pipeline ribbon, and the per-project fleet table. Shared: the
   project page and both device pages use the same shells. */
  .psec { border: 1px solid var(--panel-border); border-radius: 12px; margin: 12px 0; background: var(--panel); }
  .psec > summary { cursor: pointer; padding: 10px 16px; list-style: none; display: flex; align-items: center; gap: 10px; user-select: none; }
  .psec > summary::-webkit-details-marker { display: none; }
  .psec > summary::before { content: '▸'; color: var(--text-dim); transition: transform .12s; }
  .psec[open] > summary::before { transform: rotate(90deg); }
  .psec > summary h3 { margin: 0; font-size: 15px; display: inline; border: none; padding: 0; }
  .psec > summary .psec-count { margin-left: auto; font-size: 12px; color: var(--text-dim); white-space: nowrap; }
  .psec > .psec-body { padding: 4px 16px 14px; }
  .psec > .psec-body > .elements-section, .psec > .psec-body > .bom-section,
  .psec > .psec-body > .installs-section, .psec > .psec-body > .issues-section,
  .psec > .psec-body > .edit-form { border: none; padding-left: 0; padding-right: 0; margin-top: 0; background: transparent; }
  .psec > .psec-body h3 { display: none; }  /* the summary row already says it */
  .ribbon { display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0 4px; }
  .ribbon button { border: 1px solid var(--panel-border); background: var(--panel); color: var(--text); border-radius: 999px; padding: 4px 12px; font-size: 12px; cursor: pointer; }
  .ribbon button:hover { border-color: var(--accent, #58a6ff); }
  .ribbon .warn { color: var(--amber, #eab308); }
  .ribbon .bad { color: var(--red); }
  .health-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
  .health-chips .pill { font-size: 12px; }
  .fleet-table { width: 100%; border-collapse: collapse; font-size: 13px; }
  .fleet-table th { text-align: left; color: var(--text-dim); font-weight: 500; padding: 4px 10px 6px 0; border-bottom: 1px solid var(--panel-border); }
  .fleet-table td { padding: 6px 10px 6px 0; border-bottom: 1px solid var(--panel-border); vertical-align: middle; }
  .fleet-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; }
  .fleet-attn td { background: color-mix(in srgb, var(--red) 6%, transparent); }


/* ── pill2 (status pills) + view-toggle — shared by site.html, sites.html,
   devices.html since 2026-08-31 (sites.html had been using pill2 with no
   definition at all — the hub-panel pills rendered as plain text). */
.pill2 { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; }
.pill2.ok { background: rgba(61,180,110,.15); color: var(--green); }
.pill2.bad { background: rgba(228,86,86,.15); color: var(--red); }
.pill2.warn { background: rgba(245,185,66,.18); color: var(--amber); }
.pill2.dim { background: rgba(139,147,163,.15); color: var(--text-dim); }
.pill2.acc { background: rgba(79,140,255,.15); color: var(--accent); }
.view-toggle { display: inline-flex; border: 1px solid var(--panel-border); border-radius: 8px; overflow: hidden; }
.view-toggle button { background: none; border: none; color: var(--text-dim); padding: 5px 12px; cursor: pointer; font-size: 12px; }
.view-toggle button.active { background: var(--accent); color: #fff; }
