/* Shared styles for all UI pages — the "control plane" design system.

   The whole palette is derived from the tokens in :root, so a future light
   theme is just a second :root block away; component rules must never
   hardcode a color. Monospace carries all technical/numeric data (refs,
   counts, metrics, lock keys) — that is where a deploy tool earns character. */
:root {
  color-scheme: dark;

  --bg:            #0b0e13;
  --bg-grad-a:     #11161e;
  --bg-grad-b:     #0a0d12;
  --panel:         #12171f;
  --panel-2:       #161c26;
  --border:        #222b37;
  --border-strong: #303b4a;

  --fg:        #e8eef6;
  --fg-muted:  #93a1b3;
  --fg-dim:    #5b6675;

  --accent:      #36d6a8;          /* spring-teal: "go / healthy" */
  --accent-deep: #1d9c7a;
  --accent-glow: rgba(54, 214, 168, 0.22);

  --ok:   #3fb950;
  --warn: #d6a022;
  --err:  #f8645a;
  --busy: #4d9fef;          /* in-progress: deploying / building / running */

  --ok-soft:   rgba(63, 185, 80, 0.14);
  --warn-soft: rgba(214, 160, 34, 0.15);
  --err-soft:  rgba(248, 100, 90, 0.14);
  --busy-soft: rgba(77, 159, 239, 0.18);

  /* Text stops on the same four ramps, for status words set small. The signal
     colours above are tuned to be read as colour — a dot, a meter fill, a bar
     — where hue matters and size carries the contrast. As 0.76rem monospace
     on the matching soft fill they only just clear WCAG AA (4.9–5.9:1); these
     stops reach 8–9.3:1. Status dots keep the signal colour, so a badge still
     reads as the same green or amber from across the room. */
  --ok-text:   #8ee29a;
  --warn-text: #f3cf7d;
  --err-text:  #ffa9a2;
  --busy-text: #a3cffa;

  /* Row under the pointer. Deliberately near-invisible in isolation: its job
     is to hold the eye on one row across a six-column table, not to announce
     itself. */
  --row-hover: rgba(255, 255, 255, 0.022);

  /* The one deliberately light surface: a QR code is read by a camera, not by
     a human, and scanners expect dark modules on a light ground. It is a token
     rather than a literal so a future light theme keeps one place to change. */
  --qr-paper: #ffffff;

  --radius:    12px;
  --radius-sm: 8px;
  --shadow:    0 1px 0 rgba(255, 255, 255, 0.02) inset,
               0 8px 24px -12px rgba(0, 0, 0, 0.7);

  --mono: ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, Consolas, monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--sans);
  color: var(--fg);
  background: var(--bg);
  min-height: 100vh;
  -webkit-font-smoothing: antialiased;
}

/* The page backdrop: ambient gradients plus a faint grain / scanline texture
   for depth without imagery.

   Both live on one fixed pseudo-element rather than on the body background,
   because a viewport-anchored canvas background (`background-attachment:
   fixed`) prevents the compositor from scroll-blitting and forces the whole
   viewport to be re-rasterised on every scroll frame. A fixed element is a
   static composited layer instead — same pixels, no per-frame paint.

   The texture is composited normally. A `mix-blend-mode` here would turn
   every repaint anywhere on the page into a full-viewport read-modify-write
   blend, because a blended layer cannot be composited independently: its
   backdrop has to be read back and recombined pixel by pixel.

   The negative z-index is load-bearing, not decoration. body establishes no
   stacking context, so this paints in the root context just above the canvas
   and below every in-flow element — including page roots that are not
   positioned. At z-index 0 it would instead paint over them, and an opaque
   backdrop would hide any page whose root does not opt out with a z-index of
   its own (the login page is the one such page). */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: -1;
  background:
    repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.008) 0 1px, transparent 1px 3px),
    radial-gradient(1200px 600px at 80% -10%, var(--accent-glow), transparent 60%),
    linear-gradient(180deg, var(--bg-grad-a), var(--bg-grad-b) 40%, var(--bg));
}

/* Hide the page until Vue has compiled the in-DOM template, preventing a
   flash of the raw, un-compiled markup (literal {{ }} bindings, v-if/v-for
   elements) while the page's JS module is still loading. Each mount root
   (#app) carries the v-cloak attribute; Vue removes it automatically once
   .mount() finishes, revealing the compiled page. */
[v-cloak] {
  display: none;
}

main {
  position: relative;
  z-index: 1;
  padding: 1.8rem 1.6rem 3rem;
  max-width: 76rem;
  margin: 0 auto;
}

/* Mono carries the technical/numeric data throughout. */
code, .mono { font-family: var(--mono); }

/* ───────── links in page content ─────────

   Some links are styled where they are defined (.dom, .art-ver, .stage-url,
   .update-banner, .fact .v a). Every other link in page content — "open" in
   the dashboard's stage grid, the Caddy domains on the resources page, the
   back-links on build and deployment pages, the cross-links to Settings and
   the profile — had no colour of its own and fell back to the browser
   default: a periwinkle blue that appears nowhere else in this palette.

   Written as a plain default rather than a :not([class]) exception, because
   carrying a class is no guarantee of carrying a colour — the Caddy domains
   are class="mono", which sets a font and nothing else. Any rule that does
   name a colour is a class selector and outranks this one.

   Scoped to main because the header nav's links have their own grey/white
   scheme and would otherwise be caught by this. */
main a {
  color: var(--accent);
  text-decoration: none;
}
main a:hover { text-decoration: underline; }
main a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A link wrapping a button is a navigation target drawn as a control: the
   button paints the whole surface, so an underline can only appear beside it. */
main a:has(> button):hover { text-decoration: none; }

/* An app name is an identity, not an action: it reads as the row's label and
   only picks up the accent under the pointer. Same treatment the apps overview
   gives it (.app-unit__id h2 a), so a name looks like a name on both pages. */
.app-link {
  color: var(--fg);
  font-weight: 550;
  text-decoration: none;
}
.app-link:hover { color: var(--accent); }

/* Section eyebrow above a page or panel group. */
.eyebrow {
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--fg-dim);
  margin: 0 0 0.9rem;
}

/* ───────── top bar / navigation ───────── */

.shell-header {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  gap: 1.4rem;
  padding: 0.7rem 1.6rem;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(180deg, rgba(22, 28, 38, 0.7), rgba(18, 23, 31, 0.4));
}

.shell-header nav {
  display: flex;
  align-items: center;
  gap: 1.4rem;
  min-width: 0;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin-right: 0.6rem;
  flex: 0 0 auto;
}

.brand .mark {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  box-shadow: 0 0 12px var(--accent-glow);
}

.shell-header nav a {
  color: var(--fg-muted);
  text-decoration: none;
  font-size: 0.9rem;
  padding: 0.3rem 0;
  position: relative;
  transition: color 0.15s;
}

.shell-header nav a:hover { color: var(--fg); }
.shell-header nav a.active { color: var(--fg); }

.shell-header nav a.active::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -0.75rem;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--accent), var(--accent-deep));
  box-shadow: 0 0 10px var(--accent-glow);
}

.whoami {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  margin-left: auto;
  flex: 0 0 auto;
  color: var(--fg-muted);
  font-size: 0.88rem;
}

/* Username in the header doubles as the link to the profile page. */
.whoami-user {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--fg-muted);
  text-decoration: none;
  padding: 0.18rem 0.5rem 0.18rem 0.18rem;
  border-radius: 999px;
  border: 1px solid transparent;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.whoami-user:hover { color: var(--fg); border-color: var(--border-strong); background: var(--panel); }
.whoami-user.active { color: var(--fg); border-color: var(--accent); }

.whoami-avatar {
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 700;
  color: #06281e;
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  box-shadow: 0 0 10px var(--accent-glow);
}

/* Nine nav links, a brand and an account block need about 1100px. Below
   that the row used to push the whole page wider than the viewport — every
   page picking up a horizontal scrollbar because of the header alone, with
   "Audit log" breaking across two lines on the way. Scrolling nav on its own
   contains it: the brand stays pinned, the account block stays reachable, and
   only the links move. The scrollbar is left visible for the same reason it is
   on .table-scroll — a row that is cut off with no hint that it continues is
   worse than one that scrolls. */
@media (max-width: 1120px) {
  .shell-header nav {
    flex: 1 1 auto;
    overflow-x: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
    padding-bottom: 0.5rem;
  }
  /* Links must keep their own width — flex would otherwise shrink them until
     "Audit log" wraps, which is the thing being fixed. */
  .shell-header nav a { flex: 0 0 auto; }
  /* The active underline is positioned against the header's old padding; with
     nav scrolling it belongs to the link. */
  .shell-header nav a.active::after { bottom: -0.15rem; }
}

/* Narrow enough that the brand and the account block fill the row on their
   own — nav would be squeezed to nothing, taking the navigation with it. It
   gets its own full-width row underneath instead, and keeps scrolling there. */
@media (max-width: 820px) {
  .shell-header { flex-wrap: wrap; row-gap: 0.2rem; }
  .shell-header nav { order: 3; flex-basis: 100%; }
  /* The account block carries the two-factor state beside the user and the
     log-out button; on a phone those wrap onto a second line instead of
     pushing the header wider than the viewport. */
  .whoami { flex-wrap: wrap; justify-content: flex-end; row-gap: 0.4rem; }
}

/* ───────── flash messages ───────── */

.flash {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.6rem 1.6rem;
  border-bottom: 1px solid var(--border);
  font-size: 0.9rem;
}

.flash-info    { background: rgba(54, 214, 168, 0.10); color: var(--accent); }
.flash-success { background: var(--ok-soft);   color: var(--ok); }
.flash-error   { background: var(--err-soft);  color: var(--err); }

.flash-close {
  background: none;
  border: none;
  font-size: 1.1rem;
  cursor: pointer;
  color: inherit;
  padding: 0 0.25rem;
}

/* Explanatory prose is read line by line, so it gets a measure of its own —
   a card is up to 76rem wide, and a paragraph running that full width is
   around 135 characters, well past the point where the eye reliably finds the
   next line. Tables, field rows and headers are unaffected. */
.card p, .panel p { max-width: 66ch; }
/* Fieldset legends and status lines are labels, not prose: they are short by
   nature and must not be wrapped early by the measure above. */
.card p.leg, .panel p.leg, .card p.eyebrow, .panel p.eyebrow { max-width: none; }
/* A meta line — who, when, from which ref — is a row of facts, not a sentence.
   Capping it wraps a timestamp across two lines, which reads worse than the
   long line the cap exists to prevent. */
.card p.meta, .panel p.meta { max-width: none; }

/* ───────── panels / cards ───────── */

.card, .panel {
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1.25rem 1.35rem;
  margin-bottom: 1.5rem;
}

.card h2 {
  margin: 0 0 1rem;
  font-size: 0.95rem;
  font-weight: 650;
  letter-spacing: -0.01em;
}

/* Panel with a header row (title left, hint right). */
.panel > header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 0 1.1rem;
}

.panel > header h2 {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 650;
  letter-spacing: -0.01em;
}

.panel > header .hint {
  font-family: var(--mono);
  font-size: 0.74rem;
  color: var(--fg-dim);
}

/* ───────── forms ───────── */

label {
  display: block;
  margin: 0.75rem 0 0.25rem;
  color: var(--fg-muted);
  font-size: 0.9rem;
}

input, select, textarea {
  font: inherit;
  /* Explicit, because the default `normal` resolves differently for a select
     than for an input — a 2px difference, invisible on its own but not when
     .form-row aligns a row of fields on their bottom edge and the labels
     above them end up on two different lines. */
  line-height: 1.3;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--fg);
  width: 16rem;
  max-width: 100%;
  min-width: 0;
  box-sizing: border-box;
}

input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}

/* ───────── field sizes ─────────

   A field is as wide as the thing it holds. The default above is the middle
   size — a name, a short identifier, a dropdown — and these classes step away
   from it in both directions. Sizing the field is not cosmetic: a URL field
   that clips its own placeholder and a field for the number 1 are the same
   mistake in opposite directions, and both make the operator work to read
   what they typed.

   The class goes on the control, so the markup states what the *content*
   needs; the row around it adapts. Never mix them with an explicit width. */
.f-num  { width: 6rem; }
.f-mid  { width: 11rem; }
.f-wide { width: 28rem; }
.f-full { width: 100%; }
/* A field wrapper in a form row shrink-wraps its control, which would leave a
   full-width field with nothing to fill. This lets that one wrapper take the
   rest of the row while its neighbours keep their intrinsic size. */
.form-row > *:has(> .f-full) { flex: 1 1 20rem; }

/* Checkboxes and radios are not text fields: they take their intrinsic box,
   and the label beside them is the target. Stated here rather than left to
   per-instance overrides, which is what the sizes above replaced. */
input[type=checkbox], input[type=radio] { width: auto; }

button {
  font: inherit;
  font-size: 0.9rem;
  padding: 0.45rem 0.9rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--accent-deep);
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  color: #06281e;
  font-weight: 600;
  cursor: pointer;
  transition: filter 0.15s, border-color 0.15s;
}

button:hover { filter: brightness(1.08); }

button.secondary {
  background: var(--panel-2);
  color: var(--fg);
  border-color: var(--border-strong);
  font-weight: 500;
}

button.secondary:hover { border-color: var(--accent); filter: none; }

button.danger {
  background: var(--panel-2);
  color: var(--err);
  border-color: rgba(248, 100, 90, 0.4);
  font-weight: 500;
}

button.danger:hover { background: var(--err-soft); filter: none; }

button:disabled { opacity: 0.5; cursor: default; filter: none; }

/* ───────── keeping inline pieces apart ─────────

   Two elements side by side in a Vue template cannot rely on the space typed
   between them. The compiler drops a whitespace-only text node whenever it
   spans a newline or sits at a template boundary, so the space survives only
   while nobody reformats the markup — and disappears silently when someone
   does. Every place where a badge, a button or a value sits beside another
   one therefore uses a flex gap, which no reformatting can take away. */

.form-actions {
  margin-top: 1rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem;
}

/* A table cell holding several inline pieces — a badge and the timestamp it
   belongs to, an address and its exposure badge. */
.inline-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem;
  min-width: 0;
}

.form-row {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  align-items: flex-end;
}

/* Panels sitting side by side. Distinct from .form-row, whose flex-end
   alignment is right for a row of inputs and their submit button — it lines
   the controls up on one baseline — but wrong for cards: it hangs the shorter
   card off the bottom of the taller one, leaving a hole above it. */
.card-row {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  align-items: flex-start;
}
.card-row > * { flex: 1 1 22rem; min-width: 0; }

/* ───────── tables ───────── */

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.86rem;
}

/* Somewhere for a table to go when its columns need more room than its panel
   has. Without it the table simply draws past the panel edge — a table cannot
   shrink below the width its content demands, and `width: 100%` is a maximum
   it will ignore rather than clip. Scrolling keeps every column reachable;
   the alternative, breaking identifiers like portal_production mid-token,
   makes them harder to read than a scrollbar does.

   The scrollbar is made explicit rather than left to the platform default:
   where scrollbars are overlays they stay invisible until something scrolls,
   and a column that is both cut off and gives no hint that it can be reached
   is worse than one that is merely cut off. */
.table-scroll {
  overflow-x: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

th {
  text-align: left;
  font-weight: 600;
  color: var(--fg-dim);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  padding: 0 0.6rem 0.6rem;
}

/* Horizontal padding used to be 0.4rem against 0.62rem vertical, which reads
   as columns running into each other while the rows sit comfortably apart.
   0.6rem evens that out. It stops short of the ~0.75rem a text-heavy table
   would want: these tables are a monitor, and every extra rem of column
   padding is a container row that no longer fits on screen. */
td {
  padding: 0.62rem 0.6rem;
  border-top: 1px solid var(--border);
  vertical-align: middle;
}

/* Six and seven column tables are hard to read across. A hover tint costs
   nothing to paint and keeps the eye on one row. */
tbody tr:hover td { background: var(--row-hover); }

td.mono { font-family: var(--mono); }
.app-cell { font-weight: 550; }
.when { color: var(--fg-dim); font-family: var(--mono); font-size: 0.8rem; }

td .row-actions {
  display: flex;
  gap: 0.4rem;
  flex-wrap: wrap;
}

td .row-actions button { padding: 0.2rem 0.55rem; font-size: 0.82rem; }

.muted { color: var(--fg-muted); }
.error-text { color: var(--err); margin: 0.5rem 0 0; }

summary { cursor: pointer; }
/* A <details class="card"> reads as a collapsible section with a heading. */
details.card > summary {
  font-weight: 650;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
}
details.card[open] > summary { margin-bottom: 0.6rem; }

details pre {
  margin: 0.4rem 0 0;
  padding: 0.5rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow-x: auto;
  font-size: 0.85rem;
}

.status-ok   { color: var(--ok); }
.status-err  { color: var(--err); }
.status-warn { color: var(--warn); }

/* ───────── badges ───────── */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.16rem 0.55rem;
  border-radius: 999px;
  font-size: 0.76rem;
  font-family: var(--mono);
  border: 1px solid var(--border-strong);
  color: var(--fg-muted);
}

.badge::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}
/* A neutral label badge with no status dot (e.g. app type, resource names). */
.badge.dotless::before { display: none; }

/* Text on the light stop, dot on the signal colour. The dot defaults to
   currentColor, so each status has to name its own — that split is the whole
   point: the colour still carries the meaning at a glance, and the word stays
   readable when you actually look at it. */
.badge.ok   { color: var(--ok-text);   border-color: rgba(63, 185, 80, 0.4);  background: var(--ok-soft); }
.badge.bad  { color: var(--err-text);  border-color: rgba(248, 100, 90, 0.4); background: var(--err-soft); }
.badge.warn { color: var(--warn-text); border-color: rgba(214, 160, 34, 0.4); background: var(--warn-soft); }
.badge.busy { color: var(--busy-text); border-color: rgba(77, 159, 239, 0.4); background: var(--busy-soft); }

.badge.ok::before   { background: var(--ok); }
.badge.bad::before  { background: var(--err); }
.badge.warn::before { background: var(--warn); }
.badge.busy::before { background: var(--busy); }
/* A badge whose colour alone carries the meaning, with no leading status dot. */
.badge.plain::before { display: none; }

.pager, .pager .pager-inner {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  margin-top: 1rem;
}

/* ───────── usage meters (dashboard) ───────── */

.meter { margin: 0 0 1.15rem; }
.meter:last-child { margin-bottom: 0.2rem; }
.meter .row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 0.45rem;
}
.meter .label { font-size: 0.82rem; color: var(--fg-muted); letter-spacing: 0.01em; }
.meter .value { font-family: var(--mono); font-size: 0.8rem; color: var(--fg-muted); }
.meter .pct {
  font-family: var(--mono);
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--fg);
  margin-left: 0.55rem;
}

.track {
  height: 9px;
  border-radius: 999px;
  overflow: hidden;
  background: #0c1118;
  box-shadow: inset 0 0 0 1px var(--border);
}
.fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--accent-deep), var(--accent));
  box-shadow: 0 0 12px var(--accent-glow);
  transform-origin: left;
  animation: grow 0.9s cubic-bezier(.2, .8, .2, 1) both;
}
.fill.warn { background: linear-gradient(90deg, #9a7415, var(--warn)); box-shadow: 0 0 12px var(--warn-soft); }
.fill.crit { background: linear-gradient(90deg, #b53029, var(--err)); box-shadow: 0 0 12px var(--err-soft); }
@keyframes grow { from { transform: scaleX(0); } }

.substats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.9rem 1.6rem;
  margin-top: 1.25rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--border);
}
.substats .stat .k {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--fg-dim);
  text-transform: uppercase;
  letter-spacing: 0.12em;
}
.substats .stat .v { font-family: var(--mono); font-size: 1.0rem; color: var(--fg); margin-top: 0.2rem; }

/* ───────── service health tiles ───────── */

.tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.7rem; }
.tile {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.7rem 0.85rem;
  border-radius: var(--radius-sm);
  background: var(--panel);
  border: 1px solid var(--border);
}
.tile .name { font-size: 0.86rem; }
.tile .state { margin-left: auto; font-family: var(--mono); font-size: 0.76rem; color: var(--fg-muted); }

.dot { position: relative; width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; }
.dot.ok   { background: var(--ok); }
.dot.warn { background: var(--warn);   box-shadow: 0 0 0 3px var(--warn-soft); }
.dot.err  { background: var(--err);    box-shadow: 0 0 0 3px var(--err-soft); }
.dot.idle { background: var(--fg-dim); }

/* The healthy dot breathes. The halo is a scaled pseudo-element, not an
   animated box-shadow: box-shadow is not a compositable property, so
   animating it repaints on the main thread every frame, forever, for every
   healthy service on the page. transform and opacity stay on the compositor.

   At rest (scale 1, and so also under reduced motion) the disc is exactly the
   3px halo the other dots draw with box-shadow. It renders over the dot core
   rather than behind it, which is invisible only because --ok-soft is --ok at
   low alpha; keep those two colours in sync, or the core picks up a tint. */
.dot.ok::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  background: var(--ok-soft);
  pointer-events: none;
  animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse { 50% { transform: scale(1.4); opacity: 0; } }

/* Degraded panel: when the agent is unreachable, host facts are UNKNOWN, not
   failing — render them muted/striped, never red, so the dashboard does not
   misleadingly read as "all services down". */
.panel.degraded .track, .panel.degraded .tiles { opacity: 0.5; filter: grayscale(0.6); }
.panel.degraded .fill { background: var(--border-strong); box-shadow: none; animation: none; }
.unavail {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1rem;
  padding: 0.55rem 0.75rem;
  border-radius: var(--radius-sm);
  background: var(--warn-soft);
  border: 1px solid rgba(214, 160, 34, 0.35);
  color: #f0c560;
  font-size: 0.82rem;
}

/* ───────── counters ───────── */

.counters { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.8rem; margin-bottom: 1.4rem; }
.counter {
  padding: 0.9rem 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
}
.counter .n { font-family: var(--mono); font-size: 1.7rem; font-weight: 650; letter-spacing: -0.02em; }
.counter .n.accent { color: var(--accent); }
.counter .n.warn { color: var(--warn); }
.counter .l { font-size: 0.78rem; color: var(--fg-muted); margin-top: 0.15rem; }

/* ───────── responsive grids ───────── */

.grid { display: grid; gap: 1.1rem; }
/* Same reason as .resource-grid > .card: a grid item's default min-width is
   auto, so a panel never shrinks below the widest thing inside it and pushes
   out of its track — here the three-column substats strip, which is why that
   strip is also allowed to wrap. */
.grid > * { min-width: 0; }
.grid.cols-3 { grid-template-columns: 1.4fr 1fr; }
@media (max-width: 860px) {
  .grid.cols-3 { grid-template-columns: 1fr; }
  .counters { grid-template-columns: repeat(2, 1fr); }
}

/* ───────── container resource table (dashboard) ───────── */

.ctable td { padding: 0.7rem 0.6rem; }
.ctable .svc { font-family: var(--mono); font-size: 0.8rem; color: var(--fg-muted); }
.res { display: flex; flex-direction: column; gap: 0.32rem; min-width: 7.5rem; }
/* "180 MiB" and "/ 1.0 GiB" are two pieces, and in the containers table they
   come from two template branches — the space between them is exactly the
   kind Vue drops. Flex trims the stray whitespace of both variants and the
   gap puts back a single, reliable one. */
.res .rv { font-family: var(--mono); font-size: 0.8rem; color: var(--fg-muted); display: flex; gap: 0.3rem; }
.res .rv b { color: var(--fg); font-weight: 600; }
.res .rb { height: 5px; border-radius: 999px; background: #0c1118; box-shadow: inset 0 0 0 1px var(--border); overflow: hidden; }
.res .rf { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--accent-deep), var(--accent)); }
.res .rf.warn { background: linear-gradient(90deg, #9a7415, var(--warn)); }
.res .rf.crit { background: linear-gradient(90deg, #b53029, var(--err)); }
.res.none .rv { color: var(--fg-dim); }
tr.hot td { background: linear-gradient(90deg, var(--err-soft), transparent 70%); }
tr.hot .app-cell { color: var(--err); }

/* ───────── active locks list ───────── */

.lock {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 0;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
}
.lock:first-of-type { border-top: none; }
.lock .key { font-family: var(--mono); color: var(--accent); }
.lock .held { margin-left: auto; color: var(--fg-dim); font-family: var(--mono); font-size: 0.78rem; }

/* Orchestrated page-load: staggered reveal of the top-level sections. */
.reveal { opacity: 0; transform: translateY(8px); animation: rise 0.55s cubic-bezier(.2, .8, .2, 1) forwards; }
.reveal:nth-child(1) { animation-delay: 0.02s; }
.reveal:nth-child(2) { animation-delay: 0.08s; }
.reveal:nth-child(3) { animation-delay: 0.14s; }
.reveal:nth-child(4) { animation-delay: 0.20s; }
@keyframes rise { to { opacity: 1; transform: none; } }

/* ───────── update-available banner ───────── */

.update-banner {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin-bottom: 1.4rem;
  padding: 0.6rem 0.9rem;
  border-radius: var(--radius-sm);
  border: 1px solid rgba(54, 214, 168, 0.35);
  background: rgba(54, 214, 168, 0.08);
  color: var(--fg);
  text-decoration: none;
  font-size: 0.88rem;
}
.update-banner:hover { border-color: var(--accent); }

/* ───────── apps overview ───────── */

.apps-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.4rem;
}
.apps-head .eyebrow { margin: 0; }

/* The overview is a vertical stack of full-width app "units"; each stage is a
   lane whose 3px left rail color is the primary scan signal. */
.apps-bar { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 1.4rem; }
.apps-bar .eyebrow { margin: 0 0 0.45rem; }
.apps-summary { display: flex; flex-wrap: wrap; gap: 0.4rem 1.1rem; align-items: center; }
.apps-summary .s { display: inline-flex; align-items: baseline; gap: 0.4rem; font-size: 0.86rem; color: var(--fg-muted); }
.apps-summary .s b { font-family: var(--mono); font-size: 1.05rem; font-weight: 650; color: var(--fg); letter-spacing: -0.01em; }
.apps-summary .s.alert b { color: var(--err); }

.app-units { display: flex; flex-direction: column; gap: 1.1rem; }
.app-unit {
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.app-unit__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding: 1rem 1.2rem; }
.app-unit__id { min-width: 0; }
.app-unit__id h2 { margin: 0; font-size: 1.05rem; font-weight: 650; letter-spacing: -0.01em; }
.app-unit__id h2 a { color: var(--fg); text-decoration: none; }
.app-unit__id h2 a:hover { color: var(--accent); }
/* The app name is the link into the detail page — reveal a faint cue on hover. */
.app-unit__id h2 a::after { content: " →"; color: var(--fg-dim); opacity: 0; transition: opacity 0.15s; }
.app-unit__id h2 a:hover::after { opacity: 1; color: var(--accent); }
.app-unit__repo { margin: 0.3rem 0 0; font-family: var(--mono); font-size: 0.78rem; color: var(--fg-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.app-unit__repo .branch { color: var(--fg-muted); }
.app-unit__meta { display: flex; align-items: center; gap: 0.5rem; flex: 0 0 auto; flex-wrap: wrap; justify-content: flex-end; }
.app-unit__meta .count { font-family: var(--mono); font-size: 0.78rem; color: var(--fg-dim); }

.stage-lane {
  display: flex; align-items: center; gap: 0.65rem 0.9rem; flex-wrap: wrap;
  padding: 0.7rem 1.2rem 0.7rem 1.05rem;
  border-top: 1px solid var(--border);
  border-left: 3px solid var(--rail, var(--border-strong));
  transition: background 0.12s;
}
.stage-lane:hover { background: var(--panel); }
/* Rail color encodes operational/attention state; the lane and its expanded
   container detail share it so the rail reads as one continuous edge. */
.stage-lane.is-online,    .stage-detail.is-online,    .stage-card.is-online    { --rail: var(--accent); }
.stage-lane.is-maint,     .stage-detail.is-maint,     .stage-card.is-maint     { --rail: var(--warn); }
.stage-lane.is-offline,   .stage-detail.is-offline,   .stage-card.is-offline   { --rail: var(--fg-dim); }
.stage-lane.is-attention, .stage-detail.is-attention, .stage-card.is-attention { --rail: var(--err); }

.stage-lane__name { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.88rem; font-weight: 500; color: var(--fg); min-width: 6.5rem; }
.stage-lane__status { font-size: 0.8rem; color: var(--fg-muted); }
.stage-lane.is-online    .stage-lane__status { color: var(--ok); }
.stage-lane.is-maint     .stage-lane__status { color: var(--warn); }
.stage-lane.is-attention .stage-lane__status { color: var(--err); }

/* The deployed artifact version, pinned in mono. */
.ver { display: inline-flex; align-items: center; gap: 0.4rem; font-family: var(--mono); font-size: 0.8rem; color: var(--fg-muted); }
.ver b { color: var(--fg); font-weight: 600; }

.dom { font-family: var(--mono); font-size: 0.78rem; color: var(--accent); text-decoration: none; padding: 0.12rem 0.55rem; border-radius: 999px; border: 1px solid var(--border-strong); background: var(--panel); }
.dom:hover { border-color: var(--accent); }
.dom .ext { color: var(--fg-dim); }

.lane-spacer { flex: 1 1 1rem; }

/* Compact live-load module: peak CPU and total memory for the stage, drawn with
   the same mini-bar primitive as the container table. */
.lane-load { display: flex; gap: 1.1rem; }
.lane-metric { display: flex; flex-direction: column; gap: 0.3rem; min-width: 5.4rem; }
.lane-metric .k { font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-dim); }
.lane-metric .v { font-family: var(--mono); font-size: 0.78rem; color: var(--fg); }
.lane-metric .rb { height: 5px; border-radius: 999px; background: #0c1118; box-shadow: inset 0 0 0 1px var(--border); overflow: hidden; }
.lane-metric .rf { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, var(--accent-deep), var(--accent)); }
.lane-metric .rf.warn { background: linear-gradient(90deg, #9a7415, var(--warn)); }
.lane-metric .rf.crit { background: linear-gradient(90deg, #b53029, var(--err)); }
.lane-load.none { color: var(--fg-dim); font-size: 0.78rem; align-items: center; }

.lane-health { font-family: var(--mono); font-size: 0.78rem; color: var(--fg-dim); min-width: 1.4rem; text-align: center; }
.lane-health.ok  { color: var(--ok); }
.lane-health.bad { color: var(--err); }

/* Per-container breakdown stays collapsed until the operator opens it. */
.lane-toggle {
  font: inherit; font-family: var(--mono); font-size: 0.74rem; cursor: pointer;
  background: var(--panel-2); color: var(--fg-muted);
  border: 1px solid var(--border-strong); border-radius: 999px; padding: 0.18rem 0.6rem;
}
.lane-toggle:hover { border-color: var(--accent); color: var(--fg); filter: none; }
.lane-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.lane-toggle .chev { display: inline-block; transition: transform 0.15s; }
.lane-toggle.open .chev { transform: rotate(90deg); }
.stage-detail { padding: 0.2rem 1.2rem 0.9rem 1.05rem; border-left: 3px solid var(--rail, var(--border-strong)); }

.stage-empty { padding: 0.85rem 1.2rem; border-top: 1px solid var(--border); color: var(--fg-muted); font-size: 0.85rem; }

@media (max-width: 720px) {
  .stage-lane { gap: 0.5rem 0.8rem; }
  .lane-spacer { flex-basis: 100%; height: 0; }
}

/* Cross-link action button (the Edit/Settings link on app-settings). */
.head-action { flex: 0 0 auto; text-decoration: none; }

/* ───────── resource cards ───────── */

.resource-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr));
  gap: 1.1rem;
  margin-bottom: 1.5rem;
}
.resource-grid .card { margin-bottom: 0; }
/* A grid item's default min-width is auto, which means "never smaller than my
   content", so a card holding a table wider than its track grows past the
   track and draws over the card beside it. Pinning it to the track makes the
   table the thing that has to give, and .table-scroll is where it gives —
   the safety net for narrow windows, not the everyday layout. */
.resource-grid > .card { min-width: 0; }
/* Cards whose table carries more columns than half the page can hold span the
   full width. That is every card here: the registries all reached five or six
   columns, and a five-column table of database identifiers needs roughly 800px
   before it starts scrolling, against the ~600px a half-width track offers. */
.resource-card.wide { grid-column: 1 / -1; }
.resource-card { transition: border-color 0.15s; }
.resource-card:hover { border-color: var(--border-strong); }

.resource-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.9rem;
}
.resource-head h2 { margin: 0; }
.resource-head .hint {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 0.74rem;
  color: var(--fg-dim);
}
.resource-glyph {
  display: grid;
  place-items: center;
  width: 1.9rem;
  height: 1.9rem;
  flex: 0 0 auto;
  border-radius: var(--radius-sm);
  font-family: var(--mono);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--accent);
  background: var(--accent-glow);
  border: 1px solid rgba(54, 214, 168, 0.35);
}

/* Host-state summary bar: one scannable line of live counts above the cards,
   with the security-relevant figures (exposed ports, orphans, unmanaged)
   promoted to colour. */
.res-summary {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem 0;
  padding: 0.85rem 1.15rem; margin-bottom: 1.3rem;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow);
}
.res-summary .s { display: inline-flex; align-items: baseline; gap: 0.4rem; padding: 0 1.1rem; font-size: 0.83rem; color: var(--fg-muted); }
.res-summary .s + .s { border-left: 1px solid var(--border); }
.res-summary .s b { font-family: var(--mono); font-size: 1.2rem; font-weight: 650; color: var(--fg); letter-spacing: -0.01em; line-height: 1; }
.res-summary .s.alert, .res-summary .s.alert b { color: var(--err); }
.res-summary .s.warn, .res-summary .s.warn b { color: var(--warn); }
.res-summary .agent { margin-left: auto; display: inline-flex; align-items: center; gap: 0.45rem; font-size: 0.8rem; padding-left: 1.1rem; }
.res-summary .agent.ok { color: var(--ok); }
.res-summary .agent.down { color: var(--warn); }
.res-summary .agent .dot { width: 8px; height: 8px; border-radius: 50%; }
.res-summary .agent.ok .dot { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
.res-summary .agent.down .dot { background: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }

/* ───────── field-help tooltip ───────── */

.field-help { position: relative; display: inline-block; }
.field-help-btn {
  font: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  line-height: 1;
  width: 1.05rem;
  height: 1.05rem;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  background: var(--panel-2);
  color: var(--fg-muted);
  cursor: pointer;
  vertical-align: middle;
}
.field-help-btn:hover { border-color: var(--accent); color: var(--fg); filter: none; }
.field-help-pop {
  position: absolute;
  z-index: 60;
  left: 0;
  top: calc(100% + 0.5rem);
  width: max-content;
  max-width: 18rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow);
  color: var(--fg-muted);
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.45;
  white-space: normal;
}
/* Keep the popover on-screen at the right viewport edge. */
.field-help-pop { right: auto; }
@media (max-width: 480px) { .field-help-pop { left: auto; right: 0; } }
.field-help-pop code { font-size: 0.92em; color: var(--fg); }
/* Arrow. */
.field-help-pop::before {
  content: "";
  position: absolute;
  top: -5px;
  left: 0.4rem;
  width: 8px;
  height: 8px;
  background: var(--panel-2);
  border-left: 1px solid var(--border-strong);
  border-top: 1px solid var(--border-strong);
  transform: rotate(45deg);
}

/* ───────── create wizard ───────── */

.wizard-steps {
  list-style: none;
  display: flex;
  gap: 0.6rem;
  padding: 0;
  margin: 0 0 1.2rem;
}
.wizard-steps li {
  font-family: var(--mono);
  font-size: 0.82rem;
  color: var(--fg-dim);
  padding: 0.35rem 0.8rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel);
}
.wizard-steps li.active {
  color: var(--accent);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}
.wizard-steps li.done { color: var(--ok); border-color: rgba(63, 185, 80, 0.4); }

/* A button that reads as a link: used where the action is a disclosure or a
   substitution ("show the private key", "switch to the SSH form") rather than
   a command with consequences. */
.linkish {
  background: none;
  border: none;
  padding: 0;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
}
.linkish:hover { text-decoration: underline; }

/* A divider inside a panel, between two questions that belong to the same
   step — quieter than a second panel, clearer than whitespace alone. */
.rule {
  border: none;
  border-top: 1px solid var(--border);
  margin: 1.4rem 0 1rem;
}

/* ── the setup choice: where a new application's configuration comes from ── */

.choice-list { display: flex; flex-direction: column; gap: 0.6rem; }
.choice {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
  cursor: pointer;
}
.choice.chosen { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
.choice.disabled { opacity: 0.6; cursor: default; }
.choice > span { display: flex; flex-direction: column; gap: 0.2rem; min-width: 0; }
.choice input { margin-top: 0.2rem; accent-color: var(--accent); }

/* The candidate files, indented under the choice they belong to. A candidate
   that cannot be used stays in the list, greyed out with its reason: a file
   that was found but not offered has to say why. */
.cand-list {
  list-style: none;
  margin: 0 0 0 1.6rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
/* Direct children only: a candidate's notes are a nested list and must not
   inherit the card frame. */
.cand-list > li {
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
.cand-list > li.chosen { border-color: var(--accent); }
.cand-list > li.disabled { opacity: 0.55; }
.cand-list label { display: flex; align-items: center; gap: 0.5rem; margin: 0; cursor: pointer; }
.cand-list > li.disabled label { cursor: default; }
.cand-list small { display: block; margin-top: 0.2rem; padding-left: 1.6rem; }
.cand-list .check-list { padding-left: 1.6rem; }

.file-preview summary { cursor: pointer; color: var(--fg-muted); font-size: 0.85rem; margin: 0.6rem 0; }

/* ── the SSH deploy-key panel (shared by the wizard and app settings) ── */

.key-panel {
  margin-top: 0.6rem;
  padding: 0.9rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
/* The three items are an order of operations, not a list of options: the
   repository cannot accept the key before the forge knows it. */
.key-steps { margin: 0.8rem 0 0; padding-left: 1.3rem; }
.key-steps li { margin-bottom: 0.9rem; }
.key-steps li p { margin: 0 0 0.35rem; }
.pubkey {
  font-family: var(--mono);
  font-size: 0.8rem;
  white-space: pre-wrap;
  word-break: break-all;
  margin: 0.3rem 0;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
  color: var(--fg);
}

/* ── the wizard's stage rows ── */

.stage-rows { display: flex; flex-direction: column; gap: 0.7rem; }
.stage-row {
  padding: 0.75rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}
/* A deselected stage stays visible and editable — it is a choice, not a
   deletion — but recedes so the selected ones read as the plan. */
.stage-row.off { opacity: 0.5; }
.stage-facts {
  margin: 0.4rem 0 0;
  font-size: 0.8rem;
  color: var(--fg-muted);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}
/* Badges keep their own width in a fact line; only the text may stretch. */
.stage-facts .badge { flex: 0 0 auto; }

/* A note that belongs under its field rather than beside it. A bare <small>
   after an input already breaks to the next line because the input fills the
   column; after a select it does not. */
.field-note { display: block; margin-top: 0.25rem; }

/* ── the wizard's variables step ── */

.var-stage { margin-bottom: 1.4rem; }
.var-list { width: 100%; }
.var-list td { vertical-align: middle; padding: 0.3rem 0.5rem 0.3rem 0; }
.var-list td:first-child { white-space: nowrap; }

/* ───────── profile identity header ───────── */

.profile-id {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1.5rem;
}
.profile-avatar {
  display: grid;
  place-items: center;
  width: 3.2rem;
  height: 3.2rem;
  flex: 0 0 auto;
  border-radius: 50%;
  font-family: var(--mono);
  font-size: 1.4rem;
  font-weight: 700;
  color: #06281e;
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  box-shadow: 0 0 18px var(--accent-glow);
}
.profile-id-text h1 {
  margin: 0;
  font-size: 1.25rem;
  letter-spacing: -0.01em;
}
.profile-id-text p { margin: 0.25rem 0 0; display: flex; align-items: center; gap: 0.5rem; }
.profile-id-link {
  margin-left: auto;
  color: var(--fg-muted);
  text-decoration: none;
  font-size: 0.85rem;
}
.profile-id-link:hover { color: var(--accent); }

.totp-secret {
  display: inline-block;
  padding: 0.4rem 0.7rem;
  font-size: 1rem;
  letter-spacing: 0.12em;
  border-radius: var(--radius-sm);
  background: var(--bg);
  border: 1px solid var(--border-strong);
  color: var(--accent);
}

/* The enrolment QR code. Its modules are dark and every scanner expects them
   dark on light with a quiet zone around them, so the image gets its own light
   ground instead of sitting on the panel. */
.totp-qr {
  display: block;
  width: 12rem;
  height: auto;
  padding: 0.7rem;
  border-radius: var(--radius-sm);
  background: var(--qr-paper);
}

/* A six-digit authenticator code: wide, spaced and centred so it can be read
   back while typing it off a phone. */
.code-input {
  width: 9rem;
  font-family: var(--mono);
  font-size: 1.1rem;
  letter-spacing: 0.28em;
  text-align: center;
}

/* ───────── login page ───────── */

.login-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.login-card {
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 2rem 2.25rem;
  width: 22rem;
}

.login-card h1 {
  /* Same mark as the header brand: the login page is the first thing an
     operator sees, and it should be the same product as the page after it. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  margin: 0 0 1.25rem;
  font-size: 1.2rem;
  letter-spacing: -0.01em;
}
.login-card h1 .mark {
  width: 14px;
  height: 14px;
  border-radius: 4px;
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  box-shadow: 0 0 12px var(--accent-glow);
}

.login-card input { width: 100%; }

.login-card button[type="submit"] {
  width: 100%;
  margin-top: 1.25rem;
}

/* ───────── live log view ───────── */

.log-view {
  margin: 0;
  padding: 0.75rem 1rem;
  background: #080b10;
  color: var(--fg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  max-height: 60vh;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-word;
  font-family: var(--mono);
  font-size: 0.85rem;
  line-height: 1.4;
}

/* Log view: error lines stay red but legible on the dark pane; search
   matches get a subtle highlight, the current match a stronger one. Output
   copied from the deployed application recedes to the muted stop, so the
   deployment's own progress stays the foreground of an interleaved log. */
.log-view .error-text { color: #fca5a5; }
.log-view .app-text { color: var(--fg-muted); }
.log-view .log-match { background: rgba(214, 160, 34, 0.18); }
.log-view .log-current { background: rgba(214, 160, 34, 0.45); }

/* ───────── stage status / access control toggles ───────── */

.stage-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  margin: 0.25rem 0 0.75rem;
}
.seg-group { display: flex; align-items: center; gap: 0.5rem; }
.seg-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--fg-muted);
}
/* Segmented control: connected buttons that read clearly as one toggle. */
.seg {
  display: inline-flex;
  border: 1px solid var(--border-strong);
  border-radius: 7px;
  overflow: hidden;
}
.seg button {
  border: 0;
  border-radius: 0;
  margin: 0;
  padding: 0.3rem 0.75rem;
  background: transparent;
  color: var(--fg-muted);
  font: inherit;
  cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.seg button + button { border-left: 1px solid var(--border-strong); }
.seg button:hover { background: rgba(255, 255, 255, 0.05); color: var(--fg); }
.seg button.active {
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  color: #06281e;
  font-weight: 600;
}
.status-history { margin-left: auto; font-size: 0.85rem; }

/* Redeploy-changes action: draws attention to undeployed changes. */
button.warn-action {
  background: linear-gradient(135deg, var(--warn), #b9871a);
  color: #1c1405;
  border-color: #b9871a;
}

/* ───────── modal dialog (deploy dialog) ───────── */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(5, 7, 10, 0.6);
  backdrop-filter: blur(2px);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 4rem 1rem;
  overflow-y: auto;
  z-index: 50;
}
.modal {
  width: 38rem;
  max-width: 100%;
  margin: 0;
}
/* A dialog whose whole content is one short question (the step-up code). */
.modal-narrow { width: 26rem; }
.modal-close {
  float: right;
  background: none;
  border: none;
  color: var(--fg-muted);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  padding: 0;
}
.modal-close:hover { filter: none; color: var(--fg); }

/* Deployability / precondition lists and stage facts. */
.check-list, .facts-list {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0;
}
.check-list li { margin: 0.25rem 0; }

/* A fact line is a label followed by chips — a badge, a mono value, a link.
   It lays them out as flex items with an explicit gap rather than relying on
   the whitespace between them in the template, because Vue's compiler
   condenses whitespace-only text nodes that span a newline: a badge and the
   text after it collapse together the moment the markup is wrapped across two
   lines. Spacing that only survives while the template stays on one line is
   not spacing. */
.facts-list li {
  margin: 0.25rem 0;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.35rem;
  min-width: 0;
}

/* ───────── deployment timeline (detail page) ───────── */

.timeline {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0;
}
.timeline li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.2rem 0;
  color: var(--fg-muted);
}
.timeline li .step-marker { width: 1.2rem; text-align: center; }
.timeline li.done { color: var(--fg); }
.timeline li.current { color: var(--accent); font-weight: 600; }
.timeline li.failed { color: var(--err); font-weight: 600; }

/* Manual-intervention banner. */
.mir-box {
  border: 1px solid rgba(248, 100, 90, 0.4);
  background: var(--err-soft);
  color: var(--err);
  border-radius: var(--radius-sm);
  padding: 0.75rem 1rem;
  margin: 0.5rem 0;
}

/* ───────── app detail (operate page) ───────── */

/* Hero: app identity + a mono build-policy meta line + stage-labelled URLs. */
.app-hero { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.2rem; }
.app-hero__id { min-width: 0; }
.app-hero__id h1 { margin: 0; font-size: 1.4rem; letter-spacing: -0.02em; display: flex; align-items: center; gap: 0.55rem; flex-wrap: wrap; }
.app-hero__id .slug { font-family: var(--mono); font-size: 0.8rem; color: var(--fg-dim); font-weight: 400; }
.app-hero__repo { margin: 0.5rem 0 0; font-family: var(--mono); font-size: 0.8rem; color: var(--fg-muted); word-break: break-all; }
.app-meta { margin: 0.55rem 0 0; display: flex; flex-wrap: wrap; gap: 0.25rem 0.9rem; font-family: var(--mono); font-size: 0.76rem; color: var(--fg-dim); }
.app-meta span b { color: var(--fg-muted); font-weight: 600; }
.app-hero__action { flex: 0 0 auto; text-decoration: none; }
.stage-urls { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.95rem; }
.stage-url { display: inline-flex; align-items: center; gap: 0.4rem; font-family: var(--mono); font-size: 0.78rem; padding: 0.14rem 0.6rem; border-radius: 999px; border: 1px solid var(--border-strong); background: var(--panel); }
.stage-url .su { color: var(--fg-dim); }
.stage-url a { color: var(--accent); text-decoration: none; }
.stage-url a:hover { text-decoration: underline; }
.stage-url .ext { color: var(--fg-dim); }

/* Stage card: rail (mapped near the apps overview) + a slim two-line summary —
   identity, then the indented Status/Access controls — that expands. */
.stage-card { border-left: 3px solid var(--rail, var(--border-strong)); }
.stage-summary { display: flex; flex-direction: column; gap: 0.55rem; cursor: pointer; user-select: none; }
.ss-line { display: flex; align-items: center; gap: 0.55rem 0.9rem; flex-wrap: wrap; }
.ss-controls { padding-left: 1.5rem; }
.stage-summary .chev { color: var(--fg-dim); display: inline-block; transition: transform 0.15s, color 0.15s; }
.stage-summary:hover .chev { color: var(--accent); }
.stage-summary.open .chev { transform: rotate(90deg); }
.ss-name { font-size: 1.05rem; font-weight: 650; letter-spacing: -0.01em; display: inline-flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.ss-state { font-size: 0.82rem; color: var(--fg-muted); }
.stage-card.is-online    .ss-state { color: var(--ok); }
.stage-card.is-maint     .ss-state { color: var(--warn); }
.stage-card.is-attention .ss-state { color: var(--err); }
.ss-ver { font-family: var(--mono); font-size: 0.82rem; color: var(--accent); text-decoration: none; }
.ss-ver:hover { text-decoration: underline; }
.ss-line .spacer { flex: 1 1 1rem; }
.ss-line .link { color: var(--fg-muted); text-decoration: none; font-size: 0.84rem; }
.ss-line .link:hover { color: var(--accent); }
.stage-card__controls { display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem 1.1rem; }

.stage-body { margin-top: 1.15rem; }
.stage-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 0.95rem 1.5rem; margin: 0; padding: 1.15rem 0 0; border-top: 1px solid var(--border); }
.fact { display: flex; flex-direction: column; gap: 0.32rem; min-width: 0; }
.fact .k { font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-dim); }
.fact .v { font-size: 0.85rem; color: var(--fg); display: flex; align-items: center; gap: 0.45rem; flex-wrap: wrap; min-width: 0; }
.fact .v a { color: var(--accent); text-decoration: none; }
.fact .v a:hover { text-decoration: underline; }
.fact .v .sha { color: var(--fg-dim); }
.fact.storage .v { flex-direction: column; align-items: stretch; gap: 0.4rem; }
.fact .storeline { display: flex; justify-content: space-between; font-family: var(--mono); font-size: 0.8rem; }
.fact .storeline .pct { color: var(--fg-muted); }
.stage-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 1.15rem; padding-top: 1rem; border-top: 1px solid var(--border); }
.stage-sub { margin-top: 1rem; padding: 0.9rem 1rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--panel); }
.stage-sub h4 { margin: 0 0 0.6rem; font-size: 0.78rem; font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.1em; color: var(--fg-dim); font-weight: 600; }
tr.stopped { opacity: 0.55; }

/* Builds & Artifacts matrix. */
.bna-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.1rem; }
.bna-head h2 { margin: 0; }
.bna-bar { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; justify-content: space-between; margin-bottom: 1.15rem; }
.build-form { display: flex; gap: 0.7rem; align-items: flex-end; flex-wrap: wrap; }
.build-form label { margin: 0 0 0.25rem; }
.build-activity { display: inline-flex; align-items: center; gap: 0.55rem; font-family: var(--mono); font-size: 0.78rem; color: var(--fg-muted); }
.build-activity a { color: var(--accent); text-decoration: none; }
.bf-ref { width: 11rem; }

.matrix th.sc, .matrix td.sc { text-align: center; width: 5.4rem; }
.matrix td.sc { position: relative; }
.matrix th.sc { color: var(--fg-muted); white-space: nowrap; }
.sc .auto { color: var(--accent); cursor: help; }
/* Promotion connector between a stage and the one it depends on: colour tells the
   precondition state (green = met, red = unmet); no line when the target is
   impossible. Dots sit on top, so the line ends cleanly behind them. */
.matrix td.sc.link-ok::before, .matrix td.sc.link-bad::before {
  content: ""; position: absolute; z-index: 0; top: 50%; left: -50%; width: 100%;
  transform: translateY(-50%); opacity: 0.75;
}
.matrix td.sc.link-ok::before  { border-top: 2px dashed var(--ok); }
.matrix td.sc.link-bad::before { border-top: 2px dashed var(--err); }
.art-ver { display: inline-flex; align-items: center; gap: 0.45rem; }
.art-ver a { font-family: var(--mono); color: var(--accent); text-decoration: none; }
.art-ver a:hover { text-decoration: underline; }
.art-kebab { font: inherit; background: none; border: none; color: var(--fg-dim); cursor: pointer; padding: 0 0.2rem; font-size: 1rem; line-height: 1; }
.art-kebab:hover { color: var(--fg); }

/* Deploy-state dot. Colour = state; brightness = whether it can be deployed now.
   Opaque fills so the connector line ends hidden behind the dot. */
.sdot { display: inline-block; position: relative; z-index: 1; width: 15px; height: 15px; border-radius: 50%; border: 1.5px solid var(--border-strong); background: transparent; padding: 0; cursor: pointer; vertical-align: middle; transition: transform 0.1s; }
/* A dot's meaning lives in its colour, so a non-deployable (disabled) dot —
   current, deploying, unhealthy, blocked — keeps full opacity; disabling only
   blocks the click. Overrides the global button:disabled { opacity: 0.5 }, which
   would otherwise wash the bright "current" green down to a muted grey-green. */
.sdot:disabled { opacity: 1; cursor: default; }
.sdot:hover { transform: scale(1.25); }
.sdot:disabled:hover { transform: none; }
.sdot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.sdot.current   { background: var(--ok); border-color: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
.sdot.past      { background: #2a6038; border-color: transparent; }
.sdot.unhealthy { background: var(--warn); border-color: var(--warn); box-shadow: 0 0 0 3px var(--warn-soft); }
.sdot.failed    { background: #7a3a38; border-color: transparent; }
.sdot.deploying { background: var(--busy); border-color: var(--busy); }
.sdot.target    { background: var(--panel-2); border-color: var(--fg-muted); }
.sdot.target:hover { border-color: var(--accent); background: var(--accent-glow); }
.sdot.blocked   { background: var(--panel-2); border-color: #2a3340; cursor: not-allowed; }
.sdot.blocked:hover { transform: none; }
/* Deploy-in-progress ping. Like the dashboard's healthy dot, the halo is a
   scaled pseudo-element rather than an animated box-shadow, so the pulse runs
   on the compositor instead of repainting the deploy matrix every frame. The
   disc fills the dot's padding box, where the identically coloured background
   hides it completely, and expands past the dot as it fades. Filling the
   padding box keeps it strictly inside the dot at every dot size — the matrix
   uses 15px and the legend 12px — which matters because two antialiased circle
   edges at the same radius stack into a visible seam, and at rest that seam
   would sit on the dot for most of every cycle. 2.7 carries the 13px inner box
   (15px, less a border that rounds to 1px) out to roughly the 35px the old
   10px box-shadow spread reached.

   The growing ring is a fraction of a pixel softer than the old spread was,
   because the compositor rasterises the disc once at its layout size and
   resamples that bitmap as it scales. That is the trade for not repainting the
   matrix every frame, and it is only visible mid-ping, on an edge that is
   already fading out. */
.sdot.deploying::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: var(--busy);
  pointer-events: none;
  animation: spulse 1.1s ease-out infinite;
}
@keyframes spulse {
  0%   { transform: scale(1);   opacity: 0.8; }
  70%  { transform: scale(2.7); opacity: 0; }
  100% { transform: scale(1);   opacity: 0; }
}

.dot-legend { display: flex; flex-wrap: wrap; gap: 0.4rem 1.2rem; margin-top: 1.1rem; padding-top: 0.9rem; border-top: 1px solid var(--border); font-size: 0.74rem; color: var(--fg-muted); }
.dot-legend span { display: inline-flex; align-items: center; gap: 0.45rem; }
.dot-legend .sdot { cursor: default; width: 12px; height: 12px; }
.dot-legend .sdot:hover { transform: none; }
.dot-legend .auto { color: var(--accent); }
.leg-line { display: inline-block; width: 20px; border-top: 2px dashed var(--fg-dim); opacity: 0.8; }
.leg-line.ok  { border-top-color: var(--ok); }
.leg-line.bad { border-top-color: var(--err); }
.history-only { display: none; }
.matrix.show-all .history-only { display: table-row; }

@media (max-width: 640px) { .app-hero { flex-direction: column; } }

/* ════════════ app settings (configure page) ════════════ */

.set-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 1.4rem; }
.set-head h1 { margin: 0; font-size: 1.25rem; font-weight: 700; display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.set-head h1 .slug { font-family: var(--mono); font-size: 0.85rem; color: var(--fg-dim); font-weight: 500; }

/* The Application card splits into labelled sub-sections; the legend is a mono
   eyebrow with a trailing hairline — structural grouping, not decoration. */
.fset { border-top: 1px solid var(--border); margin-top: 1.3rem; padding-top: 1.15rem; }
.fset:first-of-type { border-top: 0; margin-top: 0; padding-top: 0; }
.fset > .leg {
  display: flex; align-items: center; gap: 0.7rem; margin: 0 0 0.8rem;
  font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.14em;
  text-transform: uppercase; color: var(--fg-dim);
}
.fset > .leg::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.fset > .leg .hint { letter-spacing: normal; text-transform: none; color: var(--fg-dim); font-size: 0.74rem; }

/* checkbox/flag rows — inputs default to a wide min-width, so opt out here. */
.flags { display: flex; flex-wrap: wrap; gap: 0.5rem 1.6rem; margin-top: 0.3rem; }
.flag { display: inline-flex; align-items: center; gap: 0.5rem; margin: 0; color: var(--fg); font-size: 0.88rem; cursor: pointer; }
.flag input { width: 15px; height: 15px; accent-color: var(--accent); }
.flag .sub { color: var(--fg-dim); font-size: 0.8rem; }

/* Settings nested under a toggle, indented so they read as belonging to it. */
.res-sub { margin: 0.55rem 0 0 1.55rem; padding-left: 0.95rem; border-left: 2px solid var(--border); }

/* Per-stage resource group caption; the "enabled" pill makes the app→stage link
   explicit — these fields exist only because the resource is enabled app-wide. */
.rescap { display: flex; align-items: center; gap: 0.5rem; margin: 1.1rem 0 0.1rem; font-size: 0.82rem; color: var(--fg-muted); }
.rescap b { font-weight: 600; color: var(--fg); }
.rescap .en { font-family: var(--mono); font-size: 0.68rem; letter-spacing: 0.04em; color: var(--accent); border: 1px solid rgba(54,214,168,0.35); background: rgba(54,214,168,0.08); border-radius: 999px; padding: 0.05rem 0.5rem; }

.help { font-family: var(--mono); font-size: 0.7rem; color: var(--fg-dim); border: 1px solid var(--border-strong); border-radius: 50%; padding: 0 0.34rem; cursor: help; margin-left: 0.3rem; }

/* Inline callout — e.g. "a saved variable version is staged but not yet live". */
.callout { display: flex; gap: 0.55rem; align-items: flex-start; padding: 0.6rem 0.85rem; border: 1px solid rgba(214,160,34,0.38); background: var(--warn-soft); border-radius: var(--radius-sm); font-size: 0.85rem; color: var(--fg); margin: 0.1rem 0 1rem; }
.callout .ci { color: var(--warn); line-height: 1.45; }
.callout b { font-weight: 650; }
.hint-line { color: var(--fg-muted); font-size: 0.82rem; }

/* delete sits apart from save — pushed to the far right, never adjacent. */
.actions-split { display: flex; justify-content: space-between; align-items: center; }

/* ── the stage console ── */
.stages-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; margin: 2.3rem 0 0; }
.stages-head .eyebrow { margin: 0 0 0.3rem; }
.stages-head h2 { margin: 0; font-size: 1.05rem; font-weight: 650; }
.stages-head .count { font-family: var(--mono); font-size: 0.8rem; color: var(--fg-dim); }

/* Tab strip: stage tabs sit on the panel's top border; the active one merges
   into the panel and shows a top accent in its own operational rail colour. */
.stage-tabs {
  display: flex; align-items: flex-end; gap: 0.3rem; margin-top: 0.85rem;
  padding: 0 0.15rem; border-bottom: 1px solid var(--border); overflow-x: auto;
}
.stage-tab {
  --rail: var(--border-strong);
  display: inline-flex; align-items: center; gap: 0.55rem; white-space: nowrap;
  padding: 0.6rem 1rem; margin-bottom: -1px;
  border: 1px solid transparent; border-bottom: 0; border-radius: 9px 9px 0 0;
  background: transparent; color: var(--fg-muted); font: inherit; font-size: 0.9rem;
  cursor: pointer; transition: background 0.12s, color 0.12s;
}
.stage-tab:hover { background: var(--panel); color: var(--fg); }
.stage-tab .tdot { width: 8px; height: 8px; border-radius: 50%; background: var(--rail); flex: 0 0 auto; }
.stage-tab .tslug { font-family: var(--mono); font-size: 0.76rem; color: var(--fg-dim); }
.stage-tab.is-online    { --rail: var(--accent); }
.stage-tab.is-maint     { --rail: var(--warn); }
.stage-tab.is-offline   { --rail: var(--fg-dim); }
.stage-tab.is-attention { --rail: var(--err); }
.stage-tab.active {
  color: var(--fg); font-weight: 600;
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  border-color: var(--border);
  box-shadow: inset 0 2px 0 var(--rail);
}
.stage-tab.active .tdot { box-shadow: 0 0 7px var(--rail); }
.stage-tab.add { color: var(--fg-dim); border: 1px dashed var(--border-strong); border-bottom: 0; }
.stage-tab.add:hover { color: var(--accent); border-color: var(--accent); }

.stage-panel {
  border: 1px solid var(--border); border-top: 0;
  border-radius: 0 0 var(--radius) var(--radius);
  background: linear-gradient(180deg, var(--panel-2), var(--panel));
  box-shadow: var(--shadow); padding: 1.3rem 1.4rem; margin-bottom: 1.5rem;
}

/* Stage panel header: identity + domain/resources left, stage-level actions right. */
.sp-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; padding-bottom: 1.05rem; border-bottom: 1px solid var(--border); margin-bottom: 1.15rem; }
.sp-id h3 { margin: 0; font-size: 1.05rem; font-weight: 650; display: flex; align-items: center; gap: 0.55rem; }
.sp-sub { margin: 0.5rem 0 0; display: flex; flex-wrap: wrap; gap: 0.45rem 0.7rem; align-items: center; }
.sp-sub .res-badges { display: inline-flex; flex-wrap: wrap; gap: 0.35rem; }
.sp-actions { display: flex; gap: 0.5rem; flex: 0 0 auto; }

/* Stacked config areas inside a stage (General / Variables / Access / Pages).
   A heavier top divider separates areas; the mono .leg legends group fields
   within an area — two levels of hierarchy. */
.sp-area { border-top: 1px solid var(--border-strong); margin-top: 1.7rem; padding-top: 1.4rem; }
.sp-area:first-of-type { border-top: 0; margin-top: 0; padding-top: 0; }
.sp-area__h { display: flex; align-items: baseline; gap: 0.6rem; margin: 0 0 0.5rem; flex-wrap: wrap; }
.sp-area__h h4 { margin: 0; font-size: 1rem; font-weight: 650; }
.sp-area__h .d { color: var(--fg-dim); font-size: 0.82rem; }

/* settings tables: checkbox cells and narrow/centred columns */
td input[type=checkbox], th input[type=checkbox] { height: 15px; accent-color: var(--accent); cursor: pointer; }
td.shrink, th.shrink { width: 1%; white-space: nowrap; }
td.mid, th.mid { text-align: center; }

/* ───────── touch and small viewports ─────────

   Below this width the pointer is a finger. Controls go to the 44px minimum
   target size, and fields stop insisting on 16rem — that min-width is what
   pushes a form out of its card on a phone, and at this size a field wants the
   full column anyway. Checkboxes and radios are excluded: they are not text
   fields and blowing them up to 100% would stretch the box, not the hit area
   (the surrounding label is already the target). */
@media (max-width: 820px) {
  input:not([type=checkbox]):not([type=radio]),
  select,
  textarea {
    width: 100%;
    min-width: 0;
    min-height: 44px;
  }
  button { min-height: 44px; }
  /* Segmented controls are a row of buttons in one pill; 44px each makes the
     pill taller than the header it sits in. 40 is the compromise. */
  .seg button { min-height: 40px; }
  .form-row > * { flex: 1 1 100%; }
  .apps-bar { flex-direction: column; align-items: stretch; }
  /* Rows of controls that fit side by side on a desktop and do not on a
     phone. The stage tabs already scroll; they just need permission to be
     narrower than their content so the scrolling actually happens. */
  .seg-group, .sp-actions { flex-wrap: wrap; }
  /* The stage header puts identity and actions on one line; at this width the
     actions go underneath instead of pushing the panel wider. */
  .sp-head { flex-wrap: wrap; }
  .stage-tabs { min-width: 0; }
  /* vh is the viewport with the browser chrome retracted, so a 60vh log pane
     is cut off by the address bar on iOS for as long as it stays expanded.
     dvh is the height actually available; the vh line stays as the fallback. */
  .log-view { max-height: 60vh; max-height: 62dvh; }
}

/* ───────── reduced motion ─────────

   Honour the OS-level "reduce motion" setting. The looping halos (healthy
   service dot, deploying stage dot) resolve to their resting frame, which is
   the static halo the other states draw, so nothing disappears — the dots just
   stop moving. The one-shot entrance animations are collapsed to their end
   state rather than removed, otherwise .reveal would stay at opacity 0 and the
   meter fills at scaleX(0). */
@media (prefers-reduced-motion: reduce) {
  .dot.ok::after,
  .sdot.deploying::after {
    animation: none;
  }
  .reveal, .fill {
    animation-duration: 1ms;
    animation-delay: 0ms;
  }
  * {
    transition-duration: 1ms !important;
  }
}
