/* dashboard.css — the widget system: rows, widgets, and chart chrome.
   ---------------------------------------------------------------------------
   Two layers, and they stay separate:

     .widget-row  owns geometry — how many columns, and one height for everything
                  in the row. PHP sets --widget-cols / --widget-height; nothing in
                  the row's markup sets a real layout property, so the media
                  queries at the end of the layout section can still win.
     .widget      owns the card — frame, header, body, the table twin. It never
                  sets its own width or height. That is the whole reason the edges
                  line up.

   Colour tokens come first. The two ordinal ramps are *selected per theme*, not
   flipped: on a light surface severity darkens, on a dark one it brightens, and
   both directions mean "more prominent = more overdue". Each set was run through
   the palette validator against this app's own surfaces (#ffffff and #222):

     light  #E85C7C → #E2013D → #AE0130 → #7A0122   all ordinal checks pass
     dark   #E2013D → #EE7290 → #F8B9C9 → #FFE4E8   all ordinal checks pass

   Reusing the light ramp on dark fails, because there the darkest step is the one
   nearest the surface and drops below the 2:1 floor.

   Both ramps have had their dim end lifted a step since. The validator clears an
   ordinal ramp whose near-surface end reaches 2:1, which is the right floor for a
   large filled area — but these ramps also paint 4px column bars, and a bar is a
   graphical object, which needs 3:1. Measured against this app's own surfaces the
   old ends were #EE7290 at 2.82:1 on white and #AE0130 at 2.16:1 on #222; a bucket
   holding 3 of 273 was effectively invisible in dark mode. The lifted ends measure
   3.36:1 and 3.26:1, and every ordinal check still passes.

   --viz-neutral was darkened for the same reason: 2.63:1 on white. It is the
   de-emphasis grey in the workload bars, and de-emphasis means recessive, not
   imperceptible.
   --------------------------------------------------------------------------- */

.viz-root {
  --viz-neutral: #8A8F99;
  --viz-track: color-mix(in srgb, var(--brand) 15%, transparent);
  --viz-grid: var(--border);

  --viz-ord-1: #E85C7C;
  --viz-ord-2: #E2013D;
  --viz-ord-3: #AE0130;
  --viz-ord-4: #7A0122;

  /* Emphasis ink, for the one figure or line that wants acting on.
     Not --brand: on the dark panel the brand measures 3.26:1, which is enough for
     a 40px number but not for a 12px line, and the overdue caption under a task is
     a 12px line. This step reads at 4.84:1 on white and 5.68:1 on #222, so the same
     token is safe at any size. */
  --alert-text: #E2013D;
}

@media (prefers-color-scheme: dark) {
  :root:where(:not([data-theme="light"])) .viz-root {
    --viz-neutral: #7e848e;
    --viz-ord-1: #E2013D;
    --viz-ord-2: #EE7290;
    --viz-ord-3: #F8B9C9;
    --viz-ord-4: #FFE4E8;
    --alert-text: #EE7290;
  }
}
:root[data-theme="dark"] .viz-root {
  --viz-neutral: #7e848e;
  --viz-ord-1: #E2013D;
  --viz-ord-2: #EE7290;
  --viz-ord-3: #F8B9C9;
  --viz-ord-4: #FFE4E8;
  --alert-text: #EE7290;
}

/* ---------------------------------------------------------------------------
   Layout — the row owns the grid
   ---------------------------------------------------------------------------
   This began as a 12-column grid with cards spanning 2, 3, 4, 5 and 6. Every row
   summed to 12, but no two rows broke at the same place, so no card edge lined up
   down the page — a sea of near-misses. Then it was one 3-column module, which
   fixed the widths but still let each card grow to its own content, so a row of
   three ended up three different heights.

   Now a row declares both: repeat(N) columns, and one height every widget in it
   takes. Content past that height scrolls inside its own widget, so nothing a
   widget contains can move the widget beside it.

   The column count and height arrive as custom properties. The media queries
   below then set grid-template-columns and grid-auto-rows *directly* — an inline
   grid-template-columns would outrank every media query and the page would never
   adapt to a narrow window.
   --------------------------------------------------------------------------- */

.widget-row {
  display: grid;
  grid-template-columns: repeat(var(--widget-cols, 3), minmax(0, 1fr));
  grid-auto-rows: var(--widget-height, min-content);
  gap: 12px;
  align-items: stretch;
}

.widget-row > .widget { grid-column: span 1 }
.widget-row > .widget[data-span="2"] { grid-column: span 2 }
.widget-row > .widget[data-span="3"] { grid-column: span 3 }
.widget-row > .widget[data-span="4"] { grid-column: span 4 }

/* Two columns, and anything asking for more than two goes full width. */
@media (max-width: 1100px) {
  .widget-row { grid-template-columns: repeat(2, minmax(0, 1fr)) }
  .widget-row > .widget[data-span="2"],
  .widget-row > .widget[data-span="3"],
  .widget-row > .widget[data-span="4"] { grid-column: 1 / -1 }
}

/* One column, and heights go back to the content: a fixed height that was right
   for a third of the width is wrong for all of it. */
@media (max-width: 760px) {
  .widget-row {
    grid-template-columns: minmax(0, 1fr);
    grid-auto-rows: min-content;
  }
  .widget-row > .widget,
  .widget-row > .widget[data-span="2"],
  .widget-row > .widget[data-span="3"],
  .widget-row > .widget[data-span="4"] { grid-column: 1 / -1 }
}

/* ---------------------------------------------------------------------------
   The section label — "My work", "Leading"
   --------------------------------------------------------------------------- */

.widget-header {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 6px;
}
.widget-header:first-child { margin-top: 0 }
.widget-header-label {
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--muted);
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
}
.widget-header-rule { flex: 1; height: 1px; background: var(--border) }
.widget-header-aside { font-size: var(--text-xs); color: var(--muted); white-space: nowrap }

/* ---------------------------------------------------------------------------
   The widget
   ---------------------------------------------------------------------------
   position: relative anchors the table panel. min-height/min-width 0 and the
   overflow rule are what keep a long list inside the height the row set instead
   of stretching it.
   --------------------------------------------------------------------------- */

.widget {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  box-shadow: var(--shadow-sm);
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

.widget-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  flex: 0 0 auto;
}
.widget-title {
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.widget-aside {
  font-size: var(--text-xs);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The body takes the leftover height and scrolls if it has to. */
.widget-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: hidden;
}

.widget-empty { font-size: var(--text-xs); color: var(--muted) }

/* Keeps the "show the numbers" link on the same line across a row of widgets. */
.widget > .viz-table { margin-top: auto; flex: 0 0 auto }

/* ---------------------------------------------------------------------------
   Widget bodies
   --------------------------------------------------------------------------- */

/* PercentWidget */
.widget-hero { display: flex; flex-direction: column; gap: 10px }
.widget-hero-value {
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1;
  color: var(--text);
  font-variant-numeric: normal;    /* proportional: tabular makes 121 look loose */
}

/* CountWidget */
.widget-count { display: flex; flex-direction: column; gap: 4px }
.widget-count-value {
  font-size: 2.5rem;
  font-weight: 700;
  line-height: 1.05;
  color: var(--text);
  font-variant-numeric: normal;
}
.widget-count-value.alert { color: var(--alert-text) }
.widget-count-caption { font-size: var(--text-xs); color: var(--muted) }

/* A caption pair under a chart. */
.widget-inline {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-size: var(--text-xs);
  color: var(--muted);
}

/* ListWidget */
.widget-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.widget-list-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  flex: 0 0 auto;
  min-width: 0;
}
.widget-list-row.framed {
  padding: 7px 9px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
  transition: border-color var(--dur) var(--ease);
}
.widget-list-row.framed:hover { border-color: color-mix(in srgb, var(--brand) 45%, var(--border)) }

.widget-list-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
}
.widget-list-primary {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--text-sm);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.widget-list-meta {
  flex: 0 0 auto;
  font-size: var(--text-xs);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.widget-list-secondary {
  font-size: var(--text-xs);
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.widget-list-secondary.alert { color: var(--alert-text) }
.widget-list-bar { min-width: 6px }

/* A status pill. The label always shows, so the colour is never the only carrier. */
.widget-pill {
  flex: 0 0 auto;
  font-size: var(--text-xs);
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
   Charts — HTML and CSS, so nothing is stretched out of shape
   ---------------------------------------------------------------------------
   These were SVG scaled with preserveAspectRatio="none", which squashed rounded
   corners into ellipses, distorted the bar widths, and stretched the column
   labels. A percentage-width div has none of those failure modes.
   --------------------------------------------------------------------------- */

/* Meter: a plain linear bar, square ends. */
.viz-meter {
  width: 100%;
  background: var(--viz-track);
  overflow: hidden;
  display: flex;
  flex: 0 0 auto;
}
.viz-meter-fill { background: var(--brand); display: block }

/* Stack: flex shares with a real 2px surface gap between fills. */
.viz-stack { display: flex; width: 100%; gap: 2px; flex: 0 0 auto }
.viz-seg { display: block; min-width: 2px }

/* Columns: value above, hairline baseline, label below. */
.viz-cols {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  width: 100%;
  flex: 0 0 auto;
}
.viz-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 4px }
.viz-col-value {
  font-size: var(--text-xs);
  font-weight: 700;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  min-height: 1em;
  line-height: 1;
}
.viz-col-plot {
  width: 100%;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  border-bottom: 1px solid var(--viz-grid);   /* hairline, solid */
}
/* Thin, capped, and never filling its slot — the leftover is air. */
.viz-col-bar { display: block; width: 60%; max-width: 24px }
.viz-col-label {
  font-size: 10px;
  line-height: 1.25;
  color: var(--muted);
  text-align: center;
  overflow-wrap: anywhere;
}

/* Sparkline: the one chart still SVG, scaled uniformly so the marker stays round. */
.viz-spark { display: block; width: 100%; height: 46px; flex: 0 0 auto }
.viz-line { fill: none; stroke: var(--brand); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round }
.viz-area { fill: var(--brand); opacity: .1 }
.viz-dot { fill: var(--brand); stroke: var(--panel); stroke-width: 2 }

/* Legend. Text wears text tokens; the swatch beside it carries identity. */
.viz-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; flex: 0 0 auto }
.viz-key { display: inline-flex; align-items: center; gap: 5px; font-size: var(--text-xs) }
.viz-swatch { width: 9px; height: 9px; border-radius: 2px; flex: 0 0 auto }
.viz-key-label { color: var(--muted) }
.viz-key-value { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums }

/* ---------------------------------------------------------------------------
   The table twin — takes the chart's place, never covers it
   ---------------------------------------------------------------------------
   Two wrong versions before this one, and they failed in opposite directions.

   As a plain <details> in the flow it grew its grid item, which grew the row,
   which padded out every neighbouring widget.

   So it became an absolutely positioned panel anchored to the widget's bottom
   edge — and the summary that opens it lives at that same bottom edge, so the
   panel covered its own toggle. Once open there was no way to close it, and no
   way to see the chart underneath either.

   Now the panel is in the flow and the *chart* steps aside for it: opening the
   details hides the widget body and the table takes the space it was using. The
   summary stays where it is and stays clickable, the widget's height is fixed by
   its row so nothing reflows, and a long table scrolls inside the panel.
   --------------------------------------------------------------------------- */

.viz-table > summary {
  font-size: var(--text-xs);
  color: var(--muted);
  cursor: pointer;
  padding: 2px 0;
  list-style: none;
  width: fit-content;
  flex: 0 0 auto;
}
.viz-table > summary::-webkit-details-marker { display: none }
.viz-table > summary::before { content: "▸ "; font-size: .9em }
.viz-table > summary:hover { color: var(--text) }

/* Open, the marker points down and the label brightens, so the way back is plain. */
.viz-table[open] > summary::before { content: "▾ " }
.viz-table[open] > summary { color: var(--text) }

/* Closed, the panel is not there at all. */
.widget > .viz-table-panel { display: none }

/* Open: the chart steps aside and the panel takes its place. Both are direct
   children of the widget, which is a flex column of known height — which is the
   only reason `flex: 1 1 0` can size the panel at all. See Chart::table(). */
.widget:has(> .viz-table[open]) > .widget-body { display: none }
.widget:has(> .viz-table[open]) > .viz-table { margin-top: 0 }
.widget:has(> .viz-table[open]) > .viz-table-panel {
  display: block;
  flex: 1 1 0;
  min-height: 0;
  overflow: auto;
}

/* Without :has() the body keeps its space and the panel would be clipped by the
   widget's overflow. Letting the widget scroll keeps every row reachable — the
   row's alignment is lost while a panel is open, but nothing becomes unreadable. */
@supports not selector(:has(*)) {
  .widget { overflow-y: auto }
  .widget > .viz-table[open] ~ .viz-table-panel { display: block }
}

.viz-table table { width: 100%; border-collapse: collapse }
.viz-table th, .viz-table td {
  text-align: left;
  padding: 3px 6px;
  font-size: var(--text-xs);
  border-bottom: 1px solid var(--border);
}
.viz-table th { color: var(--muted); font-weight: 600 }
.viz-table td { color: var(--text); font-variant-numeric: tabular-nums }
.viz-table td:last-child, .viz-table th:last-child { text-align: right }
.viz-table tr:last-child td { border-bottom: 0 }

@media (prefers-reduced-motion: reduce) {
  .widget-list-row.framed { transition: none }
}
