/* ---------- module grid ---------- */
.grid { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 16px; }
/* Several cards under one tab. Same gap as the grid, so a group reads as a
   short page rather than one enormous card. */
.stack { display: grid; gap: 16px; }
.card {
  grid-column: span var(--span, 6);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 10px;
  display: flex; flex-direction: column;
  min-width: 0;
  overflow: hidden;
  /* Makes the card a query container so KPI strips size themselves against
     the card they're in rather than the viewport. Matters because the
     sidebar collapses: at a given viewport width the card is 176px wider
     with the rail than with the full sidebar, and a viewport media query
     can't see that. */
  container-type: inline-size;
}
.card > header {
  display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
}
.card h2 { font-size: 13px; margin: 0; font-weight: 600; }
.card .sub { color: var(--muted); font-size: 12px; }
.card .body { padding: 14px; min-width: 0; flex: 1; }
.card .body.flush { padding: 0; }
/* A flush body gives its whole width to a table or a KPI strip, both of which
   want the card's edges. Prose doesn't — the notes under Pulse were sitting
   against the border with nothing between them and it. */
.card .body.flush > .hint { padding-left: 14px; padding-right: 14px; }
.card .body.flush > .hint:last-child { padding-bottom: 14px; }
.expand {
  margin-left: auto;
  background: none; border: 1px solid transparent; border-radius: 6px;
  color: var(--muted); cursor: pointer;
  padding: 3px 7px; font: inherit; font-size: 11px;
}
.card:hover .expand { border-color: var(--border); }
.expand:hover { color: var(--text); background: var(--panel-2); }
@media (max-width: 1080px) { .card { grid-column: span 12 !important; } }

/* ---------- kpis ----------
   The column count is pinned per tile count instead of being left to
   auto-fit. auto-fit derives the count from whatever width happens to be
   available, so one panel was a 9-wide bar on a large monitor and 8+1 on a
   laptop — and the orphan stretched a single tile across the full width
   beside an empty stretch of border. Every rule below lands on a complete
   rectangle, so a panel only changes shape at a width we picked.

   Tile counts actually in use: 2, 3, 4, 9, 10. Breakpoints assume a ~108px
   floor per tile plus the 1px gaps, measured against the *card* (see
   container-type on .card) rather than the viewport.

   The base rule stays on auto-fit deliberately: it's the fallback for any
   tile count not enumerated below, so adding a KPI to a panel degrades to
   today's behaviour instead of collapsing to something wrong. */
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(108px, 1fr)); gap: 1px; background: var(--border); }

/* ":has(> :nth-child(N):last-child)" reads as "has exactly N children". */
@container (min-width: 340px) {
  .kpis:has(> :nth-child(3):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .kpis:has(> :nth-child(9):last-child) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@container (min-width: 452px) {
  .kpis:has(> :nth-child(4):last-child)  { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .kpis:has(> :nth-child(8):last-child)  { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .kpis:has(> :nth-child(10):last-child) { grid-template-columns: repeat(5, minmax(0, 1fr)); }
}

/* Single bar. Eight tiles need 8x108 + 7 gaps = 871px of card, nine need 980,
   ten need 1089. A 1440px laptop gives the card 1158px with the sidebar open,
   so all three clear it — that's the whole point of dropping the floor from
   140px to ~108px. At that size the 24px value would overflow the six-digit
   counts, so the type and padding scale with the container and stop at their
   original values once there's room to spare.

   Pulse is 8 tiles on the all-time window and 9 on every other one, so it has
   to look right at both. */
@container (min-width: 871px) {
  .kpis:has(> :nth-child(8):last-child) {
    grid-template-columns: repeat(8, minmax(0, 1fr));
    --kpi-pad: clamp(10px, 1.2cqi, 14px);
    --kpi-val: clamp(17px, 2.2cqi, 24px);
  }
}
@container (min-width: 980px) {
  .kpis:has(> :nth-child(9):last-child) {
    grid-template-columns: repeat(9, minmax(0, 1fr));
    --kpi-pad: clamp(10px, 1.1cqi, 14px);
    --kpi-val: clamp(17px, 1.95cqi, 24px);
  }
}
@container (min-width: 1089px) {
  .kpis:has(> :nth-child(10):last-child) {
    grid-template-columns: repeat(10, minmax(0, 1fr));
    --kpi-pad: clamp(10px, 1.0cqi, 14px);
    --kpi-val: clamp(17px, 1.75cqi, 24px);
  }
}

.kpi { background: var(--panel); padding: var(--kpi-pad, 14px); min-width: 0; }
.kpi .k { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
.kpi .v { font-size: var(--kpi-val, 24px); font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.25; }
/* A tile holding "12 Mar 2021" rather than a number. Two thirds of the width
   of the strip is numeric, and a date set at the numeric size is the one tile
   that wraps and shoves the row taller. */
.kpi .v.date { font-size: calc(var(--kpi-val, 24px) * .72); letter-spacing: -.01em; }
.kpi .d { font-size: 12px; color: var(--muted); }
.up { color: var(--good); } .down { color: var(--bad); } .flat { color: var(--muted); }

/* ---------- freshness ----------

   How recently a card's numbers could have changed, as a tint on its edge.

   Four states, describing the pipeline rather than the data: green is rebuilt
   on the webhook delivery, blue on the ten-minute recompute, amber is the
   static build the page loads before either.

   Red is the one that is not a tier. It marks a card that should have been
   live and whose API did not answer — the data underneath is the same stale
   build as amber, but amber is by design and red is a fault. A dashboard
   silently serving yesterday's numbers looks identical to a fresh one, and
   that is exactly what this is for.

   Deliberately quiet. This is ambient state on every card at once, not an
   alert on one of them: a ring at 30-40% alpha and a hairline border, no fill,
   no glow, nothing that competes with a red number inside the card. The border
   colour is set alongside the ring so the effect survives a forced-colours or
   high-contrast mode, where box-shadow is dropped.

   `--fresh` carries the colour so the tiers differ by one line each. The base
   rule and the `fresh-down` override have equal specificity, so the override
   wins on source order — keep it last. */
.card[class*="fresh-"] {
  border-color: color-mix(in srgb, var(--fresh) 45%, var(--border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--fresh) 35%, transparent);
}
.card.fresh-instant { --fresh: var(--good); }
.card.fresh-cron    { --fresh: var(--accent); }
.card.fresh-build   { --fresh: var(--warn); }

/* Red carries a little more weight than the other three. They are ambient
   state on every card at once; this one is something that is wrong. */
.card.fresh-down {
  --fresh: var(--bad);
  border-color: color-mix(in srgb, var(--fresh) 60%, var(--border));
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--fresh) 50%, transparent);
}

/* Older browsers without color-mix get the plain border back rather than an
   invalid value, which would fall through to no border at all. */
@supports not (color: color-mix(in srgb, red 50%, blue)) {
  .card[class*="fresh-"] { border-color: var(--border); box-shadow: none; }
}
