/* Screen rules for databases, volumes, storage and the two platform screens.
   =========================================================================

   app.css owns the Console design system: tokens, the shell, and every shared
   component (sections, cards, tables, the rail, controls, status, steppers,
   banners, overlays, the log). This file is for rules that belong to one
   screen and nowhere else — the attach control's one-line form, a volume's
   backup cell, the audit timeline's rule.

   Owned by the screens implementer for:
     databases.js  dbdetail.js  storage.js  settings.js  team.js  teamaccess.js

   (settings.js is here, not in screens-apps.css: Activity and the platform
   forms are the storage surface seen from the platform's side — the pools, the
   object store and the archives — and the audit timeline's rules have nowhere
   else to live.)

   Rules:
     - Every colour comes from a token in app.css. A literal hex here is a bug
       in one of the two themes.
     - Prefix a class with the screen it belongs to (.db-attach-row,
       .vol-schedule, .audit-event) so nothing here collides with the kit.
     - If two screens need the same shape, it is not a screen rule: add it to
       view.js and app.css instead.
     - Wide content scrolls in its own container. The body never scrolls
       sideways at 390px.

   Order: databases, volumes, the platform screens, the audit timeline, team.
*/

/* ---- databases -----------------------------------------------------------
   A database is not a deployment, so its state is a count and a phase rather
   than a pill: ready-over-desired and CloudNativePG's own sentence are one
   reading, and they share a cell. */

.db-state {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px var(--sp-2);
}

/* The apps a database is attached to: links, wrapped, in the body voice. */
.db-attached {
  display: flex;
  flex-wrap: wrap;
  gap: 2px var(--sp-3);
  font-size: 13px;
}
.db-attached a { color: var(--ink-2); }
.db-attached a:hover { color: var(--accent); }
.db-unattached { font-size: 13px; color: var(--ink-3); white-space: nowrap; }

/* Attaching is one decision, so it is one line: what to attach, the variable
   it arrives in, and the button — on one baseline, stacked on a phone. */
.db-attach {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
}
.db-attach-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
  align-items: end;
  gap: var(--sp-3);
}
.db-attach-go .btn { width: 100%; }

/* A Secret or key named inside a sentence is a machine value: mono, and never
   broken in the middle of its name. */
.db-code { white-space: nowrap; }
/* The Connection card's read-only addresses say they go on when the panel is
   too narrow to show them whole. */
.db-connect .copy-row input { text-overflow: ellipsis; }

/* The line under a name in a table when it is prose rather than a machine
   value — "attached to web", "PostgreSQL 17", "Cache" — is `.cell-note`, and
   `.cell-sub` is the machine line beside it. The rule itself lives in
   screens-apps.css beside `.projects-about`, which is the same reading: one
   shape, declared once, whichever side of the console draws it.

   The masthead's machine line: the health chip with its unit beside it. */
.db-meta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* When a backup runs on its own, said beside the button that runs one now. */
.db-schedule,
.vol-schedule {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 13px;
  color: var(--ink-2);
}
.db-schedule .icon,
.vol-schedule .icon { color: var(--ink-3); }
.db-schedule.off,
.vol-schedule.off { color: var(--ink-3); }

/* ---- volumes -------------------------------------------------------------
   The policy and the last snapshot are one answer — is this data anywhere
   else, and when did it last get there — so they are one cell, the sentence
   over the stamped time. */

.vol-backup {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.vol-last { font-size: 12.5px; color: var(--ink-3); white-space: nowrap; }

/* A volume's Backups panel (docs/adr/0029): its snapshots, their restic tags
   as chips, and the runs — a failed one's log tail wraps inside its row. */
.vol-panel-head { margin: 0; font-size: 13.5px; font-weight: 600; }
.vol-tags { display: inline-flex; flex-wrap: wrap; gap: 4px; }
.vol-tag { font-size: 11.5px; padding: 0 6px; }
.vol-run-error td { padding-top: 0; }
.vol-run-log { max-height: 220px; font-size: 12px; }

/* The two irreversible things a volume can have done to it live at the foot of
   its own panel, under a rule: they are not part of the policy form above
   them, and a row of six buttons in the table was hiding which of them the
   click was going to mean. */
.vol-danger {
  border-top: 1px solid var(--line);
}

/* ---- the platform screens ------------------------------------------------ */

/* A pool's description is written by one operator for the next one, so it is
   a sentence in the sans, not a machine fact in the mono. */
.pool-note {
  margin-top: 2px;
  font-size: 12.5px;
  color: var(--ink-2);
}

/* What is wrong with a pool's ownership right now — no owner, or one that is
   missing or not Ready. It reads like the description above it and is coloured
   like the warning it is. */
.pool-warn {
  margin-top: 2px;
  font-size: 12.5px;
  color: var(--warn);
  white-space: normal;
}

/* Seven ceilings in one column is a long thin list; on a wide screen they
   read as two pairs of columns. */
@media (min-width: 900px) {
  .limits-grid .facts {
    grid-template-columns: repeat(2, max-content minmax(0, 1fr));
    column-gap: var(--sp-6);
  }
}

/* ---- the audit table -----------------------------------------------------
   An audit log is who did what: the filters above it, the action's own mark
   beside its name, and the place it landed as links into the hierarchy. */

.audit-filter {
  display: flex;
  align-items: flex-end;
  gap: var(--sp-3);
  min-width: 220px;
}


.audit-action {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.audit-action .icon { color: var(--ink-3); flex: none; }

.audit-where {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  white-space: nowrap;
  gap: 6px;
  font-size: 12.5px;
}
/* A day header: said once where the day changes, not under every time. */
.audit-day > th {
  padding-top: var(--sp-3);
  padding-bottom: var(--sp-2);
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: 12.5px;
  font-weight: 600;
}
.audit-where a { color: var(--ink-2); }
.audit-where a:hover { color: var(--accent); }
.audit-sep { color: var(--ink-3); }

@media (max-width: 639px) {
  .db-attach-row {
    grid-template-columns: minmax(0, 1fr);
    justify-content: stretch;
    align-items: stretch;
  }
  .audit-filter { width: 100%; flex-direction: column; align-items: stretch; }
}


/* ---- databases / recovery / settings / activity (worker C) ---------------
   The create wizard, the database screen's recovery window, and the two rails
   the platform's backup pages carry. Every colour is a token from app.css. */

/* The engine mark: the cylinder that stands for a database, at card size, on
   the create form's radio card and beside a database's own name. */
.engine-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  flex: none;
  border-radius: var(--radius);
  background: var(--accent-soft);
  color: var(--accent);
}
.engine-mark .icon { width: 18px; height: 18px; }

/* ---- create a database, and create a Redis --------------------------------
   Four numbered decisions down the page with the summary beside them. The
   engine is one card today, so the grid is a run of them rather than a row of
   four fixed columns — and the Redis flow puts its two mode cards in the same
   grid, which is why it is auto-fit rather than fixed at one: the cards there
   share the row between them.

   Everything else on the Redis screens is the data-side shapes above: the
   section table, .db-state, the attach row, the numbered .step-section and the
   slider. The mode is the one thing they do not share with a database, and it
   is a radio group, which app.css already styles. */

/* A segmented choice on the database and Redis forms fills its field, the way
   the app form's does, instead of stopping short of the field's edge. */
.seg-fill { display: flex; width: 100%; }
.seg-fill .seg-btn { flex: 1 1 0; min-width: 0; }

.engine-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: var(--sp-3);
}
.db-new .step-section > .form-grid,
.db-new .step-section > .field { margin-top: var(--sp-1); }

/* The storage slider and the box beside it are two views of one number. */
.slider-row {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
}
.slider {
  flex: 1 1 auto;
  min-width: 0;
  accent-color: var(--accent);
  height: 22px;
}
.slider-num {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  flex: none;
}
.slider-num .num { width: 96px; }
.slider-num .unit { font-size: 13px; color: var(--ink-2); }

/* The object's mark, its name and the one word for how it is doing are one
   line: the mark is a tile at the head of the title and the pill sits inside
   it, at the title's baseline. */
.db-title {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}
.db-title .pill { font-size: 12.5px; }
.db-title .engine-mark {
  width: 42px;
  height: 42px;
  border-radius: var(--radius);
}
.db-title .engine-mark .icon { width: 22px; height: 22px; }

/* ---- one database --------------------------------------------------------
   The attached apps on the Overview read as name-and-key pairs rather than a
   table: there are rarely more than a handful, and the key is the fact. */

.db-attach-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 5px var(--sp-2) 5px var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-tag);
  background: var(--surface-2);
  font-size: 13px;
}

/* ---- extensions ----------------------------------------------------------
   An extension is a name Postgres knows and a sentence a person needs, so the
   row carries both: the switch, the name in the machine voice, the trusted
   pill beside it, and what it is for underneath. The chips are those names
   read back — in a fact, a table cell or the create flow's summary rail. */

.db-ext-list {
  display: flex;
  flex-direction: column;
}
.db-ext-row {
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--line);
}
.db-ext-row:first-child { border-top: none; }
.db-ext-row .switch { align-items: flex-start; width: 100%; }
.db-ext-row .switch-track { margin-top: 2px; }

.db-ext-text {
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}
.db-ext-head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--sp-2);
}
.db-ext-name {
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink);
}
.db-ext-desc { font-size: 13px; color: var(--ink-2); }

.db-ext-chips {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}
.db-ext-chip {
  display: inline-block;
  padding: 2px 8px;
  border: 1px solid var(--line);
  border-radius: var(--radius-tag);
  background: var(--surface-2);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 12px;
  white-space: nowrap;
}
/* A name the cluster has not created yet is still only a request. */
.db-ext-chip.pending { border-style: dashed; color: var(--ink-3); }
.db-ext-more { font-size: 12.5px; color: var(--ink-3); }
.db-ext-none { font-size: 13px; color: var(--ink-3); }

/* The fact on the Overview and the state under the save on Updates: the chips,
   the live reading beside them, and the API's own sentence on its own line. */
.db-ext-fact {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px var(--sp-3);
}
.db-ext-why {
  flex-basis: 100%;
  font-size: 12.5px;
  color: var(--ink-2);
}

/* ---- the recovery window -------------------------------------------------
   The retention period drawn as a line with a dot per backup, placed by time,
   so a gap in the run is visible as a gap. The dots are absolutely positioned
   by a percentage the screen computes. */

.rec-window {
  position: relative;
  height: 44px;
  margin: var(--sp-2) 0;
}
.rec-track {
  position: absolute;
  top: 21px;
  left: 0;
  right: 0;
  height: 2px;
  border-radius: 1px;
  background: var(--surface-3);
}
.rec-range {
  position: absolute;
  top: 19px;
  height: 6px;
  border-radius: 3px;
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}
.rec-dot {
  position: absolute;
  top: 16px;
  width: 12px;
  height: 12px;
  margin-left: -6px;
  border-radius: 50%;
  border: 2px solid var(--surface);
  background: var(--ink-3);
}
.rec-dot.bad { background: var(--bad); }
.rec-dot.on {
  top: 12px;
  width: 20px;
  height: 20px;
  margin-left: -10px;
  background: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-soft);
}
.rec-scale {
  display: flex;
  justify-content: space-between;
  font-size: 12.5px;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* Available backups: a run of buttons, not a table — picking one repaints the
   restore rail, so each row is a control with a pressed state. */
.rec-list {
  display: flex;
  flex-direction: column;
}
.rec-item {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-3) var(--sp-4);
  border: none;
  border-top: 1px solid var(--line);
  border-left: 3px solid transparent;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.rec-item:first-child { border-top: none; }
.rec-item:hover { background: var(--surface-2); }
.rec-item.on {
  border-left-color: var(--accent);
  background: var(--accent-soft);
}
.rec-item > .icon { color: var(--ink-3); flex: none; margin-left: auto; }
.rec-item .pill { flex: none; }
.rec-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--ink-2);
}
.rec-body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  flex: 1 1 auto;
}
.rec-title { font-size: 13.5px; font-weight: 500; }
.rec-sub { font-size: 12.5px; color: var(--ink-2); }
.rec-item.on .rec-mark { background: var(--surface); color: var(--accent); }

/* Both kinds of backup share the list (docs/adr/0029): a badge says which. A
   dump is not a point to pick, so its row is not a control; its own actions
   sit on it and wrap under the text on a narrow screen. */
.rec-method { margin-right: 2px; font-size: 11.5px; padding: 0 6px; }
.rec-method.dump { background: var(--slate-soft); border-color: transparent; color: var(--slate); }
.rec-sub { overflow-wrap: anywhere; }
.rec-item.rec-dump { flex-wrap: wrap; cursor: default; }
.rec-item.rec-dump:hover { background: none; }
.rec-item.rec-dump .rec-body { flex: 1 1 220px; }
.rec-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2);
  margin-left: auto;
}
.rec-actions .btn .icon { width: 14px; height: 14px; }

/* A dump that failed to load into a restored database: the error as the
   server wrote it, wrapped, never widening the banner. */
.db-import-error { margin: 0 0 var(--sp-2); max-height: 200px; }

/* The one sentence a restore has to carry: the source is not touched. */
.rec-safe { color: var(--ink); font-weight: 500; }
.rec-safe .icon { color: var(--accent); vertical-align: -2px; }

/* The recovery window as two instants (docs/adr/0026): the local time a person
   reads, with the UTC form the API and the logs use under it. */
.db-window {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-6);
}
.db-window-end { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.db-when { display: inline-flex; flex-direction: column; gap: 1px; vertical-align: top; }
.db-when-utc { font-size: 12.5px; color: var(--ink-3); }
.db-pitr-range {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--sp-2) var(--sp-3);
  font-size: 13.5px;
}
.db-pitr-range > .field-label { flex-basis: 100%; }
.db-pitr-utc { margin: 0; font-size: 13.5px; color: var(--ink-2); }

/* Loading a dump: the run's steps, the phase it is waiting on, and the upload
   bar whose width the screen sets through the CSSOM as bytes leave. */
.db-import-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  font-size: 14px;
  font-weight: 500;
  overflow-wrap: anywhere;
}
.db-import-head .icon { color: var(--accent); flex: none; }
.db-import-phase { margin: 0; font-size: 13.5px; color: var(--ink-2); }
.db-import-bar {
  height: 8px;
  border-radius: 4px;
  background: var(--surface-3);
  overflow: hidden;
}
.db-import-fill {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width 120ms linear;
}
.db-import-count { margin: 0; font-size: 13px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.db-import-out { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.db-import-out pre.log { max-height: 320px; }
.db-import-file { max-width: 100%; font: inherit; font-size: 13.5px; }
@media (prefers-reduced-motion: reduce) {
  .db-import-fill { transition: none; }
}

/* ---- the platform's backup pages -----------------------------------------
   What relies on the object store, named rather than counted alone. */

.impact-list {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding-top: var(--sp-3);
  border-top: 1px solid var(--line);
}
.impact-row { font-size: 13px; }
.impact-row a { font-weight: 500; }

/* ---- mail ----------------------------------------------------------------
   The scopes a key carries and the six variable names an attach writes: runs
   of small badges, wrapped, because a column of one word each is a list nobody
   reads across. */

.mail-chips,
.mail-vars {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.mail-vars .badge { font-size: 11.5px; }

/* Attaching names the variables, so before an attach the badges are inputs
   that still read as badges: the same pill, the same mono and the same slate,
   as wide as the name in them. A focused one is an input again; a refused one
   is in the error colour, with the reason on the line under the row. */
.mail-vars input.mail-var {
  width: auto;
  field-sizing: content;
  min-width: 8ch;
  height: 24px;
  padding: 2px 8px;
  border-radius: var(--radius-tag);
  border-color: transparent;
  background: var(--slate-soft);
  color: var(--slate);
  font-size: 11.5px;
  font-weight: 500;
  line-height: 18px;
}
.mail-vars input.mail-var:hover { border-color: var(--line-2); }
.mail-vars input.mail-var:focus { background: var(--surface); color: var(--ink); }
.mail-vars input.mail-var[aria-invalid="true"] {
  border-color: var(--bad);
  background: var(--bad-soft);
  color: var(--bad);
}
.mail-var-error {
  margin: 0;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--bad-soft);
  color: var(--bad);
  font-size: 12.5px;
  overflow-wrap: anywhere;
}

/* The format and the scopes are two choices of the same kind, side by side;
   the sender is a picker no wider than an address needs. */
.mail-modes {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3) var(--sp-5);
}
.mail-sender { max-width: 420px; }

/* The line under a project in the workspaces table when there is nothing
   machine-written to put there: a sentence, so it is not set in the mono the
   cell's own sub-line is. */
.mail-sub { font-size: 12.5px; color: var(--ink-2); }

/* A DKIM value is a public key and an ownership token is a hash: both are far
   wider than the column and neither is read — they are copied. So the cell
   keeps one line and the copy button beside it does the work. */
.mail-value {
  max-width: 340px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---- add-ons -------------------------------------------------------------
   Settings › Add-ons draws one card per catalog entry and builds its form
   from a typed field list it has never seen before, so nothing here may assume
   a particular field: these are the shapes a generic form needs. */

/* One field of the generated form: the kit's own field, with room under it for
   the error the API can land on this key alone. */
.addon-field { display: flex; flex-direction: column; gap: 6px; }

/* A bool is a checkbox, and a checkbox is not a text box: it keeps its own
   width rather than stretching across the column the way an input does. */
.addon-field.bool .field { flex-direction: column; align-items: flex-start; }
.addon-field.bool input[type="checkbox"] {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
}

/* The sentence the controller writes about a state, under the pill it is about:
   why a probe calls it degraded, or what a run is waiting on. */
.addon-note {
  margin-top: 4px;
  font-size: 13px;
  color: var(--ink-2);
}

/* The 400 that names a field lands here rather than in the form's own slot, so
   the message sits under the box that fixes it. */
.addon-field-error {
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--bad-soft);
  color: var(--bad);
  font-size: 12.5px;
  overflow-wrap: anywhere;
}

/* The advanced fields, behind their disclosure. */
.addon-advanced {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--sp-4);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
}

/* ---- Settings › Versions (docs/adr/0048-component-versions.md) ------------
   One row per pinned component: its words, what runs, what the release would
   pick, the state, and a form with the picker and the three requests. */
.vr-head { display: inline-flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.vr-checked { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; }
.vr-checked .icon { color: var(--ink-3); }
tr.vr-off > td { opacity: 0.55; }
.vr-actions { width: 1%; }
.vr-form { display: flex; flex-direction: column; gap: 6px; min-width: 460px; }
.vr-pick { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); white-space: nowrap; }
.vr-pick select { flex: none; width: auto; min-width: 190px; max-width: 360px; }
.vr-pick .vr-typed { width: 150px; }
.vr-help { display: flex; flex-direction: column; gap: 2px; font-size: 12.5px; max-width: 480px; white-space: normal; }
/* Settings › Notifications' Firing now: what to do, under the summary, and
   the way there (docs/adr/0090). */
.n-action { display: flex; align-items: baseline; gap: 6px; margin-top: 4px; color: var(--ink); font-weight: 500; }
.n-action .icon { flex: none; color: var(--accent); align-self: center; }
td.n-open { width: 1%; white-space: nowrap; }
/* Its category filter (docs/adr/0092): a box per category with how many,
   the hidden count at the end. */
.n-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2) var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.n-filter > span:last-child { margin-left: auto; }
.n-count { margin-left: 2px; color: var(--ink-3); font-weight: 500; }
.vr-running { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
a.vr-vuln { text-decoration: none; }
a.vr-vuln:hover .chip { filter: brightness(0.96); }
.vr-vulns { max-width: 520px; }
.vr-vulns.vr-good { color: var(--ok); font-weight: 600; }
.vr-vulns a { font-weight: 400; }
tr.vr-target > td { background: var(--accent-soft); }
.vr-warn { color: var(--warn); }
.vr-bad { color: var(--bad); }
.vr-warn .icon, .vr-bad .icon { vertical-align: -0.2em; }
.vr-moves { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.vr-moves li { display: flex; align-items: center; gap: var(--sp-2); }
.vr-move-label { font-weight: 600; }
.vr-arrow { display: inline-flex; color: var(--ink-3); }

/* The run in flight (runprogress.js), an upgrade's card here and an add-on's
   on Add-ons › Catalog: a pulsing dot in the title, a bar over the installer's
   checklist that eases from step to step (or slides while there is no step to
   count yet), and the step under it. Only the bar's width and the words change
   while it runs, so nothing around it moves. */
.vr-live { border-color: var(--accent); }
.vr-progress { display: flex; flex-direction: column; gap: var(--sp-3); }
.vr-live-title { display: inline-flex; align-items: center; gap: var(--sp-2); }
.vr-pulse {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: 50%;
  background: var(--accent);
  animation: pulse 1.6s ease-in-out infinite;
}
.vr-bar { position: relative; height: 6px; overflow: hidden; border-radius: 3px; background: var(--accent-soft); }
.vr-fill { width: 0; height: 100%; border-radius: inherit; background: var(--accent); transition: width 0.8s var(--ease); }
.vr-bar.waiting .vr-fill { position: absolute; top: 0; left: 0; width: 30%; animation: vr-slide 1.4s var(--ease) infinite; }
@keyframes vr-slide {
  from { transform: translateX(-100%); }
  to { transform: translateX(340%); }
}
.vr-live-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-height: 32px;
  font-size: 13.5px;
  color: var(--ink-2);
}
/* A quiet button's words end where the card's content does, in line with
   the Follow and Copy buttons under it; its hover wash spills into the
   padding instead. */
.vr-live-foot > .btn.quiet { margin-right: -11px; }
.vr-where { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.vr-count { font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) {
  .vr-pulse { animation: none; }
  .vr-fill { transition: none; }
  .vr-bar.waiting .vr-fill { animation: none; width: 100%; opacity: 0.5; }
}

/* ---- team & access -------------------------------------------------------
   #/team is a two-column page: the people on the left, one person's access on
   the right. The rail is a form of checkboxes rather than a list of facts, so
   what it needs from here is rhythm — a sentence under every box, and the mail
   workspaces set apart from the capabilities that unlock them. */

/* The members table is repainted on every keystroke of the search beside it,
   so it lives in a host of its own inside the card and the box above it is
   never taken out from under the cursor. The host lays out as if it were not
   there; only the card's bottom corners have to be said again. */
.team-table { display: contents; }
.team-table > .table-wrap {
  border-radius: 0 0 var(--radius-card) var(--radius-card);
}

/* A person in the table: their initial, their name, and the address they sign
   in with under it. The address is a sentence a human reads, not a machine
   value, so it stays in the sans the rest of the row is in. */
.team-member {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}
.team-email {
  margin-top: 2px;
  font-size: 12.5px;
  color: var(--ink-2);
}

/* The person's own name and address, above their access: a small form of its
   own, because saving a rename is not saving a capability. */
.team-details {
  padding-bottom: var(--sp-4);
  border-bottom: 1px solid var(--line);
}

/* The catalog as a form. One block per service, each headed by the service's
   own name, so a capability is read under the thing it is about. */
.team-caps { display: flex; flex-direction: column; gap: var(--sp-4); }
.team-group { display: flex; flex-direction: column; gap: var(--sp-3); }
.team-group .eyebrow { color: var(--ink-2); }

/* One capability: the box and its title, then the catalog's own sentence about
   what it grants, indented under the label rather than beside it. */
.team-cap { display: flex; flex-direction: column; gap: 2px; }
.team-cap-desc,
.team-cap-note {
  margin: 0 0 0 26px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-2);
}

/* Why a box cannot be touched: implied by an admin's, or not available on this
   platform at all. It is the one line that has to read differently from the
   description above it. */
.team-cap-note { color: var(--ink-3); }

/* The compact editor, in the invite drawer: the same boxes with the air taken
   out, because a drawer is narrower than a rail and the person filling it in
   is choosing, not auditing. */
.team-caps.compact { gap: var(--sp-3); }
.team-caps.compact .team-group { gap: var(--sp-2); }
.team-caps.compact .team-cap-desc { display: none; }

.team-caps-head { display: flex; align-items: center; }

/* The mail half. It appears and disappears with the capability that unlocks
   it, so it carries its own heading and the sentence that keeps a grant
   honest. */
.team-mail {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}

.team-ws-list { display: flex; flex-direction: column; gap: var(--sp-3); }

/* One workspace: what they reach in it, and — only when that is named
   mailboxes — the picker under it. */
.team-ws {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
}
.team-ws-head {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}
.team-ws-name {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 500;
}
.team-ws-name .icon { color: var(--ink-3); flex: none; }
.team-ws-head select { flex: 0 0 auto; }

/* The mailboxes of one workspace: a checkbox and the level it is held at. */
.team-mailboxes {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
}
.team-mailbox {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.team-mailbox .check { flex: 1 1 auto; min-width: 0; }
/* An address is longer than the rail more often than not, and it is read from
   the left: the end of it is what gives way. */
.team-mailbox .check span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.team-mailbox select { flex: 0 0 auto; }

/* ---- the database and Redis panels, and the create panel (ADR 0030) -------
   The panel is a column about as wide as half the screen, so what was a page
   with a rail is one column of cards here, and the create forms keep their
   summary rail only while the panel is wide enough to hold it. */

.dbp-status { display: inline-flex; align-items: center; }
/* The list's own controls — the kind filter, the download and Back up now —
   share the card head and wrap under the title when the panel is narrow. */
.dbp-list-actions { flex-wrap: wrap; justify-content: flex-end; gap: var(--sp-2); }
/* A binding that is staged but not deployed: the row stays where it will be,
   in the staged amber, with the one word that says what is pending. */
.dbp-row-staged td { background: var(--staged-soft); }
.dbp-staged {
  margin-left: var(--sp-2);
  color: var(--staged);
  background: var(--staged-soft);
  border-color: var(--staged);
}

.create-pane { container-type: inline-size; display: flex; flex-direction: column; gap: var(--sp-4); }
.create-pane > form { display: flex; flex-direction: column; gap: var(--sp-4); }
.create-steps:empty { display: none; }
/* The steps stay on one line: the connectors give way before a step does, so
   no line is left dangling at the end of a row or the start of the next. */
.create-steps .wizard { flex-wrap: nowrap; }
.create-steps .wizard-line { flex: 1 1 8px; width: auto; min-width: 8px; max-width: 44px; }
/* The summary rail sticks to the top of the panel's own scroll, not of the
   page's, which has no top bar inside it. */
.create-pane .rail { top: 0; }
.create-head h2 { margin: 0; font-size: 17px; font-weight: 600; }
.create-head p { margin: var(--sp-1) 0 0; }
@container (max-width: 820px) {
  .create-pane .twocol { grid-template-columns: minmax(0, 1fr); }
  .create-pane .rail { position: static; }
}
/* Too narrow for every label: the step you are on keeps its word, the others
   keep their number, and the words stay for a screen reader. */
@container (max-width: 720px) {
  .create-steps .wizard-step:not(.on) .wizard-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* The create actions: a bar pinned to the bottom of the panel's scroll, the
   width of the panel, so Create is on the screen from the first field on —
   the summary rail it used to live in sits under the form in a panel too
   narrow for two columns. It is the last thing in the pane, so at the end of
   the scroll it rests where it would have been anyway. */
.create-foot {
  position: sticky;
  bottom: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  margin: 0 calc(-1 * var(--sp-5)) calc(-1 * var(--sp-5));
  padding: var(--sp-3) var(--sp-5);
  background: var(--surface);
  border-top: 1px solid var(--line);
  box-shadow: var(--shadow-pop);
}
.create-foot[hidden] { display: none; }
.create-foot-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  min-width: 0;
}
.create-foot-line {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 13.5px;
  color: var(--ink-2);
}
.create-foot-line strong { color: var(--ink); font-weight: 600; }
.create-foot-actions { display: flex; gap: var(--sp-2); flex: none; margin-left: auto; }

/* The radio on a create form's card is drawn, not left to the browser: a
   native unchecked radio in dark reads as a filled grey disc, which looks
   chosen. Here it is an empty ring until it is picked. */
.create-pane .radio-card input[type="radio"] {
  appearance: none;
  flex: none;
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  min-height: 0;
  padding: 0;
  border: 1.5px solid var(--ink-3);
  border-radius: 50%;
  background: var(--surface);
  cursor: pointer;
}
.create-pane .radio-card input[type="radio"]:checked {
  border-color: var(--accent);
  background: var(--accent);
  box-shadow: inset 0 0 0 3px var(--surface);
}
.create-pane .radio-card input[type="radio"]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

@media (max-width: 639px) {
  .create-foot {
    margin: 0 calc(-1 * var(--sp-4)) calc(-1 * var(--sp-4));
    padding: var(--sp-3) var(--sp-4);
  }
  .slider-row { flex-direction: column; align-items: stretch; }
  .slider-num { justify-content: space-between; }
  .rec-item { padding: var(--sp-3); }
  .mail-value { max-width: 180px; }
  .addon-advanced { grid-template-columns: 1fr; }
  .team-ws-head { align-items: stretch; flex-direction: column; }
  .team-mailbox { flex-wrap: wrap; }
}

/* ---- Settings › Bans (docs/adr/0037-intrusion-bans-fail2ban.md) ------------
   The filter strip under the list's head, the address as the row's
   disclosure and the details it opens, the ban form's two small shapes, and
   the three states that are not a list. */
.bans-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-bottom: 1px solid var(--line);
}
.bans-filters .search { flex: 0 1 300px; min-width: 200px; }
.bans-count { margin-left: 2px; color: var(--ink-3); font-weight: 500; }
.seg-btn.on .bans-count { color: inherit; opacity: 0.8; }
.bans-ip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 6px 2px 2px;
  margin-left: -2px;
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--ink);
  cursor: pointer;
  font: inherit;
}
.bans-ip:hover { background: var(--surface-2); }
.bans-ip .icon { color: var(--ink-3); flex: none; }
.bans-ip-plain { padding-left: 24px; }
tr.bans-open > td { border-bottom-color: transparent; }
tr.bans-detail > td { background: var(--surface-2); padding: var(--sp-3) var(--sp-4) var(--sp-4) 38px; }
.bans-detail-body { display: flex; flex-direction: column; gap: var(--sp-3); }
.bans-detail-body .facts { max-width: 900px; }
.bans-foot { display: flex; flex-direction: column; gap: var(--sp-3); }
.bans-more { display: flex; align-items: center; gap: var(--sp-3); }
.bans-ipfield { display: flex; flex-direction: column; gap: 6px; }
.bans-field-error {
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--bad-soft);
  color: var(--bad);
  font-size: 12.5px;
  overflow-wrap: anywhere;
}
.bans-duration { display: flex; gap: var(--sp-2); }
.bans-duration select { flex: 0 0 auto; width: auto; min-width: 150px; }
.bans-duration input { flex: 1 1 auto; }
.bans-jail-title { font-weight: 500; color: var(--ink); }
.bans-state { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.bans-state > .icon { color: var(--ink-3); flex: none; }
.bans-step { font-size: 13px; color: var(--ink-2); }
.bans-empty { display: flex; flex-direction: column; align-items: center; padding-top: var(--sp-5); }
.bans-empty > .icon { color: var(--ok); width: 28px; height: 28px; }
.bans-empty .empty-state { padding: var(--sp-2) var(--sp-5) var(--sp-5); }
.bans-empty .empty-state p { max-width: 62ch; }

/* The card a link into the catalogue named: its edge in the accent, once. */
.addon-target > .card { border-color: var(--accent-line); box-shadow: 0 0 0 1px var(--accent-line), var(--shadow-card); }
.addon-list { resize: vertical; min-height: 64px; }
@media (max-width: 639px) {
  .bans-filters .search { flex: 1 1 100%; }
  .bans-filters .seg { max-width: 100%; overflow-x: auto; }
  .bans-duration { flex-wrap: wrap; }
}
/* The Ban address dialog: the confirm's shape, a little wider for two fields. */
.drawer.confirm.bans-dialog { width: min(540px, 100%); }

/* ---- Servers (docs/adr/0049-edge-servers.md) ------------------------------
   The edge servers: servers.js, serverpanel.js, serveradd.js and the Runs on
   picker of serverpick.js. A server's resources are what its pods ask for of
   what it has, a figure over a thin bar; the join command is a long unbroken
   string, so its block wraps anywhere rather than scrolling sideways. */

.srv-state { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.srv-state-sub { display: flex; flex-wrap: wrap; gap: 2px 10px; }
.srv-state-sub .status-dot { font-size: 12px; color: var(--ink-2); }
td.srv-res { min-width: 260px; }
.srv-meters { display: grid; grid-template-columns: repeat(2, minmax(110px, 1fr)); gap: var(--sp-3); }
.srv-meter {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: 2px var(--sp-2);
  min-width: 0;
}
.srv-meter-label { font-size: 12px; color: var(--ink-2); }
.srv-meter-value { font-size: 12.5px; font-weight: 550; color: var(--ink); text-align: right; white-space: nowrap; }
.srv-meter-bar {
  grid-column: 1 / -1;
  display: block;
  height: 4px;
  border-radius: 2px;
  background: var(--surface-3);
  overflow: hidden;
}
.srv-meter-fill { display: block; height: 100%; border-radius: 2px; background: var(--ok); }
.srv-meter.warn .srv-meter-fill { background: var(--warn); }
.srv-meter.bad .srv-meter-fill { background: var(--bad); }
.srv-meter.warn .srv-meter-value { color: var(--warn); }
.srv-meter.bad .srv-meter-value { color: var(--bad); }
.srv-meter.idle .muted { text-align: right; }
.srv-fits { margin: 6px 0 0; font-size: 12.5px; line-height: 1.45; color: var(--ink-2); max-width: 46ch; }

.srv-resources .srv-fits { max-width: none; margin: 0; }

.srv-firewall { display: flex; align-items: flex-start; gap: var(--sp-2); margin: 0; font-size: 13px; color: var(--ink-2); }
.srv-firewall > .icon { flex: none; margin-top: 2px; color: var(--ink-3); }
.srv-rule {
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm);
  background: var(--warn-soft);
  color: var(--ink);
  font-size: 13px;
  overflow-wrap: anywhere;
}
.srv-list { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--ink-2); }
.srv-list li { overflow-wrap: anywhere; }

/* What an edge server is: four points, each an icon and a sentence. */
.srv-points { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.srv-point { display: flex; align-items: flex-start; gap: var(--sp-3); font-size: 14px; color: var(--ink-2); }
.srv-point strong { color: var(--ink); font-weight: 600; }
.srv-point > .icon { flex: none; margin-top: 2px; color: var(--accent); }
.srv-needs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.srv-need { display: flex; align-items: flex-start; gap: var(--sp-2); font-size: 13.5px; }
.srv-need > .icon { flex: none; margin-top: 2px; color: var(--ok); }
.srv-need > .icon[data-icon="x-circle"] { color: var(--bad); }
.srv-need > .icon[data-icon="info"] { color: var(--ink-3); }
.srv-advanced {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--sp-4);
  padding-top: var(--sp-2);
  border-top: 1px solid var(--line);
}
.srv-step { display: flex; align-items: center; gap: var(--sp-2); margin: 0; font-size: 13px; color: var(--ink-2); }
.srv-step > .icon { color: var(--ink-3); flex: none; }

/* Add server and the two pickers. */
.srv-field { display: flex; flex-direction: column; gap: 6px; }
.srv-field-error {
  margin: 0;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  background: var(--bad-soft);
  color: var(--bad);
  font-size: 12.5px;
  overflow-wrap: anywhere;
}
.srv-sizing {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-card);
  background: var(--surface-2);
  font-size: 13px;
  color: var(--ink-2);
}
.srv-sizing p { margin: 0; }
.drawer.srv-add { width: min(560px, 100vw); }
.srv-wait { display: flex; align-items: center; gap: var(--sp-2); color: var(--ink-2); }
.srv-wait > .icon { color: var(--ink-3); }
.srv-command-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
pre.log.srv-command {
  max-height: 180px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  font-size: 12px;

}
.srv-rules { margin: 0; padding-left: 18px; display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--ink-2); }
.srv-rules strong { color: var(--ink); font-weight: 600; }
.srv-choices { display: flex; flex-direction: column; gap: var(--sp-2); }
.srv-choices .radio-card > .icon { flex: none; margin-top: 2px; color: var(--ink-3); }
.srv-choices .radio-card.on > .icon { color: var(--accent); }
.srv-choices .radio-card.disabled { cursor: not-allowed; background: var(--surface-2); }
.srv-choices .radio-card.disabled:hover { border-color: var(--line-2); }
.srv-choices .radio-card.disabled .radio-card-title { color: var(--ink-3); }
.srv-choice-sub { display: block; }
.srv-choice-why { display: block; margin-top: 2px; color: var(--ink-2); overflow-wrap: anywhere; }
.drawer.confirm.srv-dialog { width: min(540px, 100%); }
.card-body > .srv-firewall { padding-top: var(--sp-3); border-top: 1px solid var(--line); }
.srv-block { display: flex; flex-direction: column; gap: var(--sp-3); padding-top: var(--sp-4); border-top: 1px solid var(--line); }
.srv-block:empty { display: none; }
/* The lists and notes above zero their own margins; inside a stack they keep
   its rhythm. */
.stack > * + :is(.srv-points, .srv-list, .srv-rules, .srv-rule, .srv-firewall, .srv-step, .srv-fits) { margin-top: var(--sp-3); }
.srv-enable-grid { display: grid; grid-template-columns: minmax(0, 460px); }
.srv-progress { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.srv-progress li { display: grid; grid-template-columns: 140px 1fr; align-items: baseline; gap: var(--sp-3); }
.srv-progress-sub { font-size: 13px; color: var(--ink-2); }

/* The worker mesh (docs/adr/0058-worker-mesh.md), mesh.js and workeradd.js:
   the members table's sessions cell names the stale peers under its verdict,
   and may wrap there rather than widen the table. A member's role and that
   line are words, so they are the sans under a cell, not .cell-sub's mono. */
.mesh-sub { margin-top: 2px; font-size: 12px; line-height: 1.5; color: var(--ink-2); }
.mesh-sub strong { color: var(--ink); font-weight: 600; }
.mesh-sessions { min-width: 150px; }
.mesh-sessions .mesh-sub { overflow-wrap: anywhere; }

/* ---- Project access (docs/adr/0051-project-access.md) --------------------- */

/* The Team rail's Projects half: the All projects level, then a role per
   project, in the mail workspaces' rows. */
.team-projects {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  padding-top: var(--sp-4);
  border-top: 1px solid var(--line);
}
.team-projects .team-ws-list { margin-top: var(--sp-1); }
.team-pj-via { font-size: 12.5px; }

/* Project › Members: wider than a form drawer, because a row carries a name,
   an address, a role and its controls. */
.drawer.members-drawer { width: min(600px, 100vw); }
.member-list { display: flex; flex-direction: column; }
.member-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3) 0;
  border-top: 1px solid var(--line);
}
.member-row:first-child { border-top: none; }
.member-row .team-member { min-width: 0; }
.member-row .cell-title { white-space: normal; }
/* Who added them and when, and the services that have not caught up, under
   the name — indented past the avatar so the column reads as one. */
.member-meta {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-2);
  padding-left: calc(32px + var(--sp-3));
  font-size: 12.5px;
  color: var(--ink-2);
}

/* The add-member picker: the search box, the directory's answers under it,
   and the role the person joins with. */
.member-add { padding-bottom: var(--sp-4); border-bottom: 1px solid var(--line); }
.member-search { display: flex; align-items: center; gap: var(--sp-2); }
.member-search .icon { color: var(--ink-3); flex: none; }
.member-search input { flex: 1 1 auto; min-width: 0; }
.member-results {
  display: flex;
  flex-direction: column;
  margin-top: var(--sp-1);
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.member-results > .note { padding: var(--sp-2) var(--sp-3); }
.member-result {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  width: 100%;
  padding: var(--sp-2) var(--sp-3);
  border: none;
  background: none;
  color: var(--ink);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.member-result + .member-result { border-top: 1px solid var(--line); }
.member-result:hover:not([disabled]),
.member-result:focus-visible { background: var(--surface-2); }
.member-result[disabled] { cursor: default; opacity: 0.6; }
.member-result .cell-title,
.member-result .team-email { display: block; }
.member-add-row { display: flex; align-items: center; gap: var(--sp-2); }
