/* ============================================================================
   Clearview "Console" — Settings
   ----------------------------------------------------------------------------
   Requires tokens/tokens.css + css/clearview-refresh.css + css/media.css
   (app shell). Namespaced `cvs-`.

   THE DIAGNOSIS
   settings.js is 3,791 lines / 191 KB, and 1,415 of those lines — 37% of the
   file — are an embedded documentation SITE (getDocumentationContent(),
   :1778–3192) that replaces the whole settings page when you click a header
   button. Documentation is not a setting.

   What is left is 63 settings across 7 tabs with no way to find one:

   - ONE global Save. saveSettings() (:1165) reads all 63 by getElementById
     across every tab — including tabs that were never opened. Roughly forty
     `if (getEl('setting-x'))` existence checks make it 180 lines long, because
     the form is conditionally rendered by role and the save has to cope.
   - NO dirty tracking and NO unsaved-changes guard. `beforeunload` appears
     zero times in the file. Edit the Server tab, click across to Cloudflare,
     navigate away — the edits are gone, silently, with nothing on screen ever
     having said they were pending.
   - _settingsWrapH4Collapsibles() (:34) retrofits structure onto the markup
     AFTER render: it walks every <h4> at any depth, collects following
     siblings until the next heading, and wraps the lot in <details>. 55 lines
     whose own comments say "The original markup is inconsistent." The sections
     are not sections; they are <div style="border-top…"> wrappers.
   - Non-admins get `readonly` inputs (but `disabled` selects). A readonly
     number field is pixel-identical to an editable one — you type, nothing
     sticks, and nothing explains why.
   - Four section titles carry the developer roadmap into an operator-facing
     page: "Realtime Transport (Phase 2)", "Server-Side Caches & Compression
     (Phase 1)", "Observability (Phase 4)", "Batch Tools, Agent Cache, Capture
     & Robocopy (Phase 5)" — in the order 2, 1, 4, 5.
   - Exactly ONE setting declares "Requires restart" (Server Port, :147) and it
     does so as a <small>. For the other 62 the operator cannot tell whether a
     change takes effect now or at the next restart.
   - previewTheme() (:1657) is not a preview: it calls applyTheme(), which
     writes localStorage['theme'] immediately. Changing the dropdown and
     navigating away without saving changes the theme permanently.
   - applyAccent() (:1669) sets --color-primary, --color-accent,
     --color-primary-900, --cv-accent and --accent-h/-s/-l — but NOT
     --cv-accent-wash, --cv-accent-edge, --cv-accent-hover or --cv-on-accent.
     A custom accent therefore leaves every wash and edge on the default cyan,
     and text on accent buttons never adapts. There is no contrast guard on the
     picker at all; it defaults to #6366f1, the indigo behind the original
     token mismatch.

   THE RESPONSE
   1. SEARCH is the primary navigation. 63 settings, one box, results from
      every tab with the tab named. Nobody remembers which tab a port is on.
   2. A declarative REGISTRY renders the form and drives the save. This is what
      makes search, dirty-diffing and per-field revert possible at all, and it
      deletes both the 180-line save and the 55-line <details> retrofit.
   3. DIRTY STATE IS VISIBLE. A save bar that counts pending changes, lists
      them, reverts them, and guards navigation.
   4. Restart-vs-hot-reload is stated per setting, in the field.
   ============================================================================ */

/* ==================================================================== layout
   Two panes: a section rail that reflects the registry, and the fields. The
   rail replaces a 7-tab strip that hid which tab a setting lived on. */
.cvs-layout {
  display: grid;
  grid-template-columns: 208px minmax(0, 1fr);
  gap: var(--cv-space-4);
  align-items: start;
}
@media (max-width: 900px) {
  .cvs-layout { grid-template-columns: minmax(0, 1fr); }
  .cvs-rail { position: static; }
}

.cvs-rail {
  position: sticky; top: var(--cv-space-2);
  display: grid; gap: 1px;
  min-width: 0;
}
.cvs-rail-group {
  padding: var(--cv-space-3) var(--cv-space-2) 4px;
  font-size: 9px; font-weight: var(--cv-weight-semibold);
  letter-spacing: var(--cv-tracking-caps); text-transform: uppercase;
  /* --cv-text-disabled is 2.6:1 — fine for a decorative label, not for the
     words that tell you how the page is organised. */
  color: var(--cv-text-muted);
}
.cvs-rail-item {
  display: grid; grid-template-columns: 16px minmax(0, 1fr) auto;
  gap: var(--cv-space-2); align-items: center;
  width: 100%; min-height: var(--cv-control); padding: 0 var(--cv-space-2);
  border: none; border-radius: var(--cv-radius);
  background: none; color: var(--cv-text-secondary);
  font: inherit; font-size: var(--cv-text-xs); text-align: left; cursor: pointer;
  transition: background var(--cv-dur-fast) var(--cv-ease), color var(--cv-dur-fast) var(--cv-ease);
}
.cvs-rail-item:hover { background: var(--cv-bg-hover); color: var(--cv-text); }
/* Accent-on-accent-wash measures 3.7:1 in the light theme — under AA for a
   12px label. The current item is marked by a BAR and full-contrast text, with
   the wash as reinforcement. Selection also stops being carried by colour
   alone, which it should not have been. */
.cvs-rail-item[aria-current="true"] {
  background: var(--cv-accent-wash); color: var(--cv-text);
  font-weight: var(--cv-weight-medium);
  box-shadow: inset 2px 0 0 var(--cv-accent);
}
.cvs-rail-item .cv-i { opacity: 0.75; }
.cvs-rail-item[aria-current="true"] .cv-i { opacity: 1; color: var(--cv-accent); }
/* A count of unsaved changes in this section, so the rail says where they are.
   Today nothing anywhere says a change is pending. */
.cvs-rail-dirty {
  min-width: 16px; height: 16px; padding: 0 4px;
  display: grid; place-items: center;
  border-radius: var(--cv-radius-full);
  background: var(--cv-warning); color: var(--cv-on-warning);
  font-family: var(--cv-font-mono); font-variant-numeric: tabular-nums;
  font-size: 9px; font-weight: var(--cv-weight-semibold);
}
.cvs-rail-lock { color: var(--cv-text-disabled); }

/* Said once per section rather than once per field. */
.cvs-locked-note {
  display: flex; align-items: center; gap: var(--cv-space-2);
  margin-top: calc(var(--cv-space-2) * -1);
  padding: var(--cv-space-2) var(--cv-space-3);
  border-radius: var(--cv-radius);
  background: var(--cv-bg-raised); color: var(--cv-text-muted);
  font-size: var(--cv-text-2xs);
}

/* ==================================================================== fields
   Label + hint on the left, control on the right, on a hard 4px grid. The
   existing .settings-field is close to this already; what it lacks is state. */
.cvs-panel { display: grid; gap: var(--cv-space-4); min-width: 0; }

.cvs-sec-head {
  display: flex; align-items: baseline; gap: var(--cv-space-2);
  padding-bottom: var(--cv-space-2);
  border-bottom: 1px solid var(--cv-border);
}
.cvs-sec-title { font-size: var(--cv-text-sm); font-weight: var(--cv-weight-semibold); }
.cvs-sec-desc { font-size: var(--cv-text-2xs); color: var(--cv-text-muted); }

.cvs-field {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 232px;
  gap: var(--cv-space-2) var(--cv-space-4);
  align-items: center;
  padding: var(--cv-space-2) var(--cv-space-2) var(--cv-space-2) var(--cv-space-3);
  border-radius: var(--cv-radius);
  /* The 3px rail is where change/lock state lives, so it never has to compete
     with the control for space. */
  box-shadow: inset 3px 0 0 transparent;
  transition: background var(--cv-dur-fast) var(--cv-ease), box-shadow var(--cv-dur-fast) var(--cv-ease);
}
.cvs-field + .cvs-field { border-top: 1px solid var(--cv-border-subtle); border-radius: 0; }
.cvs-field:hover { background: var(--cv-bg-hover); }
.cvs-field[data-dirty="true"] {
  background: var(--cv-warning-wash);
  box-shadow: inset 3px 0 0 var(--cv-warning);
}
.cvs-field[data-locked="true"] { opacity: 0.72; }
.cvs-field[data-locked="true"]:hover { background: none; }
/* Search hit — the reason the operator is looking at this field. */
.cvs-field[data-hit="true"] { box-shadow: inset 3px 0 0 var(--cv-accent); }

@media (max-width: 720px) {
  .cvs-field { grid-template-columns: minmax(0, 1fr); }
  .cvs-field-control { justify-self: start; width: 100%; }
}

.cvs-field-main { display: grid; gap: 2px; min-width: 0; }
.cvs-field-label {
  display: flex; align-items: center; gap: var(--cv-space-2); flex-wrap: wrap;
  font-size: var(--cv-text-xs); font-weight: var(--cv-weight-medium);
}
.cvs-field-hint { font-size: var(--cv-text-2xs); color: var(--cv-text-muted); }
/* Grid, not flex, so every control in the column shares one left edge and
   every unit suffix shares another. With flex the unit stole width from the
   input and a value with a unit sat at a different x than one without. */
.cvs-field-control {
  display: grid; grid-template-columns: minmax(0, 1fr) 30px;
  gap: var(--cv-space-2); align-items: center;
  width: 100%; justify-self: end;
}
.cvs-field-control > *:only-child { grid-column: 1 / -1; }
.cvs-field-control .cv-input,
.cvs-field-control .cv-select { width: 100%; }
/* Numbers right-align against their unit; a settings column is a column of
   magnitudes and reads as one. */
.cvs-field-control input[type="number"] { text-align: right; }
/* Controls that are not a value in a field: keep them at their natural size,
   pushed to the right edge. */
.cvs-field-control > .cvs-switch,
.cvs-field-control > .cv-btn-group { justify-self: end; }

/* Numbers get a unit suffix rather than baking the unit into the hint, so the
   column of values reads as a column of values. */
.cvs-unit {
  flex: none;
  font-family: var(--cv-font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--cv-text-2xs); color: var(--cv-text-muted);
  min-width: 20px;
}

/* ------------------------------------------------------------------- badges */
.cvs-tag {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 0 4px; border-radius: var(--cv-radius-sm);
  font-size: 8px; font-weight: var(--cv-weight-semibold);
  letter-spacing: var(--cv-tracking-caps); text-transform: uppercase;
  flex: none; white-space: nowrap;
}
/* "Restart required" is currently stated for exactly one of 63 settings.
   Whether a change lands now or at the next restart is not a footnote — during
   a show it decides whether the operator touches the setting at all. */
.cvs-tag { border: 1px solid transparent; background: var(--cv-bg-raised); color: var(--cv-text-secondary); }
.cvs-tag[data-t="restart"] { border-color: var(--cv-warning-edge); }
.cvs-tag[data-t="restart"] .cv-i { color: var(--cv-warning); }
.cvs-tag[data-t="live"]    { border-color: var(--cv-success-edge); }
.cvs-tag[data-t="live"] .cv-i    { color: var(--cv-success); }
.cvs-tag[data-t="admin"]   { border-color: var(--cv-border); }
.cvs-tag[data-t="admin"] .cv-i   { color: var(--cv-text-muted); }
/* On a dirty field the tag sits on the warning wash, so it needs its own
   opaque base or it inherits the tint and loses contrast twice over. */
.cvs-field[data-dirty="true"] .cvs-tag { background: var(--cv-bg-surface); }
/* The warning wash costs every muted element on the row about 0.7:1, which is
   enough to push the hint and the unit under AA. Lift them on dirty rows. */
.cvs-field[data-dirty="true"] .cvs-field-hint,
.cvs-field[data-dirty="true"] .cvs-unit { color: var(--cv-text-secondary); }

/* Previous value, shown inline on a changed field. Reverting one setting
   should not mean remembering what it used to be. */
.cvs-was {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--cv-text-2xs);
  /* On the dirty field's warning wash, muted drops under 4:1. */
  color: var(--cv-text-secondary);
}
.cvs-was b {
  font-family: var(--cv-font-mono); font-variant-numeric: tabular-nums;
  font-weight: var(--cv-weight-normal); color: var(--cv-text-secondary);
  text-decoration: line-through;
}

/* ==================================================================== toggle
   A switch, sized so the whole control is the target. */
.cvs-switch {
  position: relative; flex: none;
  width: 34px; height: 20px; padding: 0;
  border: 1px solid var(--cv-border-strong); border-radius: var(--cv-radius-full);
  background: var(--cv-bg-active); cursor: pointer;
  transition: background var(--cv-dur-fast) var(--cv-ease), border-color var(--cv-dur-fast) var(--cv-ease);
}
.cvs-switch::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 14px; height: 14px; border-radius: var(--cv-radius-full);
  background: var(--cv-text-muted);
  transition: translate var(--cv-dur-fast) var(--cv-ease), background var(--cv-dur-fast) var(--cv-ease);
}
.cvs-switch:hover { border-color: var(--cv-accent); }
.cvs-switch[aria-checked="true"] { background: var(--cv-accent); border-color: var(--cv-accent); }
.cvs-switch[aria-checked="true"]::after { translate: 14px 0; background: var(--cv-on-accent); }
.cvs-switch:disabled { cursor: not-allowed; }
.cvs-switch:disabled:hover { border-color: var(--cv-border-strong); }

/* ================================================================== save bar
   The single most important addition. There is currently no indication that a
   change is pending, no count, no revert, and no navigation guard. */
.cvs-savebar {
  flex: none; z-index: var(--cv-z-toolbar);
  display: flex; align-items: center; gap: var(--cv-space-3);
  padding: var(--cv-space-2) var(--cv-space-4);
  border-top: 1px solid var(--cv-warning-edge);
  background: var(--cv-bg-raised);
}
/* The pending count is a button; a gap-bearing flex container would otherwise
   split "3 unsaved changes" into separately-spaced words. */
.cvs-savebar-n > span { display: inline; }
.cvs-savebar[hidden] { display: none; }
.cvs-savebar-n {
  display: inline-flex; align-items: center; gap: var(--cv-space-2);
  font-size: var(--cv-text-xs);
}
.cvs-savebar-n b {
  font-family: var(--cv-font-mono); font-variant-numeric: tabular-nums;
  color: var(--cv-warning);
}

/* The list of what is actually pending. A count alone still leaves the
   operator guessing which 63rd of the page they touched. */
.cvs-pendingpanel { padding: 0; }
.cvs-pending { max-width: 420px; max-height: 300px; overflow-y: auto; padding: var(--cv-space-1); }
.cvs-pending-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto auto;
  gap: var(--cv-space-2); align-items: center;
  min-height: var(--cv-row); padding: 0 var(--cv-space-2);
  border-radius: var(--cv-radius-sm);
  font-size: var(--cv-text-2xs);
}
.cvs-pending-row:hover { background: var(--cv-bg-hover); }
.cvs-pending-name { display: grid; gap: 1px; min-width: 0; }
.cvs-pending-sec { font-size: 9px; color: var(--cv-text-muted); }
.cvs-pending-val {
  font-family: var(--cv-font-mono); font-variant-numeric: tabular-nums;
  color: var(--cv-text-secondary); white-space: nowrap;
}
.cvs-pending-val s { color: var(--cv-text-disabled); }

/* ==================================================================== search
   63 settings, 7 tabs, no search today. This is the primary navigation. */
.cvs-results { display: grid; gap: var(--cv-space-1); }
.cvs-result-sec {
  padding: var(--cv-space-2) var(--cv-space-2) 2px;
  font-size: 9px; font-weight: var(--cv-weight-semibold);
  letter-spacing: var(--cv-tracking-caps); text-transform: uppercase;
  /* Where the setting lives — the most useful part of a search result. */
  color: var(--cv-text-muted);
}
.cvs-mark {
  padding: 0 1px; border-radius: 2px;
  background: var(--cv-accent-wash); color: var(--cv-accent);
}

/* =================================================================== accent
   The picker is currently a bare <input type="color"> defaulting to #6366f1.
   Any hex is accepted, nothing checks contrast, and applyAccent() updates the
   solid colour but not the wash/edge/on-accent family — so a custom accent
   leaves half the UI cyan and can put light text on a light button. */
.cvs-swatches { display: flex; gap: var(--cv-space-2); flex-wrap: wrap; }
.cvs-swatch {
  position: relative;
  width: 30px; height: 30px; padding: 0;
  border: 2px solid transparent; border-radius: var(--cv-radius);
  background: var(--sw); cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.cvs-swatch[aria-checked="true"] { border-color: var(--cv-text); }
.cvs-swatch:hover { border-color: var(--cv-border-strong); }

.cvs-contrast {
  display: inline-flex; align-items: center; gap: var(--cv-space-2);
  padding: 2px var(--cv-space-2);
  border: 1px solid var(--cv-border); border-radius: var(--cv-radius);
  font-size: var(--cv-text-2xs);
}
.cvs-contrast b {
  font-family: var(--cv-font-mono); font-variant-numeric: tabular-nums;
  font-weight: var(--cv-weight-normal);
}
.cvs-contrast { color: var(--cv-text-secondary); }
.cvs-contrast[data-pass="true"]  { border-color: var(--cv-success-edge); }
.cvs-contrast[data-pass="true"]  > .cv-i { color: var(--cv-success); }
.cvs-contrast[data-pass="false"] { border-color: var(--cv-danger-edge); }
.cvs-contrast[data-pass="false"] > .cv-i { color: var(--cv-danger); }
/* Per-theme verdict, so a mixed result reads as mixed rather than as one
   number the operator has to interpret. */
.cvs-contrast [data-ok="true"]  { color: var(--cv-success); }
.cvs-contrast [data-ok="false"] { color: var(--cv-danger); }
.cvs-contrast-sep { color: var(--cv-text-disabled); }
.cvs-contrast > span { display: inline-flex; align-items: center; gap: 4px; }
/* The colour actually used on that surface, next to its ratio — the derived
   value, not the raw pick. */
.cvs-dot {
  width: 9px; height: 9px; border-radius: 2px; background: var(--sw);
  box-shadow: inset 0 0 0 1px rgba(128, 128, 128, 0.35);
}
.cvs-contrast-warn {
  padding: 0 4px; border-radius: var(--cv-radius-sm);
  background: var(--cv-danger-wash); color: var(--cv-danger);
  font-size: 9px; text-transform: uppercase; letter-spacing: var(--cv-tracking-caps);
  font-weight: var(--cv-weight-semibold);
}

/* Seven swatches need more than the 232px a value field gets. Widening the
   control column beats wrapping them onto three lines. */
.cvs-field[data-type="accent"] { grid-template-columns: minmax(0, 1fr) 300px; }
.cvs-field[data-type="accent"] .cvs-field-control { grid-template-columns: minmax(0, 1fr); }
.cvs-accent { display: grid; gap: var(--cv-space-2); justify-items: start; }

/* ============================================================== danger zone
   The existing implementation is sound — a password-confirmed modal with an
   explicit list of what is destroyed. It is kept; it is only given a container
   that reads as dangerous before the click, not after. */
.cvs-danger {
  border: 1px solid var(--cv-danger-edge); border-radius: var(--cv-radius-lg);
  background: var(--cv-bg-surface); overflow: hidden;
}
.cvs-danger-head {
  display: flex; align-items: center; gap: var(--cv-space-2);
  padding: var(--cv-space-2) var(--cv-space-3);
  border-bottom: 1px solid var(--cv-danger-edge);
  background: var(--cv-danger-wash); color: var(--cv-danger);
  font-size: var(--cv-text-xs); font-weight: var(--cv-weight-semibold);
}
.cvs-danger-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--cv-space-3); align-items: center;
  padding: var(--cv-space-2) var(--cv-space-3);
  border-bottom: 1px solid var(--cv-border-subtle);
}
.cvs-danger-row:last-child { border-bottom: none; }
.cvs-danger-name { font-size: var(--cv-text-xs); font-weight: var(--cv-weight-medium); }
.cvs-danger-desc { font-size: var(--cv-text-2xs); color: var(--cv-text-muted); }

/* ================================================================== service
   Cloudflare and MediaMTX are not settings pages, they are service consoles
   that happen to have settings. State first, then the controls that change it. */
.cvs-service {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--cv-space-3); align-items: center;
  padding: var(--cv-space-3);
  border: 1px solid var(--cv-border); border-radius: var(--cv-radius-lg);
  background: var(--cv-bg-raised);
}
.cvs-service[data-state="running"] { border-color: var(--cv-success-edge); }
.cvs-service[data-state="stopped"] { border-color: var(--cv-border); }
.cvs-service[data-state="error"]   { border-color: var(--cv-danger-edge); }
.cvs-service-dot {
  width: 10px; height: 10px; border-radius: var(--cv-radius-full);
  background: var(--cv-neutral); flex: none;
}
.cvs-service[data-state="running"] .cvs-service-dot {
  background: var(--cv-success);
  animation: cv-pulse 2s var(--cv-ease) infinite;
}
.cvs-service[data-state="error"] .cvs-service-dot { background: var(--cv-danger); }
@media (prefers-reduced-motion: reduce) { .cvs-service-dot { animation: none; } }
.cvs-service-name { font-size: var(--cv-text-sm); font-weight: var(--cv-weight-semibold); }
.cvs-service-meta { font-size: var(--cv-text-2xs); color: var(--cv-text-muted); }

/* Service log tail. Currently a "Recent Logs" <h4> section in two tabs with no
   distinct treatment at all. */
.cvs-log {
  max-height: 200px; overflow-y: auto;
  padding: var(--cv-space-2);
  border: 1px solid var(--cv-border); border-radius: var(--cv-radius);
  background: var(--cv-bg-inset);
  font-family: var(--cv-font-mono); font-size: var(--cv-text-2xs); line-height: 1.6;
}
.cvs-log-line { display: grid; grid-template-columns: 62px 44px minmax(0, 1fr); gap: var(--cv-space-2); }
.cvs-log-t { color: var(--cv-text-disabled); font-variant-numeric: tabular-nums; }
.cvs-log-l { text-transform: uppercase; font-size: 9px; }
.cvs-log-line[data-l="error"] .cvs-log-l { color: var(--cv-danger); }
.cvs-log-line[data-l="warn"]  .cvs-log-l { color: var(--cv-warning); }
.cvs-log-line[data-l="info"]  .cvs-log-l { color: var(--cv-info); }
.cvs-log-m { color: var(--cv-text-secondary); overflow-wrap: anywhere; }

/* ============================================================== page visibility
   Module toggles decide what appears in the sidebar. Shown as the nav they
   control, not as a list of checkboxes divorced from their effect. */
.cvs-pages { display: grid; gap: var(--cv-space-1); grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
.cvs-page {
  display: grid; grid-template-columns: 18px minmax(0, 1fr) auto;
  gap: var(--cv-space-2); align-items: center;
  min-height: var(--cv-row); padding: 0 var(--cv-space-2);
  border: 1px solid var(--cv-border); border-radius: var(--cv-radius);
  background: var(--cv-bg-surface);
  font-size: var(--cv-text-xs);
}
.cvs-page[data-on="false"] { color: var(--cv-text-disabled); background: var(--cv-bg-inset); }
.cvs-page[data-on="false"] .cv-i { opacity: 0.4; }

/* Live preview in the real sidebar: struck through and dimmed while the change
   is pending, so "removed from the sidebar" is shown rather than promised. */
.cvm-nav-item[data-pending-hidden="true"] {
  opacity: 0.4;
  text-decoration: line-through;
  text-decoration-color: var(--cv-warning);
  text-decoration-thickness: 1px;
}
