/* ============================================================================
   Clearview "Console" — component layer
   ----------------------------------------------------------------------------
   Requires tokens/tokens.css to be loaded first.

   Namespaced `cv-` throughout so it can coexist with the existing 9,990-line
   styles.css during a page-by-page migration. Nothing here uses !important
   and nothing relies on selector weight to win — the z-index scale and the
   cascade order do that work. (The current stylesheet has 33 !important
   declarations, most of them fighting inline styles.)

   Sections
     1  reset + base
     2  icons
     3  buttons
     4  form controls
     5  cards + panels
     6  tables
     7  chips, badges, status
     8  tabs + toolbars
     9  disclosure
    10  overlays: modal, popover, toast
    11  states: loading / empty / error
    12  progress + meters
    13  utilities
   ============================================================================ */

/* ============================================================ 1  reset + base */

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

/* The `hidden` attribute only gets `display: none` from the UA stylesheet, so
   ANY author rule that sets `display` silently beats it — which is exactly what
   happens to `.cv-scrim[hidden]` (display: grid) and would happen to every
   flex/grid component here. The doubled attribute selector raises this to
   specificity (0,2,0) so it wins over any single class, without reaching for
   !important. */
[hidden][hidden] { display: none; }

html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
}

body.cv {
  background: var(--cv-bg-page);
  color: var(--cv-text);
  font-family: var(--cv-font-ui);
  font-size: var(--cv-text-sm);
  line-height: var(--cv-leading-ui);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

.cv h1, .cv h2, .cv h3, .cv h4 {
  font-weight: var(--cv-weight-semibold);
  line-height: var(--cv-leading-tight);
  letter-spacing: var(--cv-tracking-tight);
}
.cv h1 { font-size: var(--cv-text-xl); }
.cv h2 { font-size: var(--cv-text-lg); }
.cv h3 { font-size: var(--cv-text-md); }
.cv h4 { font-size: var(--cv-text-sm); }

.cv a { color: var(--cv-accent); text-decoration: none; }
.cv a:hover { text-decoration: underline; text-underline-offset: 2px; }

.cv code, .cv pre, .cv kbd {
  font-family: var(--cv-font-mono);
  font-size: 0.9375em;
}

.cv ::selection { background: var(--cv-accent); color: var(--cv-on-accent); }

/* One focus ring, everywhere. Replaces the three stray :focus-visible rules in
   the current 9,990-line stylesheet, and the eight `outline: none` sites that
   removed focus without substituting anything. */
.cv :focus-visible {
  outline: none;
  box-shadow: var(--cv-focus-ring);
  border-radius: var(--cv-radius-sm);
}
/* Deliberately NOT :focus — a mouse click should not paint a ring. But keyboard
   users must never lose it, so there is no blanket outline:none anywhere. */

/* Screen-reader-only. The current stylesheet has no such utility, which is why
   icon-only controls fall back to title=. */
.cv-sr {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Scrollbars — dense UI, so they need to be thin but still grabbable. */
.cv *::-webkit-scrollbar { width: 10px; height: 10px; }
.cv *::-webkit-scrollbar-track { background: var(--cv-bg-inset); }
.cv *::-webkit-scrollbar-thumb {
  background: var(--cv-border-strong);
  border: 2px solid var(--cv-bg-inset);
  border-radius: var(--cv-radius-full);
}
.cv *::-webkit-scrollbar-thumb:hover { background: var(--cv-text-disabled); }
.cv * { scrollbar-color: var(--cv-border-strong) var(--cv-bg-inset); scrollbar-width: thin; }

/* ================================================================= 2  icons */

.cv-i {
  width: 16px; height: 16px;
  flex: none;
  display: inline-block;
  vertical-align: -0.1875em;
  color: inherit;              /* stroke is currentColor inside the sprite */
  pointer-events: none;        /* clicks belong to the control, not the glyph */
}
.cv-i-sm { width: 14px; height: 14px; }
.cv-i-lg { width: 20px; height: 20px; }
.cv-i-xl { width: 24px; height: 24px; }

.cv-i-spin { animation: cv-spin 800ms linear infinite; }
@keyframes cv-spin { to { transform: rotate(360deg); } }

/* =============================================================== 3  buttons */

.cv-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--cv-space-2);
  height: var(--cv-control);
  padding: 0 var(--cv-space-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-bg-raised);
  color: var(--cv-text);
  font: inherit;
  font-weight: var(--cv-weight-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--cv-dur-fast) var(--cv-ease),
              border-color var(--cv-dur-fast) var(--cv-ease),
              color var(--cv-dur-fast) var(--cv-ease);
}
.cv-btn:hover { background: var(--cv-bg-active); border-color: var(--cv-border-strong); }
.cv-btn:active { transform: translateY(0.5px); }

.cv-btn[disabled], .cv-btn[aria-disabled="true"] {
  color: var(--cv-text-disabled);
  background: var(--cv-bg-surface);
  border-color: var(--cv-border-subtle);
  cursor: not-allowed;
}
/* Real `disabled`, not a .btn-disabled class. The current app fakes disabled
   with a class, which leaves the control focusable and clickable. */

.cv-btn-primary {
  background: var(--cv-accent);
  border-color: var(--cv-accent);
  color: var(--cv-on-accent);
}
.cv-btn-primary:hover { background: var(--cv-accent-hover); border-color: var(--cv-accent-hover); }
.cv-btn-primary:active { background: var(--cv-accent-press); }

.cv-btn-danger {
  background: transparent;
  border-color: var(--cv-danger-edge);
  color: var(--cv-danger);
}
.cv-btn-danger:hover { background: var(--cv-danger-wash); border-color: var(--cv-danger); }

.cv-btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--cv-text-secondary);
}
.cv-btn-ghost:hover { background: var(--cv-bg-hover); color: var(--cv-text); border-color: transparent; }

.cv-btn-sm { height: var(--cv-control-sm); padding: 0 var(--cv-space-2); font-size: var(--cv-text-xs); }

/* Icon-only. The hit area is forced to 44px via a pseudo-element so the button
   can stay visually 24-30px inside a dense row without violating the
   minimum-target rule. */
.cv-btn-icon {
  width: var(--cv-control);
  padding: 0;
  position: relative;
}
.cv-btn-icon.cv-btn-sm { width: var(--cv-control-sm); }
/* `.cv-hit` MUST establish a positioning context, otherwise its absolutely
   positioned `::before` (width/height:100%) resolves against the viewport
   instead of the button — turning every non-icon hit target (e.g. a text
   `.cv-hit` button) into a full-page invisible overlay that eats all clicks.
   Icon buttons already got this via `.cv-btn-icon`; text ones did not. */
.cv-hit { position: relative; }
.cv-hit::before {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  translate: -50% -50%;
  min-width: var(--cv-hit-min);
  min-height: var(--cv-hit-min);
  width: 100%; height: 100%;
}

.cv-btn-group { display: inline-flex; }
.cv-btn-group .cv-btn { border-radius: 0; margin-left: -1px; }
.cv-btn-group .cv-btn:first-child { border-radius: var(--cv-radius) 0 0 var(--cv-radius); margin-left: 0; }
.cv-btn-group .cv-btn:last-child { border-radius: 0 var(--cv-radius) var(--cv-radius) 0; }
.cv-btn-group .cv-btn[aria-pressed="true"] {
  background: var(--cv-bg-active);
  border-color: var(--cv-border-strong);
  color: var(--cv-text);
  z-index: 1;
}

/* ========================================================= 4  form controls */

.cv-field { display: grid; gap: var(--cv-space-1); }

.cv-label {
  font-size: var(--cv-text-xs);
  font-weight: var(--cv-weight-medium);
  color: var(--cv-text-secondary);
}
.cv-label[data-required]::after { content: " *"; color: var(--cv-danger); }

.cv-input, .cv-select, .cv-textarea {
  height: var(--cv-control);
  padding: 0 var(--cv-space-2);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-bg-inset);
  color: var(--cv-text);
  font: inherit;
  width: 100%;
  transition: border-color var(--cv-dur-fast) var(--cv-ease);
}
.cv-textarea { height: auto; min-height: 64px; padding: var(--cv-space-2); resize: vertical; }
.cv-input::placeholder, .cv-textarea::placeholder { color: var(--cv-text-disabled); }
.cv-input:hover, .cv-select:hover, .cv-textarea:hover { border-color: var(--cv-border-strong); }
.cv-input:focus-visible, .cv-select:focus-visible, .cv-textarea:focus-visible {
  border-color: var(--cv-accent);
}
.cv-input[disabled], .cv-select[disabled], .cv-textarea[disabled] {
  color: var(--cv-text-disabled);
  background: var(--cv-bg-surface);
  cursor: not-allowed;
}

/* Validation. The current stylesheet has no :invalid, no error slot, no helper
   text and no required marker — form feedback is a toast that does not render. */
.cv-input[aria-invalid="true"], .cv-select[aria-invalid="true"], .cv-textarea[aria-invalid="true"] {
  border-color: var(--cv-danger);
  background: var(--cv-danger-wash);
}
.cv-hint { font-size: var(--cv-text-xs); color: var(--cv-text-muted); }
.cv-error {
  display: flex; align-items: center; gap: var(--cv-space-1);
  font-size: var(--cv-text-xs);
  color: var(--cv-danger);
}

/* Search field with a leading icon and a clear affordance. */
.cv-search { position: relative; display: flex; align-items: center; }
.cv-search > .cv-i:first-child {
  position: absolute; left: var(--cv-space-2);
  color: var(--cv-text-muted);
}
.cv-search .cv-input { padding-left: calc(var(--cv-space-2) * 2 + 16px); padding-right: var(--cv-space-6); }
.cv-search .cv-btn-icon { position: absolute; right: 2px; }

.cv-check { display: inline-flex; align-items: center; gap: var(--cv-space-2); cursor: pointer; }
.cv-check input { width: 15px; height: 15px; accent-color: var(--cv-accent); cursor: pointer; flex: none; }

/* ======================================================== 5  cards + panels */

.cv-card {
  background: var(--cv-bg-surface);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-lg);
}
/* No box-shadow. Elevation is the background step plus the border — the whole
   point of the geometry change. */

.cv-card-head {
  display: flex; align-items: center; gap: var(--cv-space-3);
  min-height: var(--cv-row-comfy);
  padding: var(--cv-space-2) var(--cv-space-3);
  border-bottom: 1px solid var(--cv-border-subtle);
}
.cv-card-title { font-size: var(--cv-text-sm); font-weight: var(--cv-weight-semibold); }
.cv-card-sub { font-size: var(--cv-text-xs); color: var(--cv-text-muted); }
.cv-card-body { padding: var(--cv-space-3); }
.cv-card-foot {
  padding: var(--cv-space-2) var(--cv-space-3);
  border-top: 1px solid var(--cv-border-subtle);
  background: var(--cv-bg-raised);
  border-radius: 0 0 var(--cv-radius-lg) var(--cv-radius-lg);
}
.cv-spacer { margin-left: auto; }

/* ================================================================ 6  tables */

.cv-table-wrap { overflow: auto; border: 1px solid var(--cv-border); border-radius: var(--cv-radius-lg); }

.cv-table { width: 100%; border-collapse: separate; border-spacing: 0; }

.cv-table th {
  position: sticky; top: 0;
  z-index: var(--cv-z-sticky);
  height: var(--cv-row);
  padding: 0 var(--cv-space-3);
  background: var(--cv-bg-raised);
  border-bottom: 1px solid var(--cv-border);
  font-size: var(--cv-text-2xs);
  font-weight: var(--cv-weight-semibold);
  letter-spacing: var(--cv-tracking-caps);
  text-transform: uppercase;
  color: var(--cv-text-muted);
  text-align: left;
  white-space: nowrap;
}
.cv-table td {
  height: var(--cv-row-dense);
  padding: 0 var(--cv-space-3);
  border-bottom: 1px solid var(--cv-border-subtle);
  vertical-align: middle;
}
.cv-table tbody tr:last-child td { border-bottom: none; }
.cv-table tbody tr:hover td { background: var(--cv-bg-hover); }

.cv-table tbody tr[aria-selected="true"] td {
  background: var(--cv-accent-wash);
  box-shadow: inset 2px 0 0 var(--cv-accent);
}

.cv-td-num { text-align: right; }
.cv-td-fit  { width: 1%; white-space: nowrap; }

/* Row actions reveal on hover but stay permanently visible on keyboard focus
   and on touch. The current Media grid shows up to 7 buttons per card at all
   times, which is the main source of its clutter. */
.cv-row-actions { display: flex; gap: var(--cv-space-1); justify-content: flex-end; opacity: 0; transition: opacity var(--cv-dur-fast) var(--cv-ease); }
tr:hover .cv-row-actions,
tr:focus-within .cv-row-actions,
.cv-tree-row:hover .cv-row-actions,
.cv-tree-row:focus-within .cv-row-actions { opacity: 1; }
/* Keyboard-reachable + visible whenever a control inside is focused, and
   always shown on touch (no hover to reveal them). */
.cv-row-actions:focus-within { opacity: 1; }
@media (hover: none) { .cv-row-actions { opacity: 1; } }

/* ================================================ 7  chips, badges, status */

.cv-chip {
  display: inline-flex; align-items: center; gap: var(--cv-space-1);
  height: var(--cv-control-sm);
  padding: 0 var(--cv-space-2);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius-full);
  background: var(--cv-bg-raised);
  color: var(--cv-text-secondary);
  font-size: var(--cv-text-xs);
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--cv-dur-fast) var(--cv-ease), color var(--cv-dur-fast) var(--cv-ease);
}
.cv-chip:hover { background: var(--cv-bg-active); color: var(--cv-text); }
.cv-chip[aria-pressed="true"] {
  background: var(--cv-accent-wash);
  border-color: var(--cv-accent-edge);
  color: var(--cv-accent);
}
.cv-chip-count {
  font-family: var(--cv-font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--cv-text-2xs);
  color: var(--cv-text-muted);
}

.cv-badge {
  display: inline-flex; align-items: center; gap: var(--cv-space-1);
  padding: 1px var(--cv-space-2);
  border: 1px solid transparent;
  border-radius: var(--cv-radius-sm);
  font-size: var(--cv-text-2xs);
  font-weight: var(--cv-weight-semibold);
  letter-spacing: var(--cv-tracking-caps);
  text-transform: uppercase;
  white-space: nowrap;
}

/* Status is carried by icon SHAPE + label + colour, never colour alone. The
   current app uses unlabelled 8px colour-only dots, which is unreadable for
   colour-vision-deficient operators — a real problem in a tool whose job is
   telling you what is and is not recording. */
.cv-badge-match,   .cv-badge-success { background: var(--cv-success-wash); border-color: var(--cv-success-edge); color: var(--cv-success); }
.cv-badge-short,   .cv-badge-warning { background: var(--cv-warning-wash); border-color: var(--cv-warning-edge); color: var(--cv-warning); }
.cv-badge-long,    .cv-badge-info    { background: var(--cv-info-wash);    border-color: var(--cv-info-edge);    color: var(--cv-info); }
.cv-badge-missing, .cv-badge-danger  { background: var(--cv-danger-wash);  border-color: var(--cv-danger-edge);  color: var(--cv-danger); }
.cv-badge-unscheduled, .cv-badge-neutral { background: var(--cv-neutral-wash); border-color: var(--cv-neutral-edge); color: var(--cv-neutral); }

/* Left status rail on a row — a glanceable verdict that survives across a room. */
.cv-rail-match       { box-shadow: inset 3px 0 0 var(--cv-match); }
.cv-rail-short       { box-shadow: inset 3px 0 0 var(--cv-short); }
.cv-rail-long        { box-shadow: inset 3px 0 0 var(--cv-long); }
.cv-rail-missing     { box-shadow: inset 3px 0 0 var(--cv-missing); }
.cv-rail-unscheduled { box-shadow: inset 3px 0 0 var(--cv-unscheduled); }

.cv-dot { width: 7px; height: 7px; border-radius: var(--cv-radius-full); flex: none; }
.cv-dot-live { background: var(--cv-danger); animation: cv-pulse 1.6s var(--cv-ease) infinite; }
@keyframes cv-pulse { 50% { opacity: 0.35; } }

/* ========================================================= 8  tabs, toolbar */

.cv-tabs {
  display: flex; gap: 2px;
  border-bottom: 1px solid var(--cv-border);
  overflow-x: auto;
  scrollbar-width: none;
}
.cv-tabs::-webkit-scrollbar { display: none; }

.cv-tab {
  display: inline-flex; align-items: center; gap: var(--cv-space-2);
  height: var(--cv-row-comfy);
  padding: 0 var(--cv-space-3);
  border: none; border-bottom: 2px solid transparent;
  background: none;
  color: var(--cv-text-secondary);
  font: inherit; font-weight: var(--cv-weight-medium);
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--cv-dur-fast) var(--cv-ease), border-color var(--cv-dur-fast) var(--cv-ease);
}
.cv-tab:hover { color: var(--cv-text); }
.cv-tab[aria-selected="true"] { color: var(--cv-accent); border-bottom-color: var(--cv-accent); }
/* Proper ARIA tabs: role="tablist" / role="tab" + aria-selected + aria-controls,
   and role="tabpanel" on the body. The current .tab has none of these and is
   click-only. */

.cv-toolbar {
  display: flex; align-items: center; gap: var(--cv-space-2);
  min-height: var(--cv-row-comfy);
  padding: var(--cv-space-2) 0;
  flex-wrap: wrap;
}

.cv-crumbs { display: flex; align-items: center; gap: var(--cv-space-1); font-size: var(--cv-text-xs); flex-wrap: wrap; }
.cv-crumbs .cv-i { color: var(--cv-text-disabled); }
.cv-crumb { color: var(--cv-text-secondary); background: none; border: none; font: inherit; padding: 2px 4px; border-radius: var(--cv-radius-sm); cursor: pointer; }
.cv-crumb:hover { color: var(--cv-accent); background: var(--cv-bg-hover); }
.cv-crumb[aria-current="page"] { color: var(--cv-text); font-weight: var(--cv-weight-medium); cursor: default; }

/* ============================================================ 9  disclosure */

.cv-tree-row {
  display: flex; align-items: center; gap: var(--cv-space-2);
  min-height: var(--cv-row);
  padding: 0 var(--cv-space-2);
  border-radius: var(--cv-radius-sm);
  cursor: pointer;
}
.cv-tree-row:hover { background: var(--cv-bg-hover); }
/* Depth as a token-driven indent + a guide line, so nesting stays readable at
   depth. The current File Compare hierarchy indents with inline margin-left. */
.cv-tree-row[data-depth="1"] { padding-left: calc(var(--cv-space-2) + 18px); }
.cv-tree-row[data-depth="2"] { padding-left: calc(var(--cv-space-2) + 36px); }
.cv-tree-row[data-depth="3"] { padding-left: calc(var(--cv-space-2) + 54px); }
.cv-tree-guide { border-left: 1px solid var(--cv-border-subtle); margin-left: 10px; }

.cv-twisty { transition: rotate var(--cv-dur) var(--cv-ease); color: var(--cv-text-muted); }
[aria-expanded="true"] > .cv-twisty { rotate: 90deg; }

/* ============================================= 10  modal, popover, toast */

.cv-scrim {
  position: fixed; inset: 0;
  z-index: var(--cv-z-modal);
  display: grid; place-items: center;
  padding: var(--cv-space-5);
  background: rgba(0, 0, 0, 0.66);
  backdrop-filter: blur(3px);
}
.cv-modal {
  width: 100%; max-width: 560px;
  max-height: 86vh;
  display: flex; flex-direction: column;
  background: var(--cv-bg-overlay);
  border: 1px solid var(--cv-border-strong);
  border-radius: var(--cv-radius-lg);
  box-shadow: var(--cv-shadow-modal);
  animation: cv-modal-in var(--cv-dur-slow) var(--cv-ease);
}
.cv-modal-lg { max-width: 880px; }
.cv-modal-sm { max-width: 400px; }
/* Named size classes rather than an inline maxWidth. The current openModal()
   sets it inline and the mapping is inverted — size:'large' yields 800px while
   the default yields 1200px. */
@keyframes cv-modal-in { from { opacity: 0; translate: 0 -4px; } }

.cv-modal-head { display: flex; align-items: center; gap: var(--cv-space-3); padding: var(--cv-space-3); border-bottom: 1px solid var(--cv-border-subtle); }
.cv-modal-body { padding: var(--cv-space-3); overflow: auto; }
.cv-modal-foot { display: flex; justify-content: flex-end; gap: var(--cv-space-2); padding: var(--cv-space-3); border-top: 1px solid var(--cv-border-subtle); }
/* Requires role="dialog" aria-modal="true" aria-labelledby on .cv-modal, plus
   focus move-in, focus trap and focus restore. The current modal has none of
   these; see MIGRATION.md. */

.cv-popover {
  position: absolute;
  z-index: var(--cv-z-popover);
  min-width: 180px;
  padding: var(--cv-space-1);
  background: var(--cv-bg-overlay);
  border: 1px solid var(--cv-border-strong);
  border-radius: var(--cv-radius);
  box-shadow: var(--cv-shadow-popover);
}
.cv-menu-item {
  display: flex; align-items: center; gap: var(--cv-space-2);
  width: 100%;
  min-height: var(--cv-row);
  padding: 0 var(--cv-space-2);
  border: none; border-radius: var(--cv-radius-sm);
  background: none; color: var(--cv-text);
  font: inherit; text-align: left; cursor: pointer;
}
.cv-menu-item:hover { background: var(--cv-bg-hover); }
.cv-menu-item-danger { color: var(--cv-danger); }
.cv-menu-sep { height: 1px; margin: var(--cv-space-1) 0; background: var(--cv-border-subtle); }

/* Toasts. The existing .toast-container / .toast CSS is dead code: showToast()
   at app.js:661 forwards to addNotification() and no JS ever creates a .toast
   element, so ~900 call sites across the app produce no visible feedback. This
   is a working implementation for it to land on. */
.cv-toasts {
  position: fixed;
  bottom: var(--cv-space-4); right: var(--cv-space-4);
  z-index: var(--cv-z-toast);
  display: grid; gap: var(--cv-space-2);
  width: min(380px, calc(100vw - var(--cv-space-6)));
  pointer-events: none;   /* the container must never intercept clicks — only the toasts do */
}
.cv-toast { pointer-events: auto; }
/* A `.cv-scrim` sets `display:grid`, which overrides the UA `[hidden]{display:none}`
   rule — so without this a hidden scrim stays on screen and blocks every click. */
.cv-scrim[hidden] { display: none !important; }
.cv-toast {
  display: flex; align-items: flex-start; gap: var(--cv-space-2);
  padding: var(--cv-space-2) var(--cv-space-3);
  background: var(--cv-bg-overlay);
  border: 1px solid var(--cv-border-strong);
  border-left-width: 3px;
  border-radius: var(--cv-radius);
  box-shadow: var(--cv-shadow-popover);
  animation: cv-toast-in var(--cv-dur) var(--cv-ease);
}
@keyframes cv-toast-in { from { opacity: 0; translate: 0 8px; } }
.cv-toast-success { border-left-color: var(--cv-success); }
.cv-toast-success .cv-i { color: var(--cv-success); }
.cv-toast-warning { border-left-color: var(--cv-warning); }
.cv-toast-warning .cv-i { color: var(--cv-warning); }
.cv-toast-error   { border-left-color: var(--cv-danger); }
.cv-toast-error .cv-i { color: var(--cv-danger); }
.cv-toast-info    { border-left-color: var(--cv-info); }
.cv-toast-info .cv-i { color: var(--cv-info); }
.cv-toast-msg { flex: 1; font-size: var(--cv-text-xs); }

/* ==================================================== 11  loading / empty / error */

/* Skeletons reserve layout so nothing shifts when data lands — the guidance
   rates this severity High, and it also satisfies the codebase's own rule
   against full-DOM-rebuild refreshes that flicker and lose scroll position.
   Use these instead of the 40px spinner the router currently paints. */
.cv-skel {
  background: linear-gradient(90deg, var(--cv-bg-raised) 25%, var(--cv-bg-active) 37%, var(--cv-bg-raised) 63%);
  background-size: 400% 100%;
  border-radius: var(--cv-radius-sm);
  animation: cv-shimmer 1.4s var(--cv-ease) infinite;
}
@keyframes cv-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }
.cv-skel-text { height: 0.75em; margin: 0.25em 0; }
.cv-skel-row  { height: var(--cv-row-dense); }

@media (prefers-reduced-motion: reduce) {
  .cv-skel { animation: none; background: var(--cv-bg-raised); }
  .cv-i-spin, .cv-dot-live { animation: none; }
}

.cv-state {
  display: grid; justify-items: center; gap: var(--cv-space-2);
  padding: var(--cv-space-7) var(--cv-space-4);
  text-align: center;
}
.cv-state .cv-i { width: 28px; height: 28px; color: var(--cv-text-disabled); }
.cv-state-title { font-size: var(--cv-text-md); font-weight: var(--cv-weight-medium); }
.cv-state-desc { font-size: var(--cv-text-xs); color: var(--cv-text-muted); max-width: 44ch; }
.cv-state-error .cv-i { color: var(--cv-danger); }
/* Every list gets all three. An empty state must name the action that resolves
   it; an error state must say what failed and offer a retry. */

/* ==================================================== 12  progress + meters */

.cv-progress {
  height: 4px;
  background: var(--cv-bg-active);
  border-radius: var(--cv-radius-full);
  overflow: hidden;
}
.cv-progress-fill {
  height: 100%;
  background: var(--cv-accent);
  border-radius: inherit;
  transition: width var(--cv-dur) var(--cv-ease);
}
.cv-progress-fill-success { background: var(--cv-success); }
.cv-progress-fill-danger  { background: var(--cv-danger); }
/* The current .progress-bar-fill builds its colour from hsl(var(--accent-h)…),
   which is undeclared — so upload and transfer bars render invisible today. */

.cv-progress-indet .cv-progress-fill {
  width: 35%;
  animation: cv-indet 1.2s var(--cv-ease) infinite;
}
@keyframes cv-indet { from { margin-left: -35%; } to { margin-left: 100%; } }

/* Meter, horizontal or vertical. One implementation — the app currently has
   three unconnected VU meters (obs card, obs preview modal, captions page),
   all mapping the same -60..0 dB range with the same 7 ticks. */
.cv-meter { position: relative; background: var(--cv-bg-inset); border: 1px solid var(--cv-border-subtle); border-radius: var(--cv-radius-sm); overflow: hidden; }
.cv-meter-h { height: 8px; width: 100%; }
.cv-meter-v { width: 8px; height: 100%; }
.cv-meter-fill { position: absolute; background: var(--cv-success); }
.cv-meter-h .cv-meter-fill { inset: 0 auto 0 0; }
.cv-meter-v .cv-meter-fill { inset: auto 0 0 0; }
.cv-meter-fill[data-level="hot"]  { background: var(--cv-warning); }
.cv-meter-fill[data-level="clip"] { background: var(--cv-danger); }
.cv-meter-mark { position: absolute; background: var(--cv-accent); }
.cv-meter-h .cv-meter-mark { top: 0; bottom: 0; width: 2px; }
.cv-meter-v .cv-meter-mark { left: 0; right: 0; height: 2px; }

/* ============================================================= 13  utilities */

/* Tabular numerals. This is the single highest-value typographic fix on the
   Media cluster: file sizes, durations, timecodes, bitrates and byte counts
   sit in live-refreshing columns, and proportional digits make them jitter on
   every tick. */
.cv-num {
  font-family: var(--cv-font-mono);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.cv-num-sm { font-size: var(--cv-text-2xs); }

.cv-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* CSS ellipsis, not a JS substring. media.js:660 truncates filenames to 30
   characters in JavaScript, which throws away information the layout could
   have shown at a wider viewport. */

.cv-mono   { font-family: var(--cv-font-mono); }
.cv-muted  { color: var(--cv-text-muted); }
.cv-dim    { color: var(--cv-text-secondary); }
.cv-2xs    { font-size: var(--cv-text-2xs); }
.cv-xs     { font-size: var(--cv-text-xs); }
/* min-width:0 is not optional on these. As a flex or grid child, the default
   min-width:auto refuses to shrink below min-content, so a long filename or a
   nowrap meta row silently pushes the whole column wider than its track — which
   is exactly what made the Suite layout's file panel spill over the stage. */
.cv-row-f  { display: flex; align-items: center; gap: var(--cv-space-2); min-width: 0; }
/* grid-template-columns must be an explicitly shrinkable track. A single
   implicit `auto` column is sized to MAX-CONTENT and, unlike flex, grid does
   not shrink tracks back to fit the container — so one long unbreakable string
   pushes the whole column past its parent. minmax(0,1fr) is the fix; min-width
   alone is not enough because it constrains the item, not the track. */
.cv-col    { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--cv-space-2); min-width: 0; }
.cv-wrap   { flex-wrap: wrap; }
.cv-grow   { flex: 1; min-width: 0; }

.cv-kbd {
  display: inline-block;
  min-width: 18px;
  padding: 1px 5px;
  border: 1px solid var(--cv-border-strong);
  border-bottom-width: 2px;
  border-radius: var(--cv-radius-sm);
  background: var(--cv-bg-raised);
  font-family: var(--cv-font-mono);
  font-size: var(--cv-text-2xs);
  color: var(--cv-text-secondary);
  text-align: center;
}

/* Responsive. Mobile-first additions rather than max-width patches — the
   current stylesheet is desktop-first with 8 ad-hoc max-width breakpoints and
   768px duplicated across 9 separate blocks. */
@media (max-width: 768px) {
  .cv-toolbar { align-items: stretch; }
  .cv-toolbar > .cv-search { width: 100%; }
  .cv-row-actions { opacity: 1; }
  .cv-scrim { padding: 0; place-items: end stretch; }
  .cv-modal { max-width: none; max-height: 92vh; border-radius: var(--cv-radius-lg) var(--cv-radius-lg) 0 0; }
  .cv-toasts { left: var(--cv-space-2); right: var(--cv-space-2); width: auto; }
}

/* ============================================================================
   SKELETON COMPOSITION
   ----------------------------------------------------------------------------
   `.cv-skel` gives the shimmer; these give it a shape. The rule that matters:
   a skeleton must occupy the SAME BOX the real content will, or it trades one
   layout shift for another.

   WHEN TO USE WHICH
     shape known, duration unknown ...... skeleton      (a file list, a waveform)
     work countable ..................... determinate progress + a count
                                          (uploading 3 of 12 · scanning 142/380)
     single control, short wait ......... in-button spinner
     under ~200ms ....................... NOTHING. A skeleton that flashes for
                                          80ms reads as a glitch. See cvLoad().
     live tick on already-rendered data . nothing — patch in place.
                                          Re-skeletoning on every refresh is the
                                          flicker this codebase already fought
                                          (docs/FLICKERING_*.md).

   Always pair with aria-busy="true" on the container, cleared when real content
   lands, so a screen reader is told the region is pending rather than empty.
   ============================================================================ */

.cv-skel-line   { height: 0.72em; border-radius: var(--cv-radius-sm); }
.cv-skel-line-sm{ height: 0.58em; border-radius: var(--cv-radius-sm); }
.cv-skel-box    { border-radius: var(--cv-radius-sm); }
.cv-skel-thumb  { aspect-ratio: 16 / 9; border-radius: 0; }
.cv-skel-chip   { height: var(--cv-control-sm); border-radius: var(--cv-radius-full); }
.cv-skel-dot    { width: 7px; height: 7px; border-radius: var(--cv-radius-full); flex: none; }
.cv-skel-btn    { height: var(--cv-control); border-radius: var(--cv-radius); }

/* Ragged line lengths so placeholder text reads as prose, not as a barcode. */
.cv-skel-para { display: grid; gap: 0.42em; }
.cv-skel-para .cv-skel-line:nth-child(4n)   { width: 62%; }
.cv-skel-para .cv-skel-line:nth-child(4n+1) { width: 94%; }
.cv-skel-para .cv-skel-line:nth-child(4n+2) { width: 81%; }
.cv-skel-para .cv-skel-line:nth-child(4n+3) { width: 88%; }

/* A loading region must not accept clicks — but it stays in the tab order via
   aria-busy so focus is never silently trapped or lost. */
[aria-busy="true"] .cv-skel { cursor: progress; }

/* Skeleton rows that mirror .cv-tree-row / .cv-table geometry exactly. */
.cv-skel-row {
  display: flex; align-items: center; gap: var(--cv-space-2);
  min-height: var(--cv-row-dense); padding: 0 var(--cv-space-2);
}
.cv-skel-row-tall { min-height: var(--cv-row); }


/* ============================================================================
   MIGRATED-IN SHARED COMPONENTS (from UI Clearview workspace) — used by the
   OBS Monitor, Batch Tools, Reflex DB and Settings prototypes. Appended so the
   Media-page edits already in this file are preserved.
   ============================================================================ */

/* Full-cell checkbox target for dense tables (13×13 → full cell) without
   growing row height. Used by Reflex DB + Batch Tools grids. */
.cv-cellcheck {
  display: grid; place-items: center;
  width: 100%; height: 100%; min-height: var(--cv-row-dense, 32px);
  margin: 0; cursor: pointer;
}

/* ---------------------------------------------------------------- STAT TILE
   A count with a label, optionally acting as a filter. Used by File Compare's
   verdict summary and the OBS Monitor's fleet triage strip. */
.cv-stats { display: grid; gap: var(--cv-space-2); grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); }
.cv-stat {
  position: relative;
  display: grid; gap: 2px;
  padding: var(--cv-space-2) var(--cv-space-3);
  border: 1px solid var(--cv-border);
  border-radius: var(--cv-radius);
  background: var(--cv-bg-surface);
  cursor: pointer;
  transition: border-color var(--cv-dur-fast) var(--cv-ease), background var(--cv-dur-fast) var(--cv-ease);
}
.cv-stat:hover { background: var(--cv-bg-hover); }
.cv-stat[aria-pressed] { padding-right: 28px; }
.cv-stat-check {
  position: absolute; top: 7px; right: 7px;
  width: 14px; height: 14px;
  display: grid; place-items: center;
  border: 1px solid var(--cv-border-strong);
  border-radius: var(--cv-radius-sm);
  color: transparent;
  transition: background var(--cv-dur-fast) var(--cv-ease),
              border-color var(--cv-dur-fast) var(--cv-ease),
              color var(--cv-dur-fast) var(--cv-ease);
}
.cv-stat[aria-pressed]:hover .cv-stat-check { border-color: var(--cv-accent); }
.cv-stat[aria-pressed="true"] {
  border-color: var(--cv-accent);
  background: var(--cv-accent-wash);
  box-shadow: inset 0 0 0 1px var(--cv-accent-edge);
}
.cv-stat[aria-pressed="true"] .cv-stat-check {
  background: var(--cv-accent); border-color: var(--cv-accent); color: var(--cv-on-accent);
}
.cv-stats-bar { display: grid; gap: var(--cv-space-1); }
.cv-stats-head {
  display: flex; align-items: center; gap: var(--cv-space-2);
  font-size: 9px; font-weight: var(--cv-weight-semibold);
  letter-spacing: var(--cv-tracking-caps); text-transform: uppercase;
  color: var(--cv-text-disabled);
}
.cv-stats-head .cv-i { width: 12px; height: 12px; }
.cv-stats-head b {
  font-weight: var(--cv-weight-semibold); color: var(--cv-accent);
  text-transform: none; letter-spacing: 0; font-size: var(--cv-text-2xs);
}
.cv-stat-n {
  font-family: var(--cv-font-mono); font-variant-numeric: tabular-nums;
  font-size: var(--cv-text-2xl, 22px); font-weight: var(--cv-weight-medium);
  line-height: 1;
}
.cv-stat-l { display: flex; align-items: center; gap: var(--cv-space-1); font-size: var(--cv-text-2xs); color: var(--cv-text-muted); }
.cv-stat-match   .cv-stat-n, .cv-stat-match   .cv-stat-l .cv-i { color: var(--cv-match); }
.cv-stat-short   .cv-stat-n, .cv-stat-short   .cv-stat-l .cv-i { color: var(--cv-short); }
.cv-stat-long    .cv-stat-n, .cv-stat-long    .cv-stat-l .cv-i { color: var(--cv-long); }
.cv-stat-missing .cv-stat-n, .cv-stat-missing .cv-stat-l .cv-i { color: var(--cv-missing); }
.cv-stat-unscheduled .cv-stat-n, .cv-stat-unscheduled .cv-stat-l .cv-i { color: var(--cv-unscheduled); }

/* ---------------------------------------------------------------- TOOLTIP
   One reused floating element driven by data-tip, shown on hover AND keyboard
   focus, wired via aria-describedby, dismissed with Escape. Positioned fixed. */
.cv-tip {
  position: fixed; top: 0; left: 0;
  z-index: var(--cv-z-tooltip, 900);
  max-width: 264px;
  padding: 5px var(--cv-space-2);
  border: 1px solid var(--cv-border-strong);
  border-radius: var(--cv-radius);
  background: var(--cv-bg-overlay);
  color: var(--cv-text);
  font-size: var(--cv-text-2xs);
  line-height: 1.45;
  box-shadow: var(--cv-shadow-popover);
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--cv-dur-fast) var(--cv-ease);
}
.cv-tip[data-open="true"] { opacity: 1; }
.cv-tip b { color: var(--cv-text); font-weight: var(--cv-weight-semibold); }
.cv-tip i { display: block; margin-top: 2px; color: var(--cv-text-muted); font-style: normal; }
.cv-tip .cv-kbd { margin-left: 4px; }
@media (prefers-reduced-motion: reduce) { .cv-tip { transition: none; } }
