/**
 * Dashboard — 1.0.30
 * ==================
 * The customisable widget board.
 *
 * Component primitives (buttons, badges, cards, stat rows, meters, notes,
 * empty/error states, motion preferences) come from cx-base.css, which is
 * loaded before this file. Only board-specific rules are below.
 *
 * The board is a 4-column grid. A widget declares its span in board units and
 * the grid handles the rest; there is no absolute positioning and no layout
 * engine, because a grid the browser computes cannot drift out of sync with
 * the layout that was saved.
 */

.dash-root {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  /* The board sizes against this element, not the viewport — the sidebar can
     collapse and the widgets should respond to the space they actually have.
     Same reasoning as the Connections detail pane. */
  container-type: inline-size;
  container-name: dashboard;
}

/* ============================================================
   HEADER
   ============================================================ */
.dash-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.dash-h1 {
  margin: 0;
  font-size: 24px;
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--text-1);
  line-height: 1.2;
}
.dash-sub {
  margin: 4px 0 0;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--cx-fg-subtle);
}
.dash-head-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dash-stamp { font-size: 12px; color: var(--cx-fg-muted); font-variant-numeric: tabular-nums; }

/* ============================================================
   EDIT BAR
   ============================================================ */
.dash-editbar {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--cx-r-lg);
  border: 1px solid var(--border-accent);
  background: var(--accent-dim);
  animation: dash-editbar-in var(--t-base) var(--ease-out) both;
}
@keyframes dash-editbar-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}
.dash-editbar-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.dash-editbar-label {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12.5px;
  color: var(--text-2);
  line-height: 1.5;
}
.dash-editbar-actions { display: flex; gap: 8px; }
.dash-add {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  padding-top: 10px;
  border-top: 1px solid var(--border-accent);
}
.dash-add-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--cx-fg-muted);
  margin-right: 2px;
}

/* ============================================================
   BOARD GRID
   ============================================================ */
.dash-board {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--sp-4);
  /* Stretch, not start. With `align-items: start` every card in a row took its
     own content height, so a four-row card sat beside a six-row one with their
     bottom edges unaligned and a ragged gap between rows. Cards on a row now
     share a height and their contents distribute into it. */
  align-items: stretch;
}
.dash-w-1 { grid-column: span 1; }
.dash-w-2 { grid-column: span 2; }
.dash-w-3 { grid-column: span 3; }
.dash-w-4 { grid-column: span 4; }

.dash-widget {
  display: flex;
  flex-direction: column;
  min-width: 0;
  transition: border-color var(--t-fast) var(--ease-out),
              box-shadow var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out),
              opacity var(--t-fast) var(--ease-out);
}
.dash-widget-head { align-items: flex-start; }
/* The body owns the leftover height so a short widget's content spreads into
   the card rather than clustering at the top over dead space. */
.dash-widget-body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
/* Where a short card's leftover height goes.
   Cards on a row share a height, so a card with five stat rows beside one
   holding a 148px dial has real slack. Two earlier attempts both spent it
   badly:
     `margin-top: auto` sank the block to the bottom edge and pooled ALL of it
     into one void under the headline — measured 208px on Connections, 247px
     on Query Throughput.
     Growing the block and distributing the rows (`space-between`) simply
     spread the same void between every pair of rows, ~60px apart, which
     breaks them up as a group and reads worse than one gap.
   Neither is better than letting the rows keep their natural density and
   sit directly under the headline. The leftover then collects at the BOTTOM
   of the card, immediately above the border, where it reads as padding
   rather than as something missing.
   An inline `margin-top` on these blocks reintroduces the first failure,
   which is why they carry a class rather than a style attribute. */
.dash-rows-lower { padding-top: 12px; }
.dash-bars { display: flex; flex-direction: column; gap: 14px; }
.dash-widget-tools { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; }

/* -- edit mode -------------------------------------------------------- */
.dash-board.is-editing .dash-widget {
  border-style: dashed;
  border-color: var(--cx-hairline-strong);
  cursor: grab;
}
.dash-board.is-editing .dash-widget:hover {
  border-color: var(--accent);
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
.dash-widget.is-dragging { opacity: 0.4; cursor: grabbing; }
/* The drop target is marked on its leading edge rather than by a full
   highlight — a whole-card tint at this size reads as selection, not as
   "the dragged card lands here". */
.dash-widget.is-drop {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}
.dash-widget.is-unavailable { opacity: 0.55; }
.dash-grip { display: inline-flex; color: var(--cx-fg-subtle); cursor: grab; padding: 0 2px; }

.dash-sizes { display: inline-flex; gap: 2px; margin-right: 2px; }
.dash-size {
  width: 22px; height: 22px;
  display: grid; place-items: center;
  border-radius: var(--cx-r-sm);
  border: 1px solid var(--cx-hairline);
  background: transparent;
  color: var(--cx-fg-muted);
  font-family: var(--font-mono);
  font-size: 11px;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.dash-size:hover { background: var(--cx-muted); color: var(--cx-fg); }
.dash-size[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-inverse);
  font-weight: 600;
}

/* ============================================================
   WIDGET INTERNALS
   ============================================================ */

/* -- the headline number ---------------------------------------------- */
.dash-big { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.dash-big-num {
  font-size: 30px;
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.025em;
  color: var(--text-1);
  font-variant-numeric: tabular-nums;
}
.dash-big-unit { font-size: 12px; font-weight: 500; color: var(--cx-fg-subtle); }
.dash-big-num .dash-big-unit { font-size: 16px; }
.dash-note-sm {
  margin: 8px 0 0;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--cx-fg-subtle);
}

/* -- health ------------------------------------------------------------
   The dial column carries the verdict as well as the number, so the space
   beneath the gauge does work instead of sitting empty — that hollow
   bottom-left corner was the card's most obvious flaw. */
.dash-health { display: flex; gap: var(--sp-5); align-items: stretch; flex-wrap: wrap; height: 100%; }
.dash-health-col {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  min-width: 148px;
}
.dash-health-dial { position: relative; width: 148px; height: 148px; }
.dash-dial { width: 148px; height: 148px; }
.dash-dial-centre {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.dash-dial-num {
  font-size: 36px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.dash-dial-label { font-size: 11px; color: var(--cx-fg-subtle); margin-top: 4px; }
.dash-health-verdict {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-align: center;
}
.dash-health-conf {
  font-size: 11px;
  color: var(--cx-fg-subtle);
  text-align: center;
  line-height: 1.45;
}
/* Which inputs were not measured. Carrying the names here is what let the
   note below the rows go away — it said the same thing a second time, and
   the duplicate made the card tall enough that every short widget sharing
   its row inherited the difference as dead space. */
.dash-health-unmeasured {
  display: block;
  margin-top: 3px;
  font-size: 10.5px;
  color: var(--cx-fg-subtle);
  opacity: 0.85;
}
.dash-health-body { flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column; }
.dash-health-body > .cx-stat-rows { margin-top: 0; }
/* The one sentence that explains the number. Not an aside — it is the point
   of the card, because the score alone was unexplainable from what was shown. */
.dash-health-because {
  margin: 0 0 12px;
  padding: 9px 11px;
  border-radius: var(--cx-r-md);
  border: 1px solid var(--cx-hairline);
  background: var(--cx-muted);
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--text-2);
}
/* The row that actually set the score gets the emphasis, not the five that
   happened to be green. */
.dash-scan-row.is-binding {
  margin: 0 -8px;
  padding: 6px 8px;
  border-radius: var(--cx-r-sm);
  background: var(--danger-bg);
}
.dash-sev { display: flex; gap: 5px; flex-wrap: wrap; }
.dash-sev-chip { border: 1px solid transparent; cursor: pointer; font-family: inherit; }
.dash-sev-chip:hover { filter: brightness(1.15); }

/* -- findings contribution --------------------------------------------
   A bare "−50" reads as a negative quantity of findings rather than points
   taken off a score. The unit is spelled out and the bar shows how much of
   that severity's own cap has been used, which is the fact an operator
   needs: past the cap, fixing more of them changes nothing. */
.dash-contrib { display: flex; flex-direction: column; gap: 10px; }
.dash-contrib-row { display: flex; flex-direction: column; gap: 4px; }
.dash-contrib-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  font-size: 12.5px;
}
.dash-contrib-name { display: flex; align-items: center; gap: 6px; color: var(--cx-fg-muted); text-transform: capitalize; }
.dash-contrib-pts { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
.dash-contrib-pts small { font-weight: 500; color: var(--cx-fg-subtle); margin-left: 2px; }
.dash-contrib-cap { font-size: 11px; color: var(--cx-fg-subtle); }

/* -- presets ------------------------------------------------------------ */
.dash-presets {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  padding-top: 10px;
  border-top: 1px solid var(--border-accent);
}
.dash-preset {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px 11px;
  border-radius: var(--cx-r-md);
  border: 1px solid var(--cx-hairline-strong);
  background: var(--cx-surface);
  text-align: left;
  cursor: pointer;
  transition: border-color var(--t-fast) var(--ease-out),
              background-color var(--t-fast) var(--ease-out),
              transform var(--t-fast) var(--ease-out);
}
.dash-preset:hover { border-color: var(--accent); transform: translateY(-1px); }
.dash-preset-name { font-size: 12.5px; font-weight: 600; color: var(--cx-fg); }
.dash-preset-desc { font-size: 11px; color: var(--cx-fg-subtle); }

/* -- list widgets (largest tables, unused indexes, by-database) --------- */
.dash-list { display: flex; flex-direction: column; gap: 2px; }
.dash-list-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  font-size: 12.5px;
  border-bottom: 1px solid var(--cx-hairline);
}
.dash-list-row:last-child { border-bottom: 0; }
.dash-list-name { flex: 1 1 auto; min-width: 0; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dash-list-name small { color: var(--cx-fg-subtle); }
.dash-list-val { flex: 0 0 auto; font-weight: 600; color: var(--cx-fg); font-variant-numeric: tabular-nums; }

/* -- meters ------------------------------------------------------------ */
.dash-bar { margin-bottom: 14px; }
.dash-bar:last-child { margin-bottom: 0; }
.dash-bar-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-3);
  font-size: 12.5px;
  margin-bottom: 5px;
}
.dash-bar-head > span:first-child { color: var(--cx-fg-muted); }
.dash-bar-head > span:last-child { font-weight: 600; font-variant-numeric: tabular-nums; }
/* The sample size the rate was computed from. Small, always present, and the
   reason "38.9% heap miss" from 18 blocks can no longer read as a fact. */
.dash-bar-note {
  display: block;
  margin-top: 5px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--cx-fg-subtle);
}

/* -- coverage ---------------------------------------------------------- */
.dash-cov { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 12px; }

/* -- clusters ---------------------------------------------------------- */
.dash-clusters { display: flex; flex-direction: column; gap: 12px; }
.dash-cluster {
  padding: 11px 13px;
  border-radius: var(--cx-r-md);
  border: 1px solid var(--cx-hairline);
  background: var(--cx-muted);
}
.dash-cluster-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  margin-bottom: 8px;
}
.dash-cluster-name { font-size: 13px; font-weight: 600; color: var(--cx-fg); }

/* -- loading skeletons -------------------------------------------------
   Each placeholder has the geometry of the widget it stands in for. A
   skeleton whose shape does not match its content is a layout shift with
   extra steps: it promises a shape and then breaks the promise, which is
   worse than showing nothing. */
.dash-skel-big { height: 30px; width: 58%; max-width: 160px; }
.dash-skel-dial { width: 148px; height: 148px; border-radius: 50%; }
.dash-skel-health { align-items: flex-start; }
.dash-skel-list { display: flex; flex-direction: column; gap: 10px; }
.dash-skel-list-row { display: flex; align-items: center; gap: 12px; }

/* -- chart ------------------------------------------------------------- */
.dash-chart-ctl { display: flex; justify-content: flex-end; margin-bottom: 8px; }
.dash-chart { height: 300px; }

/* The skeleton is an OVERLAY, not a replacement.
   Swapping the container out for a placeholder would destroy the canvas that
   `data-cx-keep` exists to preserve, and re-initialising into a box the
   browser has not laid out yet is what produced a 932px container holding a
   100px chart. The chart mounts, measures and paints at full size underneath;
   this just covers it until there is a real line to show. */
.dash-chart-wrap { position: relative; }
.dash-chart-skel {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  justify-content: flex-end;
  gap: 10px;
  padding: 12px 12px 0 44px;
  background: var(--cx-surface);
  border-radius: var(--cx-r-md);
  pointer-events: none;
}
.dash-chart-wrap.is-seeding .dash-chart-skel { display: flex; }

/* Four y-axis ticks and a baseline, so the placeholder has the shape of the
   thing it is standing in for rather than being a grey slab. */
.dash-chart-skel-axis {
  position: absolute;
  inset: 12px auto 26px 12px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  width: 24px;
}
.dash-chart-skel-axis .cx-skel { height: 7px; width: 100%; }

.dash-chart-skel-plot {
  display: flex;
  align-items: flex-end;
  gap: 4px;
  height: 76%;
  margin-bottom: 26px;
}
.dash-chart-skel-plot .cx-skel {
  flex: 1 1 0;
  min-width: 0;
  border-radius: var(--cx-r-sm) var(--cx-r-sm) 0 0;
}
/* Stagger the shimmer so it reads as one sweep across the plot rather than
   twenty-four independently pulsing bars, which is visually noisy. */
.dash-chart-skel-plot .cx-skel:nth-child(3n + 2) { animation-delay: 160ms; }
.dash-chart-skel-plot .cx-skel:nth-child(3n + 3) { animation-delay: 320ms; }
.dash-range {
  height: 26px;
  padding: 0 10px;
  border: 1px solid var(--cx-hairline);
  border-radius: var(--cx-r-sm);
  background: transparent;
  color: var(--cx-fg-muted);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease-out), color var(--t-fast) var(--ease-out);
}
.dash-range:hover { background: var(--cx-muted); color: var(--cx-fg); }
.dash-range[aria-pressed="true"] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-inverse);
  font-weight: 600;
}

/* ============================================================
   RESPONSIVE
   Container queries against the board's own width, so a collapsed sidebar
   widens the widgets instead of leaving them sized for a viewport that no
   longer describes the space they are in.
   ============================================================ */
@container dashboard (max-width: 1100px) {
  .dash-board { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash-w-3, .dash-w-4 { grid-column: span 2; }
}
@container dashboard (max-width: 620px) {
  .dash-board { grid-template-columns: minmax(0, 1fr); }
  .dash-w-1, .dash-w-2, .dash-w-3, .dash-w-4 { grid-column: span 1; }
  .dash-health { gap: var(--sp-4); }
  .dash-health-dial, .dash-dial { width: 108px; height: 108px; }
  .dash-dial-num { font-size: 26px; }
  .dash-chart { height: 240px; }
}

/* Fallback for engines without container queries — the same reflow keyed on
   the viewport, which is close enough when the sidebar width is fixed. */
@supports not (container-type: inline-size) {
  @media (max-width: 1400px) {
    .dash-board { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .dash-w-3, .dash-w-4 { grid-column: span 2; }
  }
  @media (max-width: 900px) {
    .dash-board { grid-template-columns: minmax(0, 1fr); }
    .dash-w-1, .dash-w-2, .dash-w-3, .dash-w-4 { grid-column: span 1; }
  }
}

@media (max-width: 700px) {
  .dash-h1 { font-size: 20px; }
  .dash-head-actions { width: 100%; justify-content: space-between; }
  .dash-big-num { font-size: 26px; }
  .dash-editbar-row { align-items: flex-start; }
}
