/* ===================================================================
   APP CHROME — extends the vendored blueprint tokens (styles/tokens.css,
   loaded first — see index.html) into a dense, operational console
   register. System font stack instead of the marketing site's Big
   Shoulders Display — a console you stare at all day wants legibility
   over character; color/motif carry the identity here instead of type.
   See the origincloud-console-design skill for the full rationale.

   THE ENTIRE PALETTE lives in tokens.css, not here — the neutral scale
   (--ink, --surface-0..3, --text/-soft/-muted), the reserved hues
   (--copper/--blue/--sage/--gold/--coral and their -soft variants),
   copper's fill set (--copper-hover/--copper-deep/--ink-on-copper),
   --line/--line-strong/--line-bright, and the --series-1..6 ramp.
   Don't hand-type a hex value for any of those roles in this file,
   including in the :root block below: declaring --my-surface: #14171d
   here is still hand-typing it, it has just moved up a line. That is
   how the neutral scale escaped #11 and had to be reclaimed by #35.
   If a colour you need has no token, the fix is a PR to
   evenonfridays/origincloud-brand and a re-vendor, not a value here.
   See the origincloud-product-design skill.

   A palette colour at an alpha tokens.css doesn't ship is written
   `color-mix(in srgb, var(--token) N%, transparent)`, and at alpha 0
   it is the `transparent` keyword — never an rgba() with the channels
   transcribed by hand. #245 settled that; see
   .agent/CONTEXT/DECISIONS.md for why color-mix() rather than a new
   brand token per alpha.

   What's left in :root below is deliberately NOT palette: geometry,
   the system font stack (a per-product decision, see tokens.css's
   header), and pure-black shadow/scrim alphas.

   The file's invariant needs one grep PER NOTATION, because a palette
   colour can be spelled two ways and #35's hex grep walked straight
   past the decimal one — `rgba(109, 178, 142, …)` is `--sage` just as
   surely as `#6db28e` is:
     grep -nE '#[0-9a-fA-F]{3,8}\b' styles/app.css
       → issue references in comments, nothing else.
     grep -nE 'rgba?\(' styles/app.css
       → pure-black alphas only (--shadow, --scrim, the avatar rim),
         each with a comment saying why it isn't a token.
   Anything else either notation turns up is a token that escaped.

   How THIS console spends the four elevation steps tokens.css defines
   (which step means what is shared; which element sits on which step
   is ours):
     --surface-0  app chrome — topbar, sidebar
     --surface-1  raised — cards, tables, panels
     --surface-2  nested — hover rows, inputs
     --surface-3  active/pressed
   Reach for the next step up only when an element is genuinely nested
   inside the previous one. One step is ~2 dE, so pair it with
   --line/--line-strong rather than expecting the step alone to read as
   raised — that's what the corner-bracket motif is doing.
   =================================================================== */
:root {
  --radius: 4px;
  --radius-sm: 3px;
  /* Pure-black shadow alphas, not palette colours: a drop shadow is
     absence of light, so it darkens whatever surface it falls on
     rather than tinting it toward a hue. rgba(0,0,0,a) is the only
     correct value here and there is no token for it. */
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 12px 28px -14px rgba(0, 0, 0, 0.6);
  /* The overlay scrim, same argument as --shadow and for the same
     reason it isn't a token: dimming the app behind a dialog is the
     light being taken away, not a hue being laid over it. #245: the
     modal backdrop and the ⌘K overlay were two separately-eyeballed
     near-blacks (rgb 6,8,11 and 8,10,13) for one role — the shape of
     drift #35 collapsed into --ink-on-copper. One declaration now, and
     pure black rather than either of them: a near-black picked by eye
     is the thing being removed, and neither of those two had a
     criterion behind it the way --ink-on-copper had a contrast ratio.
     No black alpha reproduces either exactly — a scrim with non-zero
     channels lifts the darks, so the alpha that would match it differs
     per surface (0.375 over --ink, 0.456 over --surface-1). This is
     therefore a deliberate ~1.8-3.3 dE76 darkening across
     --ink..--surface-3, about one elevation step, in the direction of
     more dimming on something whose entire job is to dim.
     Deliberately NOT color-mix(var(--ink) …): over --ink itself that
     scrim is a no-op, and it lands ~6/255 lighter than today. */
  --scrim: rgba(0, 0, 0, 0.6);

  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "Cascadia Code", "Roboto Mono", Consolas, monospace;
}

* {
  box-sizing: border-box;
}
html,
body {
  height: 100%;
}
body {
  margin: 0;
  background: var(--ink);
  color: var(--text);
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
.mono {
  font-family: var(--mono);
}
a {
  color: var(--blue);
  text-decoration: none;
}
a:hover {
  text-decoration: underline;
}
button {
  font-family: inherit;
}
*:focus-visible {
  outline: 2px solid var(--copper);
  outline-offset: 1px;
}
::selection {
  background: var(--copper-soft);
  color: var(--text);
}

svg.icon {
  width: 17px;
  height: 17px;
  flex-shrink: 0;
  stroke: currentColor;
  fill: none;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* ===================== APP SHELL ===================== */
.app {
  display: grid;
  grid-template-columns: 248px 1fr;
  grid-template-rows: 52px 1fr;
  grid-template-areas: "topbar topbar" "sidebar main";
  height: 100vh;
}

.topbar {
  grid-area: topbar;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 16px;
  background: var(--surface-0);
  border-bottom: 1px solid var(--line);
  position: relative;
  z-index: 20;
}
.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.01em;
  padding-right: 14px;
  margin-right: 2px;
  border-right: 1px solid var(--line);
}
.brand .mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border: 1.5px solid var(--text);
  border-radius: 3px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 700;
}
.brand .product {
  color: var(--text-muted);
  font-weight: 500;
}
.brand .product::before {
  content: "/";
  margin: 0 6px;
  color: var(--text-muted);
}

.scope-breadcrumb {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 12.5px;
}
.scope-breadcrumb a {
  display: block;
  padding: 3px 6px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  overflow: hidden;
}
.scope-breadcrumb a:hover {
  background: var(--surface-2);
}
.scope-breadcrumb .crumb-org {
  display: flex;
  align-items: center;
  gap: 5px;
  max-width: 160px;
  color: var(--text-soft);
}
.scope-breadcrumb .crumb-org-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  color: var(--text-muted);
}
.scope-breadcrumb .crumb-org-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.scope-breadcrumb .crumb-project {
  display: block;
  max-width: 160px;
  color: var(--text-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.scope-breadcrumb .crumb-org.active,
.scope-breadcrumb .crumb-project.active {
  color: var(--text);
  font-weight: 600;
}
.scope-breadcrumb .crumb-org.active .crumb-org-icon {
  color: var(--text);
}
.scope-breadcrumb a:hover .crumb-org,
.scope-breadcrumb a:hover .crumb-org-icon,
.scope-breadcrumb a:hover .crumb-project {
  color: var(--text);
}
.scope-breadcrumb .crumb-sep {
  color: var(--text-muted);
}

.topbar-search {
  flex: 1;
  max-width: 420px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border: 1px solid var(--line-strong);
  border-radius: 100px;
  background: var(--surface-1);
  color: var(--text-muted);
  font-size: 12.5px;
  font-family: var(--sans);
  cursor: pointer;
}
.topbar-search:hover {
  background: var(--surface-2);
  color: var(--text);
}
.topbar-search svg.icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}
.topbar-search span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.topbar-search kbd {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 10.5px;
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  padding: 1px 5px;
  flex-shrink: 0;
}

/* The ring expands out of the sage dot and fades — so its colour is
   --sage at an alpha tokens.css doesn't ship, not a colour of its own.
   The 0 stops are the `transparent` keyword: CSS interpolates colour
   premultiplied by alpha, so rgba(<any hue>, 0) and transparent are
   the same point, and writing the hue there would only be a fourth
   place --sage could go stale. Measured, not assumed — that swap alone
   is pixel-identical over the whole 2.4s (#245).
   One consequence to know before copying this idiom: color-mix()
   resolves to a non-legacy color(srgb …), which flips ANIMATED colour
   interpolation from sRGB to oklab. Harmless here — both endpoints are
   the same hue, so premultiplied interpolation holds that hue constant
   in either space, and the measured cost is <= 1/255 on ~4 px of the
   ring edge per frame. Re-measure if you ever animate BETWEEN two
   different hues. */
@keyframes pulse {
  0% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--sage) 55%, transparent);
  }
  70% {
    box-shadow: 0 0 0 6px transparent;
  }
  100% {
    box-shadow: 0 0 0 0 transparent;
  }
}

.topbar-right {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
}
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  background: transparent;
  border: 1px solid transparent;
  color: var(--text-soft);
  cursor: pointer;
}
.icon-btn:hover {
  background: var(--surface-2);
  border-color: var(--line-strong);
  color: var(--text);
}
.avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--copper), var(--copper-deep));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 700;
  color: var(--ink-on-copper);
  cursor: pointer;
  /* Pure-black rim, not a palette colour: it reads as the edge of the
     disc catching less light, so it must darken the gradient beneath
     it at every stop rather than tint it. --line* would put structural
     blue on a copper fill. */
  border: 1px solid rgba(0, 0, 0, 0.2);
}

/* `#272`. The topbar's `Org › Project › Service` bar - one shared trigger
   style across all three segments, because they are three views of one
   question ("where am I, and how do I change it") and styling them
   differently is what made the old split read as unrelated widgets.

   Replaces both the bare `.icon-btn` the service switcher used to be (an
   unlabeled glyph among other unlabeled glyphs) and `.scope-breadcrumb`'s
   plain text links, neither of which looked like a control with a current
   value. */
.scope-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.scope-sep {
  color: var(--text-muted);
  font-size: 13px;
  flex-shrink: 0;
}
.scope-menu {
  position: relative;
}
.scope-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  max-width: 240px;
  height: 30px;
  padding: 0 8px 0 10px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface-1);
  color: var(--text);
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.scope-trigger:hover {
  background: var(--surface-2);
  border-color: var(--line-bright);
}
.scope-trigger svg.icon {
  width: 15px;
  height: 15px;
  color: var(--text-muted);
  flex-shrink: 0;
}
.scope-trigger svg.caret {
  width: 12px;
  height: 12px;
}
.scope-trigger-label {
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The project segment on a page whose route is `NavScope::OrgOnly`
   (`nav::page_scope`). Dimmed, not disabled: picking a project from an
   org-wide page still decides where you land next, so the menu stays live
   and the `title` says why the value is greyed. */
.scope-trigger.dimmed {
  color: var(--text-muted);
  border-color: var(--line);
  font-weight: 500;
}
.scope-trigger .sq {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 3px;
  background: var(--blue-soft);
  border: 1px solid var(--line-strong);
  font-family: var(--mono);
  font-size: 9px;
  font-weight: 700;
  color: var(--blue);
  flex-shrink: 0;
}

.scope-panel {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 20;
  min-width: 260px;
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 6px;
  max-height: 340px;
  overflow-y: auto;
}
.scope-panel-sep {
  height: 1px;
  margin: 6px 4px;
  background: var(--line);
}
.scope-panel-link {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 8px;
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  color: var(--blue);
  cursor: pointer;
}
.scope-panel-link:hover {
  background: var(--surface-2);
}
.scope-panel-link svg.icon {
  width: 13px;
  height: 13px;
}
.org-switch-item.current {
  background: var(--copper-soft);
  color: var(--text);
}
.org-switch-item.current svg.icon {
  color: var(--copper);
}

.service-switch-panel {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 20;
  width: 280px;
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 6px;
}
.service-switch-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
}
.service-switch-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 12px 6px;
  border-radius: var(--radius-sm);
  font-size: 11.5px;
  color: var(--text-soft);
  text-align: center;
  cursor: pointer;
}
.service-switch-tile svg.icon {
  width: 20px;
  height: 20px;
  color: var(--text-muted);
}
.service-switch-tile:hover {
  background: var(--surface-2);
  color: var(--text);
}
.service-switch-tile.active {
  background: var(--copper-soft);
  color: var(--text);
}
.service-switch-tile.active svg.icon {
  color: var(--copper);
}

.sidebar {
  grid-area: sidebar;
  background: var(--surface-0);
  border-right: 1px solid var(--line);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: 12px 10px;
}
.org-switch {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 9px;
  margin-bottom: 14px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface-1);
  cursor: pointer;
}
.org-switch .sq {
  width: 22px;
  height: 22px;
  border-radius: 4px;
  background: var(--blue-soft);
  border: 1px solid var(--line-strong);
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--blue);
  font-weight: 700;
  flex-shrink: 0;
}
.org-switch .name {
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.25;
}
.org-switch .sub {
  font-size: 11px;
  color: var(--text-muted);
  font-family: var(--mono);
}
.org-switch svg.icon {
  margin-left: auto;
  width: 14px;
  height: 14px;
  color: var(--text-muted);
}
.org-switch-panel {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 20;
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 6px;
  max-height: 260px;
  overflow-y: auto;
}
.org-switch-panel-title {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  padding: 4px 6px 6px;
}
.org-switch-back {
  cursor: pointer;
  color: var(--blue);
}
.org-switch-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 8px;
  border-radius: var(--radius-sm);
  font-size: 12.5px;
  color: var(--text);
  cursor: pointer;
}
.org-switch-item:hover {
  background: var(--surface-2);
}
.org-switch-item svg.icon {
  width: 12px;
  height: 12px;
  color: var(--text-muted);
}
.org-switch-empty {
  padding: 8px 6px;
  font-size: 12.5px;
  color: var(--text-muted);
}

.nav-scroll {
  flex: 1;
}
.nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 9px;
  margin-bottom: 1px;
  border-radius: var(--radius-sm);
  color: var(--text-soft);
  font-size: 13px;
  cursor: pointer;
  border-left: 2px solid transparent;
  transition: background 0.12s ease, color 0.12s ease;
}
.nav-item svg.icon {
  color: var(--text-muted);
}
.nav-item:hover {
  background: var(--surface-1);
  color: var(--text);
}
.nav-item.active {
  background: var(--copper-soft);
  color: var(--text);
  border-left-color: var(--copper);
  font-weight: 600;
}
.nav-item.active svg.icon {
  color: var(--copper);
}
.nav-item .count {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--text-muted);
}

.nav-group {
  margin-top: 16px;
}
.nav-group-label {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  padding: 0 9px 6px;
}
.nav-pinned {
  margin-bottom: 4px;
}

/* `#272`. The sidebar's level-up control - swaps the active service's own
   sections for the full service list, and back. Deliberately quieter than a
   `.nav-item`: it moves you between levels rather than being a destination,
   so it must not compete with the active page for attention. No copper here
   for the same reason - copper marks where you are, and this is not a place. */
.nav-up {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin: 6px 0 2px;
  padding: 6px 9px;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font-family: var(--sans);
  font-size: 12px;
  text-align: left;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.nav-up:hover {
  background: var(--surface-1);
  color: var(--text);
}
.nav-up svg.icon {
  width: 13px;
  height: 13px;
}

.nav-empty-hint {
  margin: 12px 9px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}
.nav-empty-hint p {
  margin: 0 0 6px;
  font-size: 12px;
  color: var(--text-muted);
  line-height: 1.4;
}
.nav-empty-hint a {
  font-size: 12px;
  color: var(--blue);
}

.sidebar-foot {
  border-top: 1px solid var(--line);
  margin-top: 10px;
  padding-top: 10px;
}

.main {
  grid-area: main;
  overflow-y: auto;
  background: var(--ink);
  background-image: linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 56px 56px;
}
.topline {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 28px;
  font-size: 12.5px;
  color: var(--text-muted);
  border-bottom: 1px solid var(--line);
}
.topline a {
  color: var(--text-muted);
}
.topline a:hover {
  color: var(--blue);
}
.topline .sep {
  opacity: 0.5;
}
.topline .region-chip {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--text-soft);
  border: 1px solid var(--line-strong);
  border-radius: 100px;
  padding: 3px 10px;
  background: var(--surface-1);
}

.page-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
  padding: 22px 28px 18px;
  flex-wrap: wrap;
}
.page-header h1 {
  font-size: 22px;
  font-weight: 700;
  margin: 0 0 4px;
  letter-spacing: -0.01em;
}
.page-header p {
  margin: 0;
  color: var(--text-muted);
  font-size: 13.5px;
  max-width: 620px;
}
.page-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

.view-body {
  padding: 0 28px 40px;
}

/* ===================== BADGE: preview-data notice ===================== */
.preview-banner {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 28px 18px;
  padding: 8px 12px;
  border: 1px solid var(--gold-soft);
  background: var(--gold-soft);
  border-radius: var(--radius);
  color: var(--gold);
  font-size: 12.5px;
  font-family: var(--mono);
}

/* Same gold "notice" palette as .preview-banner, reused for a one-time-only
   secret reveal (.secret-warning - e.g. a freshly created service account's
   client secret) and for an in-page caveat about the state of the thing
   being looked at (.inline-notice - e.g. an ACL that exists but enforces
   nothing until a subnet points at it). All three are "pay attention"
   callouts, so they share one rule rather than drifting apart. */
.secret-warning,
.inline-notice {
  padding: 8px 12px;
  margin-bottom: 12px;
  border: 1px solid var(--gold-soft);
  background: var(--gold-soft);
  border-radius: var(--radius-sm);
  color: var(--gold);
  font-size: 12.5px;
}

/* ===================== BUTTONS ===================== */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 14px;
  border-radius: var(--radius);
  font-size: 13px;
  font-weight: 600;
  border: 1px solid transparent;
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.12s ease, border-color 0.12s ease, opacity 0.12s ease;
}
.btn svg.icon {
  width: 15px;
  height: 15px;
}
.btn-primary {
  background: var(--copper);
  color: var(--ink-on-copper);
}
.btn-primary:hover {
  background: var(--copper-hover);
}
.btn-secondary {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--text);
}
.btn-secondary:hover {
  background: var(--surface-2);
  border-color: var(--line-bright);
}
.btn-ghost {
  background: transparent;
  color: var(--text-soft);
}
.btn-ghost:hover {
  background: var(--surface-2);
  color: var(--text);
}

/* `#279`. Reload + its auto-reload cadence, as one segmented control: the
   body reloads now, the caret picks the interval. Joined visually (shared
   edge, one hover surface) because they are one concern - splitting them
   into two loose ghost buttons would read as two unrelated actions. */
.reload-control {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.reload-btn {
  position: relative;
  overflow: hidden;
  padding-right: 10px;
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}
.reload-caret {
  padding-left: 4px;
  padding-right: 6px;
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}
.reload-caret svg.icon {
  width: 13px;
  height: 13px;
}
/* Depleting, not filling: it shows the time *remaining* before the page
   reloads itself, so it drains toward the moment something happens rather
   than growing toward it. Copper because an imminent automatic action is a
   signal, and copper is this palette's only signal colour - but kept to a
   2px underline so a control that is always counting never competes with the
   page's real primary action. */
.reload-progress {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  background: var(--copper);
  /* Matches the tick cadence in `reload_button.rs`, so the bar glides
     between samples instead of stepping four times a second. */
  transition: width 250ms linear;
}
/* Auto-reload off (or paused in Mock): no bar at all, and no transition to
   animate it away - a zero-width bar that slid closed would read as a
   countdown that just completed. */
.reload-progress.idle {
  transition: none;
  background: transparent;
}
/* One dropdown-panel treatment, shared. `#370`'s Actions menu is the second
   consumer; keeping them on one rule is what stops a third from inventing its
   own surface and shadow. */
.reload-menu,
.action-menu-items {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 20;
  min-width: 190px;
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 6px;
}
.reload-menu-note {
  padding: 6px 8px 2px;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--text-muted);
}

.btn-danger-ghost {
  background: transparent;
  color: var(--coral);
  /* --coral-soft (0.14) is the fill weight; a border has to carry at
     roughly twice that to read at 1px, the same relationship --line
     (0.14) and --line-strong (0.28) already encode for blue. */
  border-color: color-mix(in srgb, var(--coral) 30%, transparent);
}
.btn-danger-ghost:hover {
  background: var(--coral-soft);
}
.btn-danger {
  /* Reuses --ink (already a token) for on-coral text and derives the
     hover shade with color-mix() instead of hand-typing a new hex —
     see origincloud-console#35 for why that matters here. */
  background: var(--coral);
  color: var(--ink);
  border-color: transparent;
}
.btn-danger:hover {
  background: color-mix(in srgb, var(--coral) 82%, white);
}
.btn-sm {
  padding: 5px 10px;
  font-size: 12px;
}
.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* ===================== BADGES / STATUS ===================== */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  padding: 3px 9px 3px 7px;
  border-radius: 100px;
  text-transform: uppercase;
}
.badge .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
}
.badge.running {
  background: var(--sage-soft);
  color: var(--sage);
}
.badge.running .dot {
  background: var(--sage);
  animation: pulse 2.4s ease-in-out infinite;
}
.badge.occupied {
  background: var(--sage-soft);
  color: var(--sage);
}
/* Free and healthy, claimed by nothing - blue reads as informational
   rather than as either "running" or "something is wrong". `creating` is
   the same blue pulsing; static means settled. */
.badge.available {
  background: var(--blue-soft);
  color: var(--blue);
}
.badge.available .dot {
  background: var(--blue);
}
.badge.stopped {
  /* 14% is the same tint weight as --copper-soft/--coral-soft on the
     sibling badges, so "stopped" sits at the same visual level as the
     status hues while staying on the neutral --text-muted, which is
     the point: stopped is the absence of a state, not one of them.
     tokens.css ships no --text-muted-soft, and shouldn't gain one for
     a single badge — see DECISIONS.md. */
  background: color-mix(in srgb, var(--text-muted) 14%, transparent);
  color: var(--text-muted);
}
.badge.stopped .dot {
  background: var(--text-muted);
}
.badge.terminated {
  background: var(--copper-soft);
  color: var(--copper);
}
.badge.terminated .dot {
  background: var(--copper);
}
.badge.error {
  background: var(--coral-soft);
  color: var(--coral);
}
.badge.error .dot {
  background: var(--coral);
}
.badge.pending {
  background: var(--gold-soft);
  color: var(--gold);
}
.badge.pending .dot {
  background: var(--gold);
  animation: pulse 1.6s ease-in-out infinite;
}
.badge.creating {
  background: var(--blue-soft);
  color: var(--blue);
}
.badge.creating .dot {
  background: var(--blue);
  animation: pulse 1.6s ease-in-out infinite;
}

/* ===================== PANEL / CARD ===================== */
.panel {
  position: relative;
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
}
.panel::before,
.panel::after {
  content: "";
  position: absolute;
  width: 12px;
  height: 12px;
  pointer-events: none;
}
.panel::before {
  top: -1px;
  left: -1px;
  border-top: 2px solid var(--line-bright);
  border-left: 2px solid var(--line-bright);
}
.panel::after {
  bottom: -1px;
  right: -1px;
  border-bottom: 2px solid var(--line-bright);
  border-right: 2px solid var(--line-bright);
}

/* `#278` added a fifth tile (cost), so a fixed four-column grid would leave
   one stranded on its own row. Auto-fit keeps the row full at whatever count
   the page ends up with - the tiles are peers, and none of them deserves a
   row to itself. */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin-bottom: 22px;
}
.stat-tile {
  padding: 16px 18px;
}
.stat-tile .lbl {
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-muted);
  margin-bottom: 10px;
}
.stat-tile .val {
  font-size: 26px;
  font-weight: 700;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}
/* The sub-line is empty while the tile is loading and populated after, so
   without a reserved height every tile grew 18px the moment its data
   arrived - the whole dashboard jumping as the numbers landed, which is
   what reads as a "flash" when switching project.

   Two lines, not one: the cost tile's sub-line ("since <date> · calendar
   month") wraps at these widths, and the tiles share a grid row, so every
   loaded tile is already as tall as that one. Reserving two therefore costs
   nothing once the data is in - it only stops the row collapsing before it
   arrives.

   Not solvable by holding the previous project's numbers instead:
   `refresh::latest` deliberately drops its cache on a scope change, because
   showing the old project's figures under the new project's name would be
   worse than a moment of "…". */
.stat-tile .delta {
  margin-top: 6px;
  font-size: 12px;
  line-height: 1.4;
  min-height: 2.8em;
  font-family: var(--mono);
  display: flex;
  align-items: center;
  gap: 4px;
}
.stat-tile .delta.up {
  color: var(--sage);
}
.stat-tile .delta.down {
  color: var(--coral);
}
.stat-tile .delta.flat {
  color: var(--text-muted);
}

/* `#272`. Dashboard's service panel, built from the same `SERVICES` the
   sidebar and switcher render - it replaced four hand-written links that
   named 4 of ~30 destinations and drifted every time a route moved.

   `#278` gave it the full width and turned the buttons into cards: as a
   half-width grid of identical secondary buttons it read as a second copy
   of the sidebar, which is exactly what a landing page should not be. Each
   card now carries the service's own one-line blurb, so the panel says what
   the catalog is *for* rather than only what it is called. Auto-fill rather
   than a fixed column count - seven services in a 2-column grid leaves a
   hole, and the card width is what should stay constant across breakpoints. */
.dash-services {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 10px;
}
.dash-service {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  min-width: 0;
  padding: 13px 15px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text);
  text-decoration: none;
}
.dash-service:hover {
  border-color: var(--line-strong);
  background: var(--surface-1);
}
.dash-service svg.icon {
  color: var(--copper);
  flex-shrink: 0;
  margin-top: 1px;
}
.dash-service-text {
  min-width: 0;
}
.dash-service-label {
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 3px;
}
.dash-service-blurb {
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-muted);
}

/* `#278`. The one org-scoped panel on an otherwise project-scoped page.
   Buttons, not links: switching project sets the scope signal before it
   navigates, and on Dashboard the route does not change at all - see
   `scope::switch_to_project`. */
.dash-projects {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 10px;
}
.dash-project {
  position: relative;
  text-align: left;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  font: inherit;
}
.dash-project:hover {
  border-color: var(--line-strong);
  background: var(--surface-1);
}
.dash-project.current {
  border-color: var(--copper);
}
.dash-project-name {
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 3px;
}
.dash-project-id {
  font-size: 11.5px;
  color: var(--text-muted);
}
.dash-project-current {
  position: absolute;
  top: 10px;
  right: 12px;
  font-family: var(--mono);
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--copper);
}

/* `#278`. Shown only when all three project listings loaded empty - see
   `views::dashboard`'s `project_is_empty`. Sits above the tiles because
   zeroes are not what someone with an empty project came to read. */
.dash-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  flex-wrap: wrap;
  margin-bottom: 22px;
}
.dash-cta-title {
  font-size: 15px;
  font-weight: 600;
  margin-bottom: 4px;
}
.dash-cta .btn {
  flex-shrink: 0;
}

/* `#278`. The tiles are links now - each lands on the list it counts. They
   keep `.panel`'s own frame, so this only has to undo the anchor defaults
   and add the affordance. */
a.stat-tile {
  display: block;
  color: inherit;
  text-decoration: none;
}
a.stat-tile:hover {
  border-color: var(--copper);
}

/* ===================== TOOLBAR ===================== */
.toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
  flex-wrap: wrap;
}
/* `#286` adopted this. It and `.search-input` were both orphan CSS - a chip
   design that never shipped - so the filter control uses the look that was
   already here rather than introducing a second one beside it.

   `inline-flex`, not `flex`: chips sit in the toolbar row beside the input
   and the hidden-count. And no `cursor: pointer` - the chip is read, not
   clicked; the × inside it is the only control. */
.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px 4px 11px;
  border-radius: 100px;
  border: 1px solid var(--line-strong);
  background: var(--surface-1);
  font-size: 12.5px;
  color: var(--text-soft);
  white-space: nowrap;
}
.filter-chip:hover {
  border-color: var(--line-bright);
}
.toolbar .spacer {
  flex: 1;
}

/* `#286`. The shared table filter. `.search-input` used to live here as
   orphan CSS with no user anywhere in `src/` - this replaces it rather than
   adding a third input variant beside it and `.toolbar-control`. */
.filter-bar {
  align-items: center;
}
.filter-input {
  position: relative;
  min-width: 280px;
}
.filter-input input {
  width: 100%;
}
/* A default filter is styled exactly like a user's own, deliberately: the
   reader must not have to know which is which to know a row is hidden. */
.filter-chip-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.filter-chip-x:hover {
  background: var(--line-strong);
  color: var(--text);
}
.filter-hidden {
  white-space: nowrap;
}
.filter-empty {
  text-align: center;
}
.filter-empty-title {
  font-size: 15px;
  font-weight: 600;
  margin-bottom: 4px;
}

/* ===================== TABLE ===================== */
.table-wrap {
  overflow-x: auto;
}
table.dtable {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
table.dtable th {
  text-align: left;
  font-family: var(--mono);
  font-size: 10.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  font-weight: 600;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line-strong);
  white-space: nowrap;
}
table.dtable td {
  padding: 11px 14px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}
table.dtable tbody tr {
  cursor: pointer;
  transition: background 0.1s ease;
}
table.dtable tbody tr:hover {
  background: var(--surface-2);
}
table.dtable tbody tr:last-child td {
  border-bottom: none;
}
td.num,
th.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.cell-primary {
  font-weight: 600;
}
/* `#444`. One grant inside the Access table's Grants cell: the role and
   its scope on one line, with the revoke that acts on that grant beside
   it, and when it was granted trailing behind. A row holds several, so
   they stack rather than wrap into each other. */
.grant-line {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 0;
}
.grant-line + .grant-line {
  border-top: 1px solid var(--line);
}
/* A button sized to sit inside a table cell next to text, rather than
   next to other buttons in a header. */
.btn-compact {
  padding: 1px 6px;
  font-size: 12px;
}

.cell-sub {
  color: var(--text-muted);
  font-size: 12px;
  margin-top: 1px;
}
.copyable-id {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
}
.copyable-id-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 260px;
}
.copyable-id-btn {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  opacity: 0.6;
  transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease;
}
.copyable-id-btn svg.icon {
  width: 13px;
  height: 13px;
}
.copyable-id-btn:hover {
  opacity: 1;
  background: var(--surface-2);
  color: var(--text);
}
.copyable-id-btn .copyable-id-check {
  color: var(--sage);
}
.error-note {
  color: var(--coral);
  font-size: 12.5px;
  margin: 0 0 12px;
}
.error-note-card {
  padding: 14px 16px;
  border-radius: var(--radius);
  /* Same coral border weight as .btn-danger-ghost, for the same
     reason — 0.14 fill, ~2x that on the 1px edge. */
  border: 1px solid color-mix(in srgb, var(--coral) 30%, transparent);
  background: var(--coral-soft);
  max-width: 460px;
}
.error-note-headline {
  color: var(--text);
  font-size: 13px;
  margin: 0 0 8px;
}
.error-note-details {
  margin-bottom: 10px;
}
.error-note-details summary {
  cursor: pointer;
  color: var(--text-muted);
  font-size: 12px;
  user-select: none;
}
.error-note-details summary:hover {
  color: var(--text-soft);
}
.error-note-details p {
  margin: 6px 0 0;
  color: var(--text-muted);
  font-size: 11.5px;
  word-break: break-word;
}
/* Permission refusals (#224) — shared by the load channel (.error-note-card)
   and the action channel (.toast-permission). The keys themselves are
   identifiers, so they get the same mono treatment every other id does. */
.permission-denied-lead {
  color: var(--text-soft);
  font-size: 12.5px;
  margin: 0 0 6px;
}
.permission-denied-keys {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 10px;
}
.permission-denied-keys code {
  font-family: var(--mono);
  font-size: 11.5px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--surface-2);
  color: var(--text);
  word-break: break-all;
}
.permission-denied-hint {
  font-size: 12px;
  margin: 0;
}
.row-menu {
  width: 34px;
  text-align: right;
}
.table-pager {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 4px;
  font-size: 12.5px;
  color: var(--text-muted);
}
.table-pager .spacer {
  flex: 1;
}

/* ===================== TABLE SKELETON ===================== */
.skel-bar {
  height: 12px;
  width: 70%;
  max-width: 160px;
  border-radius: 3px;
  background: var(--surface-2);
  animation: skel-pulse 1.4s ease-in-out infinite;
}
.skel-row td:first-child .skel-bar {
  width: 85%;
  max-width: 220px;
}
@keyframes skel-pulse {
  0%,
  100% {
    opacity: 0.55;
  }
  50% {
    opacity: 1;
  }
}

/* ===================== TOAST ===================== */
.toast-stack {
  position: fixed;
  top: 68px;
  right: 16px;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: 8px;
  max-width: 360px;
}
.toast {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 12px;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--surface-1);
  box-shadow: var(--shadow);
  font-size: 13px;
  color: var(--text);
  animation: toast-in 0.15s ease;
}
.toast-success {
  border-left: 3px solid var(--sage);
}
.toast-error {
  border-left: 3px solid var(--coral);
}
/* A permission refusal is several lines and carries a link, unlike every
   other toast — it stops being a one-line strip (#224). */
.toast-permission {
  align-items: flex-start;
  padding: 12px;
}
.toast-headline {
  margin: 0 0 6px;
  font-weight: 600;
}
.toast-dismiss {
  flex-shrink: 0;
  border: none;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 0 2px;
}
.toast-dismiss:hover {
  color: var(--text);
}
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ===================== MODAL ===================== */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: var(--scrim);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2000;
  animation: modal-fade-in 0.12s ease;
}
.modal-panel {
  width: 100%;
  max-width: 380px;
  margin: 16px;
  padding: 20px;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--surface-1);
  box-shadow: var(--shadow);
}
.modal-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 8px;
}
.modal-message {
  font-size: 13px;
  color: var(--text-soft);
  margin: 0 0 18px;
}
.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
@keyframes modal-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

/* ===================== EMPTY STATE ===================== */
.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  padding: 64px 24px;
  color: var(--text-muted);
}
.empty-state .ic {
  width: 52px;
  height: 52px;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--surface-2);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 18px;
}
.empty-state .ic svg.icon {
  width: 24px;
  height: 24px;
  color: var(--text-soft);
}
.empty-state h3 {
  margin: 0 0 6px;
  color: var(--text);
  font-size: 15px;
}
.empty-state p {
  margin: 0 0 18px;
  max-width: 380px;
  font-size: 13px;
}

/* ===================== TABS ===================== */
.tabs {
  display: flex;
  gap: 2px;
  border-bottom: 1px solid var(--line-strong);
  margin-bottom: 20px;
}
.tab {
  padding: 9px 4px;
  margin-right: 22px;
  font-size: 13px;
  color: var(--text-muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  background: none;
  border-top: none;
  border-left: none;
  border-right: none;
}
.tab:hover {
  color: var(--text);
}
/* Route-driven tab strips (PVS, #196) render tabs as links rather than
   buttons, so they inherit the global `a:hover` underline - suppress it so a
   linked tab and a button tab are visually identical. */
a.tab,
a.tab:hover {
  display: inline-block;
  text-decoration: none;
}
.tab.active {
  color: var(--text);
  border-bottom-color: var(--copper);
  font-weight: 600;
}

/* Protection status (#198) is a *separate axis* from a resource's lifecycle
   status - a snapshot can be `available` and `unprotected` at once - so it
   reuses the badge shape but never the lifecycle vocabulary in
   `Status::from_raw`. `protected` is a real resource state, which is what
   licenses a status colour here; `unprotected` stays deliberately neutral so
   the protected case is the one that draws the eye. */
.badge.protected {
  background: var(--sage-soft);
  color: var(--sage);
}
.badge.unprotected {
  background: var(--surface-2);
  color: var(--text-muted);
}

/* Audit log outcome (#200) is a third such axis - what the platform
   recorded about one past attempt, not a resource's lifecycle state - so it
   follows the protected/unprotected precedent above rather than
   `Status::from_raw`, where `DENIED` would have fallen through to
   "Creating".

   `DENIED` earns a status hue because a refused IAM action is exactly what
   someone opens this page to find. `SUCCESS` stays deliberately neutral: an
   audit log that shouts about every successful action buries the
   interesting rows. `outcome-unknown` is for a wire value this build's
   schema doesn't know - visibly not a success, without claiming it was a
   denial. */
.badge.outcome-success {
  background: var(--surface-2);
  color: var(--text-muted);
}
.badge.outcome-denied {
  background: var(--coral-soft);
  color: var(--coral);
}
.badge.outcome-unknown {
  background: var(--gold-soft);
  color: var(--gold);
}
/* A faint left edge on the whole row, so a denial is findable while
   scanning a long log rather than only once the eye reaches the outcome
   column. Deliberately faint - the badge is the statement, this is only a
   scanning aid. */
tr.audit-denied td {
  background: var(--coral-soft);
}
tr.audit-denied td:first-child {
  box-shadow: inset 2px 0 0 var(--coral);
}

/* Organisation admission status (#227) - a fourth badge axis, following the
   protected/unprotected and outcome-* precedents above rather than
   `Status::from_raw`, where PENDING_REVIEW falls through the wildcard to
   "Creating" and ACTIVE collapses onto "Running". Neither word describes
   whether the platform has admitted an org.

   `org-active` stays neutral for the same reason `outcome-success` does:
   it's the ordinary case, shown only on the Organisation page's own detail
   row, and colouring it would steal attention from the two states that
   actually change what the console can do. `org-pending-review` is gold
   (waiting on someone else) and `org-rejected` coral (terminal). No dot on
   any of them - these badges appear singly beside prose, not in a status
   column being scanned. */
.badge.org-active {
  background: var(--surface-2);
  color: var(--text-muted);
}
.badge.org-pending-review {
  background: var(--gold-soft);
  color: var(--gold);
}
.badge.org-rejected {
  background: var(--coral-soft);
  color: var(--coral);
}
/* A status this build's vendored schema doesn't know. Visibly not ordinary,
   without borrowing either decided state's colour - same reasoning as
   `outcome-unknown`, in the neutral shape so it can't be mistaken for
   "pending". */
.badge.org-unknown {
  background: var(--surface-2);
  color: var(--gold);
}

/* Compute instance lifecycle (#243). Compute's status is typed end to end
   (`api::models::InstanceStatus`) and no longer goes through
   `Status::from_raw`, where `INITIALIZING`, `STOPPING` and `SHUTTING_DOWN`
   all fell through the wildcard and rendered as a blue pulsing "Creating" -
   an instance being torn down looked like one being built.

   Most states reuse the lifecycle classes above, because the look is
   genuinely the same. These three had no honest counterpart: a transitional
   state wears its *destination's* hue and pulses to say it is still moving,
   so `instance-stopping` is `stopped`'s grey and `instance-shutting-down`
   is `terminated`'s copper. */
.badge.instance-stopping {
  background: rgba(124, 128, 137, 0.14);
  color: var(--text-muted);
}
.badge.instance-stopping .dot {
  background: var(--text-muted);
  animation: pulse 1.6s ease-in-out infinite;
}
.badge.instance-shutting-down {
  background: var(--copper-soft);
  color: var(--copper);
}
.badge.instance-shutting-down .dot {
  background: var(--copper);
  animation: pulse 1.6s ease-in-out infinite;
}
/* A status this build's vendored schema doesn't know. Visibly not ordinary,
   borrowing no decided state's colour - same reasoning as `org-unknown`,
   and the reason the badge must not simply say "Creating" (#94). */
.badge.instance-unknown {
  background: var(--surface-2);
  color: var(--gold);
}
.badge.instance-unknown .dot {
  background: var(--gold);
}

/* Instance health (#243) - a fifth badge axis. What the platform
   *observes*, as opposed to what was *asked for*: a RUNNING instance on an
   unreachable host is still RUNNING, and no lifecycle colour can express
   the difference.

   `health-available` keeps a neutral background with sage text. Every row
   already carries a coloured lifecycle badge, and a second green pill
   beside it would bury the rows that need attention - the same reasoning
   that keeps `outcome-success` and `org-active` neutral.

   `health-unknown` is gold and emphatically not sage: INSUFFICIENT_DATA /
   UNKNOWN means the platform lost its observer, not that the instance is
   fine, and rounding it to healthy is the one reading that actively
   misleads. `health-not-applicable` is fully muted - "not running, so
   nothing to check" is a non-statement, not a good one. No dot on any of
   them: the lifecycle badge in the neighbouring column owns the pulse. */
.badge.health-available {
  background: var(--surface-2);
  color: var(--sage);
}
.badge.health-impaired {
  background: var(--coral-soft);
  color: var(--coral);
}
.badge.health-unknown {
  background: var(--gold-soft);
  color: var(--gold);
}
.badge.health-not-applicable {
  background: var(--surface-2);
  color: var(--text-muted);
}
.badge.health-unrecognised {
  background: var(--surface-2);
  color: var(--copper);
}

/* Resource tags (#458). `origin:env` is a *fourth* such axis — which
   environment a resource belongs to, which is orthogonal to whether it is
   running, protected, or what one past request did to it — so it follows
   the protected/outcome precedent above and keeps its own classes rather
   than borrowing `Status`'s.

   The ramp is deliberate rather than six hues picked for variety: an
   environment's colour has to say how much care an action in it deserves.
   `dev` stays neutral (nothing here is load-bearing), `staging`/`qa` are
   informational blue and sage, `pre-prod` picks up gold's caution, `prod`
   is coral — the one pill that should stop a hand mid-click — and `canary`
   takes copper, distinct from all of them because a canary is neither
   production nor not. `env-unknown` is a value a newer build of the
   platform defines: shown verbatim, visibly not placed. */
.badge.env-dev {
  background: color-mix(in srgb, var(--text-muted) 14%, transparent);
  color: var(--text-muted);
}
.badge.env-staging {
  background: var(--blue-soft);
  color: var(--blue);
}
.badge.env-qa {
  background: var(--sage-soft);
  color: var(--sage);
}
.badge.env-pre-prod {
  background: var(--gold-soft);
  color: var(--gold);
}
.badge.env-prod {
  background: var(--coral-soft);
  color: var(--coral);
}
.badge.env-canary {
  background: var(--copper-soft);
  color: var(--copper);
}
.badge.env-unknown {
  background: var(--surface-2);
  color: var(--text-muted);
}


/* Sidebar switcher: the current org's state, and each listed org's, shown
   only when it isn't ACTIVE (see `components::org_switcher`). */
.org-switch-status {
  margin-top: 5px;
}
/* Stacked, not side-by-side: the panel is as narrow as the sidebar, and an
   org name plus an uppercase mono "PENDING REVIEW" pill do not fit on one
   line - side by side, the name wrapped and the badge collided with the
   chevron. */
.org-switch-item-label {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 5px;
  min-width: 0;
}

/* Shell-level banner for a non-ACTIVE org (#227). Same margins as
   .preview-banner so the two stack cleanly above a page header when a mock
   view is being viewed in a non-active org, but a block rather than a
   single line - it carries two sentences of explanation, not a label. */
.org-status-banner,
.payment-required-banner {
  margin: 0 28px 18px;
  padding: 10px 12px;
  border: 1px solid var(--gold-soft);
  background: var(--gold-soft);
  border-radius: var(--radius);
  color: var(--gold);
  font-size: 12.5px;
}
.org-status-banner-body,
.payment-required-banner-body {
  margin-top: 8px;
  line-height: 1.5;
}

/* `#326`. Shares the shape above so two stacked banners read as one system,
   but coral rather than gold: an unpaid account is not a wait-and-see state
   like PENDING_REVIEW, it is refusing operations right now, and the reader
   is the person who can fix it in two clicks. Same colour reasoning as
   `-terminal` below. */
.payment-required-banner {
  border-color: var(--coral-soft);
  background: var(--coral-soft);
  color: var(--coral);
}
.payment-required-banner p {
  margin: 0 0 6px;
}
.badge.payment-required {
  background: transparent;
  padding-left: 0;
  color: var(--coral);
}
/* The one thing to do about it, so it must not read as body text. Underlined
   rather than button-shaped: it belongs to the sentence above it, and a
   button here would compete with the page's own primary action. */
.payment-required-banner-action {
  color: var(--coral);
  font-weight: 600;
  text-decoration: underline;
}
.payment-required-banner-action:hover {
  color: var(--text);
}
/* Terminal modifier for both gold "pay attention" callout shapes. A REJECTED
   org is not a wait-and-see state, so it doesn't get the waiting colour -
   and the note must not be gold while the badge next to it is coral. */
.org-status-banner-terminal,
/* `#342`. Blue, not gold: this explains who the platform can invoice, which
   a reader needs *before* choosing - it is not a warning that something is
   wrong. Blue is the palette's info role. */
.inline-notice-info {
  border-color: var(--blue-soft);
  background: var(--blue-soft);
  color: var(--blue);
}
.inline-notice-terminal {
  border-color: var(--coral-soft);
  background: var(--coral-soft);
  color: var(--coral);
}
/* The badge brings its own background; inside the banner that reads as a
   patch on a patch. */
.org-status-banner .badge {
  background: transparent;
  padding-left: 0;
}

/* The expanded `details` blob. Pre-wrapped rather than horizontally
   scrolling: the content is opaque JSON of unknown width, and a truncated
   audit record is worse than a tall one. */
.audit-details-cell {
  background: var(--surface-2);
}
.details-json {
  margin: 8px 0 0;
  padding: 10px 12px;
  background: var(--surface-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: 12px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-word;
}

/* A `<select>` sitting inline in a `.toolbar` rather than stacked in a
   `.form-group`, where `.form-control`'s full width would swallow the row.
   Scoped under `.toolbar` on purpose: `.form-control` is declared later in
   this file, so a bare `.toolbar-control` would lose the `width` on source
   order alone. */
.toolbar .toolbar-control {
  width: auto;
  min-width: 220px;
}
.toolbar .form-label {
  margin-bottom: 0;
}

/* Trailing caveat under a table panel - needs its own separation from the
   panel above, which `.form-hint` (built for sitting tight under a form
   field) doesn't carry. */
.table-footnote {
  margin-top: 12px;
}

/* Progress meter for asynchronous operations (#198's snapshot progress).
   Blue, not copper or a status hue: this encodes "how far along", which is
   informational rather than an action signal or a resource state. Only
   rendered when the API actually gives a number - see `snapshot_detail`. */
.meter {
  height: 4px;
  background: var(--surface-2);
  border-radius: 100px;
  overflow: hidden;
}
.meter-fill {
  height: 100%;
  background: var(--blue);
  transition: width 0.2s ease;
}
/* Escalation modifiers for a meter that measures usage against an enforced
   cap (`#70`'s quota view). Unlike a progress meter, "how far along" here
   *is* a resource state - at the limit a launch fails - which is what
   licenses the status hues. A comfortable dimension keeps the plain blue
   above: it's still only informational. Threshold lives in
   `views::quota::NEAR_LIMIT_PERCENT`, not here, so one place decides it.
   #199's spend-limit progress is expected to reuse these rather than
   introduce a second usage-against-a-cap idiom. */
.meter-fill.warn {
  background: var(--gold);
}
.meter-fill.over {
  background: var(--coral);
}

/* ===================== QUOTA ===================== */
.quota-row + .quota-row {
  margin-top: 18px;
}
.quota-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 7px;
}
.quota-label {
  font-size: 13px;
  font-weight: 600;
}
.quota-value {
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}
.quota-value b {
  font-weight: 700;
  color: var(--text);
}
.quota-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-top: 6px;
}
.quota-note {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-muted);
}
.quota-note.warn {
  color: var(--gold);
}
.quota-note.over {
  color: var(--coral);
}
.quota-ask {
  font-size: 11.5px;
  color: var(--blue);
}
.quota-asof {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
  margin: 18px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--line);
  font-size: 11.5px;
  color: var(--text-muted);
}

/* Spend-alert threshold track (#199) — deliberately NOT `.meter` above and
   deliberately not shaped like a quota meter (#70), even though both plot
   "current against a configured number".

   A quota is enforced: the bar is a tank, and filling it means the next
   request is refused. A spend limit is not enforced by anything in
   origincloud-mono — its only consumer is `SpendLimitAlertJob`, which
   emails the configured recipients once per threshold crossing. So this
   affordance leads with the *marks*, not the fill: each tick is a point
   where an email goes out, the track keeps going past the last one, and
   `.threshold-track.over` renders a fill that has visibly run past the end
   rather than a bar that "ran out". Escalation is driven purely by which of
   the customer's own thresholds have been crossed — see
   `views::billing_account::alert_state`. */
.threshold-track {
  position: relative;
  height: 10px;
  border-radius: 4px;
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.threshold-fill {
  height: 100%;
  border-radius: 3px 0 0 3px;
  background: var(--blue);
  transition: width 0.2s ease;
}
.threshold-fill.crossed {
  background: var(--gold);
}
.threshold-fill.all-crossed {
  background: var(--coral);
}
/* Spend past the limit: the fill reaches the end and keeps going, with the
   overshoot shown as a chevron edge instead of a flush, "full" bar — the
   track is not a container that can be filled up. */
.threshold-track.over .threshold-fill {
  border-radius: 3px 0 0 3px;
  clip-path: polygon(0 0, calc(100% - 5px) 0, 100% 50%, calc(100% - 5px) 100%, 0 100%);
}
.threshold-tick {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 1px;
  background: var(--line-bright);
  opacity: 0.5;
}
.threshold-tick.crossed {
  background: var(--text);
  opacity: 0.85;
}
/* Legend under the track: one entry per configured threshold. */
.threshold-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  margin-top: 10px;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--text-muted);
}
.threshold-legend .crossed {
  color: var(--text-soft);
}

/* ===================== SPEC LIST ===================== */
.spec-list {
  display: grid;
  grid-template-columns: 200px 1fr;
  gap: 0;
}
.spec-list .k {
  padding: 10px 0;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--text-muted);
  border-bottom: 1px solid var(--line);
}
.spec-list .v {
  padding: 10px 0;
  font-size: 13px;
  border-bottom: 1px solid var(--line);
}

/* Two `.spec-list` blocks side by side on wide screens - e.g. splitting a
   long instance/resource summary into two shorter columns instead of one
   tall one. Collapses to stacked single-column at the same breakpoint
   `.detail-grid`/`.stat-grid` already use. */
.spec-columns {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 32px;
}
@media (max-width: 980px) {
  .spec-columns {
    grid-template-columns: 1fr;
  }
}

.detail-grid {
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: 20px;
  align-items: start;
}
@media (max-width: 980px) {
  .detail-grid {
    grid-template-columns: 1fr;
  }
}
.panel-pad {
  padding: 20px 22px;
}
.panel-title {
  font-size: 13px;
  font-weight: 700;
  margin: 0 0 4px;
}
.panel-sub {
  font-size: 12px;
  color: var(--text-muted);
  margin: 0 0 16px;
}

/* ===================== FORMS ===================== */
.form-group {
  margin-bottom: 18px;
}
.form-label {
  display: block;
  font-size: 12.5px;
  font-weight: 600;
  margin-bottom: 6px;
}
/* `#306`. Two fields on one line where they read as one thing - postal code
   and city belong together on an address, and stacking every field of a
   seven-field form makes onboarding a scroll. Collapses on a narrow
   viewport, where side-by-side inputs are worse than the scroll. */
.form-row {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 12px;
}
@media (max-width: 560px) {
  .form-row {
    grid-template-columns: 1fr;
  }
}

.form-hint {
  font-size: 12px;
  color: var(--text-muted);
  margin-top: 5px;
}
.form-control {
  width: 100%;
  padding: 8px 11px;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--surface-2);
  color: var(--text);
  font-size: 13px;
  font-family: var(--sans);
  outline: none;
}
.form-control:focus {
  border-color: var(--copper);
}
.form-control-invalid {
  border-color: var(--coral);
}
.field-error {
  font-size: 12px;
  color: var(--coral);
  margin: 5px 0 0;
}
select.form-control {
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%237c8089' stroke-width='1.4' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  padding-right: 30px;
}

.radio-cards {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}
@media (max-width: 780px) {
  .radio-cards {
    grid-template-columns: 1fr 1fr;
  }
}
.radio-card {
  position: relative;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: 12px 13px;
  cursor: pointer;
  background: var(--surface-2);
}
.radio-card input {
  position: absolute;
  top: 12px;
  right: 12px;
}
.radio-card .name {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 13px;
  margin-bottom: 6px;
}
.radio-card .spec {
  font-size: 11.5px;
  color: var(--text-muted);
  line-height: 1.5;
}
.radio-card.selected {
  border-color: var(--copper);
  background: var(--copper-soft);
}

.create-layout {
  display: grid;
  grid-template-columns: 1fr 320px;
  gap: 22px;
  align-items: start;
}
/* `#295`. A grid item's `min-width` is `auto`, which means "never narrower
   than my own min-content" - so a single wide child stops the track from
   holding it and pushes the *whole page* sideways. The Usage & Costs chart
   at hourly granularity did exactly that: 168 columns made its panel 5000px
   wide inside a 1440px viewport, and every other panel on the page went with
   it.

   Declared on the layout rather than on the one panel that tripped it: any
   panel here can grow a wide child (a table, a chart, a long mono string),
   and none of them should be able to widen the page to do it. Children that
   genuinely need the space scroll themselves - see `.bar-chart-scroll`. */
.create-layout > * {
  min-width: 0;
}
@media (max-width: 980px) {
  .create-layout {
    grid-template-columns: 1fr;
  }
}
.summary-row {
  display: flex;
  justify-content: space-between;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
  font-size: 12.5px;
}
.summary-row .k {
  color: var(--text-muted);
}
.summary-row .v {
  font-family: var(--mono);
}

/* ===================== CHARTS ===================== */
.bar-row {
  display: grid;
  grid-template-columns: 140px 1fr 70px;
  align-items: center;
  gap: 12px;
  padding: 8px 0;
}
.bar-row .lbl {
  font-size: 12.5px;
  color: var(--text-soft);
}
.bar-track {
  height: 8px;
  border-radius: 4px;
  background: var(--surface-2);
  overflow: hidden;
}
.bar-fill {
  height: 100%;
  border-radius: 4px;
}
.bar-row .amt {
  font-family: var(--mono);
  font-size: 12px;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

.kicker {
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--copper);
  margin: 0 0 6px;
}

/* LineChart (src/components/chart.rs) - inline-SVG line/area chart with a
   hover crosshair, per-series tooltip, and legend. */
.chart {
  position: relative;
}
.chart-empty {
  padding: 40px 0;
  text-align: center;
  color: var(--text-muted);
  font-size: 12.5px;
}
.chart-plot {
  position: relative;
  /* Matches .chart-svg's own height below - the axis-label overlays anchor
     to this box (top:0/bottom:0/percentages), not to .chart as a whole,
     since .chart also contains the legend underneath. */
  height: 180px;
}
.chart-svg {
  display: block;
  width: 100%;
  height: 180px;
}
.chart-grid-line {
  stroke: var(--line);
  stroke-width: 1;
}
/* Axis labels are plain HTML overlaid on the plot, positioned by percentage
   (see chart.rs) - NOT SVG `<text>`. `.chart-svg` above uses
   `preserveAspectRatio="none"` so its fixed-height, fluid-width box can fill
   the container without letterboxing, which means X and Y inside it get
   scaled by different factors whenever the container's aspect ratio isn't
   exactly VIEW_W:VIEW_H (the common case) - fine for plotted lines/gridlines,
   but it visibly stretches SVG text glyphs, so labels live out here instead. */
.chart-axis-labels-y {
  position: absolute;
  top: 0;
  left: 0;
  /* PAD_LEFT / VIEW_W from chart.rs (46 / 640) - keep in sync if either changes. */
  width: 7.1875%;
  height: 100%;
}
.chart-axis-label-y {
  position: absolute;
  left: 0;
  right: 6px;
  transform: translateY(-50%);
  text-align: right;
  color: var(--text-muted);
  font-size: 10.5px;
  font-family: var(--mono);
  white-space: nowrap;
}
.chart-axis-label-x {
  position: absolute;
  bottom: 0;
  color: var(--text-muted);
  font-size: 10.5px;
  font-family: var(--mono);
  white-space: nowrap;
}
.chart-axis-label-x-start {
  /* PAD_LEFT / VIEW_W from chart.rs. */
  left: 7.1875%;
}
.chart-axis-label-x-end {
  /* PAD_RIGHT / VIEW_W from chart.rs (10 / 640) - `right` alone
     shrink-wraps and right-aligns an absolutely positioned element with
     `left` unset, no transform needed. */
  right: 1.5625%;
}
.chart-crosshair {
  stroke: var(--line-strong);
  stroke-width: 1;
  pointer-events: none;
}
.chart-dot {
  stroke: var(--surface-1);
  stroke-width: 1.5;
  pointer-events: none;
}
.chart-tooltip {
  position: absolute;
  top: 14px;
  transform: translateX(-50%);
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  font-size: 11.5px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.1s ease;
  white-space: nowrap;
  z-index: 1;
}
.chart-tooltip.visible {
  opacity: 1;
}
.chart-tooltip-time {
  font-family: var(--mono);
  color: var(--text-muted);
  margin-bottom: 4px;
}
.chart-tooltip-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.chart-tooltip-row + .chart-tooltip-row {
  margin-top: 2px;
}
.chart-tooltip-swatch {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  flex-shrink: 0;
}
.chart-tooltip-label {
  color: var(--text-soft);
}
.chart-tooltip-value {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  margin-left: auto;
  padding-left: 10px;
}
.chart-legend {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-soft);
}
.chart-legend-item {
  display: flex;
  align-items: center;
  gap: 6px;
}
.chart-legend-swatch {
  width: 8px;
  height: 8px;
  border-radius: 2px;
  flex-shrink: 0;
}

/* ===================== COMMAND PALETTE ===================== */
.cmdk-overlay {
  display: flex;
  position: fixed;
  inset: 0;
  background: var(--scrim);
  backdrop-filter: blur(2px);
  z-index: 100;
  align-items: flex-start;
  justify-content: center;
  padding-top: 12vh;
}
.cmdk {
  width: 100%;
  max-width: 560px;
  background: var(--surface-1);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}
.cmdk-input {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
}
.cmdk-input svg.icon {
  color: var(--text-muted);
}
.cmdk-input input {
  flex: 1;
  background: none;
  border: none;
  outline: none;
  color: var(--text);
  font-size: 14px;
  font-family: var(--sans);
}
.cmdk-list {
  max-height: 340px;
  overflow-y: auto;
  padding: 6px;
}
.cmdk-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  cursor: pointer;
}
.cmdk-item:hover,
.cmdk-item.sel {
  background: var(--surface-2);
}
.cmdk-item svg.icon {
  color: var(--text-muted);
  width: 15px;
  height: 15px;
}
.cmdk-item .path {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-muted);
}
.cmdk-foot {
  display: flex;
  gap: 14px;
  padding: 9px 16px;
  border-top: 1px solid var(--line);
  font-size: 11px;
  color: var(--text-muted);
}
.cmdk-foot kbd {
  font-family: var(--mono);
  border: 1px solid var(--line-strong);
  border-radius: 3px;
  padding: 1px 5px;
  margin-right: 4px;
}

/* ===================== AUTH SCREEN ===================== */
.auth-screen {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  background-image: linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 56px 56px;
}
.auth-card {
  width: 340px;
  padding: 28px 26px;
  text-align: center;
}
/* `#310`. One dimension's row in the project quota editor: label and usage,
   the budget field, then its two writes. Wraps to stacked on narrow screens,
   where three columns of controls stop being readable. */
.quota-edit-row {
  display: grid;
  grid-template-columns: 1fr 160px auto;
  gap: 12px;
  align-items: start;
  padding: 14px 0;
  border-bottom: 1px solid var(--line);
}
.quota-edit-row:last-child {
  border-bottom: 0;
}
.quota-edit-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.quota-edit-actions {
  display: flex;
  gap: 6px;
}
/* Spans the full row: it explains the state of the whole dimension, not of
   the field it sits under. */
.quota-edit-note {
  grid-column: 1 / -1;
  margin: 0;
}
@media (max-width: 720px) {
  .quota-edit-row {
    grid-template-columns: 1fr;
  }
}

/* `#308`. The page a held organisation sees instead of the console. Shares
   the onboarding card's width and alignment - it is the other end of the
   same flow - and adds only what it needs on top. */
.review-gate {
  text-align: center;
}
/* Specific enough to beat `.auth-card-form .panel-sub`'s centring, which
   `#306` set for the onboarding form's own hint and which is declared after
   this block. Equal specificity would lose on source order alone, and the
   two-line paragraphs here read as prose, not as a caption. */
.auth-card-form.review-gate .panel-sub {
  text-align: left;
  margin-bottom: 12px;
}
.review-gate-org {
  font-family: var(--mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.review-gate-title {
  font-size: 17px;
  font-weight: 700;
  margin: 0 0 12px;
}
.review-gate-actions {
  display: flex;
  gap: 8px;
  margin-top: 18px;
}
.review-gate-actions > * {
  flex: 1;
  justify-content: center;
}
/* Muted and last: the identity matters only to someone who registered with
   the wrong one, and shouting it at everyone else would make the page look
   like an error. */
.review-gate-account {
  margin: 14px 0 0;
  text-align: center;
}

/* `#306`. Onboarding is a real form now - name plus a billing profile - not
   a login box. Centred labels over left-aligned inputs read as a mistake at
   seven fields, and 340px is too narrow for an address. The brand mark above
   it keeps the card's own centring; only the form is realigned. */
.auth-card-form {
  width: 420px;
  text-align: left;
}
.auth-card-form .brand,
.auth-card-form .panel-sub {
  text-align: center;
}

::-webkit-scrollbar {
  width: 9px;
  height: 9px;
}
::-webkit-scrollbar-thumb {
  background: var(--surface-3);
  border-radius: 5px;
}
::-webkit-scrollbar-track {
  background: transparent;
}

/* Single-series bar chart (origincloud-console#72) - one hue (--blue,
   structural/info, never a status color), 4px rounded data-end, 2px
   surface gap between bars via flex `gap`, native `title` tooltip per bar
   matching Timestamp/CopyableId's existing hover-tooltip convention. */
.bar-chart {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 160px;
  border-bottom: 1px solid var(--line);
}
.bar-chart-col {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  height: 100%;
  min-width: 4px;
}
.chart-bar {
  width: 100%;
  max-width: 24px;
  background: var(--blue);
  border-radius: 4px 4px 0 0;
  transition: opacity 0.15s ease;
  cursor: default;
}
.chart-bar:hover {
  opacity: 0.75;
}
/* Stacked column: segments are separated by a 2px surface gap so adjacent
   fills never touch (dataviz mark spec), and the whole stack keeps the
   rounded data-end the single-series bar had. */
.chart-stack {
  width: 100%;
  max-width: 24px;
  display: flex;
  flex-direction: column-reverse;
  gap: 2px;
  border-radius: 4px 4px 0 0;
  overflow: hidden;
  transition: opacity 0.15s ease;
  cursor: default;
}
.chart-stack:hover {
  opacity: 0.75;
}
.chart-seg {
  width: 100%;
  min-height: 2px;
}
.chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 8px 0 12px;
}
.chart-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-muted);
}
.chart-swatch {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  flex: none;
}
/* `#295`. The range selector under the plot: an overview of every bucket
   with a draggable window over it, so 168 hourly buckets stay reachable
   without 168 four-pixel bars being the only way to see them. Pure client
   side - the data is already fetched, this only chooses what is drawn. */
.chart-range {
  margin-top: 14px;
  border-top: 1px solid var(--line);
  padding-top: 12px;
}
.chart-range-strip {
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: 1px;
  height: 40px;
  cursor: default;
}
.chart-range-bar {
  flex: 1;
  min-width: 0;
  background: var(--blue);
  opacity: 0.85;
  border-radius: 1px 1px 0 0;
}
/* Outside the window: still drawn, so the strip stays a picture of the whole
   range rather than of the selection. */
.chart-range-bar.outside {
  opacity: 0.25;
}
.chart-range-window {
  position: absolute;
  top: 0;
  bottom: 0;
  border: 1px solid var(--copper);
  border-radius: var(--radius);
  background: rgba(199, 122, 71, 0.12);
  pointer-events: none;
}
/* The handles are the only interactive part - the window itself is
   `pointer-events: none` so a drag started on a handle is never stolen by
   the overlay it sits inside. */
.chart-range-handle {
  position: absolute;
  top: -3px;
  bottom: -3px;
  width: 11px;
  padding: 0;
  border: 1px solid var(--copper);
  border-radius: 3px;
  background: var(--surface-1);
  cursor: ew-resize;
  pointer-events: auto;
}
.chart-range-handle:hover,
.chart-range-handle:focus-visible {
  background: var(--copper);
}
.chart-range-handle-start {
  left: -6px;
}
.chart-range-handle-end {
  right: -6px;
}
.chart-range-caption {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
}
.btn-sm {
  padding: 3px 9px;
  font-size: 12px;
}

/* `#295`. Labels are positioned over their column, not laid out in one.
   Sharing a column's width ellipsised every label to "0.." once there were
   more than a dozen columns; sizing each to its text instead made this row
   the widest box on the page and dragged the whole layout sideways. Absolute
   positioning is what makes label width independent of column width. */
.bar-chart-labels {
  position: relative;
  height: 15px;
  margin-top: 6px;
}
.bar-chart-labels > span {
  position: absolute;
  transform: translateX(-50%);
  font-size: 11px;
  color: var(--text-muted);
  white-space: nowrap;
}

/* ===================== BOOT LOADING ===================== */
/* Static markup in index.html, styled before any WASM has loaded (this
   stylesheet is a render-blocking <link>, unlike the app itself) — covers
   the download+instantiate+compile gap for the wasm binary. Hidden by
   main.rs once the Leptos app has mounted; see that file for why removal
   happens there instead of a fixed timeout. */
#boot-loading {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background: var(--ink);
  background-image: linear-gradient(var(--line) 1px, transparent 1px),
    linear-gradient(90deg, var(--line) 1px, transparent 1px);
  background-size: 56px 56px;
  transition: opacity 0.25s ease;
}
#boot-loading.boot-loading--done {
  opacity: 0;
  pointer-events: none;
}
#boot-loading .boot-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1.5px solid var(--text);
  border-radius: var(--radius);
  font-family: var(--mono);
  font-size: 17px;
  font-weight: 700;
  color: var(--text);
  animation: boot-pulse 1.8s ease-in-out infinite;
}
#boot-loading .boot-label {
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text);
}
#boot-loading .boot-product {
  margin-left: 6px;
  color: var(--text-muted);
  font-weight: 500;
}
#boot-loading .boot-product::before {
  content: "/";
  margin-right: 6px;
  color: var(--text-muted);
}
#boot-loading .boot-bar {
  width: 120px;
  height: 2px;
  border-radius: 2px;
  background: var(--line-strong);
  overflow: hidden;
  position: relative;
}
#boot-loading .boot-bar::after {
  content: "";
  position: absolute;
  inset: 0;
  width: 40%;
  background: var(--copper);
  border-radius: 2px;
  animation: boot-bar-sweep 1.2s ease-in-out infinite;
}
/* One keyframe that used to hold both idioms at once — var(--copper-soft)
   on the 0% stop and a hand-transcribed decimal copper on the other two.
   Same `transparent` rule as @keyframes pulse for the 0 stops. */
@keyframes boot-pulse {
  0% {
    box-shadow: 0 0 0 0 var(--copper-soft);
  }
  70% {
    box-shadow: 0 0 0 8px transparent;
  }
  100% {
    box-shadow: 0 0 0 0 transparent;
  }
}
@keyframes boot-bar-sweep {
  0% {
    transform: translateX(-140%);
  }
  100% {
    transform: translateX(350%);
  }
}

/* Server-side paging controls (`components::Pager`, #280). Sits under a
   table the way `.table-footnote` does, but has to hold a range label and
   two buttons on one line, so it's a flex row rather than a bare
   paragraph. Ghost buttons: paging is navigation, not an action on the
   data, so nothing here competes with the page's real CTA. */
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 12px;
  flex-wrap: wrap;
}

.pager-range {
  margin: 0;
}

.pager-actions {
  display: flex;
  gap: 8px;
  flex-shrink: 0;
}

/* `#362`. A launch topic: what will happen, stated, until you open it.
   Built on `<details>`, so the marker is suppressed and the row itself is
   the affordance. */
.form-topic {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  margin-bottom: 10px;
  background: var(--surface-1);
}
.form-topic[open] {
  border-color: var(--line-strong);
}
.form-topic-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 10px 12px;
  cursor: pointer;
  list-style: none;
}
.form-topic-head::-webkit-details-marker {
  display: none;
}
.form-topic-title {
  min-width: 110px;
  font-weight: 600;
  font-size: 12.5px;
}
/* The summary is the point of the collapsed row, so it takes the space and
   the "Customise" hint stays out of its way. */
.form-topic-summary {
  flex: 1;
  color: var(--text-soft);
  font-size: 12.5px;
}
.form-topic-toggle {
  color: var(--copper);
  font-size: 12px;
}
.form-topic[open] .form-topic-toggle {
  color: var(--text-muted);
}
.form-topic-body {
  padding: 0 12px 12px;
  border-top: 1px solid var(--line);
}
.form-topic-body .form-group:first-child {
  margin-top: 12px;
}
.form-topic-docs {
  display: inline-block;
  margin-top: 4px;
  color: var(--blue);
  font-size: 12px;
}

/* `#370`. The instance detail page's Actions menu. A `<details>` popped out
   of flow, so opening it doesn't push the page-header's other controls
   around - the layout shift the issue objected to, just moved. */
.action-menu {
  position: relative;
}
.action-menu > summary {
  list-style: none;
  cursor: pointer;
}
.action-menu > summary::-webkit-details-marker {
  display: none;
}
.action-menu > summary::after {
  content: " ▾";
  color: var(--text-muted);
}
/* Only what a menu of actions needs on top of that shared panel. */
.action-menu-items {
  display: flex;
  flex-direction: column;
}
.action-menu-item {
  padding: 7px 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 12.5px;
  text-align: left;
  cursor: pointer;
}
.action-menu-item:hover:not(:disabled) {
  background: var(--surface-3);
  color: var(--copper);
}
/* Destructive at rest, not only on hover - it was a `btn-danger-ghost`
   before the menu, and demoting it to plain text until hovered would lose
   the one thing that set it apart from Start and Stop. */
.action-menu-item-danger:not(:disabled) {
  color: var(--coral);
}
.action-menu-item-danger:hover:not(:disabled) {
  background: var(--coral-soft);
  color: var(--coral);
}
/* A refused action stays listed and stays hoverable - the `title` explaining
   why is the whole point, and `cursor: not-allowed` says "this is off" more
   clearly than the dimming alone. */
.action-menu-item:disabled {
  color: var(--text-muted);
  cursor: not-allowed;
}

/* `#366`. The generate-key-pair result. Coral because losing this key is a
   real consequence, not because anything failed - it is the one screen that
   will ever carry the private half. */
.private-key-warning {
  border-color: color-mix(in srgb, var(--coral) 30%, transparent);
  background: var(--coral-soft);
}
.private-key-warning strong {
  color: var(--coral);
}
.private-key-block {
  margin: 0;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text);
  font-size: 12px;
  line-height: 1.5;
  /* A PEM body is one long unbroken run of base64. Without this it forces
     the panel wider than the viewport instead of wrapping. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 320px;
  overflow-y: auto;
}
