* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --bg: #f5f5f5;
  --bg-surface: #fff;
  --bg-alt: #f0f0f0;
  --text: #333;
  --text-muted: #555;
  --text-faint: #888;
  --border: #ddd;
  --border-light: #eee;
  --accent: #4a90d9;
  --accent-hover: #357abd;
  --header-bg: #fff;
  --header-border: #ddd;
  --table-head-bg: #fafafa;
  --table-hover: #f8f9fa;
  --error: #dc2626;
  --info: #2563eb;
  --success: #16a34a;
  --warning: #f59e0b;
  --shadow: rgba(0, 0, 0, 0.1);
  --tag-bg: #e8f0fe;
  --tag-text: #1a56db;
  /* COM terminal palette — light theme defaults. Dark variants override
     in [data-theme="dark"]. The terminal background intentionally stays
     close to surface tones (not pure white) so monospace text is easy
     on the eyes during long debug sessions. */
  --com-bg: #f7f7f2;
  --com-text: #1a1a1a;
  --com-rx: #0277bd;        /* RX raw (info blue) */
  --com-tx: #2e7d32;        /* TX raw (action green) */
  --com-packet-rx: #c25e00; /* RX framed packet (orange, darker for white bg) */
  --com-packet-tx: #ad1457; /* TX framed packet (pink, darker for white bg) */
}

[data-theme="dark"] {
  --bg: #1a1a1a;
  --bg-surface: #242424;
  --bg-alt: #2a2a2a;
  --text: #e0e0e0;
  --text-muted: #b0b0b0;
  --text-faint: #777;
  --border: #383838;
  --border-light: #2e2e2e;
  --accent: #6aabf0;
  --accent-hover: #8ec4ff;
  --header-bg: #161616;
  --header-border: #333;
  --table-head-bg: #1e1e1e;
  --table-hover: #2a2a2a;
  --error: #ef4444;
  --info: #60a5fa;
  --success: #4ade80;
  --warning: #fbbf24;
  --shadow: rgba(0, 0, 0, 0.4);
  --tag-bg: #1e3a5f;
  --tag-text: #93c5fd;
  /* COM terminal — dark theme (the original hardcoded palette). */
  --com-bg: #0f0f1a;
  --com-text: #e0e0e0;
  --com-rx: #4fc3f7;
  --com-tx: #a5d6a7;
  --com-packet-rx: #ffb74d;
  --com-packet-tx: #f48fb1;
}
[data-theme="dark"] input[type="number"],
[data-theme="dark"] input[type="date"],
[data-theme="dark"] input[type="datetime-local"] { color-scheme: dark; }

[hidden], .hidden { display: none !important; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
}

/* --- Header --- */
.app-header {
  background: var(--header-bg);
  border-bottom: 1px solid var(--header-border);
  position: sticky;
  top: 0;
  z-index: 100;
}
.header-inner {
  padding: 0 16px;
  height: 48px;
  display: flex;
  align-items: center;
  gap: 16px;
}
.logo {
  display: flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  color: var(--text);
  font-size: 1.1em;
  font-weight: 600;
  white-space: nowrap;
}
.logo-svg {
  display: inline-flex;
  width: 22px;
  height: 22px;
  align-items: center;
  justify-content: center;
  /* Default while SVG loads / no connection signal yet */
  color: var(--text-faint);
  transition: color 0.4s ease;
}
.logo-svg svg { width: 100%; height: 100%; }
[data-conn="offline"] .logo-svg { color: var(--text-faint); }
[data-conn="online"]  .logo-svg { color: var(--warning); }
[data-conn="live"]    .logo-svg { color: var(--success); }
#main-nav {
  margin-left: auto;
  display: flex;
  gap: 16px;
  align-items: center;
}
.nav-link {
  text-decoration: none;
  color: var(--text-muted);
  font-size: 14px;
  white-space: nowrap;
  padding: 4px 0;
  background: none;
  border: none;
  cursor: pointer;
  font-family: inherit;
}
.nav-link:hover { color: var(--accent); }
.nav-link.active { color: var(--accent); font-weight: 600; }

.header-right {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-left: 16px;
}

/* --- Mobile menu --- */
.menu-toggle {
  display: none;
  background: none;
  border: none;
  font-size: 22px;
  color: var(--text);
  cursor: pointer;
  padding: 4px;
  margin-left: auto;
}
.mobile-menu-panel { display: contents; }
@media (max-width: 720px) {
  .menu-toggle { display: block; }
  .app-header { position: relative; }
  .header-inner { position: relative; }
  .mobile-menu-panel { display: none; }
  .header-inner.menu-open .mobile-menu-panel {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    padding: 12px 16px;
    background: var(--bg-surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 6px 20px var(--shadow);
    position: absolute;
    right: 8px;
    top: 52px;
    z-index: 99;
    min-width: 180px;
  }
  .mobile-menu-panel #main-nav {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    margin: 0;
    width: 100%;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border-light);
  }
  .mobile-menu-panel .header-right {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    margin: 0;
    width: 100%;
  }
}

/* --- Theme toggle --- */
.theme-btn {
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 6px 8px;
  cursor: pointer;
  color: var(--text);
  display: flex;
  align-items: center;
}
.theme-btn:hover { border-color: var(--accent); }

/* --- User button --- */
.user-btn {
  display: inline-flex;
  align-items: center;
  gap: 0;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-surface);
}
.user-btn-name {
  padding: 4px 10px;
  font-weight: 600;
  color: var(--success);
  background: var(--bg);
}
.user-btn.is-admin .user-btn-name { color: var(--warning); }
.user-btn-sep {
  padding: 4px 0;
  color: var(--border);
  font-size: 0.85em;
  user-select: none;
}
.user-btn-action {
  padding: 4px 10px;
  color: var(--text-muted);
  cursor: pointer;
  border: none;
  background: none;
  font-size: 13px;
}
.user-btn-action:hover { background: var(--error); color: #fff; }

/* --- Buttons --- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 14px;
  /* Plain `.btn` (Cancel/Close) uses --text-muted for the border so the
     outline stays visible on dark backgrounds — coloured variants
     (.btn-primary / .btn-accent / .btn-warning / .btn-success / .btn-danger)
     override `border-color` with their semantic hue. */
  border: 1px solid var(--text-muted);
  border-radius: 4px;
  background: var(--bg-surface);
  color: var(--text);
  font-size: 13px;
  cursor: pointer;
  text-decoration: none;
  gap: 6px;
  font-family: inherit;
}
.btn:hover { background: var(--table-hover); }
.btn-primary {
  border-color: var(--success);
  color: var(--success);
  background: transparent;
}
.btn-primary:hover { background: var(--success); color: #fff; }
.btn-accent {
  border-color: var(--accent);
  color: var(--accent);
  background: transparent;
}
.btn-accent:hover { background: var(--accent); color: #fff; }
.btn-danger {
  border-color: var(--error);
  color: var(--error);
  background: transparent;
}
.btn-danger:hover { background: var(--error); color: #fff; }
.btn-warning {
  border-color: var(--warning);
  color: var(--warning);
  background: transparent;
}
.btn-warning:hover { background: var(--warning); color: #fff; }
.btn-success {
  border-color: var(--success);
  color: var(--success);
  background: transparent;
}
.btn-success:hover { background: var(--success); color: #fff; }
.btn-small { padding: 4px 10px; font-size: 12px; }

.btn:focus-visible,
.theme-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- Icon buttons --- */
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--bg-surface);
  color: var(--text-muted);
  cursor: pointer;
}
.icon-btn:hover { color: var(--accent); border-color: var(--accent); }
.icon-btn.active {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.icon-btn-group {
  display: inline-flex;
}
.icon-btn-group .icon-btn {
  border-radius: 0;
  border-right-width: 0;
}
.icon-btn-group .icon-btn:first-child { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }
.icon-btn-group .icon-btn:last-child {
  border-top-right-radius: 4px; border-bottom-right-radius: 4px;
  border-right-width: 1px;
}

/* --- Color picker --- */
.color-picker { position: relative; display: inline-block; }
.color-swatch-btn {
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  box-shadow: inset 0 0 0 2px var(--bg-surface);
}
.color-swatch-btn:hover { border-color: var(--accent); }
.color-picker-pop {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 50;
  display: grid;
  grid-template-columns: repeat(4, 24px);
  gap: 4px;
  padding: 6px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 4px 12px var(--shadow);
}
.color-swatch {
  width: 24px;
  height: 24px;
  border: 1px solid rgba(0,0,0,0.15);
  border-radius: 3px;
  cursor: pointer;
  padding: 0;
}
.color-swatch:hover { transform: scale(1.1); border-color: var(--text); }

/* --- Error --- */
.error {
  margin-top: 12px;
  color: var(--error);
  font-size: 13px;
}

/* --- Content --- */
.content {
  padding: 16px 16px 0;
  min-height: calc(100vh - 48px);
}
.content h2 {
  font-size: 1.2em;
  font-weight: 500;
  margin-bottom: 12px;
}

/* --- View header --- */
.devices-search {
  flex: 1;
  min-width: 0;
  max-width: 360px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-surface);
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  transition: border-color 0.1s;
}
.devices-search:focus {
  outline: none;
  border-color: var(--accent);
}
.devices-search::placeholder { color: var(--text-faint); }

.view-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  gap: 12px;
}
.view-header h2 { margin-bottom: 0; }
.view-header-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  flex-shrink: 0;
}
.view-header-actions-buttons { display: flex; align-items: center; gap: 8px; }
.view-header-actions-kebab { display: none; }
/* On narrow viewports collapse the action buttons into a kebab menu so the
   header doesn't wrap or push the title off-screen. */
@media (max-width: 720px) {
  .view-header-actions-buttons { display: none; }
  .view-header-actions-kebab { display: block; }
}

/* --- Pages --- */
.page { display: none; }
.page.active { display: block; }

/* --- Login page --- */
.login-page {
  max-width: 360px;
  margin: 80px auto;
  padding: 24px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 2px 8px var(--shadow);
}
.login-page h2 {
  margin-bottom: 16px;
  font-size: 1.15em;
  font-weight: 600;
}
.login-page .form-group { margin-bottom: 12px; }
.login-page label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 4px;
}
.login-page input[type="password"],
.login-page input[type="text"] {
  width: 100%;
  height: 36px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: 14px;
  outline: none;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
}
.login-page input:focus { border-color: var(--accent); }
.login-page .btn { width: 100%; margin-top: 4px; }

/* --- Cards --- */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 12px;
}
.card {
  background: var(--bg-surface);
  border: 1px solid var(--border-light);
  border-radius: 8px;
  padding: 14px 16px;
  border-left: 4px solid var(--text-faint);
  display: flex;
  flex-direction: column;
  gap: 8px;
  cursor: pointer;
  transition: box-shadow 0.15s, transform 0.05s;
}
.card:hover {
  box-shadow: 0 2px 10px var(--shadow);
  transform: translateY(-1px);
}
.card-online { border-left-color: var(--success); }
.card-offline { border-left-color: var(--text-faint); }
.card-error { border-left-color: var(--error); }
.card-warning { border-left-color: var(--warning); }

/* Detected (connected but unconfigured) cards — narrower, muted, dashed
   border to make them visually distinct from configured devices. */
.detected-section { margin-top: 24px; }
.detected-title {
  font-size: 14px;
  color: var(--text-muted);
  font-weight: 600;
  margin: 0 0 8px 0;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.detected-toggle {
  cursor: pointer;
  user-select: none;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.detected-toggle:hover { color: var(--text); }
.detected-chevron {
  display: inline-block;
  width: 1em;
  text-align: center;
  font-size: 12px;
}
.detected-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 10px;
}
.card-detected {
  padding: 10px 12px;
  gap: 6px;
}
/* Yellow border = currently connected (waiting to be configured).
   Gray border = recently disconnected. Solid in both cases. */
.card-detected-online  { border-left-color: var(--warning); }
.card-detected-offline { border-left-color: var(--text-faint); opacity: 0.7; }

/* Measured-data modal: "Go to" date picker injected into the title bar
   so the chart body uses the full modal width. Sits to the right of the
   title, before the close button. */
.measured-goto-wrap {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  margin-right: 12px;
  font-size: 12px;
  color: var(--text-muted);
  text-transform: none;
  letter-spacing: 0;
  font-weight: normal;
  margin-bottom: 0;
}
.measured-goto-label { font-size: 12px; }
.measured-goto-input {
  font-size: 12px;
  padding: 3px 6px;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: inherit;
}

/* Action button "loading" state — italic text + dimmed look while waiting
   for the operation to complete (e.g. Measure now waits for the modem to
   reply with the next CMD_MEASURED_VALUES packet via the stream). */
.btn.is-loading {
  font-style: italic;
  opacity: 0.7;
  cursor: progress;
}

/* MDCOM detail modal: dedicated "Measured" row showing time since the
   measurement packet arrived. Hover swaps elapsed text for "reload" hint;
   click → measure_now, text becomes "reloading…" until stream pushes the
   fresh last_measured_at and the modal re-renders. */
.measured-value {
  cursor: pointer;
  padding: 1px 6px;
  border-radius: 3px;
  transition: background 0.1s;
  font-variant-numeric: tabular-nums;
}
.measured-value:hover { background: var(--table-hover); }

/* Per-MDCOM brightness + font readout under the MD168 displays table.
   Single inline row (both values side by side) tucked under the MD168
   sub-heading via a left border + indent. */
.md168-extras {
  margin: 6px 0 4px 12px;
  padding-left: 8px;
  border-left: 2px solid var(--border-light);
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  font-size: 13px;
}
.md168-extra-item { color: var(--text); }
.md168-extra-label { color: var(--text-muted); margin-right: 2px; }
.measured-hint, .measured-reloading { display: none; }
.measured-value:hover .measured-ago    { display: none; }
.measured-value:hover .measured-hint   { display: inline; color: var(--accent); }
.measured-value.is-reloading .measured-ago,
.measured-value.is-reloading .measured-hint { display: none; }
.measured-value.is-reloading .measured-reloading {
  display: inline; color: var(--text-muted); font-style: italic;
}

.card-header-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}
.card-title { font-size: 16px; font-weight: 600; color: var(--text); flex: 1; min-width: 0; }

/* Kebab menu */
.kebab-menu { position: relative; }
.kebab-btn {
  background: none;
  border: none;
  padding: 4px;
  border-radius: 4px;
  color: var(--text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.kebab-btn:hover { background: var(--bg-alt); color: var(--text); }
.kebab-pop {
  position: absolute;
  top: calc(100% + 2px);
  right: 0;
  z-index: 50;
  min-width: 140px;
  padding: 4px;
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 4px 12px var(--shadow);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* In the modal footer (bottom of viewport) drop up so the popup doesn't
   render below the modal / viewport edge. */
.modal-footer .kebab-pop {
  top: auto;
  bottom: calc(100% + 2px);
}
.kebab-item {
  background: none;
  border: none;
  padding: 6px 10px;
  text-align: left;
  cursor: pointer;
  color: var(--text);
  font-size: 13px;
  border-radius: 4px;
  font-family: inherit;
}
.kebab-item:hover { background: var(--bg-alt); }
.kebab-item.btn-accent { color: var(--accent); }
.kebab-item.btn-accent:hover { background: var(--accent); color: #fff; }
.kebab-item.btn-warning { color: var(--warning); }
.kebab-item.btn-warning:hover { background: var(--warning); color: #fff; }
.kebab-item.btn-success { color: var(--success); }
.kebab-item.btn-success:hover { background: var(--success); color: #fff; }
.kebab-item.btn-danger { color: var(--error); }
.kebab-item.btn-danger:hover { background: var(--error); color: #fff; }
.card-subtitle { font-size: 13px; color: var(--text-muted); }
.card-meta {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 13px;
  color: var(--text-muted);
}
.card-meta-row { display: flex; gap: 6px; align-items: center; }
.card-meta-label { color: var(--text-faint); min-width: 5em; }
/* MDCOM "Power" row: Solar + Battery share one card-meta-row, sub-labels
   muted so the values stand out. Alignment with the rest of the meta is
   inherited from the standard .card-meta-row / .card-meta-label. */
.card-power-values {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 14px;
}
.card-power-cell { white-space: nowrap; }
.card-power-label { color: var(--text-faint); margin-right: 2px; }
/* "Bold" tag in the Light row — appears only when the Schmitt-trigger
   has switched the MD168 font to bold; visually marked so it stands out
   from the numeric readouts. */
.card-power-cell.tag-bold {
  color: var(--success);
  font-weight: 600;
  border: 1px solid var(--success);
  border-radius: 3px;
  padding: 0 6px;
}
.display-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  vertical-align: middle;
}
.display-link {
  text-decoration: none;
  color: var(--text);
}
.display-link:hover { color: var(--accent); }
.display-row-body {
  display: flex;
  flex: 1;
  gap: 8px;
  justify-content: space-between;
  align-items: center;
}
.display-value {
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* 7-segment display (md168) */
.seg-display {
  display: inline-flex;
  gap: 1px;
  padding: 2px 3px;
  background: #000;
  border-radius: 2px;
  box-shadow: inset 0 0 4px rgba(0,0,0,0.6);
}
.seg-digit { display: inline-flex; }
.seg-digit svg {
  display: block;
  width: 12px;
  height: 22px;
}
/* Right-align segment displays in the displays-table value column so
   digits line up across rows even when displays have different digit
   counts (1×8 vs 3 vs 4 etc). */
.seg-cell { text-align: right; }
.seg-cell .seg-display { display: inline-flex; }

/* MDISP32 device preview thumbnail (in card) */
.card-preview {
  margin-top: 6px;
  display: flex;
  justify-content: center;
}
.mdisp32-card-preview {
  display: block;
  background: #000;
  border: 1px solid var(--border);
  border-radius: 3px;
  cursor: pointer;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  transition: opacity 0.2s;
}
.mdisp32-card-preview-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 32px;
  padding: 4px 8px;
  background: var(--bg-alt);
  border: 1px dashed var(--border);
  border-radius: 3px;
  color: var(--text-faint);
  font-size: 11px;
  font-style: italic;
  cursor: pointer;
}
.mdisp32-card-preview-placeholder:hover { color: var(--accent); }

/* Autonomous-mode counter readout — replaces the bitmap preview on
   cards where working_mode == 3. Digits are sized up vs. the per-row
   md168 displays since this is the card's only visual signal. */
.mdisp32-card-autonomous {
  display: flex;
  justify-content: center;
}
.mdisp32-card-autonomous .seg-digit svg {
  width: 22px;
  height: 40px;
}
/* Same readout reused inside the Pulses modal controls — slightly
   smaller so it lines up with the neighbouring select/buttons. */
.pulses-autonomous-box { display: inline-flex; }
.pulses-autonomous-box .seg-digit svg {
  width: 14px;
  height: 26px;
}

/* MDISP32 config form (collapsible groups) */
.mdisp32-config-form .config-group {
  margin-bottom: 8px;
  border: 1px solid var(--border-light);
  border-radius: 6px;
  background: var(--bg);
}
.mdisp32-config-form summary {
  list-style: none;
  cursor: pointer;
  padding: 8px 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  user-select: none;
}
.mdisp32-config-form summary::-webkit-details-marker { display: none; }
.mdisp32-config-form summary::before {
  content: '▶';
  font-size: 0.7em;
  margin-right: 8px;
  transition: transform 0.15s;
  color: var(--text-muted);
}
.mdisp32-config-form details[open] summary::before {
  transform: rotate(90deg);
}
.mdisp32-config-form .config-group-title {
  font-weight: 600;
  flex: 1;
}
.mdisp32-config-form .config-group-badge {
  background: var(--accent);
  color: #fff;
  border-radius: 10px;
  padding: 1px 8px;
  font-size: 11px;
  font-weight: 600;
  min-width: 20px;
  text-align: center;
}
.mdisp32-config-form .config-group-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 8px 14px;
  padding: 4px 12px 12px;
}
.mdisp32-config-form .form-group {
  margin-bottom: 0;
}
.mdisp32-config-form .form-group label {
  display: block;
  margin-bottom: 2px;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 500;
  font-size: 12px;
  color: var(--text-muted);
}
.mdisp32-config-form .form-group input,
.mdisp32-config-form .form-group select {
  width: 100%;
}
.mdisp32-config-form .form-group-inline {
  display: flex;
  align-items: center;
  gap: 8px;
}
.mdisp32-config-form .form-group-inline label {
  margin: 0;
  flex: 1;
}
.mdisp32-config-form .form-group-inline input[type="checkbox"] {
  width: auto;
  flex-shrink: 0;
  margin: 0;
}
.mdisp32-config-form select.field-changed {
  border-color: var(--warning);
  background: rgba(245,158,11,0.06);
  box-shadow: inset 0 0 0 1px var(--warning);
}
.mdisp32-config-form input.field-changed {
  border-color: var(--warning);
  background: rgba(245,158,11,0.06);
  box-shadow: inset 0 0 0 1px var(--warning);
}
.mdisp32-config-form .btn-primary[disabled] {
  opacity: 0.4;
  cursor: not-allowed;
  pointer-events: none;
}

/* --- Tags --- */
.tag {
  display: inline-block;
  padding: 2px 8px;
  background: var(--tag-bg);
  color: var(--tag-text);
  border-radius: 3px;
  font-size: 12px;
  font-weight: 500;
}
/* API Park card: large value display */
.apipark-value {
  margin-top: 8px;
  font-size: 28px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--text);
  letter-spacing: -0.02em;
}
.apipark-value-empty { color: var(--text-faint); font-weight: 400; }

/* MDCOM Edit form: two responsive columns — basic info on the left,
   brightness sliders + curve canvas on the right. Stacks vertically below
   the breakpoint (narrow modal / mobile). */
.device-form-cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}
@media (max-width: 720px) {
  .device-form-cols { grid-template-columns: 1fr; gap: 0; }
}
.device-form-col { min-width: 0; }

/* MDCOM brightness sliders in the Edit form. Keep the live readout on the
   right so the user sees the exact value without hunting for a tooltip. */
.mdcom-slider-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.mdcom-slider {
  flex: 1;
  min-width: 0;
  accent-color: var(--accent);
  -webkit-appearance: none;
  appearance: none;
  height: 6px;
  background: var(--bg-alt);
  border-radius: 3px;
  border: 1px solid var(--border);
}
.mdcom-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--accent);
  border: 1px solid var(--border);
  cursor: pointer;
}
.mdcom-slider::-moz-range-thumb {
  width: 16px; height: 16px;
  border-radius: 50%;
  background: var(--accent);
  border: 1px solid var(--border);
  cursor: pointer;
}
.mdcom-slider::-moz-range-track {
  height: 4px;
  background: transparent;
  border-radius: 3px;
}
.mdcom-slider-value {
  min-width: 36px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  color: var(--text-muted);
}
/* When the api_park has a last_error, the value cell shows the error text
   spanning the full width — left-aligned, smaller font, red, wrapping. */
.apipark-value-error {
  font-size: 13px;
  font-weight: 500;
  text-align: left;
  color: var(--error);
  letter-spacing: 0;
  white-space: pre-wrap;
  word-break: break-word;
}

/* COM terminal (modal) */
.com-chat-box {
  flex: 1;
  min-height: 320px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--com-bg);
  color: var(--com-text);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
}
.com-msg {
  display: flex;
  gap: 6px;
  align-items: baseline;
  margin-bottom: 2px;
  white-space: pre-wrap;
  word-break: break-all;
}
.com-msg.com-rx { color: var(--com-rx); }
.com-msg.com-tx { color: var(--com-tx); }
/* Protocol packets — distinct (orange/amber) so they're visibly different
   from the raw keytop bytes but still readable. */
.com-msg.com-packet-rx { color: var(--com-packet-rx); }
.com-msg.com-packet-tx { color: var(--com-packet-tx); }
.com-msg.com-packet-invalid {
  color: var(--error);
  background: color-mix(in srgb, var(--error) 12%, transparent);
}
.com-reason {
  flex-shrink: 0;
  font-weight: 700;
  text-transform: uppercase;
  font-size: 0.85em;
  padding: 0 4px;
  border-radius: 3px;
  background: color-mix(in srgb, var(--error) 25%, transparent);
}
/* Lifecycle events (connect/disconnect) — bold, italic, full-width banner. */
.com-msg.com-event { font-style: italic; font-weight: 600; opacity: 0.95; }
.com-msg.com-event-up { color: var(--success); }
.com-msg.com-event-down { color: var(--error); }

/* --- Live status modal --- */
.status-modal-body {
  display: flex;
  gap: 24px;
  align-items: flex-start;
  flex-wrap: wrap;
}
.status-col { flex: 1 1 360px; min-width: 320px; }
.gpio-section { margin-top: 12px; }
.gpio-section strong {
  display: block;
  margin-bottom: 4px;
  font-size: 13px;
  opacity: 0.85;
}
.inputs-config-table {
  width: 100%;
  border-collapse: collapse;
}
.modal-body .data-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.modal-body .data-table th,
.modal-body .data-table td {
  padding: 6px 8px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
  text-align: left;
}
.modal-body .data-table thead th {
  font-weight: 600;
  opacity: 0.75;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.modal-body .data-table tbody tr:hover { background: var(--table-hover); }

/* Schedule form: hour/minute selects shouldn't span full modal width.
   Override the global form-group select rule with a narrow wrap. */
.modal-body .schedule-time select { width: auto; min-width: 72px; }
/* Schedule spinboxes (hour / minute / day) — narrow number inputs.
   Higher specificity wins over the global `.modal-body input[type=number]`
   width:100% rule. */
.modal-body input[type="number"].sched-spin {
  width: 76px;
  flex: 0 0 auto;
  text-align: center;
}
/* Schedule packet row: 2-char hex cmd input next to data input — mirrors
   the COM modal packet sender (CMD + DATA inline). */
.modal-body input[type="text"].sched-cmd-input {
  width: 64px;
  flex: 0 0 auto;
  text-align: center;
  text-transform: lowercase;
  font-family: ui-monospace, "SF Mono", Consolas, monospace;
}

/* Generic "selected toggle" pill — used for mode/cmd-type/day-of-week
   buttons in the schedule form. Filled with accent blue (not green/btn-
   primary) so the active state reads as "selected" without competing
   with primary action buttons. */
.btn-on {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent);
}
.btn-on:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.inputs-config-table th,
.inputs-config-table td {
  padding: 5px 6px;
  font-size: 13px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
.inputs-config-table thead th {
  text-align: left;
  font-weight: 600;
  opacity: 0.75;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.gpio-counters {
  width: 100%;
  border-collapse: collapse;
}
.gpio-counters th,
.gpio-counters td {
  padding: 3px 6px;
  font-size: 12px;
  border-bottom: 1px solid var(--border);
}
.gpio-counters th { text-align: left; font-weight: 500; opacity: 0.85; }
.gpio-counters td { text-align: right; font-family: ui-monospace, Menlo, monospace; }
.gpio-bits {
  display: inline-flex;
  gap: 4px;
}
.gpio-bit {
  display: inline-block;
  width: 22px;
  height: 22px;
  line-height: 22px;
  text-align: center;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 600;
  border: 1px solid var(--border);
}
.gpio-bit-on { background: var(--success); color: #fff; border-color: var(--success); }
.gpio-bit-off { background: transparent; color: var(--text); opacity: 0.45; }

/* --- Firmware flash modal --- */
.flash-modal-body { display: flex; flex-direction: column; gap: 12px; }
.flash-warning {
  padding: 10px 12px;
  border: 1px solid var(--warning);
  border-radius: 6px;
  background: color-mix(in srgb, var(--warning) 10%, transparent);
  font-size: 13px;
  line-height: 1.5;
}
.flash-form {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  flex-wrap: wrap;
}
/* Hide the native file input — `pickLabel` <label for=...> triggers it. */
.flash-file-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.flash-file-name {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 12px;
  opacity: 0.85;
}
.flash-bar {
  height: 18px;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: 4px;
  overflow: hidden;
}
.flash-bar-fill {
  height: 100%;
  background: var(--accent);
  transition: width 80ms linear;
}
.flash-bar-fill.flash-bar-ok { background: var(--success); }
.flash-bar-fill.flash-bar-err { background: var(--error); }
.flash-progress {
  font-family: ui-monospace, Menlo, monospace;
  font-size: 12px;
  opacity: 0.85;
}
.flash-status { padding: 8px 10px; border-radius: 4px; font-weight: 600; }
.flash-status-ok { background: color-mix(in srgb, var(--success) 15%, transparent); color: var(--success); }
.flash-status-err { background: color-mix(in srgb, var(--error) 15%, transparent); color: var(--error); }
.com-cmd {
  flex-shrink: 0;
  font-weight: 700;
  margin-right: 4px;
}
.com-ts { opacity: 0.85; flex-shrink: 0; }
.com-dir { width: 1em; opacity: 0.7; flex-shrink: 0; text-align: center; }
.com-data { flex: 1; }
/* Decoded packet — two-line cell: raw hex (+size) on top, framed parsed
   line below so the operator sees both representations at a glance. */
.com-data.com-data-decoded {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.com-data-parsed {
  border: 1px solid currentColor;
  border-radius: 3px;
  padding: 0 5px;
  align-self: flex-start;
  max-width: 100%;
  word-break: break-all;
}
/* CMD column gets a framed symbolic name (e.g. "MAC", "STATUS") under
   the hex byte when the packet has a server-side decoder. The name
   badge is absolutely positioned so it can extend leftward into the
   timestamp column (the row stays the same height — long names like
   CONNECTION don't push the data column rightward). The decoded data
   box on the right keeps the row tall enough for the badge to live
   on the second visual line without overlapping the next message. */
.com-cmd-named {
  position: relative;
}
.com-cmd-name {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 2px;
  font-size: 0.85em;
  font-weight: 500;
  border: 1px solid currentColor;
  border-radius: 3px;
  padding: 0 4px;
  letter-spacing: 0.04em;
  white-space: nowrap;
  pointer-events: none;
}
.com-input {
  flex: 1;
  min-width: 0;  /* allow shrinking below intrinsic size in flex row */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* Note: .modal-body input[type="text"] sets width:100% with equal
   specificity and lives later in the file. Use !important so the cmd
   field stays narrow regardless of cascade order. CMD is exactly 2 hex
   chars — give it just enough width for `0x__` style without padding. */
.com-cmd-input {
  width: 4.5em !important;
  flex-shrink: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  text-transform: lowercase;
  text-align: center;
}
/* Single-row sender layout. Mode select on the left picks the wire
   format, then optional CMD field, then the data input flexes to fill,
   Send button on the right. */
.com-input-row {
  display: flex;
  gap: 6px;
  align-items: center;
}
.com-mode-select {
  flex-shrink: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
}

.tag-success { background: rgba(22,163,74,0.15); color: var(--success); }
.tag-error { background: rgba(220,38,38,0.15); color: var(--error); }
.tag-warning { background: rgba(245,158,11,0.15); color: var(--warning); }
.tag-muted { background: var(--table-hover); color: var(--text-muted); }

/* Device type tags — distinct colors per hardware type */
.tag-type-mdcom    { background: rgba(139,92,246,0.18); color: #a78bfa; }
.tag-type-mdisp32  { background: rgba(244,114,182,0.18); color: #f472b6; }
.tag-type-pif32    { background: rgba(34,211,238,0.18); color: #22d3ee; }

/* --- Modal --- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 40px 16px;
  z-index: 1000;
  overflow-y: auto;
}
.modal {
  background: var(--bg-surface);
  color: var(--text);
  border-radius: 6px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
  max-width: 560px;
  width: 100%;
  padding: 16px 20px 20px;
  border: 1px solid var(--border);
}
/* When multiple modals are stacked (sub-modal opened from a parent), only
   the topmost is visible — previous ones stay alive in the DOM (their
   state preserved) and reappear when the top is closed. */
.modal-overlay > .modal:not(:last-child) { display: none; }
.modal-wide { max-width: 820px; }
/* COM terminal — claim most of the viewport so packet rows can spread out
   without wrapping. Chat box auto-grows to use the freed vertical space.
   Cap modal height to viewport so the page (overlay) doesn't scroll the
   whole modal when it's taller than the window — chat-box scrolls instead. */
.modal-xl {
  max-width: min(1600px, calc(100vw - 32px));
  max-height: calc(100vh - 80px);
  display: flex;
  flex-direction: column;
}
.modal-xl .modal-body {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.modal-xl .modal-body > * {
  flex: 1 1 auto;
  min-height: 0;
}
.modal-xl .com-chat-box { max-height: none; min-height: 200px; flex: 1 1 auto; }
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border-light);
}
.modal-header h2 { margin: 0; font-size: 1.05em; font-weight: 500; }
.modal-close {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: 24px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
}
.modal-close:hover { color: var(--text); }
.modal-body { margin-bottom: 14px; }
.modal-body .form-group { margin-bottom: 12px; }
.modal-body label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  margin-bottom: 4px;
}
.modal-body label.checkbox-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  font-weight: normal;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text);
  margin-bottom: 0;
}
.modal-body .datetime-field {
  display: inline-flex;
  gap: 6px;
  flex-wrap: nowrap;
  width: auto;
}
/* Sized so date+time fit side-by-side and don't grow into the rest of
   the row. Native pickers respect the `width` declaration. */
.modal-body .datetime-field > input[type="date"] {
  flex: 0 0 auto;
  width: 140px;
}
.modal-body .datetime-field > input[type="time"] {
  flex: 0 0 auto;
  width: 96px;
}
.modal-body input[type="text"],
.modal-body input[type="number"],
.modal-body input[type="password"],
.modal-body input[type="url"],
.modal-body input[type="datetime-local"],
.modal-body input[type="date"],
.modal-body input[type="time"],
.modal-body textarea {
  width: 100%;
  min-height: 36px;
  padding: 6px 10px;
  border: 1px solid var(--border);
  border-radius: 4px;
  font-size: 14px;
  outline: none;
  background: var(--bg);
  color: var(--text);
  font-family: inherit;
}
.modal-body input:focus,
.modal-body textarea:focus { border-color: var(--accent); }

/* Selects — unified custom styling so heights line up with .btn buttons
   beside them (data-source pickers, detected-device dropdowns, header
   actions, …). Native browser appearance is removed; arrow is an inline
   SVG that renders identically across themes/OSes. */
select {
  font-family: inherit;
  font-size: 13px;
  min-height: 32px;
  padding: 5px 28px 5px 10px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background-color: var(--bg-surface);
  color: var(--text);
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  cursor: pointer;
  outline: none;
  line-height: 1.3;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 6'><path fill='%23888' d='M0 0l5 6 5-6z'/></svg>");
  background-repeat: no-repeat;
  background-position: right 9px center;
  background-size: 10px 6px;
}
select:focus { border-color: var(--accent); }
select:hover { background-color: var(--table-hover); }
select:disabled { opacity: 0.6; cursor: not-allowed; }

/* In a form-group (label-above layout) the select should match the height
   of sibling text inputs (36px) so the form looks aligned vertically. */
.modal-body .form-group select {
  width: 100%;
  min-height: 36px;
  padding: 7px 28px 7px 10px;
  font-size: 14px;
  background-color: var(--bg);
}
.modal-footer {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  flex-wrap: wrap;
  padding-top: 10px;
  border-top: 1px solid var(--border-light);
}

/* --- Tables --- */
table { border-collapse: collapse; width: 100%; font-size: 13px; }
th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid var(--border-light); }
thead th { background: var(--table-head-bg); color: var(--text-muted); font-weight: 600; }
tbody tr:hover { background: var(--table-hover); }
