/* ────────────────────────────────────────────────────────────────────────────
   Tessera — customer portal

   The operator console next door is a dark instrument panel: dense, amber and
   teal, built for someone diagnosing a fault at 2am. This is the opposite room.
   A customer visits to check on their organisation and get on with their day,
   so the register here is closer to well-set stationery than to a control
   surface — warm paper, a generous measure, hairline rules, and one accent
   colour used sparingly.

   The two planes deliberately share no accent colour. Nobody should ever be
   momentarily unsure which one they are looking at.

   The one convention carried over from the console: anything a machine
   produced is set in mono. Identifiers, limits, addresses, timestamps.
   ──────────────────────────────────────────────────────────────────────────── */

@import url("fonts.css");

:root {
  /* The grounds are the logo's grout: the cream the tesserae are set into,
     sampled off the artwork at #FDFBF2 and pulled a shade back so white cards
     still lift off it. */
  --paper:      #FCFAF3;
  --card:       #FFFFFF;
  --sunken:     #F5F2E9;
  --rule:       #E6E1D6;
  --rule-soft:  #F0EDE4;

  --ink:        #16181D;
  --ink-muted:  #5B6170;
  --ink-faint:  #8A8F9C;

  /* The accent is the wordmark's navy (#072A61), lifted a little so it reads
     as a colour rather than as black when it is used for a whole button. */
  --accent:     #0B3170;
  --accent-ink: #FFFFFF;
  --accent-wash:#EDF1F8;

  /* The logo's second voice -- the teal of "HOSTING". Two values on purpose:
     the artwork's own #2480A2 sits at 4.3:1 on paper, which is fine for a rule
     or a dot and short of AA for a word, so text uses the darkened one. */
  --brand:      #072A61;
  --brand-teal: #1A6580;
  --brand-tile: #2480A2;

  --good:       #2E6F5E;
  --caution:    #8A5A11;
  --bad:        #A8342C;
  --bad-wash:   #FBEEED;

  --radius:     4px;
  --measure:    62rem;
  --rail:       15rem;

  --term-bg:    #14161B;
  --term-ink:   #E4EBF3;
  --term-edge:  #14161B;
  /* Read by the vendored terminal client. It looks for --term-fg, not
     --term-ink; without these it silently uses its own hardcoded palette and
     the terminal is the one element that ignores the theme. */
  --term-fg:        #E4EBF3;
  --term-cursor:    #2F4B7C;
  --term-selection: #33415580;

  --font-ui:   "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:      #14161B;
    --card:       #1A1D24;
    --sunken:     #20242C;
    --rule:       #2C313B;
    --rule-soft:  #23272F;

    --ink:        #E8E9ED;
    --ink-muted:  #A2A9B8;
    --ink-faint:  #6F7787;

    --accent:     #9BBCE8;
    --accent-ink: #10131A;
    --accent-wash:#1E2531;

    /* Both brand colours invert to their light-on-dark counterparts. The navy
       cannot survive here -- #072A61 on #14161B is 1.3:1 -- so on this ground
       the brand speaks in the tint, the way the dark lockup does. */
    --brand:      #C9D8F0;
    --brand-teal: #6CC0DA;
    --brand-tile: #45A6C4;

    --good:       #6FC2AC;
    --caution:    #D9A24B;
    --bad:        #E88078;
    --term-bg:    #0E1015;
    --term-ink:   #E4EBF3;
    --term-edge:  #2C313B;
    --term-fg:        #E4EBF3;
    --term-cursor:    #8FB0E8;
    --term-selection: #33415580;
    --bad-wash:   #2B1E1E;

    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --paper:      #14161B;
  --card:       #1A1D24;
  --sunken:     #20242C;
  --rule:       #2C313B;
  --rule-soft:  #23272F;
  --ink:        #E8E9ED;
  --ink-muted:  #A2A9B8;
  --ink-faint:  #6F7787;
  --accent:     #8AA9DC;
  --accent-ink: #10131A;
  --accent-wash:#1E2531;
  --good:       #6FC2AC;
  --caution:    #D9A24B;
  --bad:        #E88078;
  --term-bg:    #0E1015;
  --term-ink:   #E4EBF3;
  --term-edge:  #2C313B;
  --term-fg:        #E4EBF3;
  --term-cursor:    #8FB0E8;
  --term-selection: #33415580;
  --bad-wash:   #2B1E1E;
  color-scheme: dark;
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-ui);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

.v, code, pre, input, select, textarea, td.num, th.num {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}

a { color: var(--accent); text-underline-offset: .18em; }

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

/* ── Shell ─────────────────────────────────────────────────────────────────
   A fixed left rail and a scrolling content column.

   The rail earns its width by being permanent: the whole product is visible at
   once, grouped by what a customer is actually doing — running infrastructure,
   controlling access to it, administering the organisation around it. A tab
   strip could not show that structure, and it ran out of room the moment the
   product grew past seven sections.

   Below 60rem the rail becomes an off-canvas drawer. Not a horizontally
   scrolling strip: navigation you have to scroll sideways to discover is
   navigation most people never find. */

.shell {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  min-height: 100vh;
}

.rail {
  grid-column: 1;
  background: var(--card);
  border-right: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  height: 100vh;
  overflow-y: auto;
}

.rail-brand {
  padding: 1.1rem 1.25rem .95rem;
  display: flex;
  align-items: center;
  gap: .6rem;
  text-decoration: none;
  color: var(--ink);
  border-bottom: 1px solid var(--rule-soft);
}
.rail-brand .mark {
  width: 30px;
  height: 30px;
  flex: none;
  /* The medallion carries its own cream grout, so it needs no plate and sits
     on either ground unaltered. Only the lockup's navy wordmark has to change
     for dark -- see .brand-lockup below. */
}
.rail-brand .naming { min-width: 0; }
.rail-brand .wordmark {
  display: block;
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: .01em;
  line-height: 1.1;
}
.rail-brand .sub {
  display: block;
  font-family: var(--font-mono);
  font-size: .62rem;
  letter-spacing: .06em;
  color: var(--ink-faint);
  margin-top: .12rem;
}
.rail-brand:hover .wordmark { color: var(--accent); }

.rail-nav { padding: .6rem .65rem 1rem; flex: 1; }

.nav-group {
  font-size: .62rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding: 1rem .6rem .35rem;
}
.rail-nav > .nav-group:first-child { padding-top: .4rem; }

.rail-nav a {
  display: flex;
  align-items: center;
  gap: .55rem;
  padding: .45rem .6rem;
  border-radius: var(--radius);
  font-size: .875rem;
  color: var(--ink-muted);
  text-decoration: none;
  border-left: 2px solid transparent;
}
.rail-nav a:hover { background: var(--sunken); color: var(--ink); }
.rail-nav a[aria-current="page"] {
  background: var(--accent-wash);
  border-left-color: var(--accent);
  color: var(--ink);
  font-weight: 600;
}
.rail-nav a .badge {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: .68rem;
  color: var(--ink-faint);
}
/* A section the platform cannot offer yet is shown, not hidden: a customer
   should be able to see the shape of the product and why part of it is dark. */
.rail-nav a.gated { opacity: .55; }
.rail-nav a.gated .badge { font-family: var(--font-ui); font-style: italic; }

.rail-foot {
  border-top: 1px solid var(--rule-soft);
  padding: .8rem .9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: .7rem;
}
.rail-who { min-width: 0; }
.rail-who .orgname {
  display: block;
  font-weight: 600;
  font-size: .875rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rail-who .email {
  display: block;
  font-family: var(--font-mono);
  font-size: .7rem;
  color: var(--ink-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-decoration: none;
}
a.email:hover, a.email:focus-visible {
  color: var(--accent);
  text-decoration: underline;
}
.rail-foot select {
  width: 100%;
  font-family: var(--font-ui);
  font-size: .82rem;
  font-weight: 600;
  padding: .35rem .45rem;
}
.rail-actions { display: flex; gap: .4rem; }
.rail-actions .btn { flex: 1; justify-content: center; text-align: center; }

/* ── Mobile chrome ──────────────────────────────────────────────────────── */

.topbar { display: none; }

.scrim {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, .4);
  z-index: 40;
}

/* ── Content column ─────────────────────────────────────────────────────── */

.main { grid-column: 2; min-width: 0; }
.main-inner {
  max-width: var(--measure);
  margin: 0;
  padding: 2.25rem 2.25rem 5rem;
}

/* One page header shape, tolerant of two markup forms.
   Most views write `h1 + p` directly; newer ones wrap them in `.titles` so a
   primary action can sit alongside. Both have to lay out correctly, so the
   paragraph is given a full-width flex basis and always takes its own line
   rather than sitting beside the title. */
.page-head {
  /* Grid, not flex. With flex, the description's `max-width` clamped its
     flex base so it fitted beside the title instead of wrapping under it --
     the title and its one-line purpose ended up on the same line. Grid states
     the intent directly: titles stack in column one, actions sit in column two
     on the first row. */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: .35rem 1.5rem;
  margin-bottom: 1.75rem;
  padding-bottom: 1.1rem;
  border-bottom: 1px solid var(--rule);
}
.page-head > h1, .page-head > p, .page-head .titles { grid-column: 1; }
.page-head .actions { grid-column: 2; grid-row: 1; }

.page-head h1 {
  margin: 0;
  font-size: 1.45rem;
  font-weight: 600;
  letter-spacing: -.015em;
  line-height: 1.25;
}
.page-head p {
  margin: 0;
  color: var(--ink-muted);
  max-width: 46rem;
  font-size: .9rem;
}
.page-head .titles { min-width: 0; }
.page-head .titles p { margin: .35rem 0 0; }
.page-head .actions { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }

/* A back link sits above the title rather than beside it, so the title stays
   the first thing read on every page. */
.page-back {
  display: inline-block;
  font-size: .8rem;
  color: var(--ink-muted);
  text-decoration: none;
  margin-bottom: .5rem;
}
.page-back:hover { color: var(--ink); text-decoration: underline; }

/* ── Sections: a statement's hairline headers ───────────────────────────── */

section + section { margin-top: 2.4rem; }
/* A note or card placed before the first section still needs clearance from
   that section's hairline rule. */
.note + section, .card + section, .page-head + section { margin-top: 2rem; }
main > .note + section { margin-top: 2rem; }

.section-head {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding-bottom: .5rem;
  border-bottom: 1px solid var(--rule);
  margin-bottom: 1rem;
}
.section-head h2 {
  margin: 0;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.section-head .right { margin-left: auto; }

.card {
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.15rem 1.25rem;
}
.card + .card { margin-top: .9rem; }

/* ── The capacity ledger ────────────────────────────────────────────────────
   Usage against limits, set as a statement rather than a row of progress bars.
   Every rented platform shows this; almost all of them show it as coloured
   bars that say nothing a number would not. A ledger reads exactly, aligns on
   the decimal, and does not imply precision the figures do not have. */

.ledger { width: 100%; border-collapse: collapse; }
.ledger th {
  text-align: left;
  font-size: .68rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 700;
  padding: 0 0 .5rem;
  border-bottom: 1px solid var(--rule);
}
.ledger th.num, .ledger td.num { text-align: right; }
.ledger td {
  padding: .62rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
.ledger tr:last-child td { border-bottom: 0; }
.ledger .item { font-weight: 500; }
.ledger .sub { display: block; font-size: .76rem; color: var(--ink-faint); font-family: var(--font-ui); }
.ledger .limitless { color: var(--ink-faint); font-style: italic; font-family: var(--font-ui); font-size: .82rem; }

/* A hairline share indicator: present, but subordinate to the number. */
.share {
  display: block;
  height: 2px;
  margin-top: .35rem;
  background: var(--rule);
  border-radius: 1px;
  overflow: hidden;
}
.share i { display: block; height: 100%; background: var(--accent); }
.share.warn i { background: var(--caution); }
.share.full i { background: var(--bad); }

/* ── Tables ─────────────────────────────────────────────────────────────── */

table.list { width: 100%; border-collapse: collapse; font-size: .88rem; }
table.list th {
  text-align: left;
  font-size: .66rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 700;
  padding: .5rem .75rem;
  border-bottom: 1px solid var(--rule);
  white-space: nowrap;
}
table.list td {
  padding: .68rem .75rem;
  border-bottom: 1px solid var(--rule-soft);
  vertical-align: middle;
}
table.list tr:last-child td { border-bottom: 0; }
table.list th.num, table.list td.num { text-align: right; }
/* Any container holding a table has to be able to scroll it. A dense table --
   fingerprints, addresses, timestamps -- cannot be made to fit a phone, and the
   honest answer is to let that one table scroll rather than let it push the
   whole page sideways. Two containers are used across the views, so both get
   the behaviour instead of relying on authors picking the right one. */
.table-wrap, .card.flush { overflow-x: auto; }

/* Machine-produced values in a table stay on one line. The table scrolls, so
   the alternative -- wrapping a fingerprint over five lines -- buys nothing and
   costs the ability to scan the column at all. Outside a table, where there is
   no scroll to fall back on, they break instead. */
table.list td .v, table.list td.v, table.list td .fp { white-space: nowrap; }
.fp { word-break: break-all; }
@media (max-width: 40rem) {
  .check .fp, .checklist .fp { display: block; margin-top: .15rem; }
}

/* ── Tags ───────────────────────────────────────────────────────────────── */

.tag {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: .66rem;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: .14rem .45rem;
  border-radius: 2px;
  background: var(--sunken);
  color: var(--ink-muted);
  border: 1px solid var(--rule);
}
.tag.accent { background: var(--accent-wash); color: var(--accent); border-color: transparent; }
.tag.good   { color: var(--good); }
.tag.bad    { color: var(--bad); }
.tag.you    { background: var(--accent); color: var(--accent-ink); border-color: transparent; }

/* ── Forms ──────────────────────────────────────────────────────────────── */

.field { margin-bottom: 1.1rem; }
.field label {
  display: block;
  font-size: .8rem;
  font-weight: 600;
  margin-bottom: .3rem;
}
.field .hint { font-size: .8rem; color: var(--ink-faint); margin-top: .3rem; line-height: 1.45; }
/* A hint that is a WARNING rather than an explanation: something the customer
   should read before they save, not after they wonder. Only the ink changes --
   a box or an icon here would make an ordinary field look like an error. */
.field .hint.warn { color: var(--caution); }
.field .err  { font-size: .8rem; color: var(--bad); margin-top: .3rem; }

input[type=text], input[type=email], input[type=password], input[type=number], select, textarea {
  width: 100%;
  padding: .55rem .65rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font-size: .9rem;
}
input::placeholder { color: var(--ink-faint); }
input[aria-invalid="true"] { border-color: var(--bad); }

.btn {
  font-family: var(--font-ui);
  font-size: .85rem;
  font-weight: 600;
  padding: .5rem 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--rule);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
}
.btn:hover:not(:disabled) { background: var(--sunken); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.btn.primary:hover:not(:disabled) { filter: brightness(1.1); }
.btn.danger  { color: var(--bad); border-color: color-mix(in srgb, var(--bad) 35%, transparent); }
.btn.small   { font-size: .76rem; padding: .3rem .6rem; }
.btn-row { display: flex; gap: .6rem; align-items: center; flex-wrap: wrap; }

/* ── Notes ──────────────────────────────────────────────────────────────── */

.note {
  border: 1px solid var(--rule);
  border-left: 3px solid var(--ink-faint);
  border-radius: var(--radius);
  background: var(--card);
  padding: .85rem 1rem;
  font-size: .88rem;
}
.note h3 { margin: 0 0 .25rem; font-size: .9rem; font-weight: 600; }
.note p  { margin: 0; color: var(--ink-muted); }
.note.info    { border-left-color: var(--accent); }
.note.good    { border-left-color: var(--good); }
.note.caution { border-left-color: var(--caution); }
.note.bad     { border-left-color: var(--bad); background: var(--bad-wash); }
.note + .note { margin-top: .7rem; }

.secret {
  font-family: var(--font-mono);
  font-size: .84rem;
  word-break: break-all;
  background: var(--sunken);
  border: 1px dashed var(--rule);
  border-radius: var(--radius);
  padding: .7rem .8rem;
  margin: .6rem 0;
}

.codes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: .4rem;
  font-family: var(--font-mono);
  font-size: .84rem;
  margin: .7rem 0;
}
.codes span {
  background: var(--sunken);
  border: 1px solid var(--rule);
  border-radius: 2px;
  padding: .3rem .5rem;
  text-align: center;
}

.empty { padding: 2.2rem 1rem; text-align: center; color: var(--ink-faint); }
.empty h3 { margin: 0 0 .3rem; color: var(--ink-muted); font-size: .95rem; }
.empty p { margin: 0 auto; max-width: 32rem; font-size: .88rem; }

/* ── Entry screens ──────────────────────────────────────────────────────── */

.entry {
  /* Flex, not grid: with the lockup and the card as two grid children,
     place-items:center centres each one inside its own equal-height row and
     drives them to opposite ends of the viewport. Here they are one stack. */
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 2rem 1.5rem;
}
.entry-card {
  width: 100%;
  max-width: 24rem;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: 1.8rem 1.6rem 1.6rem;
}
/* The lockup sits above the card rather than inside it. The full artwork --
   medallion over the serif wordmark -- is the only place the brand gets to
   speak at size, and inside a 24rem card it would either crowd the form or
   shrink to the point where the tesserae turn to noise. */
.brand-lockup {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .5rem;
  margin-bottom: 1.6rem;
}
.brand-lockup .mark  { width: 62px; height: 62px; }
.brand-lockup .word  { width: 132px; height: auto; }
.brand-lockup .tagline {
  margin: .15rem 0 0;
  font-size: .8rem;
  color: var(--ink-faint);
  text-align: center;
}

/* Three-way theme switch, matching the token blocks above: an explicit choice
   wins, and "auto" follows the system. Both files are a few KB, so shipping
   the pair costs less than the flash of the wrong one. */
.brand-lockup .word.on-dark { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand-lockup .word.on-light { display: none; }
  :root:not([data-theme="light"]) .brand-lockup .word.on-dark  { display: block; }
}
:root[data-theme="dark"] .brand-lockup .word.on-light { display: none; }
:root[data-theme="dark"] .brand-lockup .word.on-dark  { display: block; }
:root[data-theme="light"] .brand-lockup .word.on-light { display: block; }
:root[data-theme="light"] .brand-lockup .word.on-dark  { display: none; }


/* The no-JS card keeps the old stacked shape; it has no lockup element. */
.entry-card .brand-mark { display: block; margin-bottom: .5rem; }
.entry-card .wordmark   { display: block; font-weight: 600; font-size: 1.05rem; }
.entry-foot {
  margin-top: 1.2rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule-soft);
  font-size: .82rem;
  color: var(--ink-muted);
  display: flex;
  justify-content: space-between;
  gap: .75rem;
  flex-wrap: wrap;
}

/* ── Utility ────────────────────────────────────────────────────────────── */

.muted { color: var(--ink-muted); }
.faint { color: var(--ink-faint); }
.tiny  { font-size: .8rem; }
.right { margin-left: auto; }
.row   { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.stack { display: flex; flex-direction: column; gap: .3rem; }
.grid2 { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr)); }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
  /* Pinned to the origin so it can never extend the page's scroll width; an
     auto-positioned absolute element still sits at its flow position, which on
     a narrow screen was enough to widen the document. */
  left: 0; top: 0;
}

/* ── Narrow screens ────────────────────────────────────────────────────────
   The rail slides in over the content instead of squeezing it. A drawer is
   discoverable from one obvious control; a horizontally scrolling nav strip is
   discoverable only by accident, which is how the previous layout lost half
   its sections on a phone. */

@media (max-width: 60rem) {
  .shell { grid-template-columns: minmax(0, 1fr); }

  .topbar {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .7rem 1rem;
    background: var(--card);
    border-bottom: 1px solid var(--rule);
    position: sticky;
    top: 0;
    z-index: 30;
  }
  .topbar .wordmark {
    text-decoration: none;
    color: var(--ink);
    display: inline-flex;
    align-items: center;
    gap: .42rem;
    font-weight: 600;
  }
  .topbar .who {
    margin-left: auto;
    font-size: .78rem;
    color: var(--ink-faint);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Confirmation, not navigation: it is the first thing to give up room. */
    min-width: 0;
  }

  .rail {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(17rem, 84vw);
    z-index: 50;
    transform: translateX(-100%);
    transition: transform .18s ease;
    box-shadow: 0 0 2rem rgba(0, 0, 0, .18);
  }
  .shell.nav-open .rail { transform: translateX(0); }
  .shell.nav-open .scrim { display: block; }

  .main { grid-column: 1; }
  .main-inner { padding: 1.5rem 1rem 4rem; }
}

@media (max-width: 26rem) {
  .main-inner { padding-left: .85rem; padding-right: .85rem; }
  .topbar .who { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* Which sign-in door this is. The login error cannot say, so the page must. */
.plane-hint {
  margin: .4rem 0 1.5rem;
  font-size: .8rem;
  line-height: 1.5;
  opacity: .7;
}

/* ── Machines and networks ──────────────────────────────────────────────────
   Added for the machine and network screens. Same register as the rest of the
   portal: hairlines, one accent, mono for anything a machine produced. */

/* Rows that lead somewhere. The whole row is the target, not just the name --
   a customer aiming at a five-character link on a phone is a customer missing. */
table.list tr.clickable { cursor: pointer; }
table.list tr.clickable:hover { background: var(--sunken); }
table.list tr.clickable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

table.list .mname { font-weight: 500; }
table.list .sub {
  display: block;
  font-size: .74rem;
  color: var(--ink-faint);
  font-family: var(--font-ui);
  margin-top: .1rem;
}

/* Status, and where it is heading when those differ. */
.statuscell { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; }
.statuscell .headed {
  font-size: .76rem;
  color: var(--ink-faint);
  font-family: var(--font-ui);
  display: inline-flex;
  align-items: center;
  gap: .25rem;
}
.statuscell .arrow { opacity: .6; }
.statusline { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; justify-content: flex-end; }

/* A machine mid-change. The pulse is the only motion in the portal, and it
   earns its place: it is the difference between "working on it" and "stuck". */
.tag.accent { position: relative; }
.tag.accent::after {
  content: "";
  display: inline-block;
  width: 5px; height: 5px;
  margin-left: .35rem;
  border-radius: 50%;
  background: currentColor;
  animation: breathe 1.6s ease-in-out infinite;
  vertical-align: middle;
}
@keyframes breathe { 0%, 100% { opacity: 1; } 50% { opacity: .2; } }

.backlink {
  display: inline-block;
  font-size: .82rem;
  color: var(--ink-muted);
  text-decoration: none;
  margin-bottom: .5rem;
}
.backlink:hover { color: var(--accent); }

/* ── Size picker ────────────────────────────────────────────────────────────
   Cards rather than a dropdown: choosing a size is a comparison, and a select
   hides every option but one at the moment the comparison matters. */
.sizes { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }
.size { position: relative; display: block; cursor: pointer; }
.size input { position: absolute; opacity: 0; width: 0; height: 0; }
.size-body {
  display: block;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: .7rem .8rem;
  background: var(--card);
}
.size:hover .size-body { border-color: var(--ink-faint); }
.size input:checked + .size-body {
  border-color: var(--accent);
  background: var(--accent-wash);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.size input:focus-visible + .size-body { outline: 2px solid var(--accent); outline-offset: 2px; }
.size-name  { display: block; font-weight: 600; font-size: .9rem; }
.size-spec  { display: block; font-size: .76rem; color: var(--ink-muted); margin-top: .2rem; }
.size-price { display: block; font-size: .74rem; color: var(--ink-faint); margin-top: .3rem; }

/* ── Allowance, shown before the form ───────────────────────────────────────
   A form that fails on submit because of a limit the customer could have been
   shown is a form that wasted their time. */
.headroom-card { padding: .9rem 1rem; }
.headroom { display: grid; gap: .9rem; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
.headroom-item { display: flex; flex-direction: column; gap: .1rem; }
.headroom-item .k {
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-weight: 600;
}
.headroom-item .vv  { font-size: .95rem; font-weight: 500; }
.headroom-item .sub { font-size: .72rem; color: var(--ink-faint); font-family: var(--font-ui); }

.stackform { max-width: 34rem; }
.attach select { min-width: 12rem; }

/* Detail pages put their controls in the section header; on a narrow screen
   that header wraps and the buttons need room to breathe. */
@media (max-width: 40rem) {
  .section-head { flex-wrap: wrap; }
  .section-head .right { margin-left: 0; width: 100%; }
  .statusline { justify-content: flex-start; }
  .sizes { grid-template-columns: 1fr; }

  /* The tab bar grew past the width of a phone when Machines and Networks were
     added, and the last four sections simply fell off the right-hand edge with
     no way to reach them. Scroll it instead of hiding it. */
  .tabs-inner {
    overflow-x: auto;
    flex-wrap: nowrap;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .tabs-inner::-webkit-scrollbar { display: none; }
  .tabs a { white-space: nowrap; flex: none; }

  /* Detail pages put controls in the section header; give them room. */
  .attach { width: 100%; }
  .attach select { flex: 1; min-width: 0; }

  /* The masthead ran 13px past the right edge of a phone, which made the whole
     page scroll sideways. The signed-in address is the part that can go: it is
     confirmation, not navigation, and the organisation name beside it already
     says which account you are in. */
  .masthead-inner { flex-wrap: wrap; row-gap: .4rem; }
  .masthead .right .v { display: none; }
  .orgpick { min-width: 0; }
  .orgpick strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}

/* Stacked blocks on a form page need air between them; without this the
   allowance card, the caution and the form read as one undifferentiated slab. */
.card + .note,
.note + .card,
.headroom-card + .note { margin-top: 1rem; }

/* ── Machine terminal ────────────────────────────────────────────────────────
   The only way into a machine today, so it gets room rather than a corner. */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(12, 16, 22, .72);
  display: grid;
  place-items: center;
  padding: 1.5rem;
  z-index: 60;
}
.modal {
  background: var(--card);
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: var(--radius, 4px);
  max-width: min(96vw, 1100px);
  width: 100%;
  max-height: 92vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: .8rem 1rem;
  border-bottom: 1px solid var(--rule);
}
.modal-head h2 { margin: 0; font-size: .95rem; }
.console-status {
  padding: .5rem 1rem;
  font-size: .78rem;
  color: var(--ink-muted);
  border-bottom: 1px solid var(--rule-soft);
}
.console-status.connected { color: var(--good); }
/* The terminal is dark whatever the page theme is; a shell that inverts with
   the site would just look broken. */
.console-frame {
  flex: 1;
  min-height: 24rem;
  background: var(--term-bg);
  overflow: hidden;
  padding: .5rem .6rem;
}
/* xterm sizes itself from its container, so the container must have a height
   of its own -- inside a flex column that means an explicit basis, or the
   terminal measures zero rows and shows nothing. */
.console-frame .xterm { height: 100%; }
.console-frame .xterm-viewport { overflow-y: auto; }
.console-status.ok { color: var(--good); }
.fp { font-size: .74rem; }
@media (max-width: 40rem) {
  .modal-backdrop { padding: 0; }
  .modal { max-height: 100vh; border-radius: 0; border: 0; }
}

/* Key selection on the create form. */
.checklist { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .5rem; }
.checklist .check { display: flex; gap: .55rem; align-items: baseline; font-weight: 400; }
.checklist .check input { margin: 0; }

/* ── Choosing between two paths ──────────────────────────────────────────────
   Used where the options are mutually exclusive and each needs a sentence of
   explanation — a radio with a bare label would not carry that.

   Selectors are qualified with label/.field because `.field label` already
   sets display:block and would otherwise win on specificity, stacking the
   radio above its own text. */
.choices { display: grid; gap: .5rem; margin-bottom: .9rem; }
.field label.choice, label.choice {
  display: flex;
  gap: .65rem;
  align-items: flex-start;
  padding: .7rem .85rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  cursor: pointer;
}
label.choice:hover { background: var(--surface-2, rgba(0,0,0,.02)); }
label.choice input { margin-top: .2rem; flex: none; }
label.choice:has(input:checked) { border-color: var(--accent); }
label.choice:has(input:disabled) { opacity: .55; cursor: not-allowed; }
label.choice strong { display: block; font-weight: 600; }
label.choice .sub { display: block; font-size: .78rem; color: var(--ink-muted); margin-top: .1rem; }

.keymode[hidden] { display: none; }

/* ── A command to copy ───────────────────────────────────────────────────────
   Wide and monospaced, with the copy button inside the block so it travels
   with the thing it copies. Scrolls rather than wrapping: a wrapped ssh
   command invites someone to paste half of it. */
/* A command to paste.
   The copy button used to be absolutely positioned over the block, which broke
   as soon as a command was long enough to scroll: the text slid underneath it
   and the end of the command was unreadable exactly when you most needed to
   check it. Putting the button in the flow above makes the overlap impossible
   rather than merely unlikely. */
.cmd {
  display: flex;
  flex-direction: column;
  margin-top: .35rem;
  border: 1px solid var(--term-edge);
  border-radius: var(--radius);
  background: var(--term-bg);
  overflow: hidden;
}
.cmd-copy {
  order: 1;
  align-self: flex-end;
  margin: .4rem .4rem 0;
}
.cmd-body {
  order: 2;
  margin: 0;
  padding: .55rem .8rem .75rem;
  background: transparent;
  color: var(--term-ink);
  font-family: var(--font-mono);
  font-size: .8rem;
  line-height: 1.6;
  overflow-x: auto;
  white-space: pre;
}

/* Output from the command a deployment runs after it pulls.
   Deliberately not .cmd-body: that is text to copy, so it must not wrap and it
   is as tall as it needs to be. This is text to READ, written by the
   customer's own build, and it can be hundreds of lines. Uncapped it pushed
   the history and the buttons off the bottom of the screen, so the one place
   somebody goes to find out why a build failed was also the place they could
   no longer reach anything else. Capped and scrolled in place, and wrapped
   rather than scrolled sideways, because a stack trace read horizontally is a
   stack trace nobody reads. */
.cmd-out {
  margin: .35rem 0 0;
  padding: .55rem .8rem;
  max-height: 14rem;
  overflow: auto;
  border: 1px solid var(--term-edge);
  border-radius: var(--radius);
  background: var(--term-bg);
  color: var(--term-ink);
  font-family: var(--font-mono);
  font-size: .78rem;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Keys installed on a machine. Fixed at build time, so this is a record of
   what is actually there rather than a list of what could be. */
.keylist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .45rem; }
.keylist li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem .8rem;
  padding: .4rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
.keylist li:last-child { border-bottom: 0; }
.keylist-name { font-weight: 500; }
.keylist-meta { font-size: .72rem; color: var(--ink-muted); }
/* Fingerprints are long and must not push the page sideways on a phone. */
.keylist-fp {
  font-size: .72rem;
  color: var(--ink-muted);
  overflow-wrap: anywhere;
  min-width: 0;
}
.field-label {
  font-size: .7rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  font-weight: 700;
  color: var(--ink-muted);
  margin-bottom: .5rem;
}

/* ── Custom machine size ───────────────────────────────────────────────────
   The preset cards remain the obvious path; these controls only appear once
   "Custom size" is chosen, so the common case never has to look at them. */
.size-custom .size-body { border-style: dashed; }

.custom-panel {
  margin-top: .75rem;
  padding: 1rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--surface-sunken, var(--card));
  display: grid;
  gap: 1.1rem;
}
.custom-panel[hidden] { display: none; }

.slider {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .35rem .75rem;
  align-items: baseline;
}
.slider > label { font-size: .82rem; font-weight: 600; }
.slider-value { display: inline-flex; align-items: baseline; gap: .3rem; justify-self: end; }
.slider-value .unit { font-size: .74rem; }
/* Typed entry, because a 2 TiB range on a slider cannot be aimed. Sized for a
   thumb, and wide enough for four digits plus a decimal. */
.slider-box {
  width: 6.5rem;
  padding: .3rem .45rem;
  font-size: .95rem;
  text-align: right;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
}
.slider-box:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The control spans both columns so the thumb has the full width to travel --
   a slider squeezed into half a row is unusable with a thumb on a phone. */
.slider > input[type="range"] { grid-column: 1 / -1; width: 100%; margin: 0; }
.slider-ends { grid-column: 1 / -1; font-size: .72rem; }

/* Generous hit area: the default thumb is around 12px, which is well under
   any reasonable touch target. */
.slider input[type="range"] { height: 1.75rem; accent-color: var(--accent); }

.custom-summary {
  display: flex;
  flex-wrap: wrap;
  gap: .35rem .75rem;
  align-items: baseline;
  padding-top: .3rem;
  border-top: 1px solid var(--rule-soft, var(--rule));
}
.cs-shape { font-size: .9rem; font-weight: 500; }
.cs-over  { font-size: .78rem; color: var(--bad); }
.cs-ok    { font-size: .78rem; }

@media (max-width: 30rem) {
  .slider { grid-template-columns: 1fr auto; }
  .custom-panel { padding: .8rem; }
}


/* ── Installer media ─────────────────────────────────────────────────────── */
/*
 * An upload is the one thing in this portal that takes minutes and can fail
 * halfway. A determinate bar is worth its bytes here in a way it is not
 * anywhere else: the number it shows is the number the server has actually
 * acknowledged, so it never runs ahead of reality and then jumps back.
 */
.upload-bar {
  height: 5px;
  border-radius: 3px;
  background: var(--sunken);
  border: 1px solid var(--rule);
  overflow: hidden;
  margin-bottom: .35rem;
}
.upload-bar span {
  display: block;
  height: 100%;
  width: 0;
  background: var(--accent);
  transition: width .25s ease-out;
}

/* Work in progress. Distinct from good and bad because "still going" is not a
 * verdict, and colouring it as one makes a normal download look like a fault. */
.tag.warn { color: var(--accent); border-color: var(--accent-wash); }

/*
 * The framebuffer console.
 *
 * noVNC draws into a canvas it owns and sizes itself; this only has to give it
 * a box with a known shape to scale against, and a dark ground so a machine
 * that has not drawn anything yet looks like a screen rather than a hole in the
 * layout.
 */
.console-frame { position: relative; }
.screen-canvas {
  /* Absolute, not height:100%.
   *
   * noVNC builds a div of width/height 100% inside whatever element it is given
   * and puts the canvas in that. Handing it a box whose own height is `100%` of
   * a flex item resolves to auto, which resolves to zero -- so the canvas came
   * out 0x0 and the console showed a perfectly black rectangle that was not the
   * machine's screen at all, but this element's own background. Exactly the bug
   * the whole transport split exists to avoid, arrived at from CSS.
   *
   * Absolute positioning against the frame's padding box gives it a definite
   * height without depending on how the flex column above resolves. */
  position: absolute;
  inset: 0;
  background: #000;
  border-radius: 3px;
  overflow: hidden;
}
.screen-canvas canvas { display: block; }

/* ═══════════════════════════════════════════════════════════════════════════
   VPN page additions.

   Added in a block at the end so the brand and layout work above stays a
   single readable file. Two rules only; everything else on that page reuses
   .choices / .checklist / .list / .tag, which already say what is needed.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The disclosure for the per-device profile options.
 *
 * Hidden by default because most people want a name and a download, and every
 * field inside it has a working answer already. `[hidden]` needs the explicit
 * rule: the element is a flex/grid child in some browsers' UA sheet and the
 * attribute's own `display:none` loses to that. */
.vpn-advanced[hidden] { display: none; }
.vpn-advanced {
  margin: 0 0 1.1rem;
  padding: .9rem 1rem 0;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--sunken);
}

/* The tunnel-mode / preshared-key badges in the device list. Wraps rather than
   scrolls: two short tags on a narrow screen should stack, not clip. */
.vpn-badges { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .2rem; }

/* ═══════════════════════════════════════════════════════════════════════════
   Sign-in alternatives (magic link, and whatever else registers itself).

   Appended as a block at the end, like the VPN additions above, so the brand
   and layout work earlier in the file stays one readable piece.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The container entry.js builds around whatever the alternatives return. */
.entry-alts { margin-top: 1.1rem; }
.entry-alt + .entry-alt { margin-top: .8rem; }

/* `[hidden]` needs saying explicitly here: an alternative that starts hidden
   while it works out whether it is usable is a flex child, and the UA sheet's
   display on that wins over the attribute's own. */
.entry-alt[hidden] { display: none; }

/* "or", ruled across the card. Two lines and a word rather than a plain
   divider, because the thing below it is a genuine alternative to the form
   above, not a continuation of it. */
.entry-or {
  display: flex;
  align-items: center;
  gap: .7rem;
  margin: 0 0 .8rem;
  color: var(--ink-faint);
  font-size: .78rem;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.entry-or::before,
.entry-or::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule);
}

/* Redeeming a sign-in link takes the card over.
 *
 * The password form is still in the DOM -- entry.js built it and owns it --
 * so this hides it rather than restructuring the card from the outside. If the
 * link turns out to be dead, the takeover class comes off and the form is
 * there, which is what somebody in that position actually needs. */
.entry-card:has(.magic-takeover) > form,
.entry-card:has(.magic-takeover) > .plane-hint,
.entry-card:has(.magic-takeover) > .entry-foot { display: none; }
.entry-card:has(.magic-takeover) .magic-takeover .entry-foot { display: block; }


/* ═══════════════════════════════════════════════════════════════════════════
   Security keys (WebAuthn)

   Appended as its own block at the end, like the sign-in alternatives above.
   Nothing earlier in this file is touched.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The honest explanation shown where a security key cannot work: reached by IP
   address, or over plain HTTP. It is deliberately NOT styled as a disabled
   button. A greyed-out control invites people to hunt for the setting that
   would enable it, and there is no such setting -- the constraint is in the
   WebAuthn specification. So this reads as a note, and the link in it is the
   only thing there is to click. */
.passkey-unavailable p { margin: 0; line-height: 1.55; }
.passkey-unavailable a { display: inline-block; margin-top: .35rem; }

/* The "type my email and try again" form, which only appears after a
   usernameless attempt has found nothing. Indented so it reads as a
   continuation of the failure above it rather than a new offer. */
.passkey-fallback {
  margin-top: .8rem;
  padding-left: .8rem;
  border-left: 2px solid var(--rule);
}
.passkey-fallback .field { margin-bottom: .6rem; }

/* Naming a key while the browser's own prompt is up. The prompt is modal and
   sits over the page, so this form has to still make sense when it reappears
   behind it. */
.passkey-add { max-width: 26rem; }

/* Badges on a key row -- "signs in alone", "verifies you", "synced" -- wrap
   under the name on a narrow screen instead of stretching the column. */
.list td .row { flex-wrap: wrap; gap: .35rem; }

/* ═══════════════════════════════════════════════════════════════════════════
   OPERATIONS PLANE — appended block, added with the single-dashboard merge.
   Nothing above this line is touched.

   ── The rule this reinterprets ─────────────────────────────────────────────

   The header of this file says the portal and the console "deliberately share
   no accent colour" so nobody is ever momentarily unsure which one they are
   looking at. That rule was written when they were two applications on two
   ports. They are now one application, and read literally the rule would now
   demand that half of one page disagree with the other half about what colour
   things are, which is not what it was protecting.

   What it was protecting is this: an operator must never mistake a destructive
   action on someone else's estate for an ordinary action on their own account.
   That danger did not go away when the two planes merged -- it got worse, because
   the two are now one click apart instead of one login apart.

   So the rule is kept where it does work and dropped where it does not:

     * GROUND, TYPE, SPACING, COMPONENTS are the portal's. An Operations screen
       is a section of this product, not a second product in a frame. Reusing
       .card, .page-head, .btn and the rest is what makes that true rather than
       merely claimed.

     * ACCENT is not. Everything that marks operator authority -- the nav group,
       the section markers, primary and destructive buttons, status pills, the
       commitment gauges -- speaks in amber, never in the customer navy. Amber
       is the console's own colour for COMMITTED, i.e. "a promise the platform
       has made", which is exactly what an operator manipulates. The two accents
       never appear on the same control, so no button is ambiguous about which
       authority is being exercised.

   The practical effect: a customer screen and an operations screen are instantly
   distinguishable at arm's length, and they are still obviously the same app.

   ── Why this is a stylesheet and not an import ─────────────────────────────

   console.css cannot be loaded into this document. The two stylesheets collide
   on 37 class names (.btn .rail .shell .main .row .panel .field .empty .pill …)
   and 8 custom properties (--ink --rule --radius --rail …); whichever loaded
   second would repaint the other's application. CSS has no way to scope a whole
   imported stylesheet to a subtree without a build step, and there is no build
   step here on purpose.

   So the console's VIEWS are reused verbatim -- that is the expensive, tested
   part, and there is exactly one copy of each -- and the class contract those
   views emit is implemented here instead, scoped to .ops-plane and expressed in
   the portal's tokens. console.css is untouched and still dresses the standalone
   console at /admin/.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The operator accent. One value per ground, defined in all three theme states
   for the same reason every other token in this file is: a colour whose only
   definition sits inside a media query is a colour that is missing whenever the
   viewer has made an explicit choice. */
:root { --ops: #8A5A11; --ops-wash: #FBF3E3; --ops-ink: #FFFFFF; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --ops: #E0A64A; --ops-wash: #2A2114; --ops-ink: #17130A; }
}
:root[data-theme="dark"] { --ops: #E0A64A; --ops-wash: #2A2114; --ops-ink: #17130A; }

/* ── The rail ───────────────────────────────────────────────────────────── */

/* The Operations group heading. The rule beneath it is the ★ from the sketch:
   a standing marker that everything below this point acts on the platform
   rather than on your own organisation. */
.rail-nav .nav-group.ops {
  color: var(--ops);
  border-top: 1px solid var(--rule);
  margin-top: .55rem;
  padding-top: .8rem;
}
.rail-nav a.ops[aria-current="page"] {
  /* The customer sections light navy; these light amber. Same shape, different
     authority, and the difference is visible without reading the label. */
  border-left-color: var(--ops);
  color: var(--ops);
  background: var(--ops-wash);
}
.rail-nav a.ops:hover { color: var(--ops); }

/* ── The plane ──────────────────────────────────────────────────────────── */

/* Applied to .main-inner itself, never to a wrapper inside it: the console's
   mutating views reload in place via `.main > div`.replaceChildren(), which
   would discard a wrapper and take this styling with it. */
.ops-plane { --accent: var(--ops); --accent-ink: var(--ops-ink); --accent-wash: var(--ops-wash); }

/* Rebinding --accent above is what carries the amber through every component
   the portal already provides -- .btn.primary, .share i, .tag.accent, focus
   rings -- without restating any of them. The customer navy is simply not in
   scope inside an Operations screen. */

/* A thin standing rule down the left of the whole plane. Present on every
   Operations screen and on no customer screen, so "which half of the product
   am I in" is answerable from peripheral vision. */
.ops-plane > .page-head {
  border-left: 2px solid var(--ops);
  padding-left: .9rem;
  margin-left: -.9rem;
}
.ops-plane > .page-head h1::after {
  content: "Operations";
  display: inline-block;
  margin-left: .6rem;
  padding: .1rem .4rem;
  border: 1px solid var(--ops);
  border-radius: 2px;
  color: var(--ops);
  font-family: var(--font-ui);
  font-size: .62rem;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  vertical-align: .35em;
}

/* Vertical rhythm for the console's page body, matching .main-inner's own. */
.ops-content > * + * { margin-top: 1.4rem; }

/* ── Panels ─────────────────────────────────────────────────────────────── */

/* The console's .panel is the portal's .card with a header strip. */
.ops-plane .panel {
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  overflow: hidden;
}
.ops-plane .panel + .panel { margin-top: 1rem; }
.ops-plane .panel-head {
  display: flex;
  align-items: center;
  gap: .8rem;
  padding: .75rem 1.05rem;
  border-bottom: 1px solid var(--rule-soft);
  background: var(--sunken);
}
.ops-plane .panel-head h2 {
  margin: 0;
  font-size: .8rem;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.ops-plane .panel-head .right { margin-left: auto; }
.ops-plane .panel-body { padding: 1.05rem; }
.ops-plane .panel-body.flush { padding: 0; }

/* The console marks instrument panels `calibrated`; there it draws a hairline
   gradient across the top. Here it is the amber edge, which is the same idea
   said in this room's vocabulary: this panel reports a promise. */
.ops-plane .panel.calibrated { border-top: 2px solid var(--ops); }

.ops-plane .grid { display: grid; gap: 1rem; }
.ops-plane .grid.two { grid-template-columns: repeat(auto-fit, minmax(21rem, 1fr)); }

/* ── Tables ─────────────────────────────────────────────────────────────── */

/* The console styles bare `table`; the portal only ever styles .ledger and
   .list, so the bare element needs a home inside the plane. */
.ops-plane table { width: 100%; border-collapse: collapse; font-size: .86rem; }
.ops-plane thead th {
  padding: .55rem 1.05rem;
  border-bottom: 1px solid var(--rule);
  color: var(--ink-faint);
  font-family: var(--font-ui);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .05em;
  text-align: left;
  text-transform: uppercase;
  white-space: nowrap;
}
.ops-plane tbody td {
  padding: .6rem 1.05rem;
  border-bottom: 1px solid var(--rule-soft);
  vertical-align: top;
}
.ops-plane tbody tr:last-child td { border-bottom: 0; }
.ops-plane th.num, .ops-plane td.num, .ops-plane .num { text-align: right; }
.ops-plane .table-scroll { overflow-x: auto; }
.ops-plane table.kv { font-size: .84rem; }
.ops-plane table.kv tbody th {
  padding: .3rem 1.05rem .3rem 0;
  border-bottom: 0;
  color: var(--ink-faint);
  font-weight: 500;
  text-align: left;
  text-transform: none;
  letter-spacing: 0;
  vertical-align: top;
  white-space: nowrap;
}
.ops-plane table.kv tbody td { border-bottom: 0; padding: .3rem 0; }

/* A row you can open. Given a visible affordance rather than only a cursor:
   an operator clicking into a customer's organisation should have been able to
   see beforehand that the row was a door. */
.ops-plane tbody tr.clickable, .ledger tr.clickable { cursor: pointer; }
.ops-plane tbody tr.clickable:hover, .ledger tr.clickable:hover { background: var(--sunken); }
.ops-plane tbody tr.clickable:focus-visible, .ledger tr.clickable:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* ── Status pills ───────────────────────────────────────────────────────── */

.ops-plane .pill {
  display: inline-flex;
  align-items: center;
  gap: .32rem;
  padding: .1rem .45rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  color: var(--ink-muted);
  font-family: var(--font-ui);
  font-size: .7rem;
  font-weight: 500;
  letter-spacing: .02em;
  white-space: nowrap;
}
.ops-plane .pill::before {
  content: "";
  width: 5px; height: 5px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.ops-plane .pill.ok      { color: var(--good);    border-color: color-mix(in srgb, var(--good) 40%, transparent); }
.ops-plane .pill.warn    { color: var(--ops);     border-color: color-mix(in srgb, var(--ops) 45%, transparent); }
.ops-plane .pill.bad     { color: var(--bad);     border-color: color-mix(in srgb, var(--bad) 45%, transparent); }
.ops-plane .pill.idle    { color: var(--ink-faint); }
.ops-plane .pill.running { color: var(--brand-teal); border-color: color-mix(in srgb, var(--brand-teal) 40%, transparent); }
.ops-plane .pill.running::before { animation: ops-pulse 1.4s ease-in-out infinite; }
@keyframes ops-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
@media (prefers-reduced-motion: reduce) {
  .ops-plane .pill.running::before { animation: none; }
}

/* ── Notices ────────────────────────────────────────────────────────────── */

/* The console's .notice is the portal's .note under a different name. Same
   left-edge form, so the two read identically when an operator moves between
   a customer screen and an operations one. */
.ops-plane .notice {
  padding: .8rem 1rem;
  border: 1px solid var(--rule);
  border-left: 3px solid var(--ink-faint);
  border-radius: var(--radius);
  background: var(--card);
}
.ops-plane .notice h3 { margin: 0 0 .25rem; font-size: .9rem; font-weight: 600; }
.ops-plane .notice p  { margin: 0; color: var(--ink-muted); }
.ops-plane .notice + .notice { margin-top: .7rem; }
.ops-plane .notice.ok   { border-left-color: var(--good); }
.ops-plane .notice.warn { border-left-color: var(--ops); }
.ops-plane .notice.bad  { border-left-color: var(--bad); background: var(--bad-wash); }

/* ── Forms ──────────────────────────────────────────────────────────────── */

/* .field, .btn, .btn-row and .empty already exist in this file and are
   inherited as they are; only what the portal has no equivalent for is added. */
.ops-plane .check { display: flex; gap: .55rem; align-items: flex-start; }
.ops-plane .check input { margin-top: .25rem; width: auto; }
.ops-plane .check label {
  font-size: .86rem;
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
}

/* ── The commitment gauge ───────────────────────────────────────────────── */

/* The console's central instrument: promised against measured, on one scale.
   The semantic is preserved exactly, because it is the semantic the whole
   architecture turns on -- amber is what we PROMISED, teal is what the
   hardware REPORTS -- and both colours exist in this palette already. */
.ops-plane .gauge { display: block; }
.ops-plane .gauge + .gauge { margin-top: .9rem; }
.ops-plane .gauge-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: .35rem;
}
.ops-plane .gauge-label {
  color: var(--ink-muted);
  font-size: .7rem;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
}
.ops-plane .gauge-figure { font-size: .78rem; color: var(--ink-muted); }
.ops-plane .gauge-figure b { color: var(--ops); font-weight: 600; }
.ops-plane .gauge-figure i { color: var(--brand-teal); font-style: normal; }
.ops-plane .gauge-track {
  position: relative;
  height: 9px;
  border-radius: 2px;
  background: var(--sunken);
  border: 1px solid var(--rule);
  overflow: hidden;
}
.ops-plane .gauge-fill {
  position: absolute; inset: 0 auto 0 0;
  background: var(--ops);
}
.ops-plane .gauge-over {
  position: absolute; inset: 0 auto 0 0;
  background: var(--bad);
}
.ops-plane .gauge-ceiling {
  position: absolute; top: -2px; bottom: -2px;
  width: 1px;
  background: var(--ink-faint);
}
/* Observed usage is a needle laid over the promise, never a second bar. Two
   bars invite the reader to add them together, which is meaningless. */
.ops-plane .gauge-needle {
  position: absolute; top: -3px; bottom: -3px;
  width: 2px;
  background: var(--brand-teal);
}
.ops-plane .gauge-scale {
  display: flex;
  justify-content: space-between;
  margin-top: .3rem;
  color: var(--ink-faint);
  font-size: .68rem;
}
.ops-plane .legend {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin-top: .7rem;
  color: var(--ink-faint);
  font-family: var(--font-ui);
  font-size: .72rem;
}
.ops-plane .legend span { display: inline-flex; align-items: center; gap: .35rem; }
.ops-plane .legend .sw { width: 11px; height: 3px; border-radius: 1px; }
.ops-plane .legend .sw.committed { background: var(--ops); }
.ops-plane .legend .sw.observed  { background: var(--brand-teal); }

/* ── Job logs ───────────────────────────────────────────────────────────── */

.ops-plane .log {
  max-height: 22rem;
  overflow-y: auto;
  padding: .7rem 1rem;
  border-radius: var(--radius);
  background: var(--term-bg);
  color: var(--term-ink);
  font-family: var(--font-mono);
  font-size: .74rem;
  line-height: 1.65;
}
.ops-plane .log-line { display: flex; gap: .7rem; }
.ops-plane .log-line time { flex: none; opacity: .55; }
.ops-plane .log-line.warn  { color: var(--caution); }
.ops-plane .log-line.error { color: var(--bad); }

/* ── Utilities the console has and the portal does not ──────────────────── */

.ops-plane .nowrap { white-space: nowrap; }
.ops-plane .spread { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.ops-plane .small  { font-size: .8rem; }
.ops-plane .hint   { font-size: .8rem; color: var(--ink-faint); margin-top: .3rem; line-height: 1.45; }
.ops-plane .err    { font-size: .8rem; color: var(--bad); margin-top: .3rem; }
.ops-plane .count {
  margin-left: auto;
  color: var(--ink-faint);
  font-size: .72rem;
}

/* ── The platform roll-up ───────────────────────────────────────────────── */

/* NOT scoped to .ops-plane, deliberately. The roll-up is the staff landing
   page and the owner asked for it in the customer dashboard's visual language:
   it is built from .card, .ledger, .share and .headroom exactly as the customer
   overview is, and it carries no Operations marker. Only the screens that ACT
   on the platform wear the amber; the one that merely reports on it does not. */

/* A sortable column header. A button, not a styled span: it is operated by
   keyboard and announced as a control. */
.sortcol {
  display: inline-flex;
  align-items: center;
  gap: .25rem;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
}
.sortcol:hover { color: var(--ink); }
.sortcol.on { color: var(--accent); }
.sortcol .caret { font-size: .7em; }
.ledger th:has(.sortcol.on) { color: var(--accent); }

/* The by-organisation table earns a little more room per row than the customer
   ledger does: it is scanned down a column rather than read across. */
.ledger.orgs td { padding-top: .55rem; padding-bottom: .55rem; }
.ledger.orgs .sub { font-size: .74rem; }

/* The one-line explanation under the sign-in button. Smaller and centred, so
   the button stays the thing the eye lands on. */
.entry-alt .hint { font-size: .78rem; text-align: center; margin-top: .45rem; }

/* ── Step-up prompt ──────────────────────────────────────────────────────────
   Shared by the Operations sections here and by the standalone console (the
   same stepup.js builds it in both), so the rules are duplicated in console.css
   rather than pulled into a third stylesheet neither page loads. They are short
   enough that a shared file would cost more than it saved.

   <dialog> supplies the top layer, the backdrop, focus containment and Escape.
   What is left is the card. Ops colouring, because what it is guarding is
   always a platform action, never a customer one -- an operator should be able
   to tell at a glance which of the two they are being asked about. */
dialog.stepup {
  border: 1px solid var(--rule);
  border-top: 2px solid var(--ops);
  border-radius: 10px;
  background: var(--card);
  color: var(--ink);
  padding: 0;
  max-width: min(26rem, calc(100vw - 2rem));
  box-shadow: 0 18px 44px rgb(0 0 0 / .28);
}
dialog.stepup::backdrop { background: rgb(0 0 0 / .45); }
.stepup-form { padding: 1.25rem; }
.stepup-form h2 { margin: 0 0 .4rem; font-size: 1.05rem; color: var(--ops); }
.stepup-form p { margin: 0 0 .9rem; font-size: .87rem; color: var(--ink-muted); }
.stepup-code {
  width: 100%;
  box-sizing: border-box;
  font: 600 1.5rem/1 ui-monospace, SFMono-Regular, Menlo, monospace;
  /* Wide tracking so six digits read as six digits and a transposed pair is
     visible while typing rather than after the code is refused. */
  letter-spacing: .38em;
  text-align: center;
  padding: .7rem .5rem;
  border: 1px solid var(--rule);
  border-radius: 8px;
  background: var(--sunken);
  color: var(--ink);
}
.stepup-code:focus { outline: 2px solid var(--ops); outline-offset: 1px; }
.stepup-actions { display: flex; gap: .5rem; justify-content: flex-end; margin-top: 1rem; }

/* ═══════════════════════════════════════════════════════════════════════════
   Console: full screen.

   Appended as a block at the end, like the VPN and sign-in additions above, so
   the brand and layout work earlier in the file stays one readable piece.

   TWO PRESENTATIONS OF ONE STATE, because there are two ways of getting there
   and a browser that refuses the first must not get a worse-looking second:

     :fullscreen        the real Fullscreen API. The browser has taken the
                        element out of the page, so the backdrop is not on
                        screen at all and the card has to paint its own ground.
     .maximised         the in-page fallback for a browser that refused --
                        iOS Safari, an embedded context without
                        allow="fullscreen". Same rules, applied through the
                        backdrop that is still there.

   Both do the same three things: take the width and height limits off the
   card, drop the border and the radius that only make sense against a
   backdrop, and let .console-frame's `flex: 1` claim everything the header
   and the status line do not.
   ═══════════════════════════════════════════════════════════════════════════ */

.modal-backdrop.maximised {
  padding: 0;
  /* Opaque rather than the usual translucent wash: at full size there is
     nothing behind this worth seeing through to, and a dark page bleeding
     through a terminal reads as a rendering fault. */
  background: var(--term-bg);
}
.modal-backdrop.maximised > .modal,
.modal.console-modal:fullscreen {
  max-width: none;
  max-height: none;
  width: 100%;
  height: 100%;
  border: 0;
  border-radius: 0;
}
/* The element is out of the page in the native case, so it cannot inherit the
   card colour from anything -- it has to say it. */
.modal.console-modal:fullscreen {
  background: var(--card);
  color: var(--ink);
}
/* What the browser paints behind a fullscreen element. Left to its default it
   is white in some engines, which flashes on the way in and shows as a hairline
   around a letterboxed framebuffer. */
.modal.console-modal:fullscreen::backdrop { background: var(--term-bg); }

/* ── The way out has to stay visible ────────────────────────────────────────
   A console swallows keystrokes by design, so an exit that exists only as a
   key is an exit somebody can lose -- and losing it on a screen with no browser
   chrome is how people end up force-quitting the tab. The whole card goes full
   screen, header included, and the control that got you here is marked while
   it is the control that gets you out. */
.modal-backdrop.fs .modal-head { flex: 0 0 auto; }
.modal-backdrop.fs .modal-head button[aria-pressed="true"] {
  border-color: var(--accent);
  color: var(--accent);
}

/* The frame is the only thing that should grow. Without an explicit basis a
   flex child sizes to its content, and xterm's content is whatever grid it
   currently has -- which is exactly the "large window, small terminal island"
   this whole block exists to prevent. */
.modal-backdrop.maximised .console-frame,
.modal.console-modal:fullscreen .console-frame {
  flex: 1 1 auto;
  min-height: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   Accounts on a machine.

   Two rules only. Everything else on the panel reuses .card / .ledger / .tag /
   .checklist / .field, which already say what is needed — the same restraint
   the VPN block above exercises, and for the same reason: a panel with its own
   private visual language is a panel that stops matching the rest of the portal
   the first time the tokens change.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The explanatory line above a form or a list. `.hint` is otherwise defined
   only inside .field, so a card-level one had no rules at all and rendered as
   body copy — the same size as the thing it was explaining. */
.accounts-panel > .hint {
  font-size: .8rem;
  color: var(--ink-faint);
  line-height: 1.5;
  margin: .1rem 0 .5rem;
}

/* An account is read down the row -- name, what state it is in, what it can do
   -- so it earns more vertical room than the dense ledgers do, and its actions
   sit under the badges rather than beside them. */
.ledger.accounts td { padding-top: .6rem; padding-bottom: .6rem; vertical-align: top; }
.ledger.accounts td .row { margin-top: .45rem; flex-wrap: wrap; gap: .35rem; }

/* ═══════════════════════════════════════════════════════════════════════════
   TURNKEY STACKS
   Appended block — nothing above this line is modified.

   Everything here reuses the existing tokens rather than introducing colours:
   a stack panel that looked like a different product would read as a bolt-on,
   and it is meant to read as what the machine IS.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The generated parameter fields sit inside the stack choice, indented under
   it, so it is visually obvious they belong to the option above and not to the
   form in general. */
.stack-params { margin-top: .9rem; padding-left: .9rem; border-left: 2px solid var(--rule); }
.stack-params .field:last-child { margin-bottom: 0; }
.stack-summary { margin: .6rem 0 0; }
.stack-choices { margin-bottom: 0; }

/* ── The setup checklist ──────────────────────────────────────────────────
   The honest part of the panel. A failed step has to be findable at a glance
   in a list where most entries passed, so it carries weight and a colour while
   the passes stay quiet. */
.stack-steps { list-style: none; margin: 1rem 0 0; padding: 0; }
.stack-step {
  display: grid;
  grid-template-columns: 1.25rem 1fr;
  gap: .1rem .5rem;
  padding: .4rem 0;
  border-top: 1px solid var(--rule-soft);
  align-items: baseline;
}
.stack-step:first-child { border-top: 0; }
.stack-step .mark { font-family: var(--font-mono); font-size: .95rem; text-align: center; }
.stack-step .what { color: var(--ink); }
.stack-step .why {
  grid-column: 2;
  color: var(--ink-muted);
  font-size: .85rem;
  margin-top: .15rem;
}
.stack-step.is-done .mark    { color: var(--good); }
.stack-step.is-failed .mark  { color: var(--bad); }
.stack-step.is-failed .what  { color: var(--bad); font-weight: 500; }
/* A step that has not reported yet is not a failure and must not look like
   one. Faint, so the eye skips it and lands on whatever did report. */
.stack-step.is-waiting .mark,
.stack-step.is-waiting .what { color: var(--ink-faint); }

/* ── The published address ────────────────────────────────────────────────── */
.stack-address {
  margin-top: 1rem;
  padding: .85rem 1rem;
  background: var(--sunken);
  border-radius: var(--radius);
}
.stack-address-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  flex-wrap: wrap;
  margin-bottom: .3rem;
}
.stack-address-head a { font-family: var(--font-mono); font-size: .95rem; }
.stack-dns { margin-top: .75rem; }
.stack-dns-table { width: 100%; border-collapse: collapse; margin-top: .4rem; }
.stack-dns-table th {
  text-align: left;
  color: var(--ink-muted);
  font-weight: 400;
  font-size: .8rem;
  padding: .25rem .75rem .25rem 0;
  white-space: nowrap;
  vertical-align: top;
}
/* The TXT value is long and gets copied by hand into somebody's DNS panel, so
   it wraps on any character rather than overflowing its container. */
.stack-dns-table td {
  font-family: var(--font-mono);
  font-size: .8rem;
  padding: .25rem 0;
  word-break: break-all;
}

.stack-db { margin-top: .9rem; }

/* ── The password, shown once ─────────────────────────────────────────────
   Deliberately loud. It is the only time this value will ever be on screen,
   and a customer who scrolls past it has lost it permanently. */
.stack-credential {
  margin-top: 1rem;
  border-left: 3px solid var(--caution);
  background: var(--card);
}
.stack-credential h3 { margin: 0 0 .3rem; font-size: 1rem; }
/* The fact tables inside a stack panel are the platform's own `ledger`; they
   only need to lose the outer margin a standalone one carries. */
.stack-credential .ledger, section .stack-address + .ledger { margin-top: .5rem; }
.stack-credential .hint { margin: 0 0 .75rem; }
.stack-credential .secret {
  font-family: var(--font-mono);
  font-size: .95rem;
  background: var(--sunken);
  padding: .2rem .45rem;
  border-radius: var(--radius);
  user-select: all;
  word-break: break-all;
}

/* ── Taking your files elsewhere ─────────────────────────────────────────────
 *
 * The export panel on a machine's page and on an installer file's page. It
 * borrows the portal's existing vocabulary -- .card, .tag, .btn-row, .table-wrap
 * -- and adds only what has no equivalent: a list that explains what a download
 * contains, a header line pairing a state badge with an expiry, and a digest
 * that has to be readable enough to compare by eye without dominating the row
 * it sits in.
 *
 * Appended at the end of the file on purpose; several people are editing this
 * stylesheet, and a block that only ever grows at the bottom is one that never
 * conflicts with theirs.
 */

.export-intro > p:first-child {
  margin-top: 0;
  max-width: 62ch;
}

/* What is in a download, and what deliberately is not. Not a bullet list:
 * these are three answers to three questions, and disc markers make them look
 * like steps to follow. */
ul.plain {
  list-style: none;
  margin: 1rem 0 1.25rem;
  padding: 0;
  display: grid;
  gap: .55rem;
  max-width: 68ch;
}

ul.plain li {
  color: var(--ink-soft, inherit);
  line-height: 1.5;
  padding-left: .9rem;
  border-left: 2px solid var(--line, currentColor);
}

ul.plain li strong { color: var(--ink, inherit); }

.export-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: .75rem;
}

.export-card + .export-card { margin-top: .9rem; }

/* A SHA-256 truncated to sixteen characters. Long enough to compare against a
 * manifest by eye, short enough not to push the Download button off a narrow
 * screen. The full value is in the title attribute and in the bundle. */
.export-card .digest {
  font-family: var(--font-mono);
  font-size: .72rem;
  color: var(--ink-faint, inherit);
  white-space: nowrap;
}

/* Delete sits away from the downloads it would destroy. */
.btn-row.end { justify-content: flex-end; margin-top: .9rem; }

@media (max-width: 640px) {
  /* The digest is the first thing worth losing: it is a convenience, and the
   * authoritative copy travels inside the bundle. */
  .export-card .digest { display: none; }
  .export-head { gap: .35rem; }
}

/* ════════════════════════════════════════════════════════════════════════════
   THE NEW-MACHINE WIZARD
   Everything below belongs to public/assets/js/portal/views/machines/ and
   nothing above it does. Appended as one block so it can be read, moved or
   removed as one thing.

   Every colour here is a token. The design this was built from carries the
   resolved light-theme hex values, and pasting those would have produced a
   wizard that stayed cream while the rest of the portal went dark -- which is
   the one visual bug a modal is guaranteed to make obvious, because it sits on
   top of everything else.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── The modal itself ────────────────────────────────────────────────────────
   <dialog> arrives with a user-agent border, padding and a max-width that all
   have to go before it can be a panel. What is kept is everything the element
   does that is not visual: the top layer, the inert page behind it, focus
   containment and Escape. */
dialog.wz-dialog {
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: var(--paper);
  color: var(--ink);
  max-width: min(96vw, 880px);
  width: 100%;
  max-height: 92vh;
  overflow: hidden;
  box-shadow: 0 18px 48px rgb(0 0 0 / .34);
  /* The dialog is the flex column; the pane inside it is what scrolls, so the
     header and the footer stay put while a long step moves under them. */
  display: flex;
  flex-direction: column;
}
dialog.wz-dialog::backdrop { background: rgb(10 12 16 / .55); }

/* Open on top of another dialog. The wizard is dimmed rather than hidden so
   the half-described machine is visibly still there -- the nested dialog is a
   detour, not a new destination, and it should not look like one. */
dialog.wz-dialog.wz-dimmed { filter: blur(1.4px); opacity: .55; }

dialog.wz-nested { max-width: min(94vw, 520px); background: var(--card); }
dialog.wz-nested::backdrop { background: rgb(10 12 16 / .42); }
dialog.wz-dialog.wz-small { max-width: min(94vw, 30rem); }

.wz-head {
  display: flex;
  align-items: center;
  gap: .65rem;
  padding: .95rem 1.2rem;
  border-bottom: 1px solid var(--rule);
  background: var(--card);
  flex: none;
}
.wz-title { margin: 0; font-size: 1rem; font-weight: 600; }
.wz-counter { margin-left: auto; font-size: .72rem; color: var(--ink-faint); }
.wz-close {
  border: 0;
  background: none;
  cursor: pointer;
  color: var(--ink-faint);
  font-size: 1.3rem;
  line-height: 1;
  padding: 0 .2rem;
  border-radius: var(--radius);
}
.wz-close:hover { color: var(--ink); }

.wz-body {
  display: grid;
  grid-template-columns: 11rem minmax(0, 1fr);
  min-height: 0;   /* lets the pane's own overflow work inside a flex column */
  flex: 1;
}

.wz-foot {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .85rem 1.2rem;
  border-top: 1px solid var(--rule);
  background: var(--card);
  flex: none;
}
.wz-foot-end { margin-left: auto; display: flex; align-items: center; gap: .6rem; }

/* ── The rail ────────────────────────────────────────────────────────────────
   The whole flow at once, not a progress bar. Every step that has been reached
   stays a real button, so changing your mind is a click rather than a walk
   backwards through the steps in between. */
.wz-nav {
  padding: 1rem .9rem;
  border-right: 1px solid var(--rule);
  background: var(--card);
  overflow-y: auto;
}
.wz-rail { list-style: none; margin: 0; padding: 0; }
.wz-step {
  position: relative;
  display: flex;
  align-items: center;
  gap: .6rem;
  width: 100%;
  padding: .32rem 0;
  border: 0;
  background: none;
  cursor: pointer;
  text-align: left;
  font-family: var(--font-ui);
  font-size: .85rem;
  color: var(--ink-faint);
  border-radius: var(--radius);
}
.wz-step[aria-disabled="true"] { cursor: default; }
.wz-step:hover:not([aria-disabled="true"]) .wz-step-label { color: var(--ink); }
.wz-step-label { font-weight: 400; }
.wz-step.current .wz-step-label { color: var(--ink); font-weight: 600; }
.wz-step.done .wz-step-label { color: var(--ink); font-weight: 500; }

.wz-bullet {
  width: 18px;
  height: 18px;
  flex: none;
  border-radius: 50%;
  border: 1px solid var(--rule);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: .62rem;
  line-height: 1;
  background: var(--card);
}
.wz-step.current .wz-bullet { border: 2px solid var(--accent); }
.wz-step.current .wz-bullet::after {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}
.wz-step.done .wz-bullet {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}
/* The connector, drawn from the bullet down to the next one. Not on the last
   item, which is what `li:last-child` is doing here rather than a modifier
   class the JS would have to remember to set. */
.wz-rail li:not(:last-child) .wz-step::after {
  content: "";
  position: absolute;
  left: 8px;
  top: 1.35rem;
  width: 1px;
  height: .95rem;
  background: var(--rule);
}
.wz-rail li:not(:last-child) .wz-step.done::after { background: var(--accent); }

.wz-rail-note {
  margin-top: 1rem;
  padding-top: .8rem;
  border-top: 1px solid var(--rule-soft);
  font-size: .72rem;
  color: var(--ink-faint);
  line-height: 1.45;
}

/* ── The pane ────────────────────────────────────────────────────────────── */
.wz-main { display: flex; flex-direction: column; min-height: 0; }
.wz-pane {
  padding: 1.4rem 1.5rem;
  overflow-y: auto;
  flex: 1;
  min-height: 22rem;
}
/* The pane takes focus on every step change so the heading is what a screen
   reader meets first. It is not an interactive element, so the focus ring that
   would draw around the whole panel is noise -- the step heading and the live
   region are what announce the move. */
.wz-pane:focus { outline: none; }
.wz-blocked { padding: 1.4rem 1.5rem; }
.wz-problem:not(:empty) { padding: 0 1.5rem 1rem; }

.wz-step-pane[hidden] { display: none; }
.wz-h { margin: 0 0 .2rem; font-size: 1.12rem; font-weight: 600; letter-spacing: -.01em; }
.wz-lede { margin: 0 0 1.3rem; color: var(--ink-muted); font-size: .88rem; }

/* Text a customer needs but cannot act on: what will happen, rather than a
   control. Deliberately not a .note -- a page of stacked notes reads as a page
   of warnings. */
.wz-fact {
  font-size: .82rem;
  color: var(--ink-muted);
  line-height: 1.5;
  background: var(--sunken);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: .7rem .85rem;
}

/* Announced but not drawn. */
.wz-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ── Choices, cards and sizes ────────────────────────────────────────────────
   Qualified with `label.` for the same reason label.choice above is: `.field
   label` sets display:block and would otherwise stack the radio above its own
   text. */
.wz-choices { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .3rem; }
.wz-choices > * { flex: 1 1 14rem; }
label.wz-choice {
  display: flex;
  gap: .55rem;
  align-items: flex-start;
  padding: .7rem .85rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  cursor: pointer;
  font-weight: 400;
  margin: 0;
}
label.wz-choice:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
label.wz-choice input { margin: .2rem 0 0; flex: none; accent-color: var(--accent); }
.wz-choice-name { display: block; font-weight: 600; font-size: .9rem; }
.wz-choice-sub { display: block; font-size: .79rem; color: var(--ink-muted); margin-top: .1rem; }

.wz-cards { display: grid; gap: .5rem; }
.wz-cards.two { grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); }
label.wz-card {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .6rem .7rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  cursor: pointer;
  font-weight: 500;
  font-size: .9rem;
  margin: 0;
}
label.wz-card:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
label.wz-card[hidden] { display: none; }
label.wz-card input { margin: 0; flex: none; accent-color: var(--accent); }
.wz-card-name { font-weight: 500; }
.wz-card-side { margin-left: auto; font-size: .75rem; color: var(--ink-faint); }

.wz-sizes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: .55rem;
  margin-bottom: 1rem;
}
label.wz-size {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas: "radio name" ".  spec" ".  price";
  gap: .1rem .5rem;
  align-items: center;
  padding: .8rem .85rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  cursor: pointer;
  font-weight: 400;
  margin: 0;
}
label.wz-size:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
label.wz-size input { grid-area: radio; margin: 0; accent-color: var(--accent); }
.wz-size-name  { grid-area: name; font-weight: 600; font-size: .92rem; }
.wz-size-spec  { grid-area: spec; font-size: .78rem; color: var(--ink-muted); margin-top: .3rem; }
.wz-size-price { grid-area: price; font-size: .72rem; color: var(--ink-faint); }

/* ── Custom size ─────────────────────────────────────────────────────────── */
.wz-custom {
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--card);
  padding: .95rem 1rem;
  margin-bottom: 1rem;
}
.wz-custom[hidden] { display: none; }
.wz-slider { display: grid; grid-template-columns: 1fr auto; gap: .3rem .8rem; margin-bottom: .9rem; }
.wz-slider label { font-size: .8rem; font-weight: 600; margin: 0; align-self: center; }
.wz-slider-value { display: flex; align-items: center; gap: .35rem; font-size: .8rem; }
.wz-slider input[type=range] { grid-column: 1 / -1; width: 100%; accent-color: var(--accent); }
.wz-slider-ends { grid-column: 1 / -1; font-size: .72rem; }
.wz-box { width: 6rem; text-align: right; padding: .3rem .4rem; font-size: .85rem; }
.wz-custom-summary { font-size: .82rem; color: var(--ink-muted); }

/* ── The allowance ───────────────────────────────────────────────────────── */
.wz-allowance {
  background: var(--sunken);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: .75rem .9rem;
}
.wz-allow-head {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: .4rem;
}
.wz-allow-row {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: .84rem;
  padding: .12rem 0;
}
.wz-allow-row > span:last-child { color: var(--ink-muted); text-align: right; }
.wz-allow-over { margin-top: .5rem; font-size: .82rem; color: var(--bad); }

/* ── Editable rows: data disks, and firewall rules ───────────────────────── */
.wz-rows { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
.wz-rows-head,
.wz-row {
  display: grid;
  grid-template-columns: 1.4fr .8fr 1fr auto;
  gap: .5rem;
  align-items: center;
}
.wz-rows-head.ports,
.wz-row.ports { grid-template-columns: .8fr .7fr 1.4fr auto; }
.wz-rows-head {
  font-size: .68rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: .4rem;
}
.wz-row input, .wz-row select { padding: .4rem .5rem; font-size: .85rem; }
.wz-row input[hidden] { display: none; }
.wz-source { display: flex; flex-direction: column; gap: .35rem; }

/* ── The storage budget ──────────────────────────────────────────────────── */
.wz-budget {
  background: var(--sunken);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: .7rem .9rem;
  margin: .8rem 0;
}
.wz-budget-line { font-size: .82rem; color: var(--ink-muted); }
.wz-budget-over { margin-top: .4rem; font-size: .82rem; color: var(--bad); }
.wz-meter {
  display: block;
  margin-top: .5rem;
  height: 3px;
  background: var(--rule);
  border-radius: 2px;
  overflow: hidden;
}
.wz-meter i { display: block; height: 100%; background: var(--accent); }

/* ── Networks ────────────────────────────────────────────────────────────── */
.wz-netlist { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
label.wz-net {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .6rem .8rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  cursor: pointer;
  font-weight: 400;
  margin: 0;
}
label.wz-net:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
label.wz-net input { margin: 0; flex: none; accent-color: var(--accent); }
.wz-net-name { font-weight: 500; font-size: .9rem; }
.wz-net-cidr { font-size: .78rem; color: var(--ink-muted); }
.wz-net-side { margin-left: auto; font-size: .78rem; color: var(--ink-faint); }
.wz-nested-body { padding: 1.2rem 1.15rem; }

/* An outlined accent button, for the two "make me one of these" actions that
   open something rather than submitting anything. */
.btn.accent { border-color: var(--accent); color: var(--accent); }
.btn.accent:hover:not(:disabled) { background: var(--accent-wash); }
.btn-row.start { justify-content: flex-start; margin-bottom: .9rem; }

/* ── Ports ───────────────────────────────────────────────────────────────── */
.wz-pills { display: flex; flex-wrap: wrap; gap: .4rem; }
.wz-pill {
  font-family: var(--font-ui);
  font-size: .8rem;
  font-weight: 600;
  padding: .35rem .7rem;
  border-radius: 999px;
  border: 1px solid var(--rule);
  background: var(--card);
  color: var(--ink-muted);
  cursor: pointer;
}
.wz-pill:hover { background: var(--sunken); }
.wz-pill.on { border-color: var(--accent); background: var(--accent-wash); color: var(--accent); }

/* What the platform allows whatever the list says. Marked with the caution
   rule rather than the accent: it is not a setting, it is a limit on the
   settings above it. */
.wz-always {
  background: var(--sunken);
  border: 1px solid var(--rule);
  border-left: 3px solid var(--caution);
  border-radius: var(--radius);
  padding: .75rem .9rem;
  margin-top: 1rem;
}
.wz-always-head { font-weight: 600; font-size: .85rem; }
.wz-always-body { font-size: .81rem; color: var(--ink-muted); margin-top: .25rem; line-height: 1.5; }

/* ── Keys and the console account ────────────────────────────────────────── */
.wz-checklist { display: flex; flex-direction: column; gap: .5rem; }
label.wz-check {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .55rem .75rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  cursor: pointer;
  font-weight: 400;
  margin: 0;
}
label.wz-check:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
label.wz-check input { margin: 0; flex: none; accent-color: var(--accent); }
label.wz-check strong { display: block; font-weight: 500; font-size: .89rem; }
.wz-fp { display: block; font-size: .7rem; }

.wz-inline {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  padding: .9rem 1rem;
  margin-bottom: 1rem;
}
.wz-inline[hidden] { display: none; }
.wz-inline .field:last-of-type { margin-bottom: 0; }

.wz-console {
  background: var(--sunken);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: .8rem .95rem;
  margin-top: 1rem;
}
.wz-console-head { font-weight: 600; font-size: .85rem; }
.wz-console-body { font-size: .81rem; color: var(--ink-muted); margin-top: .25rem; line-height: 1.5; }
.wz-two { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: .8rem; margin-top: .8rem; }
.wz-two .field { margin-bottom: 0; }

/* ── Review ──────────────────────────────────────────────────────────────── */
.wz-review { display: flex; flex-direction: column; gap: .65rem; }
.wz-review-card {
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: var(--card);
  padding: .8rem .95rem;
}
.wz-review-head { display: flex; align-items: baseline; gap: .6rem; margin-bottom: .4rem; }
.wz-review-title {
  font-size: .7rem;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-muted);
}
.wz-change {
  margin-left: auto;
  border: 0;
  background: none;
  padding: 0;
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: .8rem;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: .15em;
}
.wz-facts { width: 100%; border-collapse: collapse; }
.wz-facts td { padding: .22rem 0; font-size: .84rem; vertical-align: top; }
.wz-facts td:first-child { color: var(--ink-faint); width: 11rem; }

.wz-extra { margin-top: 1rem; }
.wz-extra > summary { cursor: pointer; font-size: .85rem; font-weight: 600; }
.wz-extra > .hint { font-size: .8rem; color: var(--ink-faint); margin: .4rem 0 .8rem; }

.wz-warn {
  background: var(--accent-wash);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  padding: .8rem .95rem;
  margin-top: 1rem;
}
.wz-warn[hidden] { display: none; }
.wz-warn-head { font-weight: 600; font-size: .87rem; }
.wz-warn-body { font-size: .81rem; color: var(--ink-muted); margin-top: .2rem; }

/* ── Narrow screens ──────────────────────────────────────────────────────────
   The rail stops being a column and becomes a scrolling row above the pane.
   It keeps every step reachable, which is the property that matters; what it
   gives up is the connector line, which is decoration. */
@media (max-width: 46rem) {
  dialog.wz-dialog { max-width: 100vw; max-height: 100vh; border-radius: 0; border: 0; }
  .wz-body { grid-template-columns: minmax(0, 1fr); }
  .wz-nav {
    border-right: 0;
    border-bottom: 1px solid var(--rule);
    padding: .6rem .8rem;
    overflow-x: auto;
  }
  .wz-rail { display: flex; gap: .9rem; }
  .wz-rail li:not(:last-child) .wz-step::after { display: none; }
  .wz-step { width: auto; white-space: nowrap; }
  .wz-rail-note { display: none; }
  .wz-pane { padding: 1.1rem 1rem; min-height: 0; }
  .wz-rows-head { display: none; }
  .wz-row, .wz-row.ports { grid-template-columns: 1fr 1fr; }
  .wz-facts td:first-child { width: 8rem; }
}

/* The filesystem, stated rather than chosen. See the header of steps/disks.js:
   ext4 is the only one the images can create at the moment the disk is made. */
.wz-fs { font-size: .82rem; color: var(--ink-muted); }

/* ────────────────────────────────────────────────────────────────────────────
   People → password reset panel

   Appended block. Nothing above this line is altered.

   The panel opens as a row inside the members table rather than as a modal.
   Two reasons: the thing it acts on is the row above it, and the result it
   produces is a secret shown once — a dialog that can be dismissed by a stray
   click on the backdrop is the wrong container for a value that cannot be
   asked for again.

   Every colour here is a token, so the dark theme is inherited rather than
   restated. The one deliberately loud element is .pw-reveal, which is the only
   place on the customer plane where a plaintext credential is ever on screen.
   ──────────────────────────────────────────────────────────────────────────── */

table.list tr.pw-row td {
  padding: 0;
  background: var(--sunken);
  border-bottom: 1px solid var(--rule);
}

.pw-panel { padding: .95rem 1rem 1.05rem; }

.pw-panel-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem .75rem;
  margin-bottom: .8rem;
}
.pw-panel-head h3 {
  margin: 0;
  font-size: .92rem;
  font-weight: 600;
  letter-spacing: -.01em;
}

/* Two equal columns, because neither option is a footnote to the other. They
   stack below the width where two columns of prose stop being readable. */
.pw-choices {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .85rem;
}
@media (max-width: 46rem) {
  .pw-choices { grid-template-columns: minmax(0, 1fr); }
}

.pw-choice {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .45rem;
  padding: .8rem .85rem;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}
.pw-choice h4 {
  margin: 0;
  font-size: .82rem;
  font-weight: 600;
}
.pw-choice p { margin: 0; line-height: 1.5; }
.pw-choice > button, .pw-choice > form { margin-top: auto; }

.pw-set {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .5rem;
  width: 100%;
}
.pw-set input {
  font-family: var(--font-mono);
  font-size: .82rem;
}

.pw-out:not(:empty) { margin-top: .85rem; }

/* The one moment a credential is on screen. Marked out so it is obvious that
   something was produced and that the page is now holding it. */
.pw-reveal {
  padding: .8rem .85rem .85rem;
  border: 1px solid var(--accent);
  border-left-width: 3px;
  border-radius: var(--radius);
  background: var(--accent-wash);
}
.pw-reveal h4 {
  margin: 0 0 .2rem;
  font-size: .84rem;
  font-weight: 600;
}
.pw-reveal p { margin: 0; line-height: 1.5; }
.pw-reveal p:last-child { margin-top: .4rem; }

/* The trigger, while its panel is open. */
table.list .btn.small.active {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent);
}

/* ────────────────────────────────────────────────────────────────────────────
   People → creating an account, and the sentence a withheld control leaves
   behind

   Appended block. Nothing above this line is altered.

   Two things live here. The first is the pair of cards at the top of the page
   that hold the two ways to add somebody — creating an account outright and
   sending an invitation — laid out side by side because neither is a footnote
   to the other, and because which one is right depends entirely on whether the
   person is reachable by email.

   The second is .why, which is the whole point of the change. Where a control
   is withheld the row now prints one sentence in the space the button would
   have occupied. It is deliberately quiet — muted, small, right-aligned under
   the actions — because it is an explanation and not a warning: nothing has
   gone wrong, the reader simply is not the person who may do that.

   Every colour is a token, so both themes are inherited rather than restated.
   ──────────────────────────────────────────────────────────────────────────── */

/* The two ways to add somebody. They stack below the width where two columns
   of prose and a form stop fitting. */
.people-add {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: .85rem;
  align-items: start;
}
@media (max-width: 60rem) {
  .people-add { grid-template-columns: minmax(0, 1fr); }
}

.people-way {
  display: flex;
  flex-direction: column;
  gap: .55rem;
}
.people-way h3 {
  margin: 0;
  font-size: .92rem;
  font-weight: 600;
  letter-spacing: -.01em;
}
.people-way p { margin: 0; line-height: 1.5; }

/* Both forms wrap rather than scroll: the fields are short and a horizontal
   scrollbar inside a card is nobody's idea of a form. */
.people-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: .6rem;
  margin-top: .15rem;
}
.people-form .field { min-width: 0; }
.people-form input.mono { font-family: var(--font-mono); font-size: .82rem; }
.people-form button { flex: 0 0 auto; }

/* The actions cell: the buttons, then whatever could not be offered. */
.row-actions {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: .3rem;
}

/* One sentence, in the space a control would have taken.
   max-width so a long explanation wraps into two tidy lines instead of
   stretching the column and squeezing everything else. */
.why {
  display: block;
  /* Stated rather than inherited: the actions column is td.num, which is set in
     the mono face for aligning figures. A sentence of prose is not a figure. */
  font-family: var(--font-ui);
  max-width: 22rem;
  font-size: .76rem;
  line-height: 1.45;
  color: var(--ink-faint);
  text-align: right;
  text-wrap: pretty;
}

/* In the role column it sits under the role label, so it reads left to right
   with the thing it is explaining. */
table.list td .stack .why { text-align: left; max-width: 16rem; }

/* A primary button loses its label on hover.
   `.btn:hover:not(:disabled)` sets `background: var(--sunken)` and matches at
   the same specificity as `.btn.primary` but later in the file, so hovering any
   primary button repaints it cream while `color` stays `--accent-ink` — white
   text on a near-white ground. `.btn.primary:hover` already exists a line below
   it, but only sets `filter`, so it never put the background back.

   Not introduced here, but it lands on "Create user" and "Send invitation",
   which are the two buttons this page is for. Restated rather than edited in
   place so this block still adds and never alters. */
.btn.primary:hover:not(:disabled) { background: var(--accent); }

/* ── Billing ─────────────────────────────────────────────────────────────────
   The billing screen reuses the portal's cards, tables, notes and tags. These
   are the few pieces it adds: the running-total figure, the per-hour rate list,
   and the card-on-file line. Tokens only, so light and dark both follow. */
.billing-current { display: flex; flex-direction: column; }
.billing-total {
  display: flex;
  align-items: baseline;
  gap: .6rem;
}
.billing-total-amount {
  font-family: var(--font-mono, var(--font-ui));
  font-size: 2rem;
  font-weight: 600;
  color: var(--ink);
  letter-spacing: -.01em;
}
.billing-total-label { text-transform: none; }

.billing-rates {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .5rem 1.4rem;
}
.billing-rate {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: .35rem 0;
  border-bottom: 1px solid var(--line, var(--sunken));
}

.billing-card-on-file {
  display: flex;
  align-items: center;
  gap: .6rem;
}
.billing-card-brand {
  text-transform: capitalize;
  font-weight: 600;
  color: var(--ink);
}

/* ─────────────────────────────────────────────────────────────────────────────
   PREPAID WALLET — appended with the on-demand billing model.

   The wallet is the customer's prepaid balance: add credit, and pay only for the
   hours a machine runs. Reuses the portal's tokens (--card, --accent, --good,
   --caution) so it reads as part of the same surface, light and dark.
   ───────────────────────────────────────────────────────────────────────────── */
.wallet-card { display: flex; flex-direction: column; }
.wallet-balance { display: flex; align-items: baseline; gap: .6rem; }
.wallet-balance-amount {
  font-size: 2.6rem; font-weight: 700; letter-spacing: -.02em; color: var(--ink);
  line-height: 1.05;
}
.wallet-balance-amount.is-empty { color: var(--caution); }
.wallet-balance-label { align-self: flex-end; padding-bottom: .35rem; }

.wallet-meta {
  display: flex; flex-wrap: wrap; gap: 1.4rem;
  margin: .9rem 0 .2rem; padding: .8rem 0;
  border-top: 1px solid var(--line, var(--sunken));
  border-bottom: 1px solid var(--line, var(--sunken));
}
.wallet-meta-item { display: flex; flex-direction: column; gap: .15rem; }
.wallet-meta-item .v { font-weight: 600; color: var(--ink); }

.wallet-topups { display: flex; flex-wrap: wrap; gap: .5rem; margin: .2rem 0 .6rem; }
.wallet-custom { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }

.wallet-modes { display: flex; flex-direction: column; gap: .7rem; }
.wallet-mode {
  display: flex; gap: .6rem; align-items: flex-start;
  padding: .7rem .8rem; border: 1px solid var(--line, var(--sunken));
  border-radius: 10px; cursor: pointer;
}
.wallet-mode:has(input:checked) { border-color: var(--accent); background: var(--accent-wash); }
.wallet-mode input { margin-top: .2rem; flex: none; }
.wallet-mode .block { display: block; margin-top: .15rem; }

.wallet-auto-fields { display: flex; gap: 1rem; flex-wrap: wrap; margin: 1rem 0 .4rem; }
.wallet-field { display: flex; flex-direction: column; gap: .3rem; }
.wallet-card-area {
  margin-top: .8rem; padding-top: .8rem;
  border-top: 1px solid var(--line, var(--sunken));
  display: flex; flex-direction: column; gap: .6rem; align-items: flex-start;
}
.wallet-card-onfile { display: flex; gap: .6rem; align-items: center; }

td.v.wallet-credit, .wallet-credit { color: var(--good); font-weight: 600; }

/* The `hidden` attribute must beat the flex display rules above, so the
   auto-refill fields actually collapse when Pause-at-zero is chosen. */
.wallet-auto-fields[hidden], .wallet-card-area[hidden] { display: none !important; }
