/* ============================================================================
   Control Freak — production stylesheet
   ----------------------------------------------------------------------------
   Adopted from the styling-phase design handoff (Y1,
   docs/superpowers/plans/2026-07-24-styling.md) as the web UI's single
   global stylesheet, replacing the earlier structural-only placeholder.
   Styles the existing markup by the class names in the design brief
   (§7). No external assets, no web fonts, no network calls — system
   font stacks plus an inline-SVG data-URI caret only. Light and dark
   via prefers-color-scheme; prefers-reduced-motion disables animation.

   This task (Y1) lands the stylesheet only — no markup changes, so the
   additions it targets (dashboard-logo, environment-cost-mtd, and the
   rest listed in the plan's Y2) are not yet present in the markup and
   render as no-ops until Y2 adds them. Every value below is the
   literal value from the approved design reference.
   ============================================================================ */

/* ---------- Design tokens ---------- */
:root {
  --cf-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --cf-mono: ui-monospace, "SF Mono", "SFMono-Regular", "Cascadia Code", "Roboto Mono", Menlo, Consolas, monospace;

  --cf-bg: #eceef2;
  --cf-panel: #ffffff;
  --cf-panel-inset: #f5f6f9;
  --cf-border: #dde1e8;
  --cf-border-strong: #c6ccd6;
  --cf-fg: #1a1f28;
  --cf-fg-dim: #5a6472;
  --cf-fg-faint: #8b93a1;

  --cf-accent: #2f66e0;      --cf-accent-fg: #ffffff;
  /* State palette WCAG audit (accessibility fix — measured against this
     file's own token values, sRGB relative-luminance contrast, so the
     next person does not have to re-derive them). The chart's five-slot
     group palette already had a ΔE/deuteranopia validation of its own
     (see BURN CHART below); this palette had none until now, and every
     figure below is a real measured ratio, not an estimate.

     Text-on-its-own-soft-background pairs (the badges/counts/pills that
     set `color: var(--cf-X); background: var(--cf-X-soft)`) at their
     actual rendered sizes (11-14px, all below the WCAG "large text"
     threshold of ~18.7px/14pt) need 4.5:1. `on`/`off`/`drift` below were
     measured failing that in LIGHT mode (3.88/3.50/4.07) and are the
     defect this audit fixes — darkened just enough to clear 4.5:1 with a
     small margin, same hue/saturation, so the colour still reads as the
     same green/grey/amber. `danger` and every DARK-mode figure already
     passed and are UNCHANGED; ratios recorded for all four regardless so
     this table stays the one place to check either mode:
       LIGHT  on/on-soft       4.63:1  (was 3.88:1 — FAIL, now fixed)
       LIGHT  off/off-soft     4.64:1  (was 3.50:1 — FAIL, now fixed)
       LIGHT  drift/drift-soft 4.65:1  (was 4.07:1 — FAIL, now fixed)
       LIGHT  danger/danger-soft 4.68:1 (already passed, unchanged)
       DARK   on/on-soft       6.06:1 (already passed, unchanged)
       DARK   off/off-soft     4.51:1 (already passed, unchanged)
       DARK   drift/drift-soft 6.15:1 (already passed, unchanged)
       DARK   danger/danger-soft 4.99:1 (already passed, unchanged)

     `-solid` tokens (below) are a SEPARATE pair, for the different case
     of a solid button fill with a fixed white label
     (`.environment-action-on-button`/`.bulk-action-button` and their
     `.danger` variants): white text needs 4.5:1 against the FILL colour,
     which is the opposite direction from the soft-background case above
     (a fill light enough to read against a dark soft background, per
     DARK mode's own `on`/`danger`, is too light for white text to read
     against IT — measured white-text-on-fill was 2.52:1/3.44:1 in DARK
     mode, both failing). LIGHT mode's plain `on`/`danger` already clear
     4.5:1 for white text (5.24:1/5.49:1) so the `-solid` tokens alias
     them there with no separate value; only DARK needs its own, darker
     pair. */
  /* The accent's own soft companion, for a pill that is neither on nor
     off: a resource switched on and not yet serving. Same relationship
     to `--cf-accent` as the four below have to their own colours. */
  --cf-accent-soft: #e7eefc;
  --cf-on: #197c45;          --cf-on-soft: #e8f4ec;
  --cf-on-solid: var(--cf-on);
  --cf-off: #646c79;         --cf-off-soft: #eef0f4;
  --cf-drift: #9b5c00;       --cf-drift-soft: #f9eed9;
  --cf-danger: #c1362a;      --cf-danger-soft: #fbe9e6;
  --cf-danger-solid: var(--cf-danger);

  --cf-shadow: 0 1px 2px rgba(20,26,35,.05), 0 1px 3px rgba(20,26,35,.04);
  --cf-radius: 12px;

  /* The distance from a viewport edge to the centred column's content
     edge: half the leftover width, plus the column's own 24px padding,
     and never less than that padding on a narrow viewport where there is
     no leftover. Full-bleed elements use it to hold their contents in
     line with the column while their own background runs edge to edge.
     Kept in step with `.dashboard-main`'s max-width and padding. */
  --cf-page-gutter: max(24px, calc((100vw - 1160px) / 2 + 24px));

  /* The centred column's own content width — what a full-bleed element
     must be at LEAST as wide as, so it lines up with everything above it
     when its own contents do not push it wider. Kept in step with
     `.dashboard-main`'s max-width and padding, as the gutter is. */
  --cf-content-width: min(1112px, calc(100vw - 48px));

  /* Density — override the two vars to switch to compact */
  --cf-card-pad: 18px;
  --cf-row-gap: 15px;
  --cf-card-w: 340px;

  /* Swim-lane edge-scroll shadow (plan N3) — a dark, low-opacity mark in
     light mode; overridden to a light one in dark mode below, since a
     dark shadow reads as no shadow at all against a near-black panel. */
  --cf-lane-edge: rgba(20,26,35,.22);

  /* Burn-chart group identity palette (plan N5) — the plan's own
     ΔE-validated table, LIGHT values, in this exact order (never
     cycled, never reused for the on/off/drift/danger/accent status
     palette above). A group beyond the 5th slot never gets a generated
     hue — see style.css's own BURN CHART section and `crate::charts`'
     module documentation for the fold-to-`Other` rule this neutral
     6th value exists for. */
  --cf-group-other: var(--cf-fg-faint);
}

@media (prefers-color-scheme: dark) {
  :root {
    --cf-bg: #0b0f15;
    --cf-panel: #151b24;
    --cf-panel-inset: #101620;
    --cf-border: #232c39;
    --cf-border-strong: #33404f;
    --cf-fg: #e6edf5;
    --cf-fg-dim: #98a4b3;
    --cf-fg-faint: #68727f;

    --cf-accent: #4d87ff;    --cf-accent-fg: #0b0f15;
    --cf-accent-soft: #12203a;
    --cf-on: #42b95a;        --cf-on-soft: #122a1b;
    --cf-off: #7d8898;       --cf-off-soft: #1a212b;
    --cf-drift: #d99320;     --cf-drift-soft: #2a2110;
    --cf-danger: #f0554c;    --cf-danger-soft: #2c1512;
    /* Darker than the plain `on`/`danger` above on purpose — see the
       `:root` block's own comment for why a solid-fill white-text button
       needs a different shade from the soft-background text case in DARK
       mode specifically. Measured: white text on `on-solid` 4.61:1 (was
       2.52:1 against plain `on` — FAIL); white text on `danger-solid`
       4.63:1 (was 3.44:1 against plain `danger` — FAIL). */
    --cf-on-solid: #2f8541;
    --cf-danger-solid: #e51f13;

    --cf-shadow: 0 1px 2px rgba(0,0,0,.45), 0 2px 8px rgba(0,0,0,.3);
    --cf-lane-edge: rgba(255,255,255,.14);

    /* Dark values from the plan's own table — separately stepped, not
       an automatic flip of the light values (plan N5: "Dark is
       separately stepped, not an automatic flip"). */
                    --cf-group-other: var(--cf-fg-faint);
  }
}

/* ---------- Base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body {
  background: var(--cf-bg);
  color: var(--cf-fg);
  font-family: var(--cf-sans);
  font-size: 14px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}
a { color: var(--cf-accent); text-decoration: none; }
a:hover { text-decoration: underline; }

@keyframes cf-pulse { 0%,100% { opacity: .5; } 50% { opacity: 1; } }
@keyframes cf-fade { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
@keyframes cf-progress { 0% { transform: translateX(-100%); } 100% { transform: translateX(280%); } }
@keyframes cf-body { from { opacity: .4; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { * { animation: none !important; } }

/* ============================================================================
   LOGIN (§6.1) — bare elements styled by descendant selectors
   ============================================================================ */
.login-page {
  min-height: 100vh;
  display: flex; align-items: center; justify-content: center;
  padding: 24px;
}
.login-page form {
  width: 100%; max-width: 360px;
  display: flex; flex-direction: column; gap: 16px;
  background: var(--cf-panel);
  border: 1px solid var(--cf-border);
  border-radius: 14px;
  box-shadow: var(--cf-shadow);
  padding: 28px 26px;
}
.login-page .login-header {
  display: flex; align-items: center; gap: 11px;
}
.login-page h1 {
  margin: 0;
  font-family: var(--cf-mono);
  font-size: 19px; font-weight: 650; letter-spacing: .01em;
}
.login-page .login-subtitle {
  font-size: 12.5px; color: var(--cf-fg-faint); margin-top: -6px;
}
.login-page input {
  width: 100%;
  font-family: var(--cf-sans); font-size: 14px;
  color: var(--cf-fg);
  background: var(--cf-panel-inset);
  border: 1px solid var(--cf-border-strong);
  border-radius: 8px;
  padding: 11px 13px;
  outline: none;
}
.login-page input:focus { border-color: var(--cf-accent); }
/* Accessibility fix: the base rule's `outline: none` left keyboard focus
   on these two fields signalled only by a 1px border-colour change —
   easy to miss, and weaker than the explicit outline this stylesheet
   already uses elsewhere for focus visibility (e.g. `.environment-lane-
   scroll:focus-visible`, `.cost-chart-hover-target:focus-visible`).
   `:focus-visible` (not plain `:focus`) so a mouse click still shows only
   the subtle border change above, matching the existing behaviour, while
   keyboard focus gets the same explicit ring every other custom-focus
   element in this file uses. */
.login-page input:focus-visible { outline: 2px solid var(--cf-accent); outline-offset: 1px; }
.login-page button[type="submit"] {
  width: 100%;
  font-family: var(--cf-sans); font-size: 14px; font-weight: 650;
  padding: 11px 14px;
  border: 1px solid transparent; border-radius: 8px;
  background: var(--cf-accent); color: var(--cf-accent-fg);
  cursor: pointer;
}
/* Proposed submitting state (see README §Behavior) */
.login-page input:disabled,
.login-page button[type="submit"]:disabled { opacity: .55; cursor: progress; }

/* ============================================================================
   DASHBOARD SHELL (§6.2)
   ============================================================================ */
/* `overflow-x: clip`, not `hidden`: the swim lanes bleed to the right
   viewport edge with a `50vw` margin, which counts the scrollbar and so
   can overshoot by half its width on a classic (non-overlay) scrollbar.
   `clip` absorbs that without making this element a scroll container —
   `hidden` would, and that would break `position: sticky` inside it. */
.dashboard-page { min-height: 100vh; overflow-x: clip; }

.dashboard-header {
  position: sticky; top: 0; z-index: 20;
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 24px;
  background: var(--cf-bg);
  border-bottom: 1px solid var(--cf-border);
}
.dashboard-title {
  font-family: var(--cf-mono);
  font-size: 15px; font-weight: 650; letter-spacing: .01em;
}
/* Optional status LED before the title — add <span class="dashboard-logo"> (see README) */
.dashboard-logo {
  width: 11px; height: 11px; border-radius: 3px;
  background: var(--cf-on); box-shadow: 0 0 0 3px var(--cf-on-soft);
  flex: none;
}
.dashboard-brand { display: flex; align-items: center; gap: 11px; }
.dashboard-actions { display: flex; align-items: center; gap: 14px; }
.dashboard-user {
  font-family: var(--cf-mono); font-size: 12.5px; color: var(--cf-fg-dim);
  white-space: nowrap;
}
/* Guards the "Signed in as " momentary gap (§6.2): username width never
   shifts the layout because the block is left-of-fixed order, not right-anchored. */
.dashboard-user strong,
.dashboard-user .dashboard-user-name { color: var(--cf-fg); font-weight: 600; }

.dashboard-body { display: flex; flex-direction: column; gap: 22px; animation: cf-body .35s ease; }

/* Content column. Accessibility note: before the markup fix that added a
   real `<main class="dashboard-main">`, the `> main` half of this selector
   never matched anything (no `<main>` was ever rendered) — only the
   `.dashboard-main` class half was live, so this rule was already applying
   via the descendant selector and the markup fix changes no layout, only
   adds the landmark the `> main` half was written for. */
.dashboard-page > main,
.dashboard-page .dashboard-main {
  max-width: 1160px; margin: 0 auto; padding: 22px 24px 40px;
  display: flex; flex-direction: column; gap: 22px;
}

/* ============================================================================
   BANNERS + EMPTY (§5.1, §5.2)
   ============================================================================ */
.banner {
  padding: 11px 15px; border-radius: 9px;
  font-size: 13px; display: flex; align-items: center; gap: 10px;
}
.banner.loading {
  background: var(--cf-panel-inset);
  border: 1px solid var(--cf-border);
  color: var(--cf-fg-dim);
  font-family: var(--cf-mono);
  animation: cf-pulse 1.4s ease-in-out infinite;
}
.banner.loading::before {
  content: ""; width: 8px; height: 8px; border-radius: 50%;
  background: var(--cf-fg-faint); flex: none;
}
.banner.error {
  background: var(--cf-danger-soft);
  border: 1px solid var(--cf-danger);
  color: var(--cf-danger);
}
.banner.error::before { content: "\26A0"; font-weight: 700; } /* ⚠ */
/* Y5 addition: the delivered `.banner` family only ships `.loading`/`.error`;
   the history panel's partial-read note (malformed log entries, skipped
   server-side but never silent) needed a third, less severe register —
   extended from the existing family using the drift token already defined
   for "state is indeterminate/incomplete", not a new colour. */
.banner.warning {
  background: var(--cf-drift-soft);
  border: 1px solid var(--cf-drift);
  color: var(--cf-drift);
}
.banner.warning::before { content: "\26A0"; font-weight: 700; } /* ⚠ */
/* Feature: per-environment error isolation. An org-wide read no longer fails
   outright because one environment could not be read — it reports that
   environment and answers for every other one — so the page needs somewhere
   to NAME the ones it could not read. Extended from the existing
   `.banner.error` family rather than given a vocabulary of its own: it is an
   error banner that happens to carry a list, so the only additions are the
   column layout a list needs and the monospace name/reason pairing every
   other listing on this page already uses. */
.unreadable-environments { flex-direction: column; align-items: stretch; gap: 8px; }
.unreadable-environments-title {
  margin: 0; font-family: var(--cf-sans); font-size: 13px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
}
.unreadable-environments-list {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 4px;
}
.unreadable-environment { display: flex; gap: 8px; flex-wrap: wrap; font-size: 12px; }
.unreadable-environment-name { font-family: var(--cf-mono); font-weight: 650; }
.unreadable-environment-reason { font-family: var(--cf-mono); opacity: .85; }

/* The shared-resource counterpart, sharing every rule above rather than
   restating them: it is the same banner reporting the same kind of gap, and
   two panels that drifted apart visually would read as two different
   severities of problem when they are one. */
.unreadable-shared-resources { flex-direction: column; align-items: stretch; gap: 8px; }
.unreadable-shared-resources-title {
  margin: 0; font-family: var(--cf-sans); font-size: 13px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
}
.unreadable-shared-resources-list {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 4px;
}
.unreadable-shared-resource { display: flex; gap: 8px; flex-wrap: wrap; font-size: 12px; }
.unreadable-shared-resource-name { font-family: var(--cf-mono); font-weight: 650; }
.unreadable-shared-resource-reason { font-family: var(--cf-mono); opacity: .85; }

/* Same list shape again, in the WARNING register rather than the error one:
   a refused list call costs calls, not correctness, and a page that shouted
   about it in red would send an operator hunting for a broken environment
   instead of granting a list permission. */
.degraded-scopes { flex-direction: column; align-items: stretch; gap: 8px; }
.degraded-scopes-title {
  margin: 0; font-family: var(--cf-sans); font-size: 13px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
}
.degraded-scopes-list {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 4px;
}
.degraded-scope { display: flex; gap: 8px; flex-wrap: wrap; font-size: 12px; }
.degraded-scope-name { font-family: var(--cf-mono); font-weight: 650; }
.degraded-scope-reason { font-family: var(--cf-mono); opacity: .85; }

.empty-state {
  padding: 22px; text-align: center;
  border: 1px dashed var(--cf-border-strong); border-radius: 9px;
  color: var(--cf-fg-dim); font-size: 13px;
}

/* ============================================================================
   BURN-RATE PANEL (§6.3)
   ============================================================================ */
.burn-rate-panel {
  background: var(--cf-panel); border: 1px solid var(--cf-border);
  border-radius: var(--cf-radius); box-shadow: var(--cf-shadow);
  padding: 20px 22px;
  display: flex; flex-direction: column; gap: 16px;
}
.burn-rate-totals {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 14px;
}
.burn-rate-total {
  display: flex; flex-direction: column; gap: 4px;
  padding: 12px 14px; background: var(--cf-panel-inset); border-radius: 9px;
}
/* Cumulative "Month to date" total is visually distinguished from the rate totals */
.burn-rate-total-mtd { box-shadow: inset 3px 0 0 var(--cf-accent); }
.burn-rate-total-label {
  font-size: 11px; font-weight: 600; letter-spacing: .04em;
  text-transform: uppercase; color: var(--cf-fg-faint);
}
.burn-rate-total-value {
  font-family: var(--cf-mono); font-size: 26px; font-weight: 650;
  color: var(--cf-fg); font-variant-numeric: tabular-nums; line-height: 1.1;
}
/* No `border-top` any more: it separated this from the headline figures
   when the two shared one panel, and once the chart moved between them
   (0.11.5) it became a rule across the top of a box's own content —
   a divider dividing nothing. */
.burn-rate-split {
  display: flex; gap: 18px; font-family: var(--cf-mono); font-size: 13px;
}
.burn-rate-split-owned, .burn-rate-split-shared { color: var(--cf-fg-dim); }
.burn-rate-split-owned strong, .burn-rate-split-shared strong { color: var(--cf-fg); font-weight: 600; }
/* `Unattributed` (multi-tenant topologies) appears only where a GCP project
   is claimed by more than one environment, so its spend belongs to none of
   them individually. Dimmed like the other two components, and dotted-
   underlined because it carries a `title` explaining itself — the term is
   unfamiliar by design, and a reader who has never seen it needs the
   affordance to ask. */
.burn-rate-split-unattributed {
  color: var(--cf-fg-dim); text-decoration: underline dotted; text-underline-offset: 3px; cursor: help;
}
/* `Total` (plan C3) joins Owned/Shared on the same line so a reader can
   check `owned + shared = total` without looking back up at the headline
   row — full-strength foreground and the same weight as the headline
   values, distinct from the two dimmer components either side of it. */
.burn-rate-split-total { color: var(--cf-fg); font-weight: 650; }

.environment-counts { display: flex; gap: 8px; }
.environment-count {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 10px; border-radius: 999px;
  font-size: 12px; font-weight: 650; font-variant-numeric: tabular-nums;
}
.environment-count-on      { color: var(--cf-on);    background: var(--cf-on-soft); }
.environment-count-off     { color: var(--cf-off);   background: var(--cf-off-soft); }
.environment-count-partial { color: var(--cf-drift); background: var(--cf-drift-soft); }
/* Redundant-with-color shape markers (§4) — prepend via ::before if the counts
   render as bare "2 on" text with no glyph in markup. */
.environment-count-on::before      { content: "\25CF "; }  /* ● */
.environment-count-off::before     { content: "\25CB "; }  /* ○ */
.environment-count-partial::before { content: "\25B2 "; }  /* ▲ */

.org-next-expiry { font-family: var(--cf-mono); font-size: 12.5px; color: var(--cf-fg-dim); }
.org-next-expiry strong,
.org-next-expiry .org-next-expiry-value { color: var(--cf-fg); }

/* ============================================================================
   SHARED RESOURCES — org-wide section (plan C3)
   ----------------------------------------------------------------------------
   Not in the delivered design handoff at all — the wire fields these cards
   render (state/cost/reference-count) landed with this phase's own
   predecessor (plan C2). Built from the delivered system's own primitives
   per Y5's established "extend, don't invent" instruction: the burn-rate
   panel's own label typography, the environment card's own card shape/
   shadow/radius (`.environment-card`), and `.resource-state-badge`
   (already styled above) for each card's live state — no new colour
   vocabulary. Sits directly below the burn-rate panel so the `Shared:`
   split figure has an itemised breakdown right underneath it to check
   against, one card per shared resource, counted exactly once.
   ============================================================================ */
.shared-resources-panel { display: flex; flex-direction: column; gap: 14px; }
.shared-resources-title {
  margin: 0; font-family: var(--cf-sans); font-size: 13px; font-weight: 650;
  color: var(--cf-fg-dim); text-transform: uppercase; letter-spacing: .04em;
}
.shared-resources-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
}
.shared-resource-card {
  display: flex; flex-direction: column; gap: 8px;
  padding: 14px 16px;
  border: 1px solid var(--cf-border); border-radius: var(--cf-radius);
  background: var(--cf-panel); box-shadow: var(--cf-shadow);
  min-width: 0;
}
/* State treatment, matching the environment cards: the badge said "on"
   while the card stayed the colour of an off one, so the two disagreed
   about the same fact on the surface whose job is showing what is on. */
/* A shared resource being switched, which the API cannot tell us: it
   reports on/off/unknown and has no state for one part-way through a
   start, so a starting database reads as OFF. The card is accented off
   the PRESENCE of its progress bar rather than a modifier class of its
   own — one fact, stated once in the markup, with no second name to keep
   in step and nothing for the design-class audit to reconstruct. */
.shared-resource-card:has(.switch-progress-bar) { border-color: var(--cf-accent); }

.shared-resource-card-on      { border-color: var(--cf-on); background: var(--cf-on-soft); }
.shared-resource-card-off     { background: var(--cf-panel); }
.shared-resource-card-unknown { border-style: dashed; }

.shared-resource-card-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.shared-resource-card-name {
  font-family: var(--cf-mono); font-size: 13px; font-weight: 650; color: var(--cf-fg);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shared-resource-card-cost {
  font-family: var(--cf-mono); font-size: 13px; font-weight: 600;
  color: var(--cf-fg); font-variant-numeric: tabular-nums;
}
.shared-resource-card-references { font-family: var(--cf-sans); font-size: 11px; color: var(--cf-fg-faint); }
/* HOLDS (feature: shared-resource holds)
   ----------------------------------------------------------------------------
   Not in the delivered design handoff (it postdates it), so built entirely
   from this system's own primitives — the hold line takes the same faint
   caption treatment as the reference-count line above it, but in the accent
   colour, because it names a claim somebody deliberately took rather than a
   fact about the topology; the action row reuses `.environment-action-button`
   and `.action-duration-select` verbatim, so a hold control and a card's own
   On/Off controls can never drift apart visually. */
.shared-resource-card-hold {
  margin: 0; font-family: var(--cf-sans); font-size: 11px; color: var(--cf-accent);
}
.shared-resource-card-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  margin-top: auto; padding-top: 10px; border-top: 1px solid var(--cf-border);
}

/* ============================================================================
   BURN CHART (plan N5)
   ----------------------------------------------------------------------------
   Not in the delivered design handoff at all (it postdates it) — built
   from this system's own panel/title/note primitives, per the same
   "extend, don't invent" instruction the swim-lanes/history/provenance
   sections above already follow. Inline SVG only, no charting library
   (strict CSP, `crate::charts`' own module documentation).

   One axis, one unit: bars (`.cost-chart-bar`) and lines
   (`.cost-chart-line`) share the SAME `£/day` scale — there is
   deliberately no second colour/position system implying a second axis.
   Gridlines stay SOLID and recessive (`--cf-border`, thin) — dashing is
   reserved for "projected" and must not be spent on the grid.

   Solid = observed, dashed = projected, stated once in the legend
   (`.cost-chart-legend-note`) rather than repeated per mark. The
   `--chart-color` custom property is what a mark's own base class reads
   its stroke/fill from, and it is set INLINE per mark by charts.rs from
   a hue derived from the group's own name — there is no numbered
   palette here to modify it with any more. A swim lane's sparkline
   derives from the same name, so a lane and its headline line match by
   construction rather than by two lists agreeing.
   ============================================================================ */
/* The next expiry names what expires, with the rest behind a disclosure
   (owner request, 2026-08-12): a bare timestamp said when without saying
   what, which could not be acted on without going to look. */
.org-next-expiry-line { margin: 0; }
.org-expiry-details { margin-top: 6px; }
.org-expiry-summary {
  font-family: var(--cf-sans); font-size: 12px; color: var(--cf-fg-dim);
  cursor: pointer;
}
.org-expiry-list {
  list-style: none; margin: 6px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 3px;
}
.org-expiry-row {
  display: flex; justify-content: space-between; gap: 12px;
  font-family: var(--cf-sans); font-size: 12px;
}
.org-expiry-name { color: var(--cf-fg); }
.org-expiry-when { color: var(--cf-fg-dim); font-variant-numeric: tabular-nums; }

/* The burn-rate panel is drawn in TWO parts with the chart between them
   (owner request, 2026-08-12): the headline per-hour/day/month figures
   lead the page, the chart follows immediately, and the split line,
   counts and next expiry read underneath the chart they explain. Both
   halves keep the panel's own chrome so they read as one surface
   interrupted, rather than as two unrelated boxes. */
.burn-rate-panel-totals { padding-bottom: 14px; }
.burn-rate-panel-detail { padding-top: 14px; }

/* Both halves of the split panel carry a title, and the second one needs
   its own: sitting under the chart with a period selector above it, an
   untitled box reads as belonging to the SELECTED period, which it never
   does — these figures are always live. */
.burn-rate-panel-title, .dashboard-section-title {
  margin: 0 0 10px; font-family: var(--cf-sans); font-size: 13px; font-weight: 650;
  color: var(--cf-fg-dim); text-transform: uppercase; letter-spacing: .04em;
}

/* The burn chart's own period selector, sitting immediately above the
   panel it controls rather than in the page header — it changes one
   chart, not the page, and a control's place is beside what it acts on.
   Its own read is separate from the estate's (dashboard.rs's
   `cost_series_data`), so changing the month redraws the chart without
   re-reading the cloud. */
.dashboard-chart-period {
  display: flex; align-items: center; gap: 8px; margin-bottom: -4px;
}
.dashboard-chart-period-label {
  font-family: var(--cf-sans); font-size: 12px; font-weight: 650;
  color: var(--cf-fg-dim); text-transform: uppercase; letter-spacing: .04em;
}
.dashboard-chart-period-select {
  font-family: var(--cf-sans); font-size: 13px;
  padding: 5px 10px; border-radius: 7px;
  border: 1px solid var(--cf-border);
  background: var(--cf-panel); color: var(--cf-fg); cursor: pointer;
}
.cost-chart-panel {
  background: var(--cf-panel); border: 1px solid var(--cf-border);
  border-radius: var(--cf-radius); box-shadow: var(--cf-shadow);
  padding: 18px 20px;
  display: flex; flex-direction: column; gap: 12px;
}
/* The chart's heading line carries its title and the period's own total,
   pushed apart — the total belongs beside the thing it totals, and the
   line was already there. It wraps rather than truncating: a long
   currency name and a large figure must not push the title out of view
   on a narrow screen. */
.cost-chart-heading {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: 8px 16px;
}
.cost-chart-total {
  font-family: var(--cf-sans); font-size: 12px; color: var(--cf-fg-dim);
}
.cost-chart-total-value {
  font-family: var(--cf-mono); color: var(--cf-fg); font-variant-numeric: tabular-nums;
}
.cost-chart-title {
  margin: 0; font-family: var(--cf-sans); font-size: 13px; font-weight: 650;
  color: var(--cf-fg-dim); text-transform: uppercase; letter-spacing: .04em;
}
.cost-chart-frame { position: relative; }
.cost-chart-svg { display: block; width: 100%; height: 200px; }

/* Structural grouping only — no rule of their own needed beyond keeping
   the static marks out of the pointer's way, so the hover-target rects
   layered on top (last, so they win hit-testing) are what the mouse
   actually reaches. */
.cost-chart-gridlines, .cost-chart-bars, .cost-chart-lines, .cost-chart-day-labels {
  pointer-events: none;
}
/* The hover-target rects layered last, on top of the static marks above
   — the one group in this chart that DOES want the pointer. */
.cost-chart-hover-targets { pointer-events: auto; }

.cost-chart-gridline { stroke: var(--cf-border); stroke-width: 1; }
.cost-chart-axis-label {
  font-family: var(--cf-mono); font-size: 9px; fill: var(--cf-fg-faint); text-anchor: end;
}
.cost-chart-day-label { font-family: var(--cf-mono); font-size: 9px; fill: var(--cf-fg-faint); text-anchor: start; }
.cost-chart-day-label-last { text-anchor: end; }

/* Bars dominate, recessive against the thin identity lines drawn over
   them (plan N5: "thin against dominant, recessive bars"). */
.cost-chart-bar { fill: var(--cf-border-strong); }
.cost-chart-bar-projected { fill: none; stroke: var(--cf-border-strong); stroke-width: 1; stroke-dasharray: 3 2; }

.cost-chart-line { fill: none; stroke: var(--chart-color); stroke-width: 1.75; }
.cost-chart-line-dashed { fill: none; stroke: var(--chart-color); stroke-width: 1.75; stroke-dasharray: 4 3; }

/* Focusable as well as hoverable (`tabindex` in `crate::charts`), so the
   crosshair and its figures are reachable without a pointer exactly as
   each swim-lane's own scroll strip is — with the same explicit outline,
   since a browser's own default on an SVG rect is inconsistent at best. */
.cost-chart-hover-target { fill: transparent; cursor: crosshair; }
.cost-chart-hover-target:focus-visible {
  outline: 2px solid var(--cf-accent); outline-offset: -2px;
}
.cost-chart-crosshair { stroke: var(--cf-fg-faint); stroke-width: 1; pointer-events: none; }

.cost-chart-tooltip-anchor {
  position: absolute; top: 4px; transform: translateX(-50%);
  pointer-events: none; z-index: 5;
}
.cost-chart-tooltip {
  min-width: 150px; padding: 8px 10px;
  background: var(--cf-panel); border: 1px solid var(--cf-border-strong); border-radius: 8px;
  box-shadow: var(--cf-shadow); font-size: 11.5px;
}
.cost-chart-tooltip-date {
  margin: 0 0 4px; font-family: var(--cf-mono); font-weight: 650; color: var(--cf-fg);
}
.cost-chart-tooltip-row {
  margin: 0; display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.cost-chart-tooltip-label { color: var(--cf-fg-dim); }
.cost-chart-tooltip-value {
  font-family: var(--cf-mono); color: var(--cf-fg); font-variant-numeric: tabular-nums;
}

.cost-chart-legend {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  padding-top: 10px; border-top: 1px solid var(--cf-border);
  font-size: 12px; color: var(--cf-fg-dim);
}
.cost-chart-legend-note { font-family: var(--cf-mono); }
.cost-chart-legend-note-solid { color: var(--cf-fg-dim); }
.cost-chart-legend-note-dashed { color: var(--cf-fg-faint); }
.cost-chart-legend-item {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--cf-sans); font-size: 12px; color: var(--cf-fg-dim);
  background: transparent; border: none; padding: 0; cursor: pointer;
}
.cost-chart-legend-item[aria-pressed="false"] { opacity: .4; text-decoration: line-through; }
.cost-chart-legend-swatch {
  display: inline-block; width: 10px; height: 10px; border-radius: 2px;
  background: var(--chart-color); flex: none;
}
/* The scope note takes a line of its own below the rest of the legend
   (`flex-basis: 100%`): the bars are org-wide while the group lines
   exclude shared resources, and a reader must not have to infer that the
   lines fail to decompose the bars. */
.cost-chart-legend-scope {
  flex-basis: 100%; font-size: 11px; color: var(--cf-fg-faint); font-style: italic;
}

/* The group-identity colour palette (plan N5) — sets `--chart-color` for
   whichever base class (`.cost-chart-line`, `.cost-chart-line-dashed`,
   `.cost-chart-legend-swatch`, `.environment-lane-sparkline-line`) also
   carries it. Fixed order, never cycled; a 6th-plus group's sparkline
   (the headline chart itself never draws one — see `crate::charts`' own
   module documentation) uses the shared neutral `other` value, never a
   generated hue. */

/* Series colour is DERIVED from the group's own name and applied as an
   inline `--chart-color` (charts.rs), so there is no numbered palette
   here any more — only the neutral swatch, for the shared-resources
   line and anything else that is not a group. */
.cost-chart-line.cost-chart-line.cost-chart-line.cost-chart-line
/* ============================================================================
   DASHBOARD FILTERS AND SORT
   ----------------------------------------------------------------------------
   Not in the delivered design handoff — a later feature addition, built
   from the system's own primitives (`.action-duration-select`'s own
   select chrome/caret; `.login-page input`'s own text-input chrome) per
   the same "extend, don't invent" instruction the swim-lanes/history
   sections above already follow. Purely client-side (`pages::dashboard`'s
   own module documentation): typing, re-selecting a state, or choosing a
   sort order never issues a new fetch, only re-filters/re-sorts the
   already-loaded environment list. `.dashboard-filter-sort` shares
   `.dashboard-filter-state`'s own select chrome — same control family,
   same visual weight.
   ============================================================================ */
.dashboard-filters {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
}
.dashboard-filter-search {
  flex: 1 1 220px; min-width: 160px;
  font-family: var(--cf-sans); font-size: 13.5px;
  color: var(--cf-fg);
  background: var(--cf-panel-inset);
  border: 1px solid var(--cf-border-strong); border-radius: 8px;
  padding: 9px 12px;
  outline: none;
}
.dashboard-filter-search:focus { border-color: var(--cf-accent); }
/* Accessibility fix — see `.login-page input:focus-visible`'s own comment;
   identical reasoning, this is the other place `outline: none` suppressed
   the native ring with only a border-colour change left in its place. */
.dashboard-filter-search:focus-visible { outline: 2px solid var(--cf-accent); outline-offset: 1px; }
.dashboard-filter-state, .dashboard-filter-sort {
  font-family: var(--cf-sans); font-size: 13.5px; color: var(--cf-fg);
  background-color: var(--cf-panel-inset);
  border: 1px solid var(--cf-border-strong); border-radius: 8px;
  padding: 9px 30px 9px 12px; cursor: pointer;
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238b93a1' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 11px center;
}
.dashboard-filter-search:focus, .dashboard-filter-state:focus, .dashboard-filter-sort:focus { border-color: var(--cf-accent); }
/* Sits directly above whichever body it describes (flat list, swim-lanes,
   or a filtered-to-nothing empty state) — same dim caption weight as
   `.environment-lane-stats`, not a heading. */
.dashboard-filter-summary {
  margin: -8px 0 0; font-size: 12px; color: var(--cf-fg-dim);
}

/* ============================================================================
   BULK ACTION BAR
   ----------------------------------------------------------------------------
   Not in the delivered design handoff either — a later feature addition,
   sitting immediately under the filter row because that is what it acts
   on: exactly the environments the filter leaves visible, with the count
   in the buttons' own labels. Built from the action panel's own
   primitives (`.environment-action-button`'s shape and weight, the
   shared `danger` modifier, `.environment-action-confirm-group`'s armed
   red inset) so a fleet switch reads as the same kind of act as a single
   card's, only bigger — never as a new visual language.

   The armed confirmation is deliberately the LOUDER of the two: it is
   the same two-click guard a single card's Off carries, but the thing on
   the other side of the second click is the whole visible list.

   The report below the bar is a `.banner`-weight panel rather than a
   toast: a toast dismisses itself, and a partial run ("3 of 5, 1
   read-only, 1 failed") is exactly the outcome an operator needs to
   still be on screen while they go and look at the failures it names.
   ============================================================================ */
.dashboard-bulk { display: flex; flex-direction: column; gap: 10px; }
.dashboard-bulk-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.bulk-actions-empty { margin: 0; font-size: 12px; color: var(--cf-fg-dim); }

/* The bulk switch-on duration picker, beside the button it qualifies.
   Defaults to each environment's own configured session, so the common
   case needs no choice made and an override is one click away. */
.bulk-duration { display: inline-flex; align-items: center; gap: 6px; }
.bulk-duration-label {
  font-family: var(--cf-sans); font-size: 12px; color: var(--cf-fg-dim);
}
.bulk-duration-select {
  font-family: var(--cf-sans); font-size: 13px;
  padding: 6px 8px; border-radius: 7px;
  border: 1px solid var(--cf-border);
  background: var(--cf-panel); color: var(--cf-fg); cursor: pointer;
}

.bulk-action-button {
  font-family: var(--cf-sans); font-size: 13px; font-weight: 600;
  padding: 7px 14px; border-radius: 7px; border: 1px solid transparent;
  line-height: 1; cursor: pointer; transition: filter .12s, opacity .12s;
  background: var(--cf-on-solid); color: #fff;
}
.bulk-action-button.danger { background: var(--cf-danger-solid); color: #fff; }
.bulk-action-button:hover:not(:disabled) { filter: brightness(1.06); }
/* In-flight disabled, matching the per-card controls exactly */
.bulk-action-button:disabled { opacity: .4; cursor: not-allowed; filter: saturate(.4); }

.bulk-action-confirm-group {
  display: flex; align-items: center; gap: 7px;
  padding: 4px 5px 4px 10px;
  background: var(--cf-danger-soft); border: 1px solid var(--cf-danger); border-radius: 8px;
}
.bulk-action-confirm-prompt { font-size: 12.5px; font-weight: 650; color: var(--cf-danger); }
.bulk-action-cancel-button {
  font-family: var(--cf-sans); font-size: 12.5px; font-weight: 600;
  padding: 6px 11px; border-radius: 7px;
  background: transparent; color: var(--cf-fg-dim);
  border: 1px solid var(--cf-border-strong); line-height: 1; cursor: pointer;
}

.bulk-report {
  position: relative;
  padding: 11px 34px 11px 13px;
  border: 1px solid var(--cf-border); border-left: 3px solid var(--cf-accent);
  border-radius: var(--cf-radius); background: var(--cf-panel);
}
.bulk-report-summary { margin: 0; font-size: 13px; color: var(--cf-fg); }
.bulk-report-failures { margin: 7px 0 0; padding-left: 18px; }
.bulk-report-failure {
  font-family: var(--cf-mono); font-size: 12px; color: var(--cf-danger); word-break: break-word;
}
.bulk-report-dismiss {
  position: absolute; top: 7px; right: 9px;
  background: none; border: none; cursor: pointer;
  font-size: 15px; line-height: 1; color: var(--cf-fg-dim);
}

/* ============================================================================
   ENVIRONMENT LIST + CARD (§6.4, §6.5)
   ============================================================================ */
.environment-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--cf-card-w), 1fr));
  gap: 16px; align-items: stretch;
}

.environment-card {
  position: relative;
  display: flex; flex-direction: column; gap: var(--cf-row-gap);
  padding: var(--cf-card-pad);
  border: 1px solid var(--cf-border);
  border-left: 3px solid var(--cf-border-strong);
  border-radius: var(--cf-radius);
  background: var(--cf-panel);
  box-shadow: var(--cf-shadow);
  /* Grid item: the .environment-list grid sizes the card, filling the row
     in equal columns at least --cf-card-w wide (owner preference over the
     brief's fixed-width cards, so the card row matches the burn-rate panel
     width above it). */
  min-width: 0;
}
/* Card-level state treatment (independent of the badge, §6.5) */
.environment-card-on      { border-left-color: var(--cf-on);    background: var(--cf-on-soft); }
.environment-card-partial { border-left-color: var(--cf-drift); background: var(--cf-drift-soft); }
.environment-card-off     { border-left-color: var(--cf-border-strong); background: var(--cf-panel); }
/* Off cards recede: greyed heading */
.environment-card-off .environment-title { color: var(--cf-fg-faint); }

/* Y5 note: `justify-content: space-between` was correct for the delivered
   2-child header (title, badge); the card's own "History" button (spec
   addendum §1, plan O3/Y2) is a third child the design never anticipated.
   Giving the title `flex: 1 1 auto` so it absorbs the free space keeps the
   badge and the new button tight together at the right edge instead of
   space-between spreading three items evenly across the row. */
.environment-card-header {
  display: flex; align-items: flex-start; gap: 8px;
}
.environment-title {
  flex: 1 1 auto; min-width: 0;
  margin: 0; font-family: var(--cf-mono);
  font-size: 14px; font-weight: 650; color: var(--cf-fg); word-break: break-word;
}

/* -- State badge (§6.5.1) -- */
.environment-state-badge, .resource-state-badge {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px; border-radius: 999px;
  font-size: 11px; font-weight: 650; line-height: 1.4; white-space: nowrap;
}
/* Switched on, but not yet SERVING — the minutes a database spends
   between its instance coming up and accepting a connection. Accented
   rather than green, because green beside an environment that is still
   visibly booting is true about the switch and misleading about the
   resource. */
.resource-state-badge-starting { color: var(--cf-accent); background: var(--cf-accent-soft); }

.environment-state-badge-on,  .resource-state-badge-on  { color: var(--cf-on);    background: var(--cf-on-soft); }
.environment-state-badge-off, .resource-state-badge-off { color: var(--cf-off);   background: var(--cf-off-soft); }
.environment-state-badge-partial { color: var(--cf-drift); background: var(--cf-drift-soft); white-space: normal; }
/* A state nobody could READ, which is neither of the other two: dashed, faint
   and wordy on purpose, matching `.shared-resource-state-unknown` so the same
   fact reads the same way wherever it is drawn. */
.resource-state-badge-unknown {
  color: var(--cf-fg-faint); background: var(--cf-panel);
  border: 1px dashed var(--cf-border-strong); white-space: normal;
}
/* Shape markers so state survives desaturation (§4). Drift already carries the
   full words "Drift — resources disagree", so no glyph is forced on it, and
   neither is one forced on Unknown, which carries its own words too. */
.environment-state-badge-on::before,  .resource-state-badge-on::before  { content: "\25CF"; }  /* ● */
.environment-state-badge-off::before, .resource-state-badge-off::before { content: "\25CB"; }  /* ○ */
.resource-state-badge-unknown::before { content: "? "; }

/* -- Session row (§6.5.2) -- */
.environment-session-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.environment-countdown {
  font-family: var(--cf-mono); font-variant-numeric: tabular-nums;
  font-size: 15px; font-weight: 600; color: var(--cf-fg);
}
.environment-countdown-indefinite {
  font-family: var(--cf-mono); font-size: 14px; font-weight: 500; color: var(--cf-fg-dim);
}
/* Resolve the Indefinite ambiguity (§6.5.2) purely in CSS: on an OFF card the
   row is irrelevant, so it recedes; it only draws the eye on an ON card. */
.environment-card-off .environment-countdown-indefinite {
  font-size: 13px; font-weight: 400; font-style: italic; color: var(--cf-fg-faint);
}
.environment-countdown-flagged {
  color: var(--cf-drift); font-weight: 600; font-size: 14px;
}
.environment-countdown-flagged::before { content: "\2691 "; } /* ⚑ */
.environment-attribution { font-family: var(--cf-mono); font-size: 12px; color: var(--cf-fg-faint); }
/*	Why an environment reads partial — shown only when it does, so it
	reads as an explanation of the badge above it rather than a field
	that is usually blank. */
.environment-state-reason { margin: 2px 0 0; font-size: 12px; color: var(--cf-fg-faint); }
/*	Which resources contradict what Control Freak last set them to — a
	different claim from the partial verdict above, and a louder one: this
	is a work item, not an explanation. Uses the DANGER token rather than
	the drift/partial one deliberately, so the two are never mistaken for
	each other on the same card; the ⚠ glyph carries it for a reader who
	cannot rely on colour, the same way `.banner.warning` does. */
.environment-drift {
  margin: 4px 0 0; padding: 6px 9px; border-radius: 7px;
  font-size: 12px; font-weight: 600;
  color: var(--cf-danger); background: var(--cf-danger-soft); border: 1px solid var(--cf-danger);
}
.environment-drift::before { content: "\26A0 "; font-weight: 700; } /* ⚠ */
/*	Which resources are switched on and still not serving — a THIRD
	condition, and not either of the two above it. The partial line explains
	a verdict; the drift banner says a resource contradicts what Control
	Freak last set it to; this says every resource is exactly where it was
	put and somebody's application is not answering. Deliberately its own
	banner rather than another line inside the drift one: an operator reading
	"drifted" goes looking for who changed the resource, which for this
	condition is nobody. Outlined rather than filled, so an ON card carrying
	one unhealthy resource does not read as loudly as a genuinely drifted
	estate; the stethoscope-free ✖ glyph carries it without colour. */
.environment-health {
  margin: 4px 0 0; padding: 6px 9px; border-radius: 7px;
  font-size: 12px; font-weight: 600;
  color: var(--cf-danger); background: transparent; border: 1px solid var(--cf-danger);
}
.environment-health::before { content: "\2716 "; font-weight: 700; } /* ✖ */

/* -- Cost block (§6.5 item 3) -- */
.environment-cost {
  display: flex; flex-direction: column; gap: 3px;
  padding: 11px 12px; background: var(--cf-panel-inset); border-radius: 8px;
}
.environment-cost-row { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.environment-cost-label { font-size: 12.5px; color: var(--cf-fg-dim); }
.environment-cost-value {
  font-family: var(--cf-mono); font-size: 13px; font-weight: 600;
  color: var(--cf-fg); font-variant-numeric: tabular-nums;
}
/* Cumulative "Spent this month" row (NEW — needs a data field, see README) */
.environment-cost-mtd { margin-top: 6px; padding-top: 9px; border-top: 1px solid var(--cf-border); }
.environment-cost-mtd .environment-cost-label { font-weight: 600; }
.environment-cost-mtd .environment-cost-value { font-size: 14.5px; font-weight: 700; }

/* -- Shared resources (§6.5 item 4) -- */
/* The shared-resource attribution lives INSIDE the cost block and behind
   a disclosure (owner request, 2026-08-16). It was floating between the
   cost block and the Resources list: untitled, indented for no reason,
   and reading as a second — worse — resource listing. It never was one.
   Every shared resource is already listed by name, kind and live state
   under Resources; what these rows uniquely carry is what the resource
   contributes to THIS environment's cost, which is cost information and
   belongs where the costs are. */
.environment-shared { margin-top: 8px; }
.environment-shared-summary {
  font-family: var(--cf-sans); font-size: 11px; color: var(--cf-fg-dim);
  cursor: pointer;
}
.shared-resource-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.shared-resource-item {
  display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px;
  padding: 7px 10px; background: var(--cf-panel-inset);
  border: 1px solid var(--cf-border); border-radius: 7px;
}
.shared-resource-name {
  font-family: var(--cf-mono); font-size: 12.5px; color: var(--cf-fg);
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.shared-resource-cost {
  font-family: var(--cf-mono); font-size: 12.5px; font-weight: 600;
  color: var(--cf-fg-dim); font-variant-numeric: tabular-nums;
}
.shared-resource-state-unknown {
  padding: 3px 9px; border-radius: 999px; font-size: 11px; font-weight: 650;
  color: var(--cf-fg-faint); background: var(--cf-panel);
  border: 1px dashed var(--cf-border-strong); white-space: nowrap;
}
.shared-resource-state-unknown::before { content: "? "; }
/* "shared with N environments (M on)" (plan C3) — a full-width caption
   line under the name/state/cost row rather than crowding onto it, since
   it is prose, not a number to align with the others. */
.shared-resource-references {
  flex-basis: 100%;
  font-family: var(--cf-sans); font-size: 10.5px; color: var(--cf-fg-faint);
}

/* -- Expandable resource section (plan R5, owner decision 4) --
   Not in the delivered design handoff at all (it predates this phase) —
   built entirely from the system's own primitives rather than a new
   visual language, the same instruction plan Y5 already applied to the
   history panel and cost provenance below: a plain <details>/<summary>
   disclosure (native browser affordance, no JS state to manage), styled
   as a slim inset strip that opens into `.shared-resource-item`'s own
   flex-row card-inset row shape, and `.resource-state-badge` (already
   styled above) for each row's live state — no new colour vocabulary. */
.environment-resource-panel {
  padding: 9px 12px; background: var(--cf-panel-inset); border-radius: 8px;
}
.environment-resource-summary {
  font-family: var(--cf-sans); font-size: 12.5px; font-weight: 600; color: var(--cf-fg-dim);
  cursor: pointer; list-style: none;                 /* the marker below replaces the native triangle */
}
.environment-resource-summary::-webkit-details-marker { display: none; }
.environment-resource-summary::before {
  content: "\25B8"; display: inline-block; margin-right: 6px;   /* ▸ */
  transition: transform .15s ease;
}
.environment-resource-panel[open] > .environment-resource-summary::before { transform: rotate(90deg); }
.resource-detail-list {
  list-style: none; margin: 9px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px;
}
/* Two lines, deliberately: the kind takes the first row on its own and the
   name, any shared tag, and the state badge share the second. On one row
   the kind and a fully-qualified service name crowded each other out and
   the name ellipsised almost immediately (owner feedback, 2026-07-26). */
.resource-detail-item {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
  padding: 7px 10px; background: var(--cf-panel);
  border: 1px solid var(--cf-border); border-radius: 7px;
}
.resource-detail-kind {
  flex: 0 0 100%;
  font-family: var(--cf-sans); font-size: 11px; color: var(--cf-fg-faint); white-space: nowrap;
}
.resource-detail-name {
  font-family: var(--cf-mono); font-size: 12.5px; color: var(--cf-fg);
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.resource-detail-shared-tag {
  font-family: var(--cf-sans); font-size: 10.5px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  color: var(--cf-fg-dim); background: var(--cf-panel-inset); border: 1px solid var(--cf-border-strong);
}

/* A resource whose live reading contradicts what Control Freak last set it
   to. The DANGER palette, not the drift one the partial verdict uses: the
   two mean different things and appear on the same card, so they must not
   share a colour. Never colour alone — the word "Drift" carries it. */
.resource-detail-drift-tag {
  font-family: var(--cf-sans); font-size: 10.5px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  color: var(--cf-danger); background: var(--cf-danger-soft); border: 1px solid var(--cf-danger);
}

/* Switched on but not yet serving — a database still coming up. Uses the
   DRIFT palette, not the on/off pair: this is a transient, attention-worthy
   in-between, and colouring it like either end would say something untrue.
   Never colour alone — the word "Starting" carries it. */
.resource-detail-starting {
  font-family: var(--cf-sans); font-size: 10.5px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  color: var(--cf-drift); background: var(--cf-drift-soft); border: 1px solid var(--cf-drift);
}
/* Switched on and NOT COMING — a Cloud Run service whose latest revision
   never started. Same pill geometry as "Starting" above and deliberately
   NOT the same palette: that one is transient and resolves itself, this one
   reads identically on the next refresh and is a work item. It takes the
   danger tokens the card-level `.environment-health` banner takes, so the
   row and the banner above it are visibly the same claim. The word
   "Unhealthy" carries it for a reader who cannot rely on colour. */
.resource-detail-unhealthy {
  font-family: var(--cf-sans); font-size: 10.5px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  color: var(--cf-danger); background: var(--cf-danger-soft); border: 1px solid var(--cf-danger);
}

/* ============================================================================
   SWIMLANES (plan N3)
   ----------------------------------------------------------------------------
   Not in the delivered design handoff at all (it postdates it) — built
   from the system's own primitives per the same "extend, don't invent"
   instruction the history/provenance section above already follows: the
   card-row's own panel chrome, `.burn-rate-unpriced`'s own "floor, not
   the whole picture" warning (reused verbatim for a lane's own unpriced
   note), and `.environment-card`'s own shape for the cards themselves.

   One lane per group, toggled in from the flat `.environment-list` by the
   header's `.dashboard-grouping-toggle`. Each lane is a flex ROW: a
   non-scrolling summary panel (`.environment-lane-summary`) on the left,
   and the group's own cards in a horizontally scrolling strip
   (`.environment-lane-scroll` → `.environment-lane-cards`) on the right.

   The summary panel needs no `position: sticky` of its own: it is a
   plain flex SIBLING of the scroll strip, not a descendant of it, so it
   is never inside the region that scrolls and is always on screen
   regardless of horizontal scroll offset — simpler than sticky
   positioning and correct by construction.

   -- The scroll track stays INSIDE the lane (owner feedback, 2026-07-28) --
   This was originally full-bleed: the track's right edge broke out of the
   centred column to the viewport edge via `margin-right: calc(50% - 50vw)`,
   with `padding-right: calc(50vw - 50%)` on the same scrolling box meant
   to bring the last card back to rest at the ordinary page gutter.

   Three things were wrong with it in practice, and they are all the same
   thing. Cards visibly spilled PAST the lane's own border, so the panel
   read as broken rather than as deliberately full-bleed. Every lane was
   as wide as the viewport whether or not it held enough cards to need
   it, which looks worst on exactly the common case of a group with two
   or three environments. And the compensating trailing padding did not
   actually hold: a scroll container's trailing padding is part of the
   scrollable area per the CSS Overflow spec, but browsers have long
   disagreed, so scrolling to the end left the last card against the raw
   viewport edge — the precise thing that padding existed to prevent.

   So the track is now an ordinary flex child, bounded by the lane's own
   border like everything else on the page. The lane carries the trailing
   gutter itself (`padding-right` on `.environment-lane`, not on the
   scrolling box) so the inset survives regardless of how a browser
   treats trailing padding inside a scroll container.

   -- The LANE bleeds right, the track still does not (owner, 2026-08-16) --
   "They still don't push the row out to the RHS of the viewport." So the
   lane's own right edge now reaches the viewport, via a negative margin
   on the `.environment-lanes` COLUMN — one rule, applied once, rather
   than per-lane arithmetic.

   This is not the reverted shape returning. What was wrong before was
   the TRACK breaking out of the lane, and each fault above followed from
   that: cards spilled past the lane's border (the border is now AT the
   viewport edge, so there is nothing to spill past), and the trailing
   gutter sat inside the scroll container (it is still on the lane, so it
   holds). The track remains an ordinary flex child of the lane, and the
   summary panel remains its non-scrolling flex sibling — the two
   properties that made the reverted version read as broken are exactly
   the two left untouched.

   One consequence from the 2026-07-28 note DOES return, knowingly: every
   lane is now viewport-wide whether or not its cards need the room, so a
   two-card group carries empty panel to its right. That is the price of
   the instruction, and the compensation is that all lanes share one right
   edge instead of ending raggedly at their own content widths.

   `50vw` counts the scrollbar, so on a classic (non-overlay) scrollbar
   the bleed overshoots the visible edge by half the scrollbar width.
   `overflow-x: clip` on the page absorbs that — `clip` rather than
   `hidden` deliberately, since `hidden` would make the page a scroll
   container and break `position: sticky` elsewhere in it.

   -- Scroll affordances (plan N3: "a trap without them") --
   Keyboard: `tabindex="0"` + `role="region"` + `aria-label` on the
   markup side is all that is needed — every evergreen browser already
   scrolls a focused, overflowing region with the arrow keys, so no
   `keydown` handler is written here. `:focus-visible` below gives that
   focus an explicit, visible outline rather than relying on the
   browser's own default (inconsistent across browsers on a `div`).
   Edge fade: the classic CSS-only "scrolling shadows" technique (two
   `local`-attached linear-gradients, matched to `--cf-panel`, that mask
   a shadow near either edge WHILE there is still content behind them to
   reveal; two `scroll`-attached radial-gradients, in `--cf-lane-edge`,
   that supply the shadow itself) — it needs no JavaScript scroll
   listener and, because it is driven by the actual scrollable overflow,
   it disappears on its own the moment a lane's content fits without
   scrolling.
   ============================================================================ */
.environment-lanes {
  display: flex; flex-direction: column; gap: 16px;

  /* Full-bleed both ways: every lane is laid out viewport-wide, and its
     PANEL is then drawn only where the strip has travelled to — see
     `.environment-lane::before`. */
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
}

/* A lane is a STRIP THAT TRAVELS. At rest it sits on the page column; its
   cards push it left as they scroll until its leading edge meets the
   viewport, where it stops; and once no cards remain to the right, its
   trailing edge comes in to the column. So its right edge is never left
   of the content's right edge, and its left edge is never right of the
   content's left edge.

   Both edges follow the scroll offset, which no CSS rule can express, so
   `__shift_lane_panel` (dashboard.rs) writes the offsets as custom
   properties and the panel below is drawn between them. A lane whose
   cards fit never scrolls, so both stay at the column and the strip
   looks like every other panel on the page — which is the 2026-07-28
   objection answered rather than traded away.

   The panel is a PSEUDO-ELEMENT rather than this box's own background,
   because the strip's edges move: clipping the lane itself would shear
   off its side borders, and resizing it would change the scrollable
   extent the handler is measuring, which would not settle. */
.environment-lane {
  position: relative;
  padding: 16px 0;

  /* The strip's two edges, named here so the panel below and the scroll
     container are drawn to the SAME edges by construction rather than by
     two copies of the arithmetic. The scroller is laid out viewport-wide
     — it must be, or a card could never be seen out in a gutter — so
     without clipping it back it paints its own background past the strip
     and lets the leading and trailing cards hang outside the rounded
     box. */
  --cf-lane-inset-start: max(0px, calc(var(--cf-page-gutter) - var(--cf-lane-clip-start, 0px)));
  /* The TRAILING inset is written directly by `__shift_lane_panel`, which
     derives it from where the cards end rather than from how far there is
     left to scroll — a lane reaching past the column but still fitting the
     viewport has nothing left to scroll, yet its panel must stretch to
     contain its cards. This declaration is the pre-script fallback only,
     and the inline property the handler sets overrides it. */
  --cf-lane-inset-end:   max(0px, calc(var(--cf-page-gutter) - var(--cf-lane-clip-end,   0px)));
}
.environment-lane::before {
  content: ""; position: absolute; z-index: 0;
  top: 0; bottom: 0;
  left:  var(--cf-lane-inset-start);
  right: var(--cf-lane-inset-end);
  border: 1px solid var(--cf-border);

  /* A corner is rounded only while it is clear of the viewport edge, and
     squares off as it arrives there — so a strip running off-screen reads
     as continuing past the edge rather than stopping short of one.
     No state and no second mechanism: the radius simply follows the same
     inset the edge itself is drawn at, so it unrolls continuously as the
     strip travels, and is exactly zero at the moment the inset is. */
  border-top-left-radius:     min(var(--cf-radius), var(--cf-lane-inset-start));
  border-bottom-left-radius:  min(var(--cf-radius), var(--cf-lane-inset-start));
  border-top-right-radius:    min(var(--cf-radius), var(--cf-lane-inset-end));
  border-bottom-right-radius: min(var(--cf-radius), var(--cf-lane-inset-end));
  background: var(--cf-panel); box-shadow: var(--cf-shadow);
}

/* The scroll indicator, replacing the native scrollbar the scroller can
   only draw across its own viewport-wide box. Its geometry is written by
   `__shift_lane_panel` in the same pass that moves the strip, so it is
   bounded to the strip by construction — its length says how much of the
   lane is on screen and its position says how far through you are. It is
   invisible on a lane with nothing to scroll. */
.environment-lane::after {
  content: ""; position: absolute; z-index: 2;
  bottom: 6px; height: 3px; border-radius: 999px;
  left:  var(--cf-lane-thumb-left,  0px);
  width: var(--cf-lane-thumb-width, 0px);
  background: var(--cf-fg-faint);
  opacity: var(--cf-lane-thumb-opacity, 0);
  transition: opacity .15s ease;
}
.environment-lane-scroll::-webkit-scrollbar { display: none; }

/* The row inside the scroller: summary first, then the cards.

   Its side padding holds the contents on the page column however wide
   the lane has grown. `100%` here resolves against the lane, so
   `(100vw - 100%) / 2` IS the lane's own offset from the viewport edge,
   and subtracting it from the page gutter leaves exactly the distance
   still to make up. At the narrow stop that is zero — the lane already
   sits on the column; at the wide stop it is the whole gutter. The
   clamp guards the middle from going negative. */
.environment-lane-track {
  display: flex; align-items: stretch;
  padding-left: var(--cf-page-gutter);
  width: max-content; min-width: 100%;
}


.environment-lane-summary {
  flex: 0 0 220px; min-width: 0;
  display: flex; flex-direction: column; gap: 6px;
  padding: 0 18px;
  border-right: 1px solid var(--cf-border);

  /* The header travels exactly the gutter's width and no further: it
     starts aligned to the content column and pins at the lane's own left
     edge — the viewport edge, once the lane has grown that far — with
     the cards passing underneath. This is why the summary lives INSIDE
     the scroller: a sibling outside it has nothing to stick within, and
     so cannot travel at all. The panel background is explicit because
     cards pass beneath and would otherwise show through. */
  position: sticky; left: 0; z-index: 1;
  background: var(--cf-panel);
}
.environment-lane-title {
  margin: 0; font-family: var(--cf-mono);
  font-size: 14px; font-weight: 650; color: var(--cf-fg); word-break: break-word;
}
/* The group's own four figures, under its counts. Tighter than a card's
   because a lane summary is a 220px column, and the rows themselves are
   the card's — the two must read as the same figures at two scales, so
   nothing here restyles them beyond size. */
.environment-lane-cost { margin-top: 2px; font-size: 12px; }
.environment-lane-cost .environment-cost-label,
.environment-lane-cost .environment-cost-value { font-size: 12px; }

.environment-lane-stats {
  margin: 0; font-family: var(--cf-mono); font-size: 12.5px; color: var(--cf-fg-dim);
  font-variant-numeric: tabular-nums;
}
/* Per-group sparkline (plan N5): that group's daily series, single line,
   no legend, self-scaled — landed into the exact space this section's
   own summary column already reserved for it. */
/* A shared resource gets the same thumbnail a swim lane does, in the
   neutral slot — it is not a group and must not borrow a group's
   identity colour. Above the header rather than beside it, so a row of
   cards keeps one baseline for their names. */
.shared-resource-card-sparkline { margin-bottom: 6px; }

.environment-lane-sparkline { margin-top: 2px; }
.environment-lane-sparkline-svg { display: block; width: 100%; height: 28px; }
.environment-lane-sparkline-line { fill: none; stroke: var(--chart-color); stroke-width: 1.5; }
.environment-lane-sparkline-empty {
  margin: 2px 0 0; font-size: 11px; color: var(--cf-fg-faint); font-style: italic;
}

.environment-lane-scroll {
  flex: 1 1 auto; min-width: 0;
  overflow-x: auto; overflow-y: hidden;

  /* The native scrollbar is suppressed because it belongs to this box,
     which is viewport-wide, so it draws right across the gutters and
     past the strip — visible in the screenshots as a bar wider than the
     lane it describes. `.environment-lane::after` replaces it with an
     indicator bounded to the strip. */
  scrollbar-width: none;

  position: relative; z-index: 1;           /* above the travelling panel drawn by `.environment-lane::before` */


  /* PAINTS NOTHING, deliberately. This box is laid out viewport-wide so
     that a card can be seen out in a gutter, which means any background
     it carries shows OUTSIDE the strip — that was the panel colour
     bleeding to both edges.

     Clipping it back to the strip was tried and reverted: `clip-path`
     takes the element's own scrollbar and its hit-testing with it, so
     the scrollbar appears but does nothing, and it CUTS OFF the cards
     rather than containing them.

     The cost is the scrolling-shadow edge fade, which lived on this
     element because the technique needs `background-attachment: local`
     on the scrolling box itself. The travelling strip and the pinned
     header now carry the "there is more this way" signal instead. */
  background: none;
}
.environment-lane-scroll:focus-visible {
  outline: 2px solid var(--cf-accent); outline-offset: -2px;
}
/* The trailing gutter lives HERE, on the cards row, and the reason is the
   whole bug's history in one line: browsers disagree about whether a SCROLL
   CONTAINER's trailing padding belongs to its scrollable extent — Safari
   drops it, so the lane ran out of scroll before the last card had cleared
   the strip's edge and the card lay across it, in Safari only. Padding on a
   child INSIDE the scroller is ordinary layout that both engines agree on,
   and a flex spacer was tried first and also came up short in Safari, its
   basis not counted into the track's max-content width.

   The extra 18px is what stops the last card resting flush against the
   strip's border. */
.environment-lane-cards {
  display: flex; align-items: stretch; gap: 16px;
  padding-left: 18px;
  flex: 0 0 auto;
}

/* The trailing gutter as a REAL BOX, and every cheaper form has now been
   tried and measured:
   
     - padding on the scroll container   — Safari drops it from the extent
     - padding on this row (a child)     — Safari drops that too: it
       reported `scrollWidth` 3262 where Chrome reported 3472, short by
       exactly this 210px, so the lane ran out of scroll with the last
       card still at the viewport edge while the strip had correctly
       pulled in to the content edge
     - a sibling spacer with `flex: 0 0 <calc>` — its BASIS is not counted
       into the track's max-content width in Safari either
   
   An empty flex item with an explicit `width` and `flex: none` is a box
   both engines measure. The 18px above the gutter is what stops the last
   card resting flush against the strip's border. */
.environment-lane-cards::after {
  content: ""; flex: none;

  /* Zero UNTIL the lane is known to overflow, which is the whole point.
     This box exists so the last card can come to rest inside the strip's
     trailing edge — but applied unconditionally it is itself overflow,
     so a lane whose cards fit comfortably still measured as scrollable
     and still claimed the full viewport width, reserving space it had
     nothing to put in. The effect grew with the screen, since the width
     is mostly the page gutter.
     
     Whether a lane overflows is a measurement, so `__shift_lane_panel`
     decides it and sets the variable; the fallback of zero is also the
     correct resting value for a lane that never scrolls. */
  width: var(--cf-lane-tail, 0px);

  /* This box is a flex child, so the row's own `gap` lands in front of it
     as well — which made the trailing space 16px wider than intended and
     left the last card sitting too far inside the strip. Cancelling the
     gap here keeps the rest gap at the 18px the width above declares,
     rather than encoding 16px of it invisibly in that number. */
  margin-left: -16px;
}
/* `min-width: 0` is load-bearing, not tidying. A flex item defaults to
   `min-width: auto`, which floors it at its MIN-CONTENT width — so a basis
   of `--cf-card-w` is only honoured while the card's contents happen to
   fit it. On a rich estate they do not: cards measured 584px and 767px
   against a 340px basis, differing per product, which read as lanes
   reserving twice the room they needed and grew with the estate rather
   than the screen. It never showed locally, where a card holds one
   resource and no cost history and so never reaches the floor.

   `overflow-wrap` is the other half: with the floor removed, a long
   unbroken name would otherwise spill out of the card instead of
   widening it. */
/* CAPPED, and the cap is what matters. The track is `width: max-content`,
   so the row is sized by its max-content — and a flex item contributes
   `max(basis, content)` to that, not its basis. A card whose unwrapped
   content is wider than `--cf-card-w` therefore inflates the ROW while
   still USING 340px itself, which reads as a lane reserving twice the
   room it needs while every card looks the right size. Measured live:
   three 340px cards in a 2577px row, which is exactly three ~767px
   contributions.

   `max-width` caps the contribution, `min-width: 0` removes the
   min-content floor beneath it, and `overflow-wrap` is what stops a long
   unbroken name spilling out once neither can widen the card. */
.environment-lane-cards .environment-card {
  flex: 0 0 var(--cf-card-w);
  width: var(--cf-card-w);
  max-width: var(--cf-card-w);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ============================================================================
   ACTION PANEL (§6.6)
   ============================================================================ */
.environment-actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  justify-content: flex-end;                 /* single control (On / Refresh) sits right */
  margin-top: auto;                          /* pin to card bottom, aligning across cards */
  padding-top: 13px; border-top: 1px solid var(--cf-border);
}
/* Read-only environments: the actions row still exists (it carries the
   card's bottom rule and the margin-top:auto that aligns every card's
   footer), but holds a note where the controls would be — so a read-only
   card stays the same shape as its neighbours instead of collapsing and
   breaking the swim-lane's alignment. */
.environment-actions-read-only {
  margin: 0; font-size: 12px; color: var(--cf-fg-dim); text-align: right;
}

.environment-action { display: flex; align-items: center; gap: 6px; }
.environment-action-extend { margin-right: auto; }  /* Extend left, Off right on ON cards */

/* Duration dropdowns — never disabled during in-flight (§6.6) */
.action-duration-select {
  font-family: var(--cf-mono); font-size: 12.5px; color: var(--cf-fg);
  background-color: var(--cf-panel-inset);
  border: 1px solid var(--cf-border-strong); border-radius: 6px;
  padding: 6px 26px 6px 10px; cursor: pointer;
  appearance: none; -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%238b93a1' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 9px center;
}

/* Buttons */
.environment-action-button {
  font-family: var(--cf-sans); font-size: 13px; font-weight: 600;
  padding: 7px 14px; border-radius: 7px; border: 1px solid transparent;
  line-height: 1; cursor: pointer; transition: filter .12s, opacity .12s;
}
.environment-action-on-button      { background: var(--cf-on-solid); color: #fff; }
.environment-action-extend-button  { background: var(--cf-accent);   color: var(--cf-accent-fg); }
/* Green reads as "start this up", which is only what the control does on an
   environment that is actually off. On a running one it sets an auto-off
   timer, and on a drifted one it forces a state — both adjustments to
   something already running, so they take the accent colour that Extend
   (the other adjust-in-place action) already uses. */
.environment-action-on-button.environment-action-adjust-button {
  background: var(--cf-accent); color: var(--cf-accent-fg);
}
.environment-action-button.danger  { background: var(--cf-danger-solid); color: #fff; }  /* Off + confirm */
.environment-action-off-cancel-button {
  background: transparent; color: var(--cf-fg-dim);
  border-color: var(--cf-border-strong); font-size: 12.5px; padding: 6px 11px;
}
.environment-action-button:hover:not(:disabled) { filter: brightness(1.06); }
/* In-flight disabled (§6.6) — unambiguous */
.environment-action-button:disabled { opacity: .4; cursor: not-allowed; filter: saturate(.4); }

/* Armed off-confirmation (§6.6) */
.environment-action-confirm-group {
  display: flex; align-items: center; gap: 7px;
  padding: 4px 5px 4px 10px;
  background: var(--cf-danger-soft); border: 1px solid var(--cf-danger); border-radius: 8px;
}
.environment-action-confirm-prompt { font-size: 12.5px; font-weight: 650; color: var(--cf-danger); }

/* Per-card in-flight progress bar — needs a <div class="environment-card-progress">
   as the card's first child while a request is in flight (see README).
   Since the switch progress queue below landed, this is what an EXTEND
   renders: an action that moves no resource has no queue to show, and an
   indeterminate line is the honest rendering of a request whose only step
   is itself. An on/off renders `.switch-progress` instead. */
.environment-card-progress {
  position: absolute; top: 0; left: 0; right: 0; height: 2px;
  overflow: hidden; border-radius: var(--cf-radius) var(--cf-radius) 0 0;
}
.environment-card-progress::after {
  content: ""; display: block; width: 36%; height: 100%;
  background: var(--cf-accent); animation: cf-progress 1.1s ease-in-out infinite;
}

/* ============================================================================
   SWITCH PROGRESS QUEUE (feature: switch progress queue)
   ----------------------------------------------------------------------------
   Not in the delivered design handoff at all — it postdates it, like the
   history panel and cost provenance before it, so it is built from the
   system's own primitives rather than a new visual language: the card-inset
   panel shape `.environment-resource-panel` already uses, the pill geometry
   of `.resource-detail-*`, and the existing status palette with nothing
   added to it.

   The one rule that shapes every choice here: this panel is read while
   something is SLOW, so state legibility beats decoration. Each row's phase
   is carried by a word, a shape marker, AND a colour — never colour alone
   (§4) — and the phases use the tokens whose meaning is already established
   elsewhere: `on` for done, `accent` for the step actually running, `drift`
   for switched-but-not-serving (exactly as `.resource-detail-starting`
   does), and the faint foreground for everything not reached yet, so the
   queue's remaining work visibly recedes. */
.switch-progress {
  display: flex; flex-direction: column; gap: 9px;
  padding: 11px 12px; margin-bottom: 2px;
  background: var(--cf-panel-inset);
  border: 1px solid var(--cf-border); border-left: 3px solid var(--cf-accent);
  border-radius: 8px;
}
.switch-progress-summary {
  margin: 0; font-family: var(--cf-sans); font-size: 12.5px; font-weight: 600; color: var(--cf-fg);
}
/* Track + fill. The fill's width is set inline (the elapsed fraction, or a
   fixed sliver where nothing was estimated — see `progress_fill_style`), so
   nothing here may set a width of its own. */
.switch-progress-bar {
  height: 5px; overflow: hidden; border-radius: 999px; background: var(--cf-border);
}
.switch-progress-bar-fill {
  height: 100%; border-radius: 999px; background: var(--cf-accent);
  transition: width .4s linear;
}
/* No estimate to measure against: the same looping sweep the plain in-flight
   bar uses, so "running, position unknown" never reads as a stalled
   determinate bar sitting at one third. */
.switch-progress-bar-indeterminate {
  animation: cf-progress 1.1s ease-in-out infinite; transition: none;
}
.switch-progress-note {
  margin: 0; font-family: var(--cf-sans); font-size: 11px; color: var(--cf-drift);
}
.switch-progress-steps {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 5px;
}
/* Same two-line row shape as `.resource-detail-item`, for the same reason:
   a fully-qualified service name and its kind crowd each other out on one
   line (owner feedback, 2026-07-26). */
.switch-progress-step {
  display: flex; flex-wrap: wrap; align-items: center; gap: 3px 9px;
  padding: 6px 10px 6px 26px; position: relative;
  background: var(--cf-panel);
  border: 1px solid var(--cf-border); border-radius: 7px;
}
.switch-progress-step::before {
  position: absolute; left: 9px; top: 6px;
  font-size: 11px; line-height: 1.35;
}
.switch-progress-step-kind {
  flex: 0 0 100%;
  font-family: var(--cf-sans); font-size: 11px; color: var(--cf-fg-faint); white-space: nowrap;
}
.switch-progress-step-name {
  font-family: var(--cf-mono); font-size: 12.5px; color: var(--cf-fg);
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.switch-progress-step-shared-tag {
  font-family: var(--cf-sans); font-size: 10.5px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  color: var(--cf-fg-dim); background: var(--cf-panel-inset); border: 1px solid var(--cf-border-strong);
}
.switch-progress-step-phase {
  font-family: var(--cf-sans); font-size: 11px; font-weight: 650;
  text-transform: uppercase; letter-spacing: .04em;
}
/* The one figure that moves every second. Mono so its digits do not shift
   the row's layout as they tick. */
.switch-progress-step-elapsed {
  font-family: var(--cf-mono); font-size: 12px; font-weight: 650; color: var(--cf-fg);
  font-variant-numeric: tabular-nums;
}
.switch-progress-step-estimate {
  font-family: var(--cf-sans); font-size: 11px; color: var(--cf-fg-faint);
}
/* Phase modifiers. Marker glyph + phase-word colour, per phase — the word
   itself is always present in the markup, so a reader who cannot rely on
   colour loses nothing. */
.switch-progress-step-waiting { opacity: .72; }
.switch-progress-step-waiting::before { content: "\25CB"; color: var(--cf-fg-faint); }  /* ○ */
.switch-progress-step-waiting .switch-progress-step-phase { color: var(--cf-fg-faint); }
.switch-progress-step-in-progress {
  border-color: var(--cf-accent); box-shadow: inset 2px 0 0 var(--cf-accent);
}
.switch-progress-step-in-progress::before { content: "\25B8"; color: var(--cf-accent); }  /* ▸ */
.switch-progress-step-in-progress .switch-progress-step-phase { color: var(--cf-accent); }
.switch-progress-step-starting::before { content: "\25D0"; color: var(--cf-drift); }  /* ◐ */
.switch-progress-step-starting .switch-progress-step-phase { color: var(--cf-drift); }
.switch-progress-step-done::before { content: "\2713"; color: var(--cf-on); }  /* ✓ */
.switch-progress-step-done .switch-progress-step-phase { color: var(--cf-on); }
.switch-progress-step-no-signal { opacity: .72; }
.switch-progress-step-no-signal::before { content: "\2013"; color: var(--cf-fg-faint); }  /* – */
.switch-progress-step-no-signal .switch-progress-step-phase { color: var(--cf-fg-faint); }

/* Header buttons */
.dashboard-refresh-button {
  font-family: var(--cf-sans); font-size: 13px; font-weight: 600;
  padding: 7px 14px; border-radius: 7px; border: 1px solid transparent;
  background: var(--cf-accent); color: var(--cf-accent-fg); cursor: pointer;
}
/* `refreshing` (dashboard.rs's own in-flight signal) disables this
   button for the round trip. `cursor: progress`, not `not-allowed`: this
   is a fetch under way, not a refusal — the same distinction
   `.login-page input:disabled`/`.login-page button[type="submit"]
   :disabled` already draw for their own in-flight submit. */
.dashboard-refresh-button:disabled { opacity: .55; cursor: progress; }
/* How old the data on screen is (owner ruling, 2026-08-10: no
   auto-refresh). Ticks once a second off the same signal the countdowns
   use, so it sits beside the Refresh button in the same monospace,
   dimmed register as `.dashboard-user` — an ambient fact, not a control.
   Staleness is stated in the TEXT as well ("— may be stale"); this rule
   only reinforces it, never carries it alone, per the same
   never-colour-only rule the drift badge follows. */
.dashboard-updated {
  font-family: var(--cf-mono); font-size: 12.5px; color: var(--cf-fg-dim);
  white-space: nowrap;
}
.dashboard-updated-stale { color: var(--cf-drift); }
.dashboard-logout-button {
  font-family: var(--cf-sans); font-size: 13px; font-weight: 600;
  padding: 7px 14px; border-radius: 7px;
  background: transparent; color: var(--cf-fg-dim);
  border: 1px solid var(--cf-border-strong); cursor: pointer;
}
/* Y5 addition: the design handoff never mentions history at all (see this
   file's own `HISTORY VIEW AND COST PROVENANCE` section below) — styled
   as a neutral secondary action, the same visual weight as Log out, since
   it is a read-only lookup rather than a mutating or navigational one. */
/* Plan N3's header toggle shares the same neutral secondary-action
   weight — it is a view preference, not a mutating or navigational
   control either. */
.dashboard-history-button,
.dashboard-grouping-toggle {
  font-family: var(--cf-sans); font-size: 13px; font-weight: 600;
  padding: 7px 14px; border-radius: 7px;
  background: transparent; color: var(--cf-fg-dim);
  border: 1px solid var(--cf-border-strong); cursor: pointer;
}
.dashboard-history-button:hover,
.dashboard-grouping-toggle:hover { color: var(--cf-fg); border-color: var(--cf-fg-dim); }

/* ============================================================================
   TOASTS (§5.3)
   ============================================================================ */
.toast-list {
  position: fixed; top: 16px; right: 16px; z-index: 60;
  display: flex; flex-direction: column; gap: 10px;
  width: min(380px, calc(100vw - 32px));
}
.toast {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 12px 13px;
  background: var(--cf-panel); border: 1px solid var(--cf-border);
  border-left: 3px solid var(--cf-danger); border-radius: 9px;
  box-shadow: var(--cf-shadow); animation: cf-fade .18s ease;
}
.toast-message {
  flex: 1; font-size: 13px; line-height: 1.45; color: var(--cf-fg);
  word-break: break-word; white-space: pre-wrap;   /* multi-line refusals wrap, never truncate */
}
.toast-dismiss {
  flex: none; width: 26px; height: 26px;
  display: flex; align-items: center; justify-content: center;
  font-size: 18px; line-height: 1; color: var(--cf-fg-dim);
  background: transparent; border: 1px solid var(--cf-border); border-radius: 6px;
  cursor: pointer;
}
.toast-dismiss:hover { color: var(--cf-fg); border-color: var(--cf-border-strong); }

/* ============================================================================
   HISTORY VIEW AND COST PROVENANCE (plan task Y5)
   ----------------------------------------------------------------------------
   Genuine design gap, not an oversight to second-guess: the delivered
   handoff (control-freak.css / the .dc.html reference / its README) never
   mentions the action-history view, the per-card "History" link, or any
   provenance label at all — no class name, no screen, no token. Everything
   below is built ONLY from primitives the delivered system already
   defines (panel chrome, badge/pill colour+shape semantics, the
   `.banner`/`.empty-state` family, the auto-fit responsive technique
   `.burn-rate-totals` already uses), per plan task Y5's own instruction
   to extend rather than invent a parallel visual language.

   Recorded gap for a design follow-up: the delivered system has no
   genuine multi-column TABLE pattern to map the history list onto (the
   plan's own wording assumes one exists). The closest analogue is
   `.shared-resource-list`/`.shared-resource-item`'s flex-row shape, which
   is what `.history-event` below extends — a real design pass may want a
   purpose-built dense table treatment instead, especially once history
   lists grow long enough to want sortable/sticky column headers.
   ============================================================================ */

/* -- Per-card "History" link (spec addendum §1, plan O3) --
   Sized and pill-shaped to sit beside the state badge in the card header
   as a matching pair, but kept neutral (never on/off/drift-coloured) since
   it carries no state of its own — see the `.environment-card-header`
   layout note above for why the header no longer uses space-between. */
.environment-history-button {
  font-family: var(--cf-sans); font-size: 11px; font-weight: 650;
  padding: 3px 9px; border-radius: 999px; line-height: 1.4; white-space: nowrap;
  background: transparent; color: var(--cf-fg-faint);
  border: 1px solid var(--cf-border-strong); cursor: pointer;
}
.environment-history-button:hover { color: var(--cf-fg-dim); border-color: var(--cf-fg-dim); }

/* -- Cost block: the "no rate at all" case (spec addendum §2) --
   The delivered CSS styles the three known-rate rows and the mtd row but
   never this one — the paragraph that stands in for them all when the
   provenance is unknown. Faint and italic, matching the same "recede,
   don't alarm" treatment the off-card indefinite-session row already uses
   for its own de-emphasised case. */
.environment-cost-unknown {
  margin: 0; font-size: 12.5px; color: var(--cf-fg-faint); font-style: italic;
}

/* -- Cost-provenance caption --
   "Provenance maps onto its badge patterns" (plan Y5): a small coloured
   chip rather than plain text, reusing the exact soft-background/border
   pairing `.environment-state-badge`'s colour tokens already define per
   source, plus `.shared-resource-state-unknown`'s own dashed-border
   "unknown" treatment for the unknown case. Not pill-shaped (`border-
   radius: 999px`) like the state badges — this caption's text is prose
   (it can carry a full observation-window timestamp range), which wraps
   badly inside a fully-rounded pill; a soft rounded box reads as the same
   badge family without that failure mode. */
.environment-cost-provenance {
  margin: 6px 0 0; padding: 4px 8px;
  border-radius: 6px; border: 1px solid var(--cf-border);
  background: var(--cf-panel); font-size: 10.5px; line-height: 1.4;
  color: var(--cf-fg-dim);
}
.environment-cost-provenance-observed { color: var(--cf-fg-dim); }
.environment-cost-provenance-averaged {
  color: var(--cf-drift); background: var(--cf-drift-soft); border-color: var(--cf-drift);
}
.environment-cost-provenance-override {
  color: var(--cf-accent); background: var(--cf-panel-inset); border-color: var(--cf-border-strong);
}
.environment-cost-provenance-unknown {
  color: var(--cf-fg-faint); font-style: italic; background: transparent; border-style: dashed;
}

/* -- Org-wide "some figures have no known cost" note --
   Reuses the drift/amber token already meaning "indeterminate, don't
   trust the total blindly" for `.environment-card-partial`/
   `.environment-count-partial`, plus that badge's own shape-marker
   convention (§4: state is never colour-only) via the same ▲ glyph. */
.burn-rate-unpriced {
  margin: 0; font-size: 12px; color: var(--cf-drift);
}
.burn-rate-unpriced::before { content: "\25B2 "; } /* ▲ */

/* -- History panel --
   A fixed viewport-anchored drawer: not a new idea (`.toast-list` is
   already fixed-positioned chrome floating over the page), just applied
   at panel scale using `.burn-rate-panel`'s own panel/border/shadow look
   instead of inventing a modal/backdrop treatment the delivered system
   has no example of at all. `top: 64px` is an approximation of the
   sticky header's own rendered height, not a measured constant. */
.history-panel {
  position: fixed; top: 64px; right: 16px; bottom: 16px;
  width: min(480px, calc(100vw - 32px));
  z-index: 40; /* below .toast-list (60), above page content */
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--cf-panel); border: 1px solid var(--cf-border);
  border-radius: var(--cf-radius); box-shadow: var(--cf-shadow);
  animation: cf-fade .18s ease;
}
.history-panel-header {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px 16px; border-bottom: 1px solid var(--cf-border); flex: none;
}
.history-panel-title {
  margin: 0; font-family: var(--cf-mono); font-size: 14px; font-weight: 650; color: var(--cf-fg);
}
.history-panel-close {
  font-family: var(--cf-sans); font-size: 12.5px; font-weight: 600;
  padding: 6px 11px; border-radius: 6px;
  background: transparent; color: var(--cf-fg-dim);
  border: 1px solid var(--cf-border-strong); cursor: pointer;
}
.history-panel-close:hover { color: var(--cf-fg); border-color: var(--cf-fg-dim); }
.history-panel-body {
  flex: 1; overflow-y: auto; padding: 14px 16px;
  display: flex; flex-direction: column; gap: 10px;
}
/* The partial-read note (malformed log entries) — a `.banner.warning`. */
.history-malformed-note { margin: 0; }

/* -- History event list --
   The delivered system's own closest thing to a table row: extends
   `.shared-resource-item`'s flex row (panel-inset background, bordered,
   rounded) rather than a genuine `<table>` treatment (see this section's
   own header comment for that recorded gap). `flex-wrap` plus giving the
   outcome column `flex-basis: 100%` lets a long refusal/failure detail
   drop to its own line with no breakpoint needed — the same auto-fit-
   without-a-breakpoint idea `.burn-rate-totals` already uses. */
.history-event-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px;
}
.history-event {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px;
  padding: 8px 10px;
  background: var(--cf-panel-inset); border: 1px solid var(--cf-border); border-radius: 7px;
  font-size: 12.5px;
}
.history-event-time {
  font-family: var(--cf-mono); font-size: 11.5px; color: var(--cf-fg-faint); white-space: nowrap;
}
.history-event-actor {
  font-family: var(--cf-mono); font-size: 12px; color: var(--cf-fg-dim);
}
.history-event-action {
  font-family: var(--cf-mono); font-size: 11px; font-weight: 650; color: var(--cf-fg);
  text-transform: uppercase; letter-spacing: .04em;
  padding: 2px 7px; border-radius: 999px;
  background: var(--cf-panel); border: 1px solid var(--cf-border);
}
.history-event-target {
  font-family: var(--cf-mono); font-size: 12px; color: var(--cf-fg);
}
/* Full-width so a long "refused: ..."/"failed: ..." detail wraps on its
   own line rather than squeezing the metadata fields above it. Coloured
   text with a redundant glyph, not a pill — see this section's own header
   comment on why prose content doesn't take the fully-rounded badge
   shape. */
.history-event-outcome {
  flex: 1 1 100%; font-size: 12px; font-weight: 600; word-break: break-word;
}
.history-event-outcome-success { color: var(--cf-on); }
.history-event-outcome-success::before { content: "\25CF "; } /* ● */
.history-event-outcome-refused { color: var(--cf-drift); }
.history-event-outcome-refused::before { content: "\25B2 "; } /* ▲ */
.history-event-outcome-failed { color: var(--cf-danger); }
.history-event-outcome-failed::before { content: "\26A0 "; } /* ⚠ */

/* ============================================================================
   RESPONSIVE (down to tablet, §3)
   ============================================================================ */
@media (max-width: 820px) {
  :root { --cf-card-w: 100%; }
  .environment-card { flex-basis: 100%; }
  .dashboard-header { flex-wrap: wrap; }
  /* Y5: the fixed-width drawer would be cramped or clipped below tablet
     width — pin it edge-to-edge instead of a fixed right-anchored width. */
  .history-panel { left: 16px; right: 16px; width: auto; }
  /* Plan N3: "the sticky summary stacks ABOVE its lane rather than
     beside it — it otherwise eats the width permanently." Stacking the
     lane's own flex row into a column is the whole change.

     The lane's trailing gutter moves with it. In row mode that 18px of
     right padding is the scroll track's gutter and belongs on the lane;
     stacked, it would sit OUTSIDE the summary's own 18px as well,
     insetting the summary 36px on the right against 18px on the left and
     stopping its divider short of the lane's border. So the lane drops
     its horizontal padding here and the track carries its own. */
  .environment-lane { flex-direction: column; padding: 16px 0; }
  .environment-lane-scroll { padding-right: 18px; }
  .environment-lane-summary {
    flex-basis: auto; width: auto;
    border-right: none; border-bottom: 1px solid var(--cf-border);
    padding: 0 18px 12px;
  }
}
