/* ---------------------------------------------------------------
   Tech9IQ Design System — sole theme for the whole portal (customer +
   admin). Base tokens live in /assets/tech9iq-tokens.css, vendored from
   the design system's own colors_and_type.css. The shared component
   layer below is kept byte-identical to public/styles.css so both apps
   stay in sync; admin-only widgets follow after the shared block.

   LIGHT ONLY — see public/styles.css for the full rationale, including
   why the four status text colours are darkened brand hues rather than
   verbatim tokens.
----------------------------------------------------------------- */
@import url('/assets/tech9iq-tokens.css');

:root {
  /* Surfaces — DS light mode: white page furniture on a Light Gray canvas */
  --bg: var(--color-white);
  --canvas: var(--color-light-gray);
  --panel: var(--color-white);
  --panel-raised: var(--color-light-gray);
  --hairline: var(--color-blue-light);
  --hairline-strong: var(--color-gray-mid);

  /* Text — DS: headings Deep Navy, body Charcoal, muted Gray Dark */
  --text: var(--color-deep-navy);
  --text-soft: var(--color-charcoal);
  --muted: var(--color-tech-blue);
  --faint: var(--color-gray-dark);

  /* Accent — Tech Blue is the DS primary; Bright Cyan is its interactive state */
  --accent: var(--color-tech-blue);
  --accent-strong: var(--color-deep-navy);
  --accent-soft: var(--color-blue-light);
  --accent-soft-text: var(--color-deep-navy);
  --accent-interactive: var(--color-bright-cyan);

  /* Status — DS hues, text shades darkened to AA on their own tint.
     Aligned to the AWS Management module's own badge tones (its
     tailwind.config.js calls these "the design system's own documented
     values") wherever that doesn't fail contrast: --critical here matches
     its danger.700/danger.50 exactly (4.62:1, passes AA). --warn-soft
     matches its warning.50 tint, but --warn keeps this app's own darker
     foreground rather than adopting its warning.700 (#b7770d) — that value
     is only 3.42:1 against warning.50, below AA for this size of text;
     this app's #8A5B10 against the same lighter tint is 5.40:1, so the
     background aligns for visual consistency without regressing contrast. */
  --good: #3A7A3B;
  --good-soft: #D5EDD5;
  --critical: #c0392b;
  --critical-soft: #FCE8E6;
  --warn: #8A5B10;
  --warn-soft: #FEF5E0;
  --info: #1A7AA8;
  --info-soft: #D0EEF9;

  --shadow: var(--shadow-sm);
  --shadow-raised: var(--shadow-lg);
  --radius: var(--radius-lg);
  --sans: var(--font-primary);
  --mono: var(--font-mono);
  --brand-gradient: var(--gradient-horizontal);

  /* Density pass, calibrated against Cloudscape (cloudscape.design) — an
     ops console reads closer to that than to a marketing site, and the
     brand's own type scale (14px base) already tracks Cloudscape's body-m
     almost exactly. The two real gaps: `body` never actually set that
     14px base at all (see below — every unclassed <p>/<div>/<li> was
     quietly rendering at the browser's 16px default instead), and the
     corner radius on buttons/inputs was borrowed from the DS's larger
     --radius-lg/xl scale rather than the tighter one an ops console reads
     as "compact." Overridden here rather than in the vendored tokens
     file, so a future re-sync of tech9iq-tokens.css from the source
     design system doesn't silently undo it. (--btn-padding-y/
     --input-padding-y are real, separate tokens the login page's own
     form uses — untouched here, that page isn't part of this pass.) */
  --btn-radius: var(--radius-md);
  --input-radius: var(--radius-md);

  /* One scrim for every "dim the page behind a blocking surface" overlay —
     the modal, the form dialog, and the mobile nav backdrop each used to
     hardcode their own slightly different navy/opacity, so the same kind of
     dimming looked like three different treatments depending on which one
     happened to be open. */
  --scrim: rgba(23, 32, 42, .45);
  --scrim-strong: rgba(23, 32, 42, .7);
}

* { box-sizing: border-box; }
/* Every element class below sets its own `display`, which — per the
   cascade — beats the UA stylesheet's `[hidden] { display: none }` even
   at equal specificity, because author rules always win over UA ones
   regardless of source order. That silently defeated `el.hidden = true`
   on anything with a class of its own — a .btn, first hit on the
   onboarding Approve/Reject/Show-credentials buttons, stayed visible no
   matter what JS set. One rule here, ahead of every class that follows,
   fixes it file-wide instead of patching each class that happens to
   collide with `hidden` as it's found. */
[hidden] { display: none !important; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--canvas);
  color: var(--text);
  font-family: var(--sans);
  /* The design system's own 14px base, actually applied at the root —
     without this, anything that doesn't carry one of the font-size
     utility classes below (a bare <p>/<div>/<li> used for copy) fell
     back to the browser's 16px default with its own looser default line
     height, which is exactly the kind of gap that makes a page feel
     bigger than its own type scale says it should. */
  font-size: var(--fs-base);
  line-height: var(--lh-snug);
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--accent-soft); color: var(--accent-soft-text); }
button, input { font-family: inherit; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--accent-interactive); outline-offset: 2px; border-radius: var(--radius-sm); }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}

/* tabular-nums alone lines up each digit's own width, but never told the cell
   to actually sit at the right edge — every currency/quantity column in the
   app (Billing, Rate Card, Invoices, Insights, Compute capacity...) rendered
   flush left like ordinary text, so figures of different lengths started at
   the same left edge instead of lining up on the ones place. */
.num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; text-align: right; }
.mono { font-family: var(--mono); }

/* ---------------------------- AUTH ---------------------------- */
.auth-shell { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: var(--space-6); }
.auth-card {
  background: var(--panel); border: 1px solid var(--hairline); border-radius: var(--radius-xl);
  padding: var(--space-8) var(--space-8) var(--space-6); width: 380px; box-shadow: var(--shadow-md);
  position: relative; overflow: hidden;
}
.auth-card::before {
  content: ""; position: absolute; top: 0; left: 0; right: 0; height: 4px;
  background: var(--gradient-horizontal);
}

/* ---------------------------- LOGO ---------------------------- */
.brand-logo { display: inline-block; line-height: 0; }
/* The wordmark carries the "your intelligent cloud" tagline inside the
   artwork, so it needs a little more height than a plain mark to stay
   legible in the rail. */
.brand-logo img { display: block; height: 32px; width: auto; }
.brand-logo.lg img { height: 44px; }

.brand-mark { margin-bottom: var(--space-4); }
.auth-card h1 { margin: 0 0 3px; font-size: var(--fs-xl); font-weight: var(--fw-bold); color: var(--text); letter-spacing: var(--ls-tight); }
.auth-eyebrow {
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); letter-spacing: var(--ls-widest);
  text-transform: uppercase; color: var(--accent); margin-bottom: var(--space-2);
}
.subtitle { margin: 0 0 var(--space-6); color: var(--text-soft); font-size: var(--fs-sm); line-height: var(--lh-relaxed); }
#login-form label { display: block; margin-bottom: var(--space-4); font-size: var(--fs-sm); color: var(--text-soft); font-weight: var(--fw-semibold); }
#login-form input {
  display: block; width: 100%; margin-top: 6px; padding: var(--input-padding-y) var(--space-3);
  background: var(--color-white); border: var(--input-border); border-radius: var(--input-radius);
  color: var(--text); font-size: var(--fs-base);
}
#login-form input:focus { outline: none; border-color: var(--input-focus-border); box-shadow: 0 0 0 3px rgba(70, 196, 242, 0.18); }
#login-form button {
  width: 100%; padding: var(--btn-padding-y); border: none; border-radius: var(--btn-radius);
  background: var(--gradient-horizontal); color: var(--fg-inverse);
  font-weight: var(--btn-font-weight); font-size: var(--fs-base); cursor: pointer;
  transition: var(--btn-transition);
}
#login-form button:hover { opacity: 0.9; box-shadow: var(--shadow-md); }
#login-form button:disabled { opacity: 0.6; cursor: default; box-shadow: none; }
.error { color: var(--critical); font-size: var(--fs-sm); margin-top: var(--space-3); }
.auth-foot { margin-top: var(--space-6); font-size: var(--fs-xs); color: var(--faint); text-align: center; line-height: var(--lh-normal); }

/* ---------------------------- SHELL ---------------------------- */
.shell {
  display: grid; grid-template-columns: 248px 1fr; min-height: 100vh;
  transition: grid-template-columns 180ms ease;
}
body.rail-collapsed .shell { grid-template-columns: 76px 1fr; }
@media (prefers-reduced-motion: reduce) { .shell { transition: none; } }

/* Three regions: the brand and the account footer stay put, the nav scrolls
   between them. Before this the rail was one 100vh column with no overflow
   handling, so once the section count grew past the viewport the Platform links
   were rendered below the fold and simply unreachable — no scrollbar, no clue
   they existed. */
/* position:sticky always opens its own stacking context — but with no
   z-index of its own, that context sits in the "auto" bucket, which loses to
   ANY positively-z-indexed element anywhere else on the page, however small.
   That's not a hypothetical: a table's sticky column header is only
   z-index:2, and that alone was enough to paint over the entire rail —
   flyout included, despite its own z-index:230, because 230 only means
   anything relative to siblings *inside* this box. Giving the rail itself an
   explicit z-index moves its whole subtree into the same "compared by value"
   bucket the table header is in, where 400 simply wins. */
.rail {
  background: var(--panel); border-right: 1px solid var(--hairline);
  display: flex; flex-direction: column;
  padding: var(--space-5) var(--space-3);
  position: sticky; top: 0; height: 100vh; overflow: visible; z-index: 400;
}

.rail-nav {
  flex: 1 1 auto;
  min-height: 0;              /* without this a flex child refuses to shrink */
  /* Eleven categories (~460px, expanded or collapsed) fit any viewport this
     app targets, unlike the twenty-plus flat links this replaced — so this no
     longer needs to scroll. That matters beyond tidiness: overflow-y:auto
     paired with overflow-x:visible does NOT mean "scroll vertically, never
     clip horizontally" — the spec computes a visible axis to auto the moment
     its partner isn't visible, so a flyout would be silently clipped by this
     element the instant a category was hovered. Fully visible on both axes
     is the only way an absolutely-positioned flyout can escape this box. */
  overflow: visible;
  display: flex; flex-direction: column; gap: 2px;
  margin: 0 calc(var(--space-2) * -1);
  padding: 0 var(--space-2);
}
.rail-brand { flex: 0 0 auto; display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-2) var(--space-4); min-width: 0; }
.rail-brand img { max-width: 132px; flex-shrink: 0; }
.rail-brand-sub { font-size: var(--fs-xs); font-weight: var(--fw-semibold); letter-spacing: var(--ls-wider); text-transform: uppercase; color: var(--muted); white-space: nowrap; }

/* ── Category rail + flyout submenus ──────────────────────────────────────
   Expanded is the default: every category shows its own icon and label, the
   same as any conventional sidebar — this is the "full view". Collapsing
   trades those labels back for width, narrowing to an icon-only rail where
   the same destinations open as a flyout instead — matching the vendor
   console screenshot this was modelled on. Either way, a destination this
   environment cannot back yet (RDS, Kafka, CDN, WAF...) stays listed as "Not
   available" rather than hidden — the roadmap stays visible without
   pretending it works today. */
.rail-cats { display: flex; flex-direction: column; gap: 2px; width: 100%; }
.rail-cat-group { position: relative; }
.rail-cat {
  display: flex; align-items: center; gap: var(--space-3);
  width: 100%; min-height: 42px; padding: 0 var(--space-3); border: none; border-radius: var(--radius-md);
  background: transparent; color: var(--text-soft); cursor: pointer; position: relative;
  font-size: var(--fs-sm); font-weight: var(--fw-medium); text-align: left;
  transition: var(--btn-transition);
}
.rail-cat svg { width: 19px; height: 19px; flex-shrink: 0; opacity: 0.75; }
.rail-cat:hover { background: var(--panel-raised); color: var(--text); }
.rail-cat:hover svg { opacity: 1; }
.rail-cat.active, .rail-cat.cat-active { background: var(--accent-soft); color: var(--accent-soft-text); }
.rail-cat.active svg, .rail-cat.cat-active svg { opacity: 1; }
.rail-cat.cat-active::before, .rail-cat.active::before {
  content: ""; position: absolute; left: -6px; top: 20%; bottom: 20%;
  width: 3px; border-radius: var(--radius-full); background: var(--accent);
}
/* Expanded: the label is just the row's own text — truncate rather than
   wrap, the same fix the footer below needed for the same reason. */
.rail-cat-label { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.flyout-panel {
  /* Flush against the icon column, not offset from it: :hover on the group
     holds for as long as the pointer is over ANY descendant, flyout included
     — but only if there is no gap between them. A gap is dead space that
     belongs to neither element, and a mouse crossing it drops the ancestor's
     :hover and the panel vanishes before a diagonal move can reach an item
     lower down. Touching edges need no bridging pseudo-element either. */
  position: absolute; left: 100%; top: 0;
  min-width: 250px; max-width: 300px; background: var(--panel); border: 1px solid var(--hairline);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg); padding: var(--space-2);
  /* JS sets an exact pixel max-height on open, measured against the actual
     window (see positionFlyout in dashboard.js) — this is only what applies
     before that runs (or if it can't), so a panel is never fully unbounded. */
  max-height: calc(100vh - 24px); overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--hairline-strong) transparent;
  z-index: 230; display: none;
}
.flyout-panel::-webkit-scrollbar { width: 6px; }
.flyout-panel::-webkit-scrollbar-thumb { background: var(--hairline-strong); border-radius: var(--radius-full); }
.flyout-panel::-webkit-scrollbar-track { background: transparent; }
/* A category near either end of the rail can have less room on its default
   side than its own list needs — Billing (last, and the longest list) is
   the case this was written for. JS adds this the moment that's true,
   opening the panel from the icon's bottom edge instead of past the
   window's; see positionFlyout for how it decides which side wins when
   neither has enough room for the whole list. */
.flyout-panel.align-bottom { top: auto; bottom: 0; }
.rail-cat-group:hover .flyout-panel, .rail-cat-group.open .flyout-panel { display: block; }
.flyout-title { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--ls-wider); font-weight: var(--fw-semibold); color: var(--faint); padding: var(--space-2) var(--space-3) var(--space-1); }
.flyout-group { display: flex; flex-direction: column; gap: 1px; }
.flyout-divider {
  font-size: 10px; text-transform: uppercase; letter-spacing: var(--ls-wide); font-weight: var(--fw-semibold);
  color: var(--faint); padding: var(--space-3) var(--space-3) var(--space-1); border-top: 1px solid var(--hairline); margin-top: var(--space-1);
}
.flyout-note { margin: 0; padding: var(--space-1) var(--space-3) var(--space-2); font-size: var(--fs-xs); line-height: var(--lh-relaxed); color: var(--muted); max-width: 30ch; }
.navlink.disabled { cursor: default; color: var(--faint); }
.navlink.disabled:hover { background: transparent; color: var(--faint); }
.navlink.disabled svg { opacity: 0.4; }
.navlink.disabled .tag { color: var(--faint); }

/* Expanded, the flyout heading would just repeat the name the row itself
   already shows next to its icon — shown only once collapsing removes that
   inline label (see the collapsed block further down). */
.flyout-title { display: none; }

/* ── Collapse toggle ──────────────────────────────────────────────────── */
.rail-collapse-btn {
  display: flex; align-items: center; gap: var(--space-3); width: 100%;
  padding: var(--space-2) var(--space-3); margin-top: var(--space-2); border: none; border-radius: var(--radius-md);
  background: transparent; color: var(--faint); font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  cursor: pointer; transition: var(--btn-transition);
}
.rail-collapse-btn:hover { background: var(--panel-raised); color: var(--text-soft); }
.rail-collapse-btn svg { width: 15px; height: 15px; flex-shrink: 0; transition: transform 180ms ease; }
@media (prefers-reduced-motion: reduce) { .rail-collapse-btn svg { transition: none; } }

/* ── Collapsed mode ───────────────────────────────────────────────────────
   Scoped to this breakpoint, not just to body.rail-collapsed, so a
   collapse choice made at desktop width can never leak into the mobile
   drawer below (720px) — that drawer's own rules always win there because
   these two media queries can never both match at once. */
@media (min-width: 721px) {
  body.rail-collapsed .rail { padding-left: var(--space-2); padding-right: var(--space-2); }
  body.rail-collapsed .rail-brand { flex-direction: column; gap: var(--space-2); }
  body.rail-collapsed .rail-brand img { max-width: 36px; }
  body.rail-collapsed .rail-brand-sub { display: none; }

  body.rail-collapsed .rail-cat { justify-content: center; padding: 0; gap: 0; }
  /* The label has nowhere left to sit inline — it becomes the same floating
     name-tag the reference screenshot shows next to a permanently-iconised
     rail: visible on hover/focus only. */
  body.rail-collapsed .rail-cat-label {
    position: absolute; left: calc(100% + 10px); top: 50%; transform: translateY(-50%);
    background: var(--color-white); color: var(--text); border: 1px solid var(--hairline-strong);
    border-radius: var(--radius-sm); box-shadow: var(--shadow-md); padding: 4px 9px;
    font-size: var(--fs-xs); font-weight: var(--fw-semibold); white-space: nowrap;
    opacity: 0; pointer-events: none; transition: opacity 120ms ease; z-index: 240;
    flex: none; overflow: visible; /* undo the expanded row's truncation rules */
  }
  body.rail-collapsed .rail-cat:hover .rail-cat-label,
  body.rail-collapsed .rail-cat:focus-visible .rail-cat-label { opacity: 1; }
  /* Dashboard has no flyout, so its floating tag is the only label it gets —
     leave it be. Every other category's flyout opens on the same hover and
     already carries its own heading with the same name; showing the
     floating tag too would sit directly on top of that heading. */
  body.rail-collapsed .rail-cat-group:has(.flyout-panel) .rail-cat-label { opacity: 0; }
  /* ...and that heading only has something to say once the inline label
     above is the thing that's hidden. */
  body.rail-collapsed .flyout-title { display: block; }

  body.rail-collapsed .rail-collapse-btn { justify-content: center; padding-left: 0; padding-right: 0; }
  body.rail-collapsed .rail-collapse-btn svg { transform: rotate(180deg); }
  body.rail-collapsed .rail-collapse-label { display: none; }

  body.rail-collapsed .rail-foot-text { display: none; }
  body.rail-collapsed .rail-foot { justify-content: center; padding-left: 0; }
}

.navlink {
  display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
  border: none; background: transparent; color: var(--text-soft); font-size: var(--fs-sm);
  font-weight: var(--fw-medium); text-align: left; cursor: pointer; width: 100%; transition: var(--btn-transition);
}
.navlink svg { width: 17px; height: 17px; flex-shrink: 0; opacity: 0.75; }
.navlink:hover { background: var(--panel-raised); color: var(--text); }
.navlink.active { background: var(--accent-soft); color: var(--accent-soft-text); font-weight: var(--fw-semibold); }
.navlink.active svg { opacity: 1; }
.navlink .tag {
  margin-left: auto; font-size: 10px; font-weight: var(--fw-semibold); letter-spacing: var(--ls-wide);
  padding: 2px 5px; border-radius: var(--radius-sm); background: var(--panel-raised); color: var(--faint);
  border: 1px solid var(--hairline); text-transform: uppercase;
}
.navlink.active .tag { background: var(--color-white); border-color: var(--hairline-strong); color: var(--accent); }

.rail-foot {
  flex: 0 0 auto;
  border-top: 1px solid var(--hairline);
  margin-top: var(--space-3); padding-top: var(--space-3);
  display: flex; align-items: center; gap: var(--space-3); padding-left: var(--space-2);
  min-width: 0; /* the child below can only truncate if this can shrink past its content */
}
.avatar {
  width: 30px; height: 30px; border-radius: var(--radius-full); background: var(--accent-soft);
  display: grid; place-items: center; font-size: var(--fs-xs); font-weight: var(--fw-bold);
  color: var(--accent-soft-text); flex-shrink: 0;
}
/* This used to have no overflow handling at all: at 76px permanently-narrow,
   a real name plus "Provider Admin" had nowhere to go but a second and third
   line, stretching the footer row well past its icon and pushing the rail's
   own border into the canvas. Truncated, not wrapped, the same fix the
   category labels above needed for the same reason. */
.rail-foot-text { line-height: var(--lh-tight); min-width: 0; overflow: hidden; }
.rail-foot-text strong { display: block; font-size: var(--fs-sm); font-weight: var(--fw-semibold); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rail-foot-text span { display: block; font-size: var(--fs-xs); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------------------------- MAIN ---------------------------- */
.main { display: flex; flex-direction: column; min-width: 0; }
.topbar { display: flex; align-items: center; gap: var(--space-4); padding: var(--space-3) var(--space-6); border-bottom: 1px solid var(--hairline); background: var(--panel); }

.search-wrap { position: relative; flex: 1; max-width: 560px; margin-right: auto; }
.search {
  display: flex; align-items: center; gap: var(--space-2);
  background: var(--panel-raised); border: 1px solid var(--hairline); border-radius: var(--input-radius);
  padding: var(--space-2) var(--space-3); color: var(--muted); font-size: var(--fs-sm);
}
.search:focus-within { border-color: var(--input-focus-border); background: var(--color-white); }
.search svg { width: 15px; height: 15px; flex-shrink: 0; }
.search input {
  flex: 1; min-width: 0; border: none; background: none; color: var(--text); font-size: var(--fs-sm); outline: none;
}
.search input::placeholder { color: var(--muted); }
.search kbd { font-family: var(--mono); font-size: var(--fs-xs); background: var(--panel); border: 1px solid var(--hairline-strong); border-radius: var(--radius-sm); padding: 1px 5px; color: var(--faint); }
.search-results {
  display: none; position: absolute; top: calc(100% + 6px); left: 0; right: 0;
  background: var(--panel); border: 1px solid var(--hairline); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg); z-index: 150; max-height: 60vh; overflow-y: auto;
}
.search-results.open { display: block; }
.search-group-label {
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--ls-wider); font-weight: var(--fw-semibold);
  color: var(--faint); padding: var(--space-2) var(--space-3) var(--space-1);
}
.search-result-row {
  display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); cursor: pointer; font-size: var(--fs-sm);
}
.search-result-row:hover { background: var(--panel-raised); }
.search-result-row .sr-sub { color: var(--faint); font-size: var(--fs-xs); margin-left: auto; }
.search-empty { padding: var(--space-4); font-size: var(--fs-sm); color: var(--muted); }

.topbar-right { display: flex; align-items: center; gap: var(--space-3); margin-left: auto; }
.icon-btn {
  width: 34px; height: 34px; display: grid; place-items: center; border-radius: var(--radius-md);
  border: 1px solid var(--hairline); background: var(--panel); color: var(--text-soft); cursor: pointer; position: relative;
}
.icon-btn svg { width: 16px; height: 16px; }
.icon-btn:hover { background: var(--panel-raised); border-color: var(--hairline-strong); }
.icon-btn .dot { position: absolute; top: -3px; right: -3px; width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--critical); border: 2px solid var(--panel); }

.canvas { flex: 1; padding: var(--space-6) var(--space-8) var(--space-16); overflow-x: hidden; }
.page { display: none; }
.page.active { display: block; animation: rise 0.25s ease; }
@keyframes rise { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* flex-wrap, not just flex: a heading plus a row of page actions has no
   breakpoint of its own anywhere in this file, and .canvas clips overflow-x
   — without wrap, a narrow viewport once the two together outgrew it simply
   cut the excess off rather than dropping the buttons to a second line. */
.page-head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-5); }
.page-head h1 { font-size: var(--fs-xl); margin: 0 0 var(--space-1); letter-spacing: -0.015em; text-wrap: balance; font-weight: var(--fw-bold); color: var(--text); }
.page-head p { margin: 0; color: var(--text-soft); font-size: var(--fs-sm); max-width: 62ch; line-height: var(--lh-normal); }

/* ── "How this works" ─────────────────────────────────────────────────────
   Every page used to open with a paragraph explaining itself, and everyone read
   all of it, every time, before reaching the thing they came for. Several of
   those paragraphs record a real distinction worth keeping — a credit is not a
   discount, a run rate is not an invoice — so the text is demoted rather than
   deleted: closed by default, one click away, and out of the path of someone
   who already knows. */
.page-info { margin-top: var(--space-2); max-width: 62ch; }
.page-info summary {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--accent);
  list-style: none; padding: 2px 0; width: fit-content;
}
.page-info summary::-webkit-details-marker { display: none; }
.page-info summary::before {
  content: "i"; display: grid; place-items: center;
  width: 14px; height: 14px; border-radius: var(--radius-full);
  border: 1.5px solid currentColor; font-size: 9px; font-weight: var(--fw-bold);
  font-style: italic; font-family: Georgia, serif;
}
.page-info summary:hover { color: var(--accent-strong); }
.page-info[open] summary { margin-bottom: var(--space-2); }
.page-info p {
  margin: 0; padding: var(--space-3) var(--space-4);
  background: var(--panel-raised); border-left: 2px solid var(--hairline-strong);
  border-radius: 0 var(--radius-md) var(--radius-md) 0;
  font-size: var(--fs-xs); line-height: var(--lh-relaxed); color: var(--text-soft);
}

/* Buttons per DS §Buttons: Title Case (never uppercase), 8px radius,
   gradient fill for the primary action, outline for everything else. */
.btn {
  display: inline-flex; align-items: center; gap: 6px; border-radius: var(--btn-radius);
  border: 1px solid var(--hairline-strong); background: var(--panel); color: var(--text);
  font-size: var(--fs-sm); font-weight: var(--fw-semibold); padding: 6px var(--space-3);
  cursor: pointer; white-space: nowrap; transition: var(--btn-transition);
  /* .btn is a <button> almost everywhere, which never underlines on its
     own — but a handful of real links (the AWS module card, the 404 page)
     wear it too, and without this they render as an underlined hyperlink
     wearing a button's clothes. Belongs on the base rule, not repeated as
     a page-scoped override every time one more shows up. */
  text-decoration: none;
}
.btn:hover { background: var(--panel-raised); border-color: var(--accent); color: var(--accent-strong); }
/* A standalone toggle (not one of a .segmented group) still needs to show
   its own pressed state — "Locked only" on Compute is the first of these. */
.btn.active { background: var(--accent-soft); color: var(--accent-soft-text); border-color: var(--accent-soft-text); font-weight: var(--fw-semibold); }
/* Feedback belongs on the press, not the response that follows it: the
   click has to feel caught before a request even goes out. */
.btn:active, .navlink:active, .tabstrip .tab:active, .linky:active {
  transform: scale(0.97); transition: transform 90ms ease-out;
}
.btn.primary { background: var(--gradient-horizontal); border-color: transparent; color: var(--fg-inverse); }
.btn.primary:hover { opacity: 0.9; box-shadow: var(--shadow-md); color: var(--fg-inverse); }
.btn svg { width: 15px; height: 15px; }
/* The bulk selection toolbar's action buttons now grey out when nothing
   selected can currently do them (see refreshSelectionBar) — needs its own
   :hover override too, or a disabled button still visibly reacts to a
   pointer hovering over it, which reads as "this is clickable". */
.btn:disabled, .btn.danger:disabled {
  opacity: .45; cursor: not-allowed; color: var(--muted); border-color: var(--hairline-strong);
}
.btn:disabled:hover, .btn.danger:disabled:hover {
  background: var(--panel); border-color: var(--hairline-strong); color: var(--muted); box-shadow: none;
}

.kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-3); margin-bottom: var(--space-6); }
.kpi { background: var(--panel); border: var(--card-border); border-radius: var(--radius); padding: var(--space-4) var(--space-5); box-shadow: var(--shadow); }
.kpi-label { font-size: var(--fs-xs); color: var(--muted); font-weight: var(--fw-semibold); display: flex; align-items: center; justify-content: space-between; text-transform: uppercase; letter-spacing: var(--ls-wide); }
.kpi-value { font-size: var(--fs-2xl); font-weight: var(--fw-bold); margin-top: var(--space-2); letter-spacing: -0.025em; color: var(--text); }
.kpi-value small { font-size: var(--fs-sm); font-weight: var(--fw-medium); color: var(--muted); }
.kpi-delta { font-size: var(--fs-xs); margin-top: 3px; display: flex; align-items: center; gap: var(--space-1); font-weight: var(--fw-medium); }
.kpi-delta.up { color: var(--good); }
.kpi-delta.down { color: var(--critical); }
.spark { display: block; margin-top: var(--space-2); width: 100%; height: 30px; }

.panel { background: var(--card-bg); border: var(--card-border); border-radius: var(--card-radius); box-shadow: var(--card-shadow); }
.panel-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--hairline); }
.panel-head h2 { font-size: var(--fs-base); margin: 0; font-weight: var(--fw-semibold); color: var(--text); }
.panel-head .count { font-size: var(--fs-xs); color: var(--muted); }
.panel-head .spacer { flex: 1; }

.grid-2 { display: grid; grid-template-columns: 1.4fr 1fr; gap: var(--space-4); align-items: start; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-4); }
@media (max-width: 980px) { .grid-2, .grid-3 { grid-template-columns: 1fr; } }
.stack { display: flex; flex-direction: column; gap: var(--space-4); }

.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
thead th {
  text-align: left; font-weight: var(--fw-semibold); color: var(--muted); padding: 6px var(--space-3);
  border-bottom: 1px solid var(--hairline); white-space: nowrap; font-size: var(--fs-xs);
  text-transform: uppercase; letter-spacing: var(--ls-wide); background: var(--panel-raised);
}
tbody td { padding: 7px var(--space-3); border-bottom: 1px solid var(--border-subtle); vertical-align: middle; color: var(--text-soft); }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--panel-raised); }
td.name { font-weight: var(--fw-semibold); color: var(--text); }
td.sub { color: var(--muted); font-size: var(--fs-xs); }

/* Full pill shape + a same-tone border, matching the AWS Management
   module's StatusBadge (rounded-full, border-<tone>-50) — was a small-
   radius rectangle with no border. The leading dot (::before) is this
   app's own addition on top of that shape, kept as-is. */
.chip { display: inline-flex; align-items: center; gap: 5px; font-size: var(--fs-xs); font-weight: var(--fw-semibold); padding: 3px var(--space-2); border-radius: var(--radius-full); border: 1px solid transparent; }
.chip::before { content: ""; width: 5px; height: 5px; border-radius: var(--radius-full); background: currentColor; }
.chip.good { background: var(--good-soft); color: var(--good); border-color: var(--good-soft); }
.chip.critical { background: var(--critical-soft); color: var(--critical); border-color: var(--critical-soft); }
.chip.warn { background: var(--warn-soft); color: var(--warn); border-color: var(--warn-soft); }
.chip.info { background: var(--info-soft); color: var(--info); border-color: var(--info-soft); }
.chip.muted { background: var(--panel-raised); color: var(--text-soft); border: 1px solid var(--hairline-strong); }
.chip.muted::before { display: none; }

.bar-track { width: 100%; height: 6px; border-radius: var(--radius-full); background: var(--panel-raised); border: 1px solid var(--hairline); overflow: hidden; }
.bar-fill { height: 100%; border-radius: var(--radius-full); background: var(--gradient-horizontal); }
.bar-fill.good { background: var(--color-brand-green); }
.bar-fill.critical { background: var(--color-danger); }
.quota-cell { display: flex; flex-direction: column; gap: var(--space-1); min-width: 120px; }
.quota-cell .nums { display: flex; justify-content: space-between; font-size: var(--fs-xs); color: var(--muted); }

.row-actions { display: flex; gap: var(--space-1); justify-content: flex-end; }
.row-actions button {
  border: 1px solid var(--hairline); background: var(--panel); color: var(--text-soft);
  border-radius: var(--radius-md); padding: var(--space-1) var(--space-2); font-size: var(--fs-xs);
  font-weight: var(--fw-medium); cursor: pointer; transition: var(--btn-transition);
}
.row-actions button:hover { background: var(--accent-soft); color: var(--accent-soft-text); border-color: var(--accent); }

/* A destructive row action. Neutral until pointed at, so a Delete sitting beside
   a Publish does not shout for attention it has not earned, then unmistakable on
   hover — the moment it matters is the moment before the click. */
.row-actions button.danger { color: var(--color-danger); border-color: var(--hairline); }
.row-actions button.danger:hover {
  background: var(--color-danger); color: var(--color-white); border-color: var(--color-danger);
}

.toolbar { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-4); border-bottom: 1px solid var(--hairline); flex-wrap: wrap; }
.filter-chip {
  border: 1px solid var(--hairline-strong); background: var(--panel); color: var(--text-soft);
  padding: 5px var(--space-3); border-radius: var(--radius-sm); font-size: var(--fs-xs); font-weight: var(--fw-medium); cursor: pointer;
}
.filter-chip.active { background: var(--accent-soft); color: var(--accent-soft-text); border-color: var(--accent); font-weight: var(--fw-semibold); }
.toolbar .search { max-width: 240px; padding: 5px var(--space-2); margin-left: auto; }

/* DS "Information Card": light gray fill with a Tech Blue left accent border */
.empty-note {
  display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-5);
  background: var(--panel-raised); color: var(--text-soft); border-left: 3px solid var(--accent);
  border-bottom: 1px solid var(--hairline); font-size: var(--fs-sm);
}
.empty-note svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--accent); }

.feed { list-style: none; margin: 0; padding: var(--space-1) 0; }
.feed li { display: flex; gap: var(--space-3); padding: var(--space-3) var(--space-5); border-bottom: 1px solid var(--border-subtle); font-size: var(--fs-sm); }
.feed li:last-child { border-bottom: none; }
.feed .dot-icon { width: 24px; height: 24px; border-radius: var(--radius-md); flex-shrink: 0; display: grid; place-items: center; margin-top: 1px; }
.feed .dot-icon svg { width: 13px; height: 13px; }
.feed .dot-icon.good { background: var(--good-soft); color: var(--good); }
.feed .dot-icon.critical { background: var(--critical-soft); color: var(--critical); }
.feed .dot-icon.info { background: var(--info-soft); color: var(--info); }
.feed .dot-icon.warn { background: var(--warn-soft); color: var(--warn); }
.feed .feed-body { flex: 1; }
.feed .feed-title { color: var(--text-soft); }
.feed .feed-meta { color: var(--faint); font-size: var(--fs-xs); margin-top: 2px; }

.capacity-row { padding: var(--space-3) var(--space-5); border-bottom: 1px solid var(--border-subtle); }
.capacity-row:last-child { border-bottom: none; }
.capacity-row .top { display: flex; justify-content: space-between; font-size: var(--fs-sm); margin-bottom: var(--space-2); }
.capacity-row .top .label { color: var(--text-soft); font-weight: var(--fw-semibold); }
.capacity-row .top .val { color: var(--muted); }

.role-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--space-3); padding: var(--space-5); }
.role-card { border: 1px solid var(--hairline); border-radius: var(--radius-lg); padding: var(--space-4); background: var(--panel-raised); }
.role-card h3 { margin: 0 0 var(--space-1); font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); }
.role-card p { margin: 0; font-size: var(--fs-xs); color: var(--text-soft); line-height: var(--lh-normal); }
.role-card .role-count { margin-top: var(--space-2); font-size: var(--fs-xs); color: var(--muted); }

.loading, .empty { color: var(--muted); font-size: var(--fs-sm); padding: var(--space-5); text-align: center; }
.footer-note { margin-top: var(--space-8); font-size: var(--fs-xs); color: var(--faint); text-align: center; line-height: var(--lh-relaxed); }
.footer-note b { color: var(--text-soft); font-weight: var(--fw-semibold); }

/* ---------------------------- QUICK ACTIONS ---------------------------- */
.quick-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: var(--space-3); margin-bottom: var(--space-6); }
.quick-action {
  display: flex; flex-direction: column; gap: var(--space-2); align-items: flex-start;
  background: var(--panel); border: var(--card-border); border-radius: var(--radius);
  padding: var(--space-4) var(--space-5); box-shadow: var(--shadow); cursor: pointer; text-align: left; width: 100%;
  transition: var(--btn-transition);
}
.quick-action:hover { border-color: var(--accent); box-shadow: var(--shadow-md); transform: translateY(-1px); }
.quick-action .qa-icon {
  width: 32px; height: 32px; border-radius: var(--radius-md); background: var(--gradient-cyan-green);
  color: var(--fg-inverse); display: grid; place-items: center;
}
.quick-action .qa-icon svg { width: 17px; height: 17px; }
.quick-action .qa-label { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); }
.quick-action .qa-sub { font-size: var(--fs-xs); color: var(--muted); }

/* ---------------------------- FORMS ---------------------------- */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3) var(--space-4); }
.form-grid.single { grid-template-columns: 1fr; }
.field { display: flex; flex-direction: column; gap: 5px; font-size: var(--fs-sm); }

/* A form long enough to need grouping (Launch VM's is the case this was
   built for) reads as one undifferentiated grid of inputs without it — the
   same problem the detail view's own .detail-section-label already solved,
   given the same treatment here. The hairline above each (skipped on the
   first) is what actually reads as "a new group starts here" rather than
   just another label floating in the grid. */
.field-section-label {
  grid-column: 1 / -1;
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--ls-wider); font-weight: var(--fw-semibold);
  color: var(--muted); padding-top: var(--space-3); margin-top: var(--space-1);
  border-top: 1px solid var(--hairline);
}
.form-grid > .field-section-label:first-child { padding-top: 0; margin-top: 0; border-top: none; }

/* Launch VM outgrew the 560px every other form dialog is sized for — nine
   fields at that width was either two cramped columns or the orphaned-field
   gap this was written to fix. A little more room, not modal-wide's full
   viewport treatment (that one is sized for a machine's entire detail view,
   not a form). */
.modal.modal-form-wide { max-width: 640px; }
.field.span-2 { grid-column: 1 / -1; }
.field label { font-weight: var(--fw-semibold); color: var(--text-soft); }
.field .hint { font-size: var(--fs-xs); color: var(--faint); font-weight: var(--fw-regular); }
.field input, .field select, .field textarea {
  background: var(--color-white); border: var(--input-border); border-radius: var(--input-radius);
  padding: 6px var(--space-3); font-size: var(--fs-sm); color: var(--text); font-family: inherit;
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none; border-color: var(--input-focus-border); box-shadow: 0 0 0 3px rgba(70, 196, 242, 0.18);
}
.field textarea { resize: vertical; min-height: 64px; }
/* A checkbox reads wrong stacked under a bold heading label the way every
   other .field does — this is the inline "[x] Do the thing" row instead,
   first needed for Launch VM's "force password change at first logon". */
.checkbox-field {
  display: flex; align-items: center; gap: var(--space-2);
  font-weight: var(--fw-regular); color: var(--text); cursor: pointer;
}
.checkbox-field input { width: auto; }
.form-error {
  grid-column: 1 / -1; font-size: var(--fs-sm); color: var(--critical); background: var(--critical-soft);
  border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); display: none;
}
.form-error.visible { display: block; }
.form-actions { grid-column: 1 / -1; display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-1); }

/* ---------------------------- MODAL ---------------------------- */
/* A modal is meant to block everything behind it — but at z-index 100 it sat
   below the search dropdown (150), the notification dropdown (150), and a
   sidebar flyout (230): hover a category or leave a dropdown open while a
   modal is up, and that transient UI rendered on top of what was supposed to
   be the one surface blocking the whole page. Raised above the mobile nav
   drawer (600) too, so a modal always wins regardless of what else is open. */
.modal-overlay {
  position: fixed; inset: 0; background: var(--scrim); backdrop-filter: blur(6px);
  display: none; align-items: flex-start; justify-content: center; padding: 6vh var(--space-5); z-index: 650; overflow-y: auto;
}
.modal-overlay.open { display: flex; }
/* Blur and scale animate together, so the dialogue reads as a surface arriving
   rather than a box that faded in. A flat opacity fade was the cheaper move,
   but it is exactly the difference between a material and a layer. */
.modal { animation: modal-rise 200ms cubic-bezier(.2,.7,.3,1); }
.modal {
  background: var(--panel); border: 1px solid var(--hairline); border-radius: var(--radius-xl);
  width: 100%; max-width: 560px; box-shadow: var(--shadow-xl);
}
@keyframes modal-rise {
  from { opacity: 0; transform: translateY(10px) scale(0.97); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-transparency: reduce) {
  .modal-overlay { backdrop-filter: none; background: var(--scrim-strong); }
}
.modal-head { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-5) var(--space-6); border-bottom: 1px solid var(--hairline); }
.modal-head h3 { margin: 0; font-size: var(--fs-md); font-weight: var(--fw-semibold); flex: 1; color: var(--text); letter-spacing: -0.01em; }
.modal-head p { margin: 3px 0 0; font-size: var(--fs-xs); color: var(--muted); }
.modal-close {
  width: 30px; height: 30px; border-radius: var(--radius-md); border: 1px solid var(--hairline); background: var(--panel-raised);
  color: var(--text-soft); cursor: pointer; display: grid; place-items: center; flex-shrink: 0;
}
.modal-close:hover { background: var(--critical-soft); color: var(--critical); border-color: var(--critical-soft); }
.modal-body { padding: var(--space-5) var(--space-6) var(--space-6); }

/* ---------------------------- TOASTS ---------------------------- */
/* A surface card, not a coloured block. Colour carries the *kind* — on the icon
   and a 3px edge — while the text stays on the same background as every other
   panel in the console, so it is readable at the contrast the rest of the
   product is designed at. A saturated fill behind body copy is what made these
   look like a debug overlay. */
.toast {
  background: var(--panel); border: 1px solid var(--hairline);
  border-left: 3px solid var(--hairline-strong);
  border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  padding: var(--space-3) var(--space-4); display: flex; gap: var(--space-3);
  align-items: flex-start; animation: toast-in 0.2s ease;
}
.toast.success { border-left-color: var(--good); }
.toast.error   { border-left-color: var(--critical); }
.toast.pending { border-left-color: var(--info); }
.toast.info    { border-left-color: var(--info); }
/* Wraps rather than truncating: a message worth interrupting someone for is
   worth showing in full. */
.toast .toast-title { overflow-wrap: anywhere; }
.toast .toast-close { margin-top: 1px; opacity: .6; transition: opacity 120ms ease; }
.toast .toast-close:hover { opacity: 1; }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.toast .toast-icon {
  width: 24px; height: 24px; border-radius: var(--radius-md); display: grid; place-items: center; flex-shrink: 0; margin-top: 1px;
}
.toast .toast-icon svg { width: 14px; height: 14px; }
.toast.pending .toast-icon { background: var(--info-soft); color: var(--info); }
.toast.success .toast-icon { background: var(--good-soft); color: var(--good); }
.toast.error .toast-icon { background: var(--critical-soft); color: var(--critical); }
.toast.info .toast-icon { background: var(--info-soft); color: var(--info); }
.toast .toast-body { flex: 1; min-width: 0; }
.toast .toast-title { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); }
.toast .toast-meta { font-size: var(--fs-xs); color: var(--muted); margin-top: 2px; }
.toast .toast-close { background: none; border: none; color: var(--faint); cursor: pointer; padding: 0; flex-shrink: 0; }
.toast .spinner {
  width: 13px; height: 13px; border-radius: var(--radius-full); border: 2px solid var(--info-soft); border-top-color: var(--info);
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .toast .spinner { animation: none; } }

/* ---------------------------- DETAIL VIEW ---------------------------- */
.detail-section { margin-bottom: var(--space-5); }
.detail-section:last-child { margin-bottom: 0; }
.detail-section-label {
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--ls-wider); font-weight: var(--fw-semibold);
  color: var(--muted); margin-bottom: var(--space-2);
}
.detail-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); gap: var(--space-3) var(--space-4); }
.detail-item { display: flex; flex-direction: column; gap: 2px; }
.detail-item .k { font-size: var(--fs-xs); color: var(--muted); font-weight: var(--fw-medium); }
.detail-item .v { font-size: var(--fs-sm); color: var(--text); font-weight: var(--fw-semibold); }
.detail-item .v.mono { font-weight: var(--fw-regular); }
.category-chip {
  display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--fs-xs); padding: 2px var(--space-2);
  border-radius: var(--radius-sm); background: var(--panel-raised); border: 1px solid var(--hairline-strong);
  color: var(--text-soft); margin: 0 var(--space-2) var(--space-2) 0;
}

/* ================= ADMIN-ONLY: DASHBOARD WIDGET GRID ================= */
.dash-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-4); }
/* The common laptop range (1024-1400px) used to jump straight from 4 columns
   to 2 with nothing in between — every KPI widget was roughly twice as
   wide/tall as its content needed at exactly the width most people actually
   use this at. */
@media (max-width: 1400px) { .dash-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 1024px) { .dash-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 720px) { .dash-grid { grid-template-columns: 1fr; } }

/* Three chart panels, deliberately unequal (the donut's legend needs more
   room than a column strip or a ranked-bar list) — a fixed 3-up row down to
   tablet width, where it drops to the same 1-per-row every other dash-grid
   uses rather than fighting for space. */
.dash-grid-charts { grid-template-columns: 1.15fr 1fr 1fr; }
@media (max-width: 1400px) { .dash-grid-charts { grid-template-columns: 1.15fr 1fr 1fr; } }
@media (max-width: 1024px) { .dash-grid-charts { grid-template-columns: 1fr; } }

.widget { display: flex; flex-direction: column; min-height: 200px; }
.widget[data-goto] { cursor: pointer; }
.widget[data-goto]:hover { border-color: var(--accent); box-shadow: var(--shadow-md); }
/* Same affordance for cloud-gate.js's own delegated nav attributes
   (data-open-page/data-open-manage-users) — data-goto is dashboard.js's
   own Nutanix-only click handler (bound once at load, and gotoPage()
   only knows Nutanix page ids), so a cross-tenant panel like the
   Consolidated Dashboard's charts uses this pair instead, not data-goto. */
.widget[data-open-page], .widget[data-open-manage-users] { cursor: pointer; }
.widget[data-open-page]:hover, .widget[data-open-manage-users]:hover { border-color: var(--accent); box-shadow: var(--shadow-md); }

/* Insights panel — real, computed action items, not decoration. */
.insight-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; background: var(--hairline); border-radius: var(--radius, 8px); overflow: hidden; }
.insight-row {
  display: flex; align-items: center; gap: var(--space-3);
  background: var(--panel); padding: var(--space-3) var(--space-4);
  cursor: pointer; transition: background 120ms ease;
}
.insight-row:hover { background: var(--panel-raised); }
.insight-row .insight-icon { flex: none; width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; }
.insight-row .insight-icon svg { width: 16px; height: 16px; }
.insight-row.warn .insight-icon { background: var(--warn-soft); color: var(--warn); }
.insight-row.critical .insight-icon { background: var(--critical-soft); color: var(--critical); }
.insight-row.good .insight-icon { background: var(--good-soft); color: var(--good); }
.insight-row .insight-text { flex: 1; font-size: var(--fs-sm); }
.insight-row .insight-text b { font-weight: var(--fw-semibold); }
.insight-row .insight-arrow { color: var(--muted); flex: none; }
.card-stat-line { display: flex; align-items: center; gap: var(--space-2); margin-top: var(--space-2); padding-top: var(--space-2); border-top: 1px solid var(--hairline); font-size: var(--fs-xs); color: var(--text-soft); }
.card-stat-line b { color: var(--text); font-size: var(--fs-sm); font-weight: var(--fw-semibold); }
.widget-body { padding: var(--space-4) var(--space-5); flex: 1; display: flex; flex-direction: column; gap: var(--space-2); }
.widget-empty {
  align-items: center; justify-content: center; text-align: center; gap: var(--space-3);
  color: var(--faint); padding: var(--space-5);
}
.widget-empty svg { opacity: 0.55; }
.widget-empty p { margin: 0; font-size: var(--fs-xs); line-height: var(--lh-relaxed); max-width: 32ch; }

.stat-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); font-size: var(--fs-sm); }
.stat-row .stat-name { font-weight: var(--fw-semibold); color: var(--text-soft); cursor: pointer; }
.stat-row .stat-name:hover { color: var(--accent); text-decoration: underline; }
.stat-row .stat-value { font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; color: var(--text); }
.stat-row-bar { display: flex; flex-direction: column; gap: 5px; }

.big-stat { display: flex; align-items: baseline; gap: 6px; }
.big-stat .n { font-size: var(--fs-2xl); font-weight: var(--fw-bold); letter-spacing: -0.025em; color: var(--text); }
.big-stat .u { font-size: var(--fs-xs); color: var(--muted); font-weight: var(--fw-medium); }
.severity-row { display: flex; gap: var(--space-4); margin-top: var(--space-1); }
.severity-cell { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: 1; }
.severity-cell .n { font-size: var(--fs-lg); font-weight: var(--fw-bold); }
.severity-cell .l { font-size: 10px; text-transform: uppercase; letter-spacing: var(--ls-wide); color: var(--muted); font-weight: var(--fw-semibold); }

/* Was an inline style on the widget itself (flex-direction:row), which meant
   no media query could ever touch it without !important — moved here so the
   narrow-width rule below can override it normally. */
#w-top-consumers { display: flex; flex-direction: row; gap: 18px; }
@media (max-width: 900px) {
  /* Three columns side by side left rank/name/value crowding each other once
     the widget itself was down to ~250-300px (a 2-column .dash-grid on a
     narrow phone) — stacked, each column keeps its own full width instead. */
  #w-top-consumers { flex-direction: column; gap: var(--space-4); }
}
.tc-col { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.tc-col h4 { margin: 0 0 2px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--ls-wider); color: var(--faint); font-weight: var(--fw-semibold); }
.tc-row { display: flex; align-items: center; gap: var(--space-2); font-size: var(--fs-sm); }
.tc-rank { width: 15px; flex-shrink: 0; color: var(--faint); font-weight: var(--fw-semibold); font-size: var(--fs-xs); }
.tc-name {
  flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  cursor: pointer; font-weight: var(--fw-medium); color: var(--text-soft);
}
.tc-name:hover { color: var(--accent); text-decoration: underline; }
.tc-value { font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; flex-shrink: 0; color: var(--text); }

/* ---------------------------- HEALTH SCORE ---------------------------- */
.health-gauge { display: flex; align-items: center; gap: var(--space-4); }
.health-ring {
  width: 64px; height: 64px; border-radius: var(--radius-full); display: grid; place-items: center; flex-shrink: 0;
  font-size: var(--fs-lg); font-weight: var(--fw-bold);
}
.health-ring.good { background: var(--good-soft); color: var(--good); border: 3px solid var(--good); }
.health-ring.warn { background: var(--warn-soft); color: var(--warn); border: 3px solid var(--warn); }
.health-ring.critical { background: var(--critical-soft); color: var(--critical); border: 3px solid var(--critical); }
.health-label { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); }
.health-factors { margin-top: 2px; font-size: var(--fs-xs); color: var(--muted); line-height: var(--lh-relaxed); }
.health-factors b { color: var(--text-soft); font-weight: var(--fw-semibold); }

/* ---------------------------- ALLOCATION / DISTRIBUTION ---------------------------- */
.alloc-row { display: flex; justify-content: space-between; align-items: baseline; font-size: var(--fs-sm); padding: 3px 0; }
.alloc-row .l { color: var(--muted); font-weight: var(--fw-medium); }
.alloc-row .v { font-weight: var(--fw-bold); font-variant-numeric: tabular-nums; color: var(--text); }
.dist-legend { display: flex; gap: var(--space-3); flex-wrap: wrap; margin-top: var(--space-2); font-size: var(--fs-xs); color: var(--text-soft); }
.dist-legend span { display: inline-flex; align-items: center; gap: 5px; }
.dist-dot { width: 8px; height: 8px; border-radius: var(--radius-full); display: inline-block; }

/* ---------------------------- DASHBOARD TOOLBAR ---------------------------- */
.dash-toolbar { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

/* Segmented control (data density) — same visual weight as .btn so the
   toolbar reads as one row of controls. */
.segmented { display: flex; border: 1px solid var(--hairline-strong); border-radius: var(--btn-radius); overflow: hidden; }
.segmented button {
  border: none; background: var(--panel); color: var(--text-soft); padding: var(--space-2) var(--space-3);
  cursor: pointer; font-size: var(--fs-xs); font-weight: var(--fw-medium);
}
.segmented button:hover { background: var(--panel-raised); }
.segmented button.active { background: var(--accent-soft); color: var(--accent-soft-text); font-weight: var(--fw-semibold); }

/* ---------------------------- PER-WIDGET RANGE SELECT ---------------------------- */
.range-select {
  background: var(--panel-raised); border: 1px solid var(--hairline-strong); border-radius: var(--radius-sm);
  color: var(--text-soft); font-size: var(--fs-xs); font-weight: var(--fw-medium); padding: 3px 6px; cursor: pointer;
}
.range-select:focus { border-color: var(--input-focus-border); outline: none; }

/* ---------------------------- NOTIFICATION BELL ---------------------------- */
.notif-wrap { position: relative; }
.notif-dropdown {
  /* #toast-host learned this lesson already (min(380px, calc(100vw - 32px)))
     — a fixed 330px sitting flush against the right edge of a narrow phone
     topbar has nothing stopping it running past the left edge of the screen
     too. */
  display: none; position: absolute; top: calc(100% + 8px); right: 0; width: min(330px, calc(100vw - 24px));
  background: color-mix(in srgb, var(--panel) 90%, transparent);
  backdrop-filter: blur(16px) saturate(160%);
  border: 1px solid var(--hairline); border-radius: var(--radius-xl);
  box-shadow: var(--shadow-lg); z-index: 150; overflow: hidden;
  transform-origin: top right;
  animation: menu-grow 160ms cubic-bezier(.2,.7,.3,1);
}
.notif-dropdown.open { display: block; }
@media (prefers-reduced-transparency: reduce) {
  .notif-dropdown { background: var(--panel); backdrop-filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .notif-dropdown { animation: rise 1ms linear; }
}
.notif-head { padding: var(--space-3) var(--space-4); font-size: var(--fs-sm); font-weight: var(--fw-semibold); border-bottom: 1px solid var(--hairline); color: var(--text); }
.notif-dropdown .feed { max-height: 320px; overflow-y: auto; }
.notif-viewall {
  width: 100%; padding: var(--space-3); border: none; border-top: 1px solid var(--hairline); background: var(--panel-raised);
  color: var(--accent); font-weight: var(--fw-semibold); font-size: var(--fs-sm); cursor: pointer;
}
.notif-viewall:hover { background: var(--accent-soft); color: var(--accent-soft-text); }

/* ---------------------------- WIDGET DRAG & DROP ---------------------------- */
.panel-head { cursor: grab; }
.panel-head select, .panel-head button { cursor: pointer; }
.widget.dragging { opacity: 0.4; }
.widget.drag-over { outline: 2px dashed var(--accent); outline-offset: 2px; }
.widget.widget-hidden { display: none !important; }

/* ---------------------------- WIDGET PICKER ---------------------------- */
.widget-picker-list { display: flex; flex-direction: column; gap: 2px; max-height: 50vh; overflow-y: auto; }
.widget-picker-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-3);
  padding: var(--space-2) var(--space-1); border-bottom: 1px solid var(--border-subtle); font-size: var(--fs-sm);
  font-weight: var(--fw-medium); color: var(--text-soft);
}
.widget-picker-row:last-child { border-bottom: none; }
.wp-toggle {
  position: relative; width: 36px; height: 20px; border-radius: var(--radius-full); border: none;
  background: var(--hairline-strong); cursor: pointer; flex-shrink: 0;
}
.wp-toggle::after {
  content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;
  border-radius: var(--radius-full); background: var(--color-white); transition: transform 0.15s ease;
}
.wp-toggle.on { background: var(--accent); }
.wp-toggle.on::after { transform: translateX(16px); }
@media (prefers-reduced-motion: reduce) { .wp-toggle::after { transition: none; } }

/* ---------------------------- DATA DENSITY ---------------------------- */
.dash-grid.density-compact { gap: var(--space-2); }
.dash-grid.density-compact .widget { min-height: 0; }
.dash-grid.density-compact .panel-head { padding: var(--space-2) var(--space-3); }
.dash-grid.density-compact .widget-body { padding: var(--space-2) var(--space-3); gap: 5px; }
.dash-grid.density-compact .panel-head h2 { font-size: var(--fs-sm); }
.dash-grid.density-compact .big-stat .n { font-size: var(--fs-lg); }
.dash-grid.density-compact .health-ring { width: 48px; height: 48px; font-size: var(--fs-base); }

/* ---------------------------- DASHBOARD PROFILES ---------------------------- */
.dash-select-wrap { position: relative; }
.dash-profile-menu {
  display: none; position: absolute; top: calc(100% + 6px); left: 0; min-width: 210px;
  background: color-mix(in srgb, var(--panel) 90%, transparent);
  backdrop-filter: blur(14px) saturate(160%);
  border: 1px solid var(--hairline); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg); z-index: 150; overflow: hidden;
  transform-origin: top left;
  animation: menu-grow 140ms cubic-bezier(.2,.7,.3,1);
}
.dash-profile-menu.open { display: block; }
@media (prefers-reduced-transparency: reduce) {
  .dash-profile-menu { background: var(--panel); backdrop-filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .dash-profile-menu { animation: rise 1ms linear; }
}
.dash-profile-row {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  padding: var(--space-2) var(--space-3); font-size: var(--fs-sm); cursor: pointer; border-bottom: 1px solid var(--border-subtle);
  color: var(--text-soft);
}
.dash-profile-row:last-child { border-bottom: none; }
.dash-profile-row:hover { background: var(--panel-raised); }
.dash-profile-row.active { color: var(--accent-strong); font-weight: var(--fw-semibold); }
.dash-profile-row .dp-delete { color: var(--faint); font-size: var(--fs-xs); padding: 2px 6px; border-radius: var(--radius-sm); }
.dash-profile-row .dp-delete:hover { color: var(--critical); background: var(--critical-soft); }
.dash-profile-new {
  width: 100%; padding: var(--space-2) var(--space-3); border: none; background: var(--panel-raised); color: var(--accent);
  font-weight: var(--fw-semibold); font-size: var(--fs-sm); cursor: pointer;
}
.dash-profile-new:hover { background: var(--accent-soft); color: var(--accent-soft-text); }
.btn[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-soft-text); }

/* ---------------------------- SPARKLINE ---------------------------- */
.mini-spark { display: block; width: 64px; height: 20px; flex-shrink: 0; }

/* ── Metering & billing pages ────────────────────────────────────────────
   Added for Promotions, Invoices, Cost Insights and Collector Health. Every
   value comes from the token layer — no raw hex, per the engineering standard. */

.note-inline {
  margin: 0 var(--space-5) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--hairline);
  border-left: 3px solid var(--color-tech-blue);
  border-radius: var(--radius);
  background: var(--panel-raised);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.note-inline strong { color: var(--text); }
.note-inline.warn {
  border-left-color: var(--color-warning);
  color: var(--text);
}
.note-inline.error {
  border-left-color: var(--color-danger);
  color: var(--text);
}

.panel-foot {
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--hairline);
  font-size: var(--fs-sm);
  color: var(--muted);
}
.panel-foot strong { color: var(--text); }

/* A row that is present but not contributing — a discount line, a zeroed
   add-on. Dimmed rather than hidden, because "why is this zero" is a question
   people ask. */
tbody tr.off td { color: var(--muted); }

/* ── Navigation polish ───────────────────────────────────────────────────
   The rail is the one surface an operator touches on every visit, so it is
   worth the detail. */

.navlink {
  position: relative;
  line-height: var(--lh-tight);
}

/* An accent bar rather than only a fill: on a page of tables, a colour change
   alone is easy to lose, and the bar reads at a glance from the corner of an
   eye. */
.navlink.active::before {
  content: "";
  position: absolute;
  left: calc(var(--space-3) * -1 + 2px);
  top: 20%; bottom: 20%;
  width: 3px; border-radius: var(--radius-full);
  background: var(--accent);
}

.navlink:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* Touch targets stay at least 40px so the rail is usable on a tablet at 2am,
   which is when an ops console actually gets used. */
.navlink { min-height: 38px; }

/* ── Narrow viewports ────────────────────────────────────────────────────
   The rail is a permanent 76px icon column by default now (see "Icon-rail
   categories" above), so there is no separate collapse step to make here —
   only the canvas gets a little breathing room back at this width. */
@media (max-width: 1100px) {
  .canvas { padding-left: var(--space-5); padding-right: var(--space-5); }
}

@media (max-width: 720px) {
  .canvas { padding-left: var(--space-4); padding-right: var(--space-4); }
  .kpi-grid { grid-template-columns: 1fr 1fr; }
  .grid-2 { grid-template-columns: 1fr; }
}

/* ── Footer ──────────────────────────────────────────────────────────── */
.footer-note {
  color: var(--muted);
  font-size: var(--fs-xs);
}


/* ── Links inside tables ─────────────────────────────────────────────────
   Rows used to be dead ends: a tenant, a VM or an invoice was text you then
   had to go and find yourself. */
.linky {
  color: var(--accent-soft-text); text-decoration: none;
  font-weight: var(--fw-semibold); border-bottom: 1px solid transparent;
}
.linky:hover { border-bottom-color: currentColor; }
.linky:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

/* ── Toasts ──────────────────────────────────────────────────────────────
   alert() blocked the page and could only say one thing at a time. Sits
   above even a blocking modal (650) on purpose: a toast is transient system
   feedback ("saved", "failed") that should still reach you regardless of
   what dialog you're in the middle of. */
#toast-host, .toast-stack {
  position: fixed; right: var(--space-5); bottom: var(--space-5); z-index: 700;
  display: flex; flex-direction: column; gap: var(--space-2);
  width: min(380px, calc(100vw - 32px));
  /* The stack is only ever a few cards tall; the container must not swallow
     clicks on the page behind it. */
  pointer-events: none;
}
#toast-host > *, .toast-stack > * { pointer-events: auto; }
/* There is one toast skin, defined with .toast above, and two things that
   raise them: this simple host, and the action tracker's #toast-stack. They
   used to have separate rules that both matched .toast, and the later one —
   a solid red fill — won on cascade order, so an action toast rendered as a
   red block with a dark navy title sitting unreadably on top of it. */
.toast.leaving { opacity: 0; transform: translateY(4px); transition: 180ms ease-in; }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }

/* ── Form dialog ─────────────────────────────────────────────────────────
   Replaces prompt() chains. Fields are shown together because four answers
   combine into one irreversible action, and you should see them as a set
   before committing. Same tier as .modal-overlay (650) — both are "the one
   blocking surface", so neither should ever be able to render under a
   dropdown, a sidebar flyout, or the other. */
.form-dialog-overlay {
  position: fixed; inset: 0; z-index: 650;
  background: var(--scrim);
  display: grid; place-items: center; padding: var(--space-4);
}
.form-dialog {
  background: var(--panel); border-radius: var(--radius);
  width: min(520px, 100%); max-height: min(86vh, 760px);
  display: flex; flex-direction: column;
  box-shadow: 0 24px 60px -20px rgba(0,0,0,.45);
}
.fd-head { padding: var(--space-5) var(--space-5) var(--space-3); border-bottom: 1px solid var(--hairline); }
.fd-head h3 { margin: 0; font-size: var(--fs-lg); letter-spacing: var(--ls-tight); }
.fd-head p { margin: var(--space-2) 0 0; color: var(--text-soft); font-size: var(--fs-sm); line-height: var(--lh-relaxed); }

.fd-body { padding: var(--space-5); overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-4); }
.fd-field { display: flex; flex-direction: column; gap: 5px; }
.fd-field > span { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text); }
.fd-field .req { font-weight: var(--fw-medium); color: var(--muted); font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: var(--ls-wide); }
.fd-field input[type="text"], .fd-field input[type="number"], .fd-field input[type="date"], .fd-field select {
  font: inherit; color: var(--text); background: var(--panel);
  border: 1px solid var(--hairline-strong); border-radius: var(--input-radius);
  padding: var(--space-2) var(--space-3); width: 100%;
}
.fd-field input:focus-visible, .fd-field select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent);
}
.fd-field em { font-style: normal; font-size: var(--fs-xs); color: var(--muted); line-height: var(--lh-normal); }
.fd-check { flex-direction: row; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
.fd-check input { width: 17px; height: 17px; accent-color: var(--accent); }
.fd-check em { flex-basis: 100%; }

.fd-error {
  background: var(--critical-soft, var(--panel-raised)); color: var(--critical);
  border: 1px solid currentColor; border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3); font-size: var(--fs-sm);
}
.fd-foot {
  padding: var(--space-4) var(--space-5); border-top: 1px solid var(--hairline);
  display: flex; justify-content: flex-end; gap: var(--space-2);
}

/* ── Search ──────────────────────────────────────────────────────────── */
.search-result-row.sel { background: var(--accent-soft); }

/* Freshness, stated rather than implied. A spinner says "we are busy"; this
   says "what you are looking at is current", which is the question an operator
   actually has. */
.sync-age {
  font-size: var(--fs-xs);
  color: var(--muted);
  white-space: nowrap;
  display: inline-flex; align-items: center; gap: 6px;
}
.sync-age::before {
  content: ""; width: 6px; height: 6px; border-radius: var(--radius-full);
  background: var(--good); flex-shrink: 0;
}
@media (max-width: 900px) { .sync-age { display: none; } }

/* Filter controls sit in the table toolbar rather than above the panel, so the
   count beside them updates in the same glance. */
.filter-field { display: flex; align-items: center; }
.filter-field input[type="search"] {
  font: inherit; font-size: var(--fs-sm); color: var(--text);
  background: var(--panel); border: 1px solid var(--hairline-strong);
  border-radius: var(--input-radius); padding: var(--space-2) var(--space-3);
  min-width: 220px;
}
.filter-field input:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
@media (max-width: 720px) { .filter-field input[type="search"] { min-width: 140px; } }

/* ── Skeletons ───────────────────────────────────────────────────────────
   "Loading…" as bare text makes a page look broken for the second it is there.
   Shaped placeholders read as content arriving. */
.skeleton-block { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-4) var(--space-5); }
.skeleton-row span {
  display: block; height: 12px; border-radius: var(--radius-full);
  background: linear-gradient(90deg, var(--panel-raised) 25%, var(--hairline) 37%, var(--panel-raised) 63%);
  background-size: 400% 100%;
  animation: skeleton-sweep 1.4s ease-in-out infinite;
}
@keyframes skeleton-sweep { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) {
  .skeleton-row span { animation: none; background: var(--panel-raised); }
}

/* A quieter button for utilities that sit beside a heading. */
.btn-quiet {
  border-color: transparent; background: transparent; color: var(--muted);
  font-size: var(--fs-xs); padding: var(--space-1) var(--space-2);
}
.btn-quiet:hover { color: var(--accent-soft-text); background: var(--panel-raised); border-color: var(--hairline); }

.back-link { display: inline-block; margin-bottom: var(--space-2); font-size: var(--fs-sm); }

/* The version you are looking at, in a list of versions. */
tr.row-selected td { background: var(--accent-soft); }
tr.row-selected td:first-child { box-shadow: inset 3px 0 0 var(--accent); }

/* ── Service codes ────────────────────────────────────────────────────────
   A service is "Per vCPU", not "compute.vcpu". The code is an internal
   identifier and printing it under every row is noise on a page whose job is
   to show prices.

   It stays in the DOM rather than being removed, because two things read the
   text of these tables and would quietly lose the ability to find anything by
   code: the filter box above every table, and the CSV export. Hiding it
   visually keeps both working. It is also on the row's title, so hovering
   still answers "what is this called in the API". */
.code-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* ── Icon-only chrome ─────────────────────────────────────────────────────
   Seventeen pages carried a labelled "Refresh" button beside their real
   actions, all the same width, all competing for the same attention as the
   thing that actually changes something. Auto-update is on by default, so the
   button is a fallback: it keeps its place and loses its voice. */
.btn.btn-icon, button.btn-icon {
  padding: 0; width: 32px; height: 32px; display: grid; place-items: center; flex: none;
}
.btn-icon svg { width: 15px; height: 15px; }
.btn-icon:active svg { transform: rotate(180deg); transition: transform .3s ease; }
/* The hamburger also carries .btn-icon (for its shared 32px icon-button
   sizing), so it inherited this refresh spin too — pressing "open menu"
   visibly spun the three-line glyph 180° like a refresh action, which isn't
   what it does. Same specificity as the rule above, so this wins by simply
   coming after it. */
button.hamburger-btn:active svg { transform: none; transition: none; }


/* ═══════════════ COMPUTE LIST ═══════════════
   The table did not fit, for two reasons that compounded: the uuid was printed
   twice — once under the name and once in its own column, thirty-six monospace
   characters wrapping to four lines — and every row carried five action buttons.
   Both are gone. The uuid lives on the row as data and on the detail panel; the
   actions live behind one control, or on a selection. */

.toolbar-right { display: flex; align-items: center; gap: var(--space-2); margin-left: auto; }

.select-field { display: inline-flex; align-items: center; gap: var(--space-2); }
.select-field > span { font-size: var(--fs-xs); color: var(--muted); font-weight: var(--fw-medium); }
.select-field select {
  font: inherit; font-size: var(--fs-xs); font-weight: var(--fw-medium);
  color: var(--text); background: var(--panel); cursor: pointer;
  border: 1px solid var(--hairline-strong); border-radius: var(--radius-md);
  padding: 5px var(--space-2); min-width: 104px;
}
.select-field select:hover { border-color: var(--accent); }

/* Column views. Fewer columns at a time beats a horizontal scrollbar: a column
   you have to scroll to is one you will not read. */
.table-select .col-capacity, .table-select .col-placement { display: none; }
.table-select.view-general .col-general { display: table-cell; }
.table-select.view-capacity .col-capacity { display: table-cell; }
.table-select.view-capacity .col-general { display: none; }
.table-select.view-placement .col-placement { display: table-cell; }
.table-select.view-placement .col-general { display: none; }
/* All three column groups at once — the default now, so General/Capacity/
   Placement no longer have to be switched between to see one VM's full
   picture. table-wrap already scrolls horizontally (see its overflow-x
   rule) for whatever this doesn't fit at the current viewport width. */
.table-select.view-all .col-general,
.table-select.view-all .col-capacity,
.table-select.view-all .col-placement { display: table-cell; }

/* Selection */
.col-check { width: 34px; padding-right: 0 !important; }
.col-check input { width: 15px; height: 15px; cursor: pointer; accent-color: var(--accent); }
.col-actions { width: 40px; text-align: right; }

/* Delete-lock — locked (the default) reads as muted/neutral so ninety
   ordinary rows don't look alarmed; unlocked is the one that should catch
   the eye, since it's the row now one click away from being deletable. */
.col-lock { width: 30px; padding-right: 0 !important; }
.lock-toggle {
  background: none; border: none; padding: 4px; border-radius: var(--radius-sm);
  color: var(--faint); cursor: pointer; display: grid; place-items: center;
}
.lock-toggle svg { width: 15px; height: 15px; }
.lock-toggle:hover { background: var(--panel-raised); color: var(--text-soft); }
.lock-toggle:not(.locked) { color: var(--warn); }
.lock-toggle:not(.locked):hover { background: var(--warn-soft); }
tbody tr.row-selected { background: var(--accent-soft); }

/* A small recognizable glyph before an OS name (see osIcon() in
   dashboard.js) — inline with the text it labels, not a separate column,
   since a VM table already has enough columns. */
.os-icon { display: inline-flex; vertical-align: -3px; margin-right: 5px; color: var(--text-soft); }
.os-icon svg { width: 15px; height: 15px; }
tbody tr.row-selected:hover { background: var(--accent-soft); }

.selection-bar {
  display: flex; align-items: center; gap: var(--space-3);
  padding: var(--space-2) var(--space-4);
  background: var(--accent-soft); border-bottom: 1px solid var(--accent);
  animation: slide-down 140ms ease-out;
}
@keyframes slide-down { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.selection-count { font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--accent-soft-text); }
.selection-actions { display: flex; gap: var(--space-2); }
.selection-actions .btn { padding: 4px var(--space-3); font-size: var(--fs-xs); }
.selection-bar .btn-quiet { margin-left: auto; font-size: var(--fs-xs); }
.btn.danger { color: var(--color-danger); border-color: var(--hairline); }
.btn.danger:hover { background: var(--color-danger); color: var(--color-white); border-color: var(--color-danger); }

/* Row menu */
.kebab {
  background: none; border: 1px solid transparent; border-radius: var(--radius-md);
  color: var(--muted); font-size: 17px; line-height: 1; cursor: pointer;
  padding: 2px 7px; letter-spacing: 1px;
}
.kebab:hover { background: var(--panel-raised); border-color: var(--hairline); color: var(--text); }
.kebab-menu {
  position: absolute; z-index: 300; width: 168px; padding: var(--space-1);
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  backdrop-filter: blur(14px) saturate(160%);
  border: 1px solid var(--hairline);
  border-radius: var(--radius-md); box-shadow: var(--shadow-lg);
  display: flex; flex-direction: column;
  /* Anchored at the corner nearest the kebab that opened it (the JS positions
     it below-right of the trigger), so it visibly grows from the control
     rather than materialising from nowhere on the page. */
  transform-origin: top right;
  animation: menu-grow 140ms cubic-bezier(.2,.7,.3,1);
}
@keyframes menu-grow {
  from { opacity: 0; transform: scale(0.94) translateY(-4px); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-transparency: reduce) {
  .kebab-menu { background: var(--panel); backdrop-filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .kebab-menu { animation: rise 1ms linear; }
}
.kebab-menu button {
  text-align: left; background: none; border: 0; cursor: pointer;
  font: inherit; font-size: var(--fs-xs); color: var(--text-soft);
  padding: 7px var(--space-3); border-radius: var(--radius-sm);
}
.kebab-menu button:hover { background: var(--panel-raised); color: var(--text); }
.kebab-menu button.danger { color: var(--color-danger); }
.kebab-menu button.danger:hover { background: var(--color-danger); color: var(--color-white); }
.kebab-menu button:disabled {
  color: var(--faint); cursor: not-allowed; opacity: .55;
}
.kebab-menu button.danger:disabled:hover,
.kebab-menu button:disabled:hover { background: none; color: var(--faint); }

/* Grouping */
tbody tr.group-row td {
  background: var(--panel-raised); padding: 6px var(--space-4);
  border-bottom: 1px solid var(--hairline); border-top: 1px solid var(--hairline);
}
tbody tr.group-row:hover td { background: var(--panel-raised); }
.group-name {
  font-size: var(--fs-xs); font-weight: var(--fw-bold); color: var(--text);
  text-transform: uppercase; letter-spacing: var(--ls-wide);
}
.group-count {
  margin-left: var(--space-2); font-size: var(--fs-xs); color: var(--muted);
  background: var(--panel); border: 1px solid var(--hairline);
  border-radius: var(--radius-full); padding: 0 7px; font-weight: var(--fw-semibold);
}

/* A long list is read by scrolling, and a column heading that scrolls away
   takes the meaning of every row with it. This used to cap every table at
   nearly the full viewport height (100vh - 300px) — harmless for a page with
   one table, but Infrastructure stacks five of them (clusters, hosts,
   datacenters, racks, network devices), and each independently claimed
   almost the whole screen: five nested near-full-height scrollboxes instead
   of one page that scrolls. A fixed, human-scale cap means a table long
   enough to need its own scrollbar gets one sized to actually read from,
   not one sized to the window it happens to be open in. */
.table-wrap { max-height: 420px; overflow: auto; }
.table-wrap thead th { position: sticky; top: 0; z-index: 2; }

/* Compute is a single-table page — the fixed cap above exists to stop a
   multi-table page (Infrastructure) from turning into five stacked
   scrollboxes, which doesn't apply here: this table effectively *is* the
   page. Capping it anyway just adds a second, cramped inner scrollbar on
   top of the page's own — reported directly against production, on a
   94-row table with every column now shown by default. Only max-height
   changes, not overflow: with no height cap the box is exactly as tall as
   its content, so overflow:auto (inherited from the rule above) never has
   anything to scroll internally — the page scrolls once, the ordinary
   way, and the sticky header and horizontal scrollbar both keep working
   exactly as before. */
#page-compute .table-wrap { max-height: none; }

/* Compute's Prev/Next + "Page N of M" — the flat (ungrouped) view of a
   filtered list is paginated client-side (see applyVmFilter/
   renderVmPagination in dashboard.js), 25 rows at a time, rather than the
   full list rendering — and, before this, requiring a scroll — every load. */
.pagination { display: flex; align-items: center; justify-content: center; gap: var(--space-3); padding: var(--space-3) 0; }
.pagination .btn-icon { width: 28px; height: 28px; font-size: var(--fs-md); line-height: 1; }
.pagination .btn-icon:disabled { opacity: .35; cursor: not-allowed; }

/* Compute's own row density — 2-3px smaller than the shared table default,
   scoped here rather than in the global --fs-* tokens (which every table
   and every other page reads from too): this is about fitting more of a
   94-row list on screen, not a site-wide type-scale change. */
#page-compute .table-wrap table { font-size: 10px; }
#page-compute .table-wrap thead th { font-size: 9px; padding: 5px var(--space-4); }
#page-compute .table-wrap tbody td { padding: 6px var(--space-4); }

/* A name is a link to somewhere, and should look like one. */
button.linky {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-weight: var(--fw-semibold); color: var(--text);
  text-align: left;
}
button.linky:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }


/* ═══════════════ MACHINE DETAIL ═══════════════
   Prism's VM page is a tab strip over one machine with its actions along the
   top. Ours was a single scroll ending in two tables, and the performance
   figures were printed as text — "CPU utilization 0.1%". A number is not a
   reading: a ring that is nearly empty says "idle" before you have read it. */

/* A machine's detail is a page's worth of tables, not a dialogue. It takes the
   window, less a margin that keeps the dimmed page visible behind it — so it
   still reads as something you opened and can close, rather than a navigation. */
.modal.modal-wide {
  max-width: none; width: calc(100vw - 48px);
  max-height: calc(100vh - 48px); display: flex; flex-direction: column;
}
.modal.modal-wide .modal-body { overflow: auto; flex: 1; }
.modal.modal-wide .detail-grid { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); }
@media (max-width: 720px) { .modal.modal-wide { width: calc(100vw - 16px); } }

.vm-head { min-width: 0; }
.vm-head-meta { display: flex; align-items: center; gap: var(--space-3); margin-top: 5px; flex-wrap: wrap; }
.vm-head-actions { display: flex; gap: var(--space-2); margin-left: auto; margin-right: var(--space-3); }
.vm-head-actions .btn { padding: 4px var(--space-3); font-size: var(--fs-xs); }

.tabstrip {
  display: flex; gap: 2px; padding: 0 var(--space-5);
  border-bottom: 1px solid var(--hairline); background: var(--panel);
  overflow-x: auto;
}
.tabstrip .tab {
  background: none; border: 0; border-bottom: 2px solid transparent;
  padding: 9px var(--space-3); cursor: pointer; white-space: nowrap;
  font: inherit; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--muted); transition: color 120ms ease, border-color 120ms ease;
}
.tabstrip .tab:hover { color: var(--text); }
.tabstrip .tab.active { color: var(--accent); border-bottom-color: var(--accent); }

/* Gauges */
.gauge-row { display: flex; gap: var(--space-6); flex-wrap: wrap; margin-bottom: var(--space-5); }
.gauge { position: relative; text-align: center; min-width: 108px; }
.gauge-svg { width: 84px; height: 84px; transform: rotate(-90deg); }
.gauge-track { fill: none; stroke: var(--panel-raised); stroke-width: 7; }
.gauge-value {
  fill: none; stroke-width: 7; stroke-linecap: round;
  transition: stroke-dashoffset 700ms cubic-bezier(.2,.7,.3,1);
}
.gauge-svg.good .gauge-value { stroke: var(--good); }
.gauge-svg.warn .gauge-value { stroke: var(--color-warning, #E8B460); }
.gauge-svg.critical .gauge-value { stroke: var(--color-danger); }
.gauge-text { position: absolute; top: 30px; left: 0; right: 0; line-height: 1.15; }
.gauge-text strong { display: block; font-size: var(--fs-sm); color: var(--text); font-variant-numeric: tabular-nums; }
.gauge-text span { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: var(--ls-wide); }
.gauge-hint { font-size: 10px; color: var(--faint); margin-top: 2px; }

/* Meters. Each has a stated reference, because a bar with a hidden scale
   invites a comparison between two things that are not comparable. */
.meter-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-4); }
.meter-top { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); }
.meter-top span { font-size: var(--fs-xs); color: var(--muted); }
.meter-top strong { font-size: var(--fs-sm); color: var(--text); font-variant-numeric: tabular-nums; }
.meter-unit { font-size: 10px; color: var(--muted); font-weight: var(--fw-regular); }
.meter-track { height: 5px; background: var(--panel-raised); border-radius: var(--radius-full); margin-top: 5px; overflow: hidden; }
.meter-fill {
  height: 100%; background: var(--accent); border-radius: var(--radius-full);
  animation: meter-grow 600ms cubic-bezier(.2,.7,.3,1);
}
@keyframes meter-grow { from { width: 0 !important; } }
.meter-note { font-size: 10px; color: var(--faint); margin-top: 3px; }

/* VM detail's Metrics tab — assigned + live utilization, each with a small
   history built up client-side from polling (see loadVmPerformance): no
   historical-query dependency on Nutanix's Groups API beyond the single
   /stats call this page already made once per tab open. */
.metric-matrix { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--space-4); margin-top: var(--space-5); }
.metric-card { background: var(--card-bg); border: var(--card-border); border-radius: var(--card-radius); padding: var(--space-3); }
.metric-card-head { display: flex; justify-content: space-between; align-items: baseline; gap: var(--space-2); }
.metric-card-label { font-size: var(--fs-xs); color: var(--muted); text-transform: uppercase; letter-spacing: var(--ls-wide); }
.metric-card-value { font-size: var(--fs-sm); color: var(--text); font-variant-numeric: tabular-nums; }
.metric-card-caption { font-size: 10px; color: var(--faint); margin-top: 2px; }
.sparkline { width: 100%; height: 40px; margin-top: var(--space-2); display: block; }
.sparkline-line { fill: none; stroke: var(--accent); stroke-width: 2; vector-effect: non-scaling-stroke; }
.sparkline-area { fill: var(--accent); opacity: .12; stroke: none; }
.sparkline-empty { font-size: 10px; color: var(--faint); text-align: center; padding-top: var(--space-3); }

.pad { padding: var(--space-4); }

@media (prefers-reduced-motion: reduce) {
  .gauge-value, .meter-fill { transition: none; animation: none; }
}


/* A pointer to where a thing actually lives, in place of a copy of it. */
.panel-note {
  margin-top: var(--space-4); padding: var(--space-3) var(--space-4);
  background: var(--panel); border: 1px solid var(--hairline);
  border-radius: var(--radius-lg); font-size: var(--fs-xs); color: var(--text-soft);
}


/* Tenant quota and account blocks. */
.quota-body { padding: var(--space-4); }
.quota-row { margin-bottom: var(--space-4); }
.quota-row:last-child { margin-bottom: 0; }
.quota-label {
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); color: var(--muted);
  text-transform: uppercase; letter-spacing: var(--ls-wide); margin-bottom: 5px;
}
.quota-body .detail-grid { padding: 0; }


/* ═══════════════ CHARTS ═══════════════
   Six steps of one hue, not six colours. Categorical colour says "these are
   different kinds of thing"; a cost breakdown is one kind of thing in different
   amounts, so the only job the colour has is rank — and a mono ramp stays
   readable for anyone who cannot separate red from green. */

.chart-body { padding: var(--space-4); }
.tabstrip-page {
  padding: 0; margin: var(--space-5) 0 var(--space-4);
  background: none; border-bottom: 1px solid var(--hairline);
}
.tabpane { animation: rise 160ms ease-out; }

/* Donut */
.donut-wrap { display: flex; gap: var(--space-5); align-items: center; flex-wrap: wrap; }
.donut { width: 150px; height: 150px; transform: rotate(-90deg); flex: none; }
.donut circle { transition: stroke-dasharray 500ms cubic-bezier(.2,.7,.3,1); }
.donut text { transform: rotate(90deg); transform-origin: 70px 70px; text-anchor: middle; }
.donut-total { font-size: 15px; font-weight: 700; fill: var(--text); }
.donut-cap { font-size: 8px; fill: var(--muted); text-transform: uppercase; letter-spacing: .1em; }

.legend { list-style: none; margin: 0; padding: 0; flex: 1; min-width: 210px; }
.legend li {
  display: flex; align-items: center; gap: var(--space-2);
  padding: 4px 0; font-size: var(--fs-xs); border-bottom: 1px solid var(--border-subtle);
}
.legend li:last-child { border-bottom: 0; }
.swatch { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.legend-label { color: var(--text-soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.legend-value { margin-left: auto; font-weight: var(--fw-semibold); color: var(--text); font-variant-numeric: tabular-nums; }
.legend-pct { color: var(--muted); min-width: 42px; text-align: right; font-variant-numeric: tabular-nums; }

/* Columns */
.columns { display: flex; gap: var(--space-3); align-items: flex-end; height: 168px; }
.column { flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; min-width: 0; }
.column-value { font-size: 10px; font-weight: var(--fw-semibold); color: var(--text); margin-bottom: 4px; }
.column-track { flex: 1; width: 100%; display: flex; align-items: flex-end; }
.column-fill {
  width: 100%; background: var(--accent); border-radius: 3px 3px 0 0;
  animation: col-grow 500ms cubic-bezier(.2,.7,.3,1);
}
@keyframes col-grow { from { height: 0 !important; } }
.column-label {
  font-size: 10px; color: var(--muted); margin-top: 5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}

/* Ranked bars */
.ranked { display: flex; flex-direction: column; gap: 7px; }
.ranked-row { display: flex; align-items: center; gap: var(--space-3); font-size: var(--fs-xs); }
.ranked-label {
  width: 160px; flex: none; color: var(--text-soft);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ranked-track { flex: 1; height: 16px; background: var(--panel-raised); border-radius: 3px; overflow: hidden; }
.ranked-fill { height: 100%; border-radius: 3px; animation: meter-grow 500ms cubic-bezier(.2,.7,.3,1); }
.ranked-value {
  width: 92px; text-align: right; font-weight: var(--fw-semibold);
  color: var(--text); font-variant-numeric: tabular-nums;
}

/* The actions belong on one line with the title, not wrapped under it. */
.page-actions { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }

@media (prefers-reduced-motion: reduce) {
  .donut circle, .column-fill, .ranked-fill { animation: none; transition: none; }
}


/* An edit offered where the value is shown, rather than in a menu somewhere
   else. Quiet until the section is hovered, so it does not compete with the
   numbers it sits beside. */
.detail-section-label { display: flex; align-items: center; gap: var(--space-2); }
.section-edit {
  background: none; border: 0; cursor: pointer; padding: 0 2px;
  font: inherit; font-size: var(--fs-xs); font-weight: var(--fw-semibold);
  color: var(--accent); text-transform: none; letter-spacing: normal;
  opacity: 0; transition: opacity 120ms ease;
}
.detail-section:hover .section-edit, .section-edit:focus-visible { opacity: 1; }
.section-edit:hover { text-decoration: underline; text-underline-offset: 2px; }


/* ═══════════════ MOBILE: a real drawer, and a topbar that shrinks ═══════════════
   Below 1100px the rail already collapsed to icons, permanently — every
   destination stayed reachable but nothing was labelled. Below 720px that is
   not enough screen to work from at all, so the rail becomes a proper
   off-canvas drawer with full labels, opened by a hamburger and closed by its
   backdrop, Escape, or picking a destination. */

/* button.hamburger-btn, not .hamburger-btn: this element also carries
   .btn-icon, whose `button.btn-icon { display: grid }` rule has element+class
   specificity — a bare class selector here loses to it every time, which is
   why "display: none" never actually took effect above 720px. Matching its
   specificity (and winning on source order) is what makes it hide at all. */
button.hamburger-btn { display: none; }

.rail-backdrop {
  position: fixed; inset: 0; background: var(--scrim);
  z-index: 550; opacity: 0; pointer-events: none; transition: opacity 200ms ease;
}
.rail-backdrop.show { opacity: 1; pointer-events: auto; }

@media (max-width: 720px) {
  .shell { grid-template-columns: 1fr; }
  button.hamburger-btn { display: grid; place-items: center; }

  /* The icon-only collapse from the 1100px breakpoint still applies at this
     width; re-expand it only while the drawer is actually open, so a rail
     sitting off-screen costs nothing and a rail pulled on screen is usable. */
  .rail {
    position: fixed; top: 0; bottom: 0; left: 0; width: 84vw; max-width: 300px;
    padding-left: var(--space-4); padding-right: var(--space-4);
    transform: translateX(-100%); transition: transform 220ms cubic-bezier(.2,.7,.3,1);
    z-index: 600; box-shadow: var(--shadow-lg);
    background: color-mix(in srgb, var(--panel) 94%, transparent);
    backdrop-filter: blur(20px) saturate(160%);
  }
  @media (prefers-reduced-transparency: reduce) {
    .rail { background: var(--panel); backdrop-filter: none; }
  }
  @media (prefers-reduced-motion: reduce) {
    .rail { transition: opacity 150ms ease; transform: none !important; opacity: 0; pointer-events: none; }
    .rail.rail-open { opacity: 1; pointer-events: auto; }
  }
  .rail.rail-open { transform: translateX(0); }
  .rail.rail-open .rail-brand-sub,
  .rail.rail-open .rail-foot-text { display: block; }
  .rail.rail-open .rail-brand img { max-width: none; }
  .rail.rail-open .rail-foot { justify-content: flex-start; padding-left: var(--space-2); }

  /* The desktop rail keeps this fully visible so a flyout can escape it
     un-clipped (see .rail-nav). Off-canvas there is no flyout to protect —
     every category can be expanded at once inside the drawer, which is
     taller than the screen — so scrolling comes back here, and only here. */
  .rail-nav { overflow-y: auto; overscroll-behavior: contain; }

  /* Off-canvas, a hover flyout has nothing to hover — the drawer becomes an
     accordion instead: tapping a category shows its own destinations inline,
     full-width and labelled, right below it. */
  .rail-cat {
    justify-content: flex-start; padding-left: var(--space-3); padding-right: var(--space-3);
    gap: var(--space-3); font-size: var(--fs-sm); font-weight: var(--fw-medium);
  }
  .rail-cat svg { width: 18px; height: 18px; }
  .rail-cat-label {
    position: static; transform: none; background: none; border: none; box-shadow: none;
    padding: 0; margin-left: auto; opacity: 1; pointer-events: none; font-weight: var(--fw-medium);
    color: inherit;
  }
  .flyout-panel {
    position: static; box-shadow: none; border: none; border-radius: 0;
    padding: 0 0 var(--space-2) var(--space-6); min-width: 0; max-width: none;
    max-height: none; overflow: visible; /* the drawer itself scrolls; the panel need not */
  }
  /* The category name is already shown inline next to its icon; repeating it
     as the flyout's own heading would just be the same word twice. */
  .flyout-title { display: none; }
  .flyout-note { max-width: none; }
  /* Off-canvas, "collapsed" and "full" both just mean "the drawer", so the
     desktop-only toggle for switching between them has nothing to do here. */
  .rail-collapse-btn { display: none; }

  /* The topbar itself. flex:1 with no min-width does not shrink below its
     content's natural width — that is the actual cause of the page-wide
     horizontal scroll on a phone, not any one wide element. */
  .topbar { padding-left: var(--space-3); padding-right: var(--space-3); gap: var(--space-2); }
  .search-wrap, .search, #search-input, .topbar-right { min-width: 0; }
  .search-wrap { max-width: none; }
  .search kbd { display: none; } /* a keyboard shortcut hint means nothing on a phone */
  .sync-age { display: none; }
  #logout-btn { padding: 6px var(--space-2); font-size: var(--fs-xs); }
}

/* A phone in one hand: the same overflow cause, tighter. */
@media (max-width: 420px) {
  .canvas { padding-left: var(--space-3); padding-right: var(--space-3); }
  .search-wrap svg { display: none; }
}


/* ═══════════════ 404 ═══════════════
   Rendered by server.js for anything that matches no route and no static file.
   It links this stylesheet, so it is never a second, forgotten design system —
   just a page with no .shell around it. */
.notfound-page {
  min-height: 100vh; display: flex; flex-direction: column; align-items: center;
  justify-content: center; text-align: center; padding: var(--space-6);
  background: var(--canvas);
}
.notfound-logo { height: 32px; width: auto; margin-bottom: var(--space-6); }
.notfound-code {
  font-size: 72px; font-weight: var(--fw-bold); color: var(--accent-soft);
  line-height: 1; letter-spacing: -.02em;
}
.notfound-page h1 { font-size: var(--fs-lg); color: var(--text); margin: var(--space-3) 0 var(--space-2); }
.notfound-page p { color: var(--text-soft); font-size: var(--fs-sm); max-width: 46ch; margin: 0 0 var(--space-5); }
.notfound-page .btn.primary { text-decoration: none; }


/* ═══════════════ Cloud picker ═══════════════
   The account-level "which cloud" screen — now the rail's own content
   before a cloud is picked, and the Consolidated Dashboard (#page-
   consolidated in dashboard.html) in the canvas, matching how the real
   ops console always shows rail + topbar + canvas together rather than a
   modal blocking the whole page. AWS/Azure/IBM Cloud/IBM Power aren't
   built yet — their rows say so honestly (a toast, not a dead click).
   Only Tech9IQ -> Nutanix leads anywhere; picking it is remembered in
   localStorage (same convention as railCollapsed above) so a returning
   admin isn't re-asked every load. #rail-cats and #page-overview are the
   entire existing app, untouched — this only decides which of the two
   rail contents / two starting pages is visible. */
body.cloud-chosen #rail-cloud-picker,
body:not(.cloud-chosen) #rail-cats,
body:not(.cloud-chosen) .rail-switch-cloud { display: none; }

/* IBM Power shares the same #rail-cats container as Nutanix (see the
   rail-cat-group[data-provider="ibm"] entries in dashboard.html) — only
   one provider's groups show at a time, keyed off body[data-active-cloud],
   set by enterNutanix()/enterIBMPower() (cloud-gate.js / ibm-power.js) and
   cleared by switchCloud(). Default (no attribute yet) shows neither,
   which is fine — #rail-cats itself is already hidden until a cloud is
   chosen at all (see the block above). */
body[data-active-cloud="ibm-power"] .rail-cat-group[data-provider="nutanix"],
body:not([data-active-cloud="ibm-power"]) .rail-cat-group[data-provider="ibm"] { display: none; }

/* IBM Power's own pages/nav — same shape as Nutanix's own .navlink/.page,
   a separate class only so ibm-power.js's click handling never collides
   with dashboard.js's real .navlink listeners (which call routeTo(),
   unaware IBM pages exist at all). */
.navlink-ibm {
  display: flex; align-items: center; gap: var(--space-3); padding: var(--space-2) var(--space-3); border-radius: var(--radius-md);
  border: none; background: transparent; color: var(--text-soft); font-size: var(--fs-sm);
  font-weight: var(--fw-medium); text-align: left; cursor: pointer; width: 100%; transition: var(--btn-transition);
}
.navlink-ibm:hover { background: var(--panel-raised); color: var(--text); }
.navlink-ibm.active { background: var(--accent-soft); color: var(--accent-soft-text); font-weight: var(--fw-semibold); }
.navlink-ibm .tag {
  margin-left: auto; font-size: 10px; font-weight: var(--fw-semibold); letter-spacing: var(--ls-wide);
  padding: 2px 5px; border-radius: var(--radius-sm); background: var(--panel-raised); color: var(--faint);
  border: 1px solid var(--hairline); text-transform: uppercase;
}

.rail-cloud-picker { display: flex; flex-direction: column; gap: 2px; padding: 0 var(--space-1); }
.cloud-pick-item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  width: 100%; padding: 8px var(--space-3); border: none; border-radius: var(--radius-md);
  background: none; color: var(--text); font-size: var(--fs-sm); font-weight: var(--fw-medium);
  cursor: pointer; text-align: left; transition: background 140ms ease, color 140ms ease, transform 140ms ease;
}
.cloud-pick-item:hover { background: var(--panel-raised); transform: translateX(1px); }
.cloud-pick-item:hover .cloud-pick-label svg { color: var(--accent); opacity: 1; }
.cloud-pick-item:active { transform: translateX(1px) scale(0.99); }
.cloud-pick-label { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
.cloud-pick-label svg { width: 17px; height: 17px; flex-shrink: 0; opacity: 0.7; transition: color 140ms ease, opacity 140ms ease; }
.cloud-pick-item .chev { color: var(--muted); transition: transform 140ms ease; }
.cloud-pick-item[aria-expanded="true"] .chev { transform: rotate(90deg); color: var(--accent); }
.cloud-pick-item .tag {
  font-size: 10px; font-weight: var(--fw-semibold); color: var(--muted);
  background: var(--panel-raised); border: 1px solid var(--hairline); border-radius: var(--radius-full);
  padding: 1px 7px;
}
.cloud-pick-item-ghost { color: var(--muted); border: 1px dashed var(--hairline-strong); }
.cloud-pick-item-ghost .cloud-pick-label svg { opacity: 0.5; }

/* The picker's own equivalent of .rail-cat.cat-active — same accent-soft
 * treatment, so whichever cross-tenant page is open reads as "selected"
 * in the sidebar the same way the real nav already does. */
.cloud-pick-item.cloud-pick-item-active,
.cloud-pick-subitem.cloud-pick-item-active {
  background: var(--accent-soft); color: var(--accent-soft-text);
}
.cloud-pick-item.cloud-pick-item-active:hover { background: var(--accent-soft); transform: none; }
.cloud-pick-item.cloud-pick-item-active .cloud-pick-label svg { opacity: 1; color: var(--accent); }
.cloud-pick-sub { display: flex; flex-direction: column; gap: 1px; padding-left: var(--space-4); }
.cloud-pick-subitem {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-2);
  width: 100%; padding: 6px var(--space-3); border: none; border-radius: var(--radius-md);
  background: none; color: var(--text-soft); font-size: var(--fs-xs); font-weight: var(--fw-regular);
  cursor: pointer; text-align: left;
}
.cloud-pick-subitem:hover { background: var(--panel-raised); color: var(--text); }
.cloud-pick-divider { height: 1px; margin: var(--space-2) var(--space-3); background: var(--hairline); }

.rail-switch-cloud {
  display: flex; align-items: center; gap: 6px; width: 100%; margin: 0 0 var(--space-3);
  padding: 6px var(--space-2); border: none; background: none; color: var(--muted);
  font-size: var(--fs-xs); font-weight: var(--fw-semibold); cursor: pointer; text-align: left;
}
.rail-switch-cloud:hover { color: var(--accent); }
body.rail-collapsed .rail-switch-cloud span { display: none; }

/* Consolidated Dashboard summary stats — each a clickable entry point into
 * the page it's counting, not just a static tile. */
.summary-stat {
  text-align: left; cursor: pointer;
  transition: transform 160ms cubic-bezier(.2,.7,.3,1), box-shadow 160ms ease, border-color 160ms ease;
}
.summary-stat:hover { transform: translateY(-2px); box-shadow: var(--shadow-raised); border-color: var(--accent); }
.summary-stat .widget-body { display: flex; flex-direction: column; gap: 2px; }
.summary-stat .widget-body svg {
  width: 20px; height: 20px; margin-bottom: var(--space-2); color: var(--accent); opacity: 0.85;
}
.summary-stat .widget-body .sub { margin: 0; font-size: var(--fs-xs); color: var(--muted); text-transform: uppercase; letter-spacing: .03em; }
.summary-stat .widget-body h2 {
  margin: 0; font-size: 28px; font-weight: var(--fw-bold); letter-spacing: -0.02em; color: var(--text);
}
