/* PleasePatch console.
 *
 * Embedded in the binary via rust-embed — no CDN, no external font, nothing
 * the console needs the internet to render. The CSP is `default-src 'self'`
 * with no 'unsafe-inline', so there are no inline styles anywhere either: every
 * visual decision lives here.
 *
 * The product is a patching console for Linux fleets, and it is read by people
 * who spend their day in terminals and dashboards. The design follows from
 * that: dense but not cramped, quiet by default so that colour means something
 * when it appears, and typographically plain. Numbers are tabular everywhere
 * they are compared — version strings and counts in a column that do not line
 * up are harder to scan than they look.
 *
 * Light only. There is no theme switch.
 */

:root {
  /* Cool ink neutrals. Not grey — every step carries a little blue, which is
     what stops a page of white cards reading as a default template. */
  --bg: #f3f6f8;
  --surface: #ffffff;
  --surface-2: #f7f9fb;
  --surface-3: #eaeff4;

  --border: #dce4ea;
  --border-strong: #c3d0da;

  --text: #0d1a22;
  --text-2: #33474f;
  --muted: #5f7480;

  /* Deep cyan. The status ramp already owns green, amber and red, so an accent
     drawn from any of them would make a badge ambiguous — which rules out the
     obvious terminal-phosphor greens. Cyan is the one signal colour left that
     collides with nothing, and it reads as instrumentation rather than
     marketing. */
  --accent: #0e7490;
  --accent-hover: #155e75;
  --accent-soft: #ecfeff;
  --accent-border: #a5f3fc;
  --accent-text: #ffffff;

  /* Status. Each has a fill, a border and a text tone so a badge can be built
     from one family and stay legible on white. */
  --ok: #0f7a3d;
  --ok-bg: #eaf7ef;
  --ok-text: #0b6733;
  --ok-border: #b3e3c6;

  --warn: #9a5400;
  --warn-bg: #fdf5e6;
  --warn-text: #8a4b00;
  --warn-border: #f0d49a;

  --alert: #bf2233;
  --error-bg: #fdeef0;
  --error-text: #a01b2a;
  --error-border: #f4c2c8;

  --radius-sm: 5px;
  --radius: 7px;
  --radius-lg: 10px;

  /* Borders carry most of the separation; shadows only lift what floats. */
  --shadow-xs: 0 1px 2px rgba(13, 26, 34, 0.05);
  --shadow-sm: 0 1px 3px rgba(13, 26, 34, 0.09), 0 1px 2px rgba(13, 26, 34, 0.05);
  --shadow-md: 0 10px 28px rgba(13, 26, 34, 0.13);
  --ring: 0 0 0 3px rgba(14, 116, 144, 0.20);

  /* One measure for the top bar, the page and the footer. */
  --page: 1280px;
  --gutter: 1.5rem;

  /* Two faces, used semantically rather than decoratively: prose a person
     wrote is sans, and anything a machine produced — a hostname, a version, an
     id, a count, a timestamp — is mono. The type tells you which you are
     reading. No web fonts: the CSP is `default-src 'self'`, and a console that
     needs the internet to render its own text would be a poor advertisement
     for a product that patches airgapped fleets. */
  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  /* 14px body. Denser than a marketing site and about right for a console
     that routinely shows a package list forty rows long. */
  font-size: 0.875rem;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

/* ------------------------------------------------------------- top bar */

.topbar {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  /* Aligns the brand and nav with the page container below without needing a
     wrapper element: the extra padding is whatever centring the container
     would have added. */
  padding: 0 max(var(--gutter), calc((100vw - var(--page)) / 2 + var(--gutter)));
  height: 56px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 30;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 650;
  font-size: 0.95rem;
  letter-spacing: -0.015em;
  color: var(--text);
  text-decoration: none;
  white-space: nowrap;
}

.brand:hover { text-decoration: none; }

/* The mark is the product: three tiers, smallest first. A staged rollout goes
   canary, then wider, then everything — so the bars grow downward. It is the
   one shape this console could not borrow from another product. */
.mark { color: var(--accent); flex: none; }

.topbar nav { display: flex; align-items: center; gap: 0.15rem; }

.topbar a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.85rem;
  font-weight: 500;
  padding: 0.4rem 0.7rem;
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

.topbar a:hover { color: var(--text); background: var(--surface-3); }

.account { margin-left: auto; display: flex; align-items: center; gap: 0.75rem; }
.who { color: var(--muted); font-size: 0.85rem; }
.inline { display: inline; margin: 0; }

.linkish {
  background: none;
  border: 0;
  padding: 0;
  color: var(--accent);
  font: inherit;
  cursor: pointer;
  text-decoration: none;
}

.linkish:hover { text-decoration: underline; }

/* ------------------------------------------------------- account menu */

.menu { position: relative; }

/* The default marker would sit beside the caret we draw ourselves. */
.menu > summary { list-style: none; cursor: pointer; }
.menu > summary::-webkit-details-marker { display: none; }

.menu-trigger {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.35rem 0.6rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: 0.85rem;
  /* No max-width here. With border-box, one on the button is spent on the
     padding, the gap and the chevron before the text sees any of it, which
     truncated ordinary addresses. The cap is on the text span instead. */
  min-width: 0;
}

.menu-trigger:hover { color: var(--text); background: var(--surface-3); }
.menu-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.menu[open] > .menu-trigger { color: var(--text); background: var(--surface-3); border-color: var(--border); }

/* Long addresses must not push the nav around, but ordinary ones must fit.
   26ch clears a typical work address; anything longer ellipses and stays
   readable in full via the trigger's title and on the Account page. */
.menu-trigger .who {
  min-width: 0;
  max-width: 26ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.menu[open] .chevron { transform: rotate(180deg); }
.chevron { flex: none; transition: transform 0.12s ease; }

.menu-items {
  position: absolute;
  right: 0;
  top: calc(100% + 0.4rem);
  z-index: 40;
  min-width: 12rem;
  padding: 0.3rem;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-md);
}

.menu-items a,
.menu-items button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0.45rem 0.6rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  text-decoration: none;
  cursor: pointer;
}

.menu-items a:hover,
.menu-items button:hover { background: var(--surface-3); }
.menu-items form { margin: 0; }

/* Signing out is not the same kind of action as visiting a page. Scoped to the
   account chrome: written unscoped, it also painted the split button's "Patch
   only" red and drew a divider above it. */
.account .menu-items form {
  border-top: 1px solid var(--border);
  margin-top: 0.3rem;
  padding-top: 0.3rem;
}
.account .menu-items button { color: var(--alert); }

/* --------------------------------------------------------------- layout */

/* A sticky footer: without it, a short page leaves the build number stranded
   halfway up an empty screen, which reads as a page that failed to finish
   loading. */
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1 0 auto; }

.page {
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
  padding: 1.75rem var(--gutter) 3rem;
}

/* Kept as a hook, but the board now shares the one measure — the management
   board being wider than every other page made it look like a different
   application. */
.page.centred {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 3rem var(--gutter);
}

h1 {
  font-size: 1.4rem;
  font-weight: 650;
  letter-spacing: -0.015em;
  margin: 0 0 0.35rem;
}

h2 {
  font-size: 1.0625rem;
  font-weight: 620;
  letter-spacing: -0.01em;
  margin: 0;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* A readable measure. On a 1280px page an explanatory paragraph was running to
   about 150 characters a line — roughly double what anyone reads comfortably,
   and the reason several cards looked like documentation rather than interface.
   Tables and forms are unaffected: they want the width. */
/* A reading measure for body prose. It applies to paragraphs that stand on
   their own in the page, where there is no container edge to measure them
   against. */
main p { max-width: 74ch; }

/* Inside a card, the card is the measure. Capping the text as well leaves a
   full-width card with its prose stopping halfway and the rest empty, which
   reads as a layout that broke rather than a column that was chosen — the
   Quick Patch, Agent version and Danger zone cards all did this. */
.card p { max-width: none; }

/* The same argument for annotations that sit directly under a full-width table
   or card: a hint is one or two lines of UI text, not an essay, and stopping it
   at 74ch beside a 1230px table looks like a mistake. Body prose keeps its
   measure above; this is only for the labelled kinds.

   Not restricted to direct children: partials wrap their own markup in a div
   for htmx to swap, so the cycle-status empty state is a grandchild of main and
   a `>` here would silently skip it. */
main p.hint,
main p.muted,
main p.small,
/* An alert is a status box, not prose. Capped at a reading measure it sits
   short of the cards it belongs with and reads as a panel that failed to
   load — the same ragged edge everything else on these pages just lost. */
main p.alert { max-width: none; }

.muted { color: var(--muted); }
.hint { color: var(--muted); font-size: 0.8125rem; margin: 0.4rem 0 0; }

/* Live password rules, driven by password.js.
   The rules themselves come from the server via data- attributes; this only
   dresses them. `.hint[data-pw-fallback]` beside it is what a browser with the
   script blocked still shows, so neither state leaves the person guessing. */
.pw-rules {
  list-style: none;
  margin: 0.5rem 0 0.15rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.pw-rule {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  color: var(--muted);
}
.pw-mark {
  position: relative;
  flex: none;
  width: 0.85rem;
  height: 0.85rem;
  border: 1.5px solid currentColor;
  border-radius: 50%;
  opacity: 0.4;
}
/* `display: flex` above is an author rule and so beats the user agent's
   `[hidden] { display: none }`. Without this the rejections that are meant to
   stay out of sight until they apply are visible from the first paint. */
.pw-rule[hidden] { display: none; }
.pw-rule.ok { color: var(--ok-text); }
.pw-rule.bad { color: var(--error-text); }
.pw-rule.ok .pw-mark,
.pw-rule.bad .pw-mark { opacity: 1; border-color: currentColor; background: currentColor; }
/* A tick, drawn rather than typed, so it needs no font that has the glyph. */
.pw-rule.ok .pw-mark::after {
  content: "";
  position: absolute;
  left: 0.27rem;
  top: 0.08rem;
  width: 0.16rem;
  height: 0.35rem;
  border: solid var(--surface);
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}
.small { font-size: 0.8125rem; }

ul.plain { margin: 0.5rem 0 0; padding-left: 1.1rem; }
ul.plain li { margin: 0.15rem 0; }

.sr-only,
.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.page-head {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  margin-bottom: 1.25rem;
}

.page-head h1 { margin: 0; }
.page-head > h1 { margin-right: auto; }

.breadcrumb { margin: 0 0 0.75rem; font-size: 0.8125rem; }
.breadcrumb a { color: var(--muted); text-decoration: none; }
.breadcrumb a:hover { color: var(--text); }
.crumbs { margin: 0 0 0.75rem; font-size: 0.8125rem; color: var(--muted); }
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--text); }

h2.section {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 1.75rem 0 0.75rem;
}

h2.section .button { font-size: 0.8rem; padding: 0.3rem 0.7rem; margin-left: auto; }

.count {
  color: var(--muted);
  font-weight: 500;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

/* A count beside a page title. On a card `--surface-3` would read as a chip,
   but these headings sit on `--bg`, which is nearly the same tone — the badge
   was invisible. It gets the card colour and a hairline instead. */
h1 .count, h2 .count {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.05rem 0.5rem;
  font-size: 0.75rem;
  vertical-align: 3px;
}

.count-alert { color: var(--alert); font-weight: 600; }
h2 .count-alert, h1 .count-alert {
  background: var(--error-bg);
  border-color: var(--error-border);
  color: var(--error-text);
}

.spaced { margin-top: 1rem; }
.spaced-tight { margin-top: 0.5rem; }

/* ------------------------------------------------------------- cards */

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1.25rem;
  box-shadow: var(--shadow-xs);
}

.card + .card { margin-top: 1rem; }
.card.narrow { width: 100%; max-width: 400px; padding: 1.75rem; }

/* Cards use the width they are given. A form card used to be capped at 640px
   with a `wide` class to opt out, which meant the page-wide default was the
   narrow one and every full-width card had to ask. That got the balance
   backwards: a card sitting at 640px in a 1280px page reads as a panel that
   failed to load, and the odd one out on a page of full-width cards reads worse
   still. The only card with a width of its own is `.narrow`, the sign-in box,
   which sets it a few rules below. */

.card.narrow + .card.narrow { margin-top: 1.25rem; }
.card.empty { text-align: left; }
.card.narrow .table-scroll { margin-top: 0.6rem; }
.card.narrow form { margin-top: 0.9rem; }
.card.narrow p.small { margin: 0.6rem 0 0; text-align: center; }
.card.narrow p.small:first-of-type { margin-top: 1.1rem; }

/* The lockup on a sign-in card. The mark and the wordmark are one centred
   line, not a mark stacked above a heading: separated, they read as two
   unrelated things at the top of an anonymous box asking for a password.
   Shared by both sign-in pages, which is why it lives here rather than being
   written once for each. */
.signin-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  margin: 0 0 0.5rem;
  color: var(--accent);
}
.signin-brand .mark { color: inherit; }

/* The engine's badge sits on its own centred line directly beneath, so the
   lockup stays identical to the console's and the badge is the one thing that
   differs — which is the whole job it has on this page. */
.signin-badge { margin: 0 0 0.85rem; text-align: center; }

/* The sign-in card is the only page with no chrome around it, so it carries
   its own vertical rhythm. */
.page.centred .card.narrow { box-shadow: var(--shadow-sm); }

/* ---------------------------------------------------------------- tiles */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 0.75rem;
  margin-bottom: 1.5rem;
}

.tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1rem 1.1rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  box-shadow: var(--shadow-xs);
}

.tile-value {
  font-size: 1.6rem;
  font-weight: 620;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.tile-label {
  color: var(--muted);
  white-space: nowrap;
  font-size: 0.75rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.tile-alert .tile-value { color: var(--alert); }
.tile-warn .tile-value { color: var(--warn); }

a.tile { text-decoration: none; color: inherit; transition: border-color 0.12s ease, box-shadow 0.12s ease; }
a.tile:hover { border-color: var(--accent-border); box-shadow: var(--shadow-sm); }

/* Cards stack with a margin between them, which inside a grid pushes the
   second column down and leaves the two panels misaligned. The grid gap
   already does that job here. */
.panels > .card { margin-top: 0; }

.panels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
  gap: 1rem;
  align-items: start;
}

/* A panel's heading sits inside its card, so it loses the outer margin a
   section heading carries on the page. Written as `h2.panel-head` because
   `h2.section` would otherwise out-specify it and keep its 1.75rem top margin,
   pushing the heading down inside the card. */
h2.panel-head { margin: 0 0 0.5rem; font-size: 0.95rem; }
.panel-head .linkish { margin-left: auto; font-size: 0.8125rem; font-weight: 500; }

/* A panel table is a summary, not the record: no header row, and the card
   already supplies the frame. */
.panels table.grid td { padding: 0.5rem 0.25rem; }
.panels table.grid tbody tr:first-child td { border-top: 0; }

/* ---------------------------------------------------------------- forms */

.stack { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 1.25rem; }

.stack label {
  font-size: 0.8125rem;
  font-weight: 550;
  color: var(--text-2);
  margin-top: 0.6rem;
}

input[type="email"],
input[type="password"],
input[type="text"],
input[type="number"],
input[type="time"],
input[type="search"],
select {
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.875rem;
  width: 100%;
}

input::placeholder { color: var(--muted); }

input:focus-visible,
select:focus-visible,
button:focus-visible,
summary:focus-visible,
a:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
}

input.narrow { width: 7rem; }

/* A textarea is the one field that wants the width it is given, so it is
   exempt from the cap on inputs below. Monospace because the Notes page holds
   pasted machine output as often as prose. */
textarea {
  width: 100%;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font-family: var(--mono);
  font-size: 0.82rem;
  line-height: 1.6;
  resize: vertical;
}

textarea:focus-visible { outline: none; border-color: var(--accent); box-shadow: var(--ring); }

/* Fields fill the card, for the same reason the card fills the page: a field
   stopping two thirds of the way across reads as unfinished, and the empty
   third beside it is space nothing uses. The sign-in card is unaffected —
   `.card.narrow` is 400px, so its fields are naturally short.

   Two exceptions, and they are about the value rather than the layout. A
   number or a time holds three or four characters, and a 1200px box for "300"
   is not using the space, it is advertising that it has nothing to put there.
   These keep a width that matches what they hold. */
form.stack input[type="number"],
form.stack input[type="time"] { max-width: 12rem; }

/* A submit button is as wide as its label. The exception is the sign-in card
   and its siblings, where a full-width button is the expected shape of the one
   thing that screen is for. */
form.stack button { align-self: flex-start; }
.card.narrow form.stack button { align-self: stretch; }

/* A number input followed by the sentence that explains it, with nothing
   between them, reads as one run-on control. */
table.grid td input.narrow { margin-right: 0.55rem; }

/* Two hints in a row, and a button after one, need air. */
.hint + .hint { margin-top: 0.35rem; }
.hint + button, .hint + .button { margin-top: 0.9rem; }

/* --------------------------------------------------------------- buttons */

/* Base first, variants after. `.filters button` and `button.primary` have the
   same specificity, so with primary written first a primary button inside a
   filter bar would lose to the neutral style purely on source order. */
button {
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 550;
  cursor: pointer;
  box-shadow: var(--shadow-xs);
}

button:hover { background: var(--surface-3); }

/* The page's main action. Also on `<a class="button">`, which is why the two
   share a rule: a link that acts like a button must look like one. */
.button,
button.primary {
  display: inline-block;
  padding: 0.5rem 0.9rem;
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent);
  color: var(--accent-text);
  text-decoration: none;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 550;
  cursor: pointer;
  box-shadow: var(--shadow-xs);
  transition: background 0.12s ease, border-color 0.12s ease;
}

.button:hover,
button.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

/* Destructive actions are outlined, not filled. Revoking a system or ending
   every session should require a deliberate look, and a wall of red buttons
   stops meaning anything. */
button.danger,
.linkish.danger {
  border: 1px solid var(--error-border);
  background: var(--surface);
  color: var(--alert);
  padding: 0.5rem 0.9rem;
  border-radius: var(--radius);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 550;
  cursor: pointer;
  box-shadow: none;
}

button.danger:hover,
.linkish.danger:hover { background: var(--error-bg); }

/* Secondary action: an anchor that should look like the neutral button rather
   than the primary one. */
a.button-quiet,
.button.button-quiet {
  background: var(--surface);
  border-color: var(--border-strong);
  color: var(--text);
}

a.button-quiet:hover,
.button.button-quiet:hover { background: var(--surface-3); border-color: var(--border-strong); }

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.2rem;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: none;
  color: var(--muted);
  cursor: pointer;
  list-style: none;
  box-shadow: none;
}

.icon-button::-webkit-details-marker { display: none; }
.icon-button:hover { color: var(--text); background: var(--surface-3); }
.icon-button:focus-visible { outline: none; box-shadow: var(--ring); }

/* A split button: primary action, plus a menu of the alternatives.
   `position: relative` and `inline-flex` together are what make the dropdown
   land under the button rather than at the edge of the page — the group becomes
   the containing block, and shrinks to the width of its two buttons instead of
   spanning the card. Stated here rather than inherited from `.menu`, so it
   cannot drift if that rule changes for the account chrome it was written for. */
.split {
  display: inline-flex;
  align-items: stretch;
  gap: 0;
  margin-top: 1rem;
  position: relative;
}

/* Both halves must be the same height. The form and the <details> are the flex
   items, so `align-items: stretch` sizes *them* — the button and the summary
   inside are not touched, which left the chevron half short and top-aligned.
   Each half is a flex container in turn, so the control inside fills it. */
.split > form,
.split-more { display: flex; margin: 0; }

.split .primary,
.split-more > summary { flex: 1; justify-content: center; }

.split .primary { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.split-more { position: static; }

.split-more > summary {
  display: inline-flex;
  align-items: center;
  background: var(--accent);
  color: var(--accent-text);
  border: 1px solid var(--accent);
  border-left-color: rgba(255, 255, 255, 0.3);
  border-top-left-radius: 0; border-bottom-left-radius: 0;
  border-top-right-radius: var(--radius); border-bottom-right-radius: var(--radius);
  padding: 0.5rem 0.6rem;
  cursor: pointer;
  box-shadow: var(--shadow-xs);
}

.split-more > summary:hover,
.split-more[open] > summary { background: var(--accent-hover); color: var(--accent-text); }
.split .menu-items { left: 0; right: auto; min-width: 11rem; }

/* ------------------------------------------------- machine-written text */

/* The rule: prose a person wrote is sans; anything a machine produced is mono.
   A hostname, a version, a package name, an id, a count, a timestamp. It is
   not decoration — it tells a reader at a glance which of the two they are
   looking at, and it makes two version strings in adjacent rows comparable
   character by character, which is the entire job of that table. */
code,
.facts code,
.tile-value,
.count,
.pill,
.lane-count,
.footer,
.mono,
.system-name,
td.mono, td.mono a {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}

/* Identifiers run a little large in mono beside the sans around them. */
td.mono, td.mono a, .system-name, .mono { font-size: 0.92em; letter-spacing: -0.01em; }

/* Applied by class, never by column position. A first column holds a hostname
   on one page and a project name on another, and a project name is something a
   person typed — putting it in the machine face would say the opposite of what
   the rule is for. "just now" and "no cycle yet" are prose too, however
   numeric the column they sit in. */

/* Micro-labels read as instrument legends rather than headings. */
table.grid thead th,
.fact-label,
.tile-label,
.org-label,
.output-label {
  font-family: var(--mono);
  font-weight: 500;
  letter-spacing: 0.06em;
}

/* --------------------------------------------------------------- alerts */

.alert {
  margin: 1rem 0 0;
  padding: 0.7rem 0.9rem;
  border-radius: var(--radius);
  border: 1px solid transparent;
  font-size: 0.85rem;
}

.alert.error { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); }
.alert.warn { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }
.alert.ok { background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); }

/* -------------------------------------------------------------- filters */

.filters {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
}

.filters input[type="search"] { width: 15rem; }

.filters label {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.8125rem;
  color: var(--text-2);
  white-space: nowrap;
  cursor: pointer;
}

.chip {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--muted);
  text-decoration: none;
  font-size: 0.8125rem;
  font-weight: 500;
}

.chip:hover { border-color: var(--accent-border); color: var(--text); }
.chip-on { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }

/* --------------------------------------------------------------- tables */

.table-scroll {
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

/* A card already draws the border and the corners. */
.card .table-scroll { border: 0; border-radius: 0; background: none; }

table.grid {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
}

table.grid th,
table.grid td { padding: 0.6rem 0.85rem; text-align: left; }

table.grid thead th {
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
}

.card table.grid thead th { background: none; }

table.grid tbody tr + tr td { border-top: 1px solid var(--border); }
table.grid tbody tr:hover { background: var(--surface-2); }

table.grid th.num,
table.grid td.num { text-align: right; }
table.grid td.num { font-variant-numeric: tabular-nums; }

table.grid a { color: var(--text); text-decoration: none; font-weight: 550; }
table.grid a:hover { color: var(--accent); }

.row-bad td { background: var(--error-bg); }
.row-bad:hover td { background: var(--error-bg); }

/* ---------------------------------------------------------------- pills */

.pill {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  border: 1px solid transparent;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  white-space: nowrap;
  vertical-align: 1px;
}

.pill-online,
.pill-succeeded,
.pill-completed { background: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-text); }

.pill-stale,
.pill-warn,
.pill-pending,
.pill-dispatched,
.pill-running { background: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-text); }

.pill-alert,
.pill-revoked,
.pill-failed,
.pill-expired,
.pill-halted,
.pill-abandoned { background: var(--error-bg); border-color: var(--error-border); color: var(--error-text); }

/* ----------------------------------------------------------------- facts */

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 1rem 1.75rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1.1rem 1.25rem;
  box-shadow: var(--shadow-xs);
  margin-bottom: 0.5rem;
}

.facts > div {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  font-size: 0.85rem;
  min-width: 0;
}

.fact-label {
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.facts code { font-family: var(--mono); font-size: 0.8em; word-break: break-all; }
.facts code.small { font-size: 0.75em; }

/* ---------------------------------------------------------------- pager */

.pager { display: flex; align-items: center; gap: 0.75rem; margin-top: 1rem; }
.pager a { color: var(--text); text-decoration: none; }
.pager a:hover { color: var(--accent); }
.pager .disabled { color: var(--muted); }

/* --------------------------------------------------------------- spinner */

.spinner {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  vertical-align: -1px;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Respect a user who has asked for less motion. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: none; border-top-color: var(--border); }
}

/* --------------------------------------------------------- code + output */

pre {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.8rem 0.95rem;
  overflow-x: auto;
  font-family: var(--mono);
  font-size: 0.8rem;
  line-height: 1.5;
  margin: 0.6rem 0 0;
}

code { font-family: var(--mono); font-size: 0.85em; }

.output-label {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin: 0.9rem 0 0;
}

pre.output { max-height: 22rem; overflow: auto; }

/* --------------------------------------------------------------- footer */

.footer {
  flex: none;
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
  padding: 0.9rem var(--gutter) 1.5rem;
  border-top: 1px solid var(--border);
  color: var(--muted);
  text-align: center;
  font-size: 0.7rem;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------- fleet views */

.pkg { white-space: nowrap; }

/* Without this the package name takes every spare pixel and the two version
   columns huddle at the right-hand edge, which is exactly the comparison the
   table exists to make. Scoped to this one table: written as
   `th:first-child` it also stretched the first column of the audit trail and
   every other grid on the site. */
table.updates td:first-child, table.updates th:first-child { width: 45%; }
.pkg-security { color: var(--alert); display: inline-block; vertical-align: -2px; margin-right: 0.3rem; }
.pkg-security-spacer { display: inline-block; width: 13px; margin-right: 0.3rem; }

select { width: auto; min-width: 12rem; }

/* --------------------------------------------------------- patch cycles */

.cycle-head { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }

/* The tiers of a running cycle, as a ladder. Each rung is a tier; the
   connector between them is the delay. */
.tier-ladder { display: flex; flex-direction: column; gap: 0; margin-top: 1rem; }

.tier-ladder .tier + .tier { position: relative; margin-top: 1.1rem; }

/* The line joining one tier to the next, drawn in the gap above it. */
.tier-ladder .tier + .tier::before {
  content: "";
  position: absolute;
  left: 1.25rem;
  top: -1.1rem;
  height: 1.1rem;
  width: 2px;
  background: var(--border-strong);
}

.tier {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 0.85rem 1rem;
  box-shadow: var(--shadow-xs);
}

/* The tier being worked on right now is the one the eye should find. */
.tier-current { border-color: var(--accent-border); box-shadow: var(--shadow-sm); }
.tier-completed { background: var(--surface-2); }
.tier-halted { border-color: var(--error-border); background: var(--error-bg); }

.tier-head { display: flex; align-items: baseline; gap: 0.6rem; }
.tier-name { font-weight: 600; }
.tier-meta { margin-left: auto; color: var(--muted); font-size: 0.8125rem; font-variant-numeric: tabular-nums; }

progress.bar {
  display: block;
  width: 100%;
  height: 6px;
  border: 0;
  border-radius: 999px;
  margin-top: 0.6rem;
  background: var(--surface-3);
  appearance: none;
  -webkit-appearance: none;
}

progress.bar::-webkit-progress-bar { background: var(--surface-3); border-radius: 999px; }
progress.bar::-webkit-progress-value {
  background: var(--accent);
  border-radius: 999px;
  transition: width 0.3s ease;
}
progress.bar::-moz-progress-bar { background: var(--accent); border-radius: 999px; }

.tier-completed progress.bar::-webkit-progress-value { background: var(--ok); }
.tier-completed progress.bar::-moz-progress-bar { background: var(--ok); }
.tier-halted progress.bar::-webkit-progress-value { background: var(--alert); }
.tier-halted progress.bar::-moz-progress-bar { background: var(--alert); }

/* ------------------------------------------------------- management board */

.org-banner {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  flex-wrap: wrap;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 1rem 1.25rem;
  margin-bottom: 1.25rem;
  box-shadow: var(--shadow-xs);
}

.org-label {
  color: var(--muted);
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.org-name { font-size: 1.25rem; font-weight: 650; letter-spacing: -0.015em; margin: 0; }
.org-meta { margin-left: auto; display: flex; align-items: center; gap: 0.75rem; color: var(--muted); font-size: 0.8125rem; }
.org-meta .button { font-size: 0.8rem; padding: 0.3rem 0.7rem; }

.board-help { color: var(--muted); font-size: 0.8125rem; margin: 0 0 1rem; }
.board { display: flex; flex-direction: column; gap: 0.75rem; }
.board.is-busy { opacity: 0.6; pointer-events: none; }

/* ---------------------------------------------- a collection, collapsed */

.collection {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-xs);
  /* No `overflow: hidden`. It was here to keep the body's square corners inside
     the rounded card, and it also clipped the rename popover — which is
     absolutely positioned below the summary, so on a *collapsed* collection
     there is no body underneath and the popover fell outside the box and was
     cut in half. The corners are handled by rounding the head and body
     themselves, a few rules down, which clips nothing. */
}

.collection-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.85rem 1rem;
  cursor: pointer;
  list-style: none;
  user-select: none;
}

.collection-head::-webkit-details-marker { display: none; }
.collection-head:hover { background: var(--surface-2); }

/* Rounding, in place of clipping the whole card. Closed, the head is the only
   thing showing and takes all four corners; open, it keeps the top two and the
   body takes the bottom two. */
.collection-head { border-radius: var(--radius-lg); }
.collection[open] .collection-head {
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}

.chevron-slot { display: inline-flex; color: var(--muted); transition: transform 0.12s ease; }
.collection[open] .chevron-slot { transform: rotate(90deg); }

.collection-name { font-weight: 620; }
.collection-facts { margin-left: auto; display: flex; align-items: center; gap: 0.6rem; font-size: 0.8125rem; color: var(--muted); }

.collection-body {
  padding: 0.9rem 1rem 1.1rem;
  background: var(--surface-2);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
}
.collection-settings { margin-left: 0.2rem; font-size: 0.8rem; padding: 0.3rem 0.7rem; }

/* ------------------------------------------------------------- rename */

.rename > summary { list-style: none; cursor: pointer; }
.rename > summary::-webkit-details-marker { display: none; }
.rename > summary:hover { color: var(--text); }
/* Secondary action: an anchor that should look like the neutral button rather
   than the primary one. */
a.button-quiet,
.button.button-quiet {
  background: var(--surface);
  border-color: var(--border-strong);
  color: var(--text);
}

a.button-quiet:hover,
.button.button-quiet:hover { background: var(--surface-3); border-color: var(--border-strong); }

.icon-button { color: var(--text); background: var(--surface-3); }

.rename-form {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.3rem;
  box-shadow: var(--shadow-md);
}

.rename-form input[type="text"] { width: 11rem; padding: 0.3rem 0.5rem; font-size: 0.8125rem; }
.rename-form button { padding: 0.3rem 0.6rem; font-size: 0.8125rem; }

/* The popover is positioned by a wrapper so opening it cannot move the row —
   an editor that reflows the thing it is editing is disorienting, and inside a
   collapsed project it used to open where nobody could see it. */
.lane-head .rename { position: relative; }
.lane-head .rename .rename-form { position: absolute; left: 0; top: calc(100% + 0.3rem); z-index: 10; }
.lane-head .rename .rename-form input[type="text"] { width: 10rem; }

.rename-slot { display: inline-flex; position: relative; z-index: 5; }
.rename-slot .rename-form { position: absolute; left: 0; top: calc(100% + 0.3rem); z-index: 10; }
.rename-slot .rename-form input[type="text"] { width: 12rem; }

/* --------------------------------------------------------- swim lanes */

.lanes {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.75rem;
  align-items: start;
}

.lane {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.7rem 0.8rem;
  min-height: 6rem;
}

/* An unused third tier should not claim the same visual weight as one doing
   work: it slims down and steps back. */
.lane-empty {
  background: none;
  border-style: dashed;
  min-height: 3.5rem;
}

.lane-empty .lane-name,
.lane-empty .lane-delay { color: var(--muted); }
.lane-empty .lane-count { display: none; }
.lane-empty:hover,
.lane-empty:has(.drop.is-over) { border-color: var(--accent-border); background: var(--surface); }

.lane-head { display: flex; align-items: baseline; gap: 0.5rem; }
.lane-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.lane-count {
  color: var(--muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* `margin-left: auto` only ever meant "sit at the right of the lane header".
   On a paragraph it combines with the 74ch reading measure to push the text
   right of a wide container — which is what centred the Unassigned line. It is
   a header element, so it says so. */
.lane-head .lane-delay { margin-left: auto; }
.lane-delay { color: var(--muted); font-size: 0.75rem; white-space: nowrap; }

.unassigned { margin-top: 0.75rem; }
.unassigned:has(.drop-hint-good) { border-color: var(--ok-border); }
.unassigned .drop-wide { min-height: 3rem; }

/* ------------------------------------------------------- drop targets */

/* A <ul>, so it arrives with a marker and a 40px indent. Both have to go: the
   items are draggable cards, not a bulleted list, and the empty-state line was
   showing a stray dot away to the left of itself. */
.drop {
  list-style: none;
  padding-left: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  margin-top: 0.6rem;
  padding: 0.25rem;
  border-radius: var(--radius-sm);
  min-height: 2.5rem;
}

.drop.is-over {
  background: var(--accent-soft);
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
}

.drop-hint {
  color: var(--muted);
  font-size: 0.75rem;
  text-align: center;
  padding: 0.5rem 0;
}

.drop.is-over .drop-hint { color: var(--accent); }
.drop-wide .drop-hint { padding: 0.75rem 0; }
.drop-hint-good { color: var(--ok-text); }
.drop.is-over .drop-hint-good { color: var(--ok-text); }

/* ----------------------------------------------------------- a system */

.system {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.4rem 0.6rem;
  cursor: grab;
  font-size: 0.8125rem;
}

.system:hover { border-color: var(--border-strong); box-shadow: var(--shadow-xs); }
.system.is-dragging { opacity: 0.4; cursor: grabbing; }

/* Nothing to drag: revoked systems will not patch wherever they are put. */
.system.status-revoked { cursor: default; opacity: 0.6; }

.system-name { color: var(--text); text-decoration: none; font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.system-name:hover { color: var(--accent); }
.system-meta { margin-left: auto; display: flex; align-items: center; gap: 0.35rem; }

.dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--muted); }
.status-online .dot { background: var(--ok); }
.status-stale .dot { background: var(--warn); }
.status-revoked .dot { background: var(--alert); }

/* ----------------------------------------------------- upgrade choices */

.choices { margin-top: 0.75rem; }
.choices-label { color: var(--muted); font-size: 0.8125rem; margin: 0 0 0.5rem; }
.choice-row { display: flex; gap: 0.5rem; flex-wrap: wrap; }

.choice {
  padding: 0.45rem 0.8rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 0.85rem;
  cursor: pointer;
}

.choice:hover { border-color: var(--accent-border); background: var(--surface-2); }
.choice:focus-visible { outline: none; box-shadow: var(--ring); }
.choice-default { border-color: var(--accent); background: var(--accent); color: var(--accent-text); }
.choice-default:hover { background: var(--accent-hover); border-color: var(--accent-hover); }

/* --------------------------------------------------------- narrow screens */

@media (max-width: 900px) {
  .lanes { grid-template-columns: 1fr; }
  .topbar { gap: 0.75rem; padding: 0 1rem; }
  .topbar nav { overflow-x: auto; }
  .page { padding: 1.25rem 1rem 3rem; }
}

/* Tier lanes on the collection page. The Manage board draws lanes side by side
   because it is about moving systems between them; this page stacks them,
   because it is about the order they run in and that order is vertical. */
.lanes-stacked { display: flex; flex-direction: column; gap: 0.9rem; }
.lanes-stacked .lane { width: 100%; }

/* The tier's number, so the reading order is unambiguous even when the names
   are things like "Canary" and "Prod" that carry no sequence of their own. */
.lane-position {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 50%;
  background: var(--surface-3);
  color: var(--muted);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* The name is editable in place, so it is an input that reads as a heading
   until it is focused. */
.lane-name-input {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 22rem;
  font-weight: 600;
  background: transparent;
  border-color: transparent;
}
.lane-name-input:hover { border-color: var(--border); }
.lane-name-input:focus { background: var(--surface); border-color: var(--accent); }

/* The row of controls that says when this tier starts. Wraps rather than
   scrolls: it holds five controls on the widest tier and a phone has room for
   about two. */
.lane-when {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0.6rem 0 0.2rem;
}
.lane-when select,
.lane-when input[type="time"],
.lane-when input[type="number"] { max-width: 12rem; }
.lane-when input[type="number"] { max-width: 5rem; }
.lane-when-label { color: var(--muted); font-size: 0.8125rem; }

/* The mode select names the whole condition, so it needs room for its longest
   option rather than the width a browser picks from the shortest. */
.lane-mode { min-width: 16rem; max-width: 20rem; }

/* The two groups of controls the mode chooses between. Only one is shown once
   tiers.js runs; both are visible, and both correct, without it. */
.lane-when-group { display: inline-flex; align-items: center; gap: 0.4rem; }
.lane-when-group[hidden] { display: none; }

/* The systems in the tier. Read-only here — moving one is the Manage board's
   job, and this page links to it rather than growing a second way to do it. */
.lane-systems { list-style: none; margin: 0.5rem 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.4rem; }
.lane-system {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  font-size: 0.8125rem;
}

/* Security updates outstanding on a system in a tier. Named for what it means
   rather than for the colour, matching the other pills. */
.pill-bad { background: var(--error-bg); color: var(--error-text); border-color: var(--error-border); }
