/* The palette, unchanged from the flat-tab portal — same greens, same warm
   greys, same pills. Only the layout around it is new.

   Declared three times rather than once, because a toggle cannot override a
   media query. Bare :root is light; the media block applies dark only when the
   viewer has not explicitly chosen light; the attribute block wins over both.
   No colour has its only definition inside a conditional. */
:root {
  --bg: #fbfbfa;
  --panel: #ffffff;
  --ink: #1c1c1a;
  --muted: #6d6c66;
  --line: #e3e2dd;
  --accent: #2f5d50;
  --good-bg: #e2efe6;
  --good-ink: #1f5136;
  --bad-bg: #f8e3e0;
  --bad-ink: #8a2b1c;
  --warn-bg: #f6eeda;
  --warn-ink: #7a5a15;
  --shadow: 0 1px 2px rgba(28, 28, 26, 0.06), 0 8px 24px rgba(28, 28, 26, 0.06);
  --sidebar: 232px;
  --topbar: 52px;

  /* The spacing scale. It is here because it was not anywhere: twelve
     declarations referenced var(--space-N) against a variable no stylesheet
     defined, and an unresolvable var() is invalid at computed-value time — so
     every one of them silently became nothing. That is why the roster lines and
     the deployment cards had their text against the border. Not a taste
     problem; a missing definition.

     Four steps, 4/8/12/20px. Two adjacent steps have to be tellable apart or
     the scale is decoration, which is why there is no 16px between 3 and 4. */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1.25rem;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #16161a;
    --panel: #1e1e23;
    --ink: #ecebe6;
    --muted: #9a988f;
    --line: #32323a;
    --accent: #7fb8a4;
    --good-bg: #1d3a2c;
    --good-ink: #a4dcbe;
    --bad-bg: #3d2320;
    --bad-ink: #f0a99b;
    --warn-bg: #3a3120;
    --warn-ink: #e3c583;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
  }
}

:root[data-theme="dark"] {
  --bg: #16161a;
  --panel: #1e1e23;
  --ink: #ecebe6;
  --muted: #9a988f;
  --line: #32323a;
  --accent: #7fb8a4;
  --good-bg: #1d3a2c;
  --good-ink: #a4dcbe;
  --bad-bg: #3d2320;
  --bad-ink: #f0a99b;
  --warn-bg: #3a3120;
  --warn-ink: #e3c583;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* ===== Topbar ===== */

.topbar {
  position: sticky;
  top: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  height: var(--topbar);
  padding: 0 1rem;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}

.topbar-right {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.brand {
  font-weight: 650;
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  margin: 0;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--bg);
  color: var(--muted);
  cursor: pointer;
}

.icon-btn:hover { color: var(--ink); }

/* The profile is a placeholder until preferences exist, so it is deliberately
   an outline rather than anything that suggests an identity is loaded. */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--muted);
  text-decoration: none;
}

.avatar:hover { color: var(--ink); }

/* Only ever visible below the layout breakpoint. */
.menu-btn { display: none; }

/* ===== Layout ===== */

.shell {
  display: flex;
  align-items: flex-start;
}

.sidebar {
  flex: 0 0 var(--sidebar);
  width: var(--sidebar);
  position: sticky;
  top: var(--topbar);
  height: calc(100vh - var(--topbar));
  overflow-y: auto;
  padding: 1rem 0.6rem 2rem;
  border-right: 1px solid var(--line);
  background: var(--panel);
}

.sidebar-overlay { display: none; }

.nav-section { margin-bottom: 0.15rem; }

.nav-section > a {
  display: block;
  padding: 0.4rem 0.65rem;
  border-radius: 5px;
  color: var(--ink);
  text-decoration: none;
  font-size: 0.9rem;
  font-weight: 550;
}

.nav-section > a:hover { background: var(--bg); }
.nav-section > a.on { color: var(--accent); background: var(--bg); }

.nav-pages {
  margin: 0.1rem 0 0.5rem;
  padding: 0 0 0 0.65rem;
  list-style: none;
  border-left: 1px solid var(--line);
  margin-left: 0.65rem;
}

.nav-pages a {
  display: block;
  padding: 0.25rem 0.5rem;
  border-radius: 4px;
  color: var(--muted);
  text-decoration: none;
  font-size: 0.85rem;
}

.nav-pages a:hover { color: var(--ink); }
.nav-pages a.on { color: var(--accent); font-weight: 600; }

main {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 1100px;
  padding: 1.5rem 1.5rem 4rem;
}

h1 { font-size: 1.5rem; margin: 0 0 1.25rem; letter-spacing: -0.02em; }
h2 { font-size: 1rem; margin: 2rem 0 0.65rem; letter-spacing: -0.01em; }
h3 { font-size: 0.9rem; margin: 1.25rem 0 0.5rem; }

section:first-of-type h2 { margin-top: 0.5rem; }

/* The breadcrumb, which is also the page heading — see views/partials/head.ejs.
   Small and uppercase because it is navigation first: the page says what it is
   in its own content, and a 1.5rem shout of "AGENTS › RUNS" would say it
   twice. Wraps rather than scrolls, so three levels still fit on a phone. */
.crumbs { margin: 0 0 1.25rem; }

.crumbs h1 {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem;
}

.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--ink); text-decoration: underline; }
.crumbs a[aria-current="page"] { color: var(--ink); }
.crumbs .sep { color: var(--muted); opacity: 0.6; }

/* A run id, an app name, a host name. Left as it is written everywhere else,
   because shouting an identifier makes it harder to match against the one in
   the URL or the table you came from. */
.crumbs .name { text-transform: none; letter-spacing: 0; }

/* ===== Tables ===== */

/* Wide tables scroll inside their own box rather than pushing the page sideways
   — the run and event tables are both wider than a phone. */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
}

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--panel);
  font-size: 0.875rem;
}

/* A table not wrapped in .table-wrap keeps the border it used to draw itself. */
table:not(.table-wrap > table) {
  border: 1px solid var(--line);
  border-radius: 6px;
  overflow: hidden;
}

th, td {
  text-align: left;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
}

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

tbody tr:last-child td { border-bottom: none; }

td a { color: var(--accent); }

/* Date over time, so the eye can run down the times while the date stays
   available on the rows where it changes. */
.ts { white-space: nowrap; line-height: 1.3; }
.ts .d { display: block; color: var(--muted); font-size: 0.78rem; }
.ts .t { display: block; }

.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.82em;
}

.payload {
  max-width: 380px;
  overflow-wrap: anywhere;
  color: var(--muted);
}

.num { text-align: right; white-space: nowrap; }

/* ===== Pills, notes, cards ===== */

.pill {
  display: inline-block;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  font-size: 0.75rem;
  background: var(--line);
  color: var(--muted);
  white-space: nowrap;
}

.pill.good { background: var(--good-bg); color: var(--good-ink); }
.pill.bad { background: var(--bad-bg); color: var(--bad-ink); }
.pill.warn { background: var(--warn-bg); color: var(--warn-ink); }

/* ===== One run's tests =====

   A list rather than a table: the fields are a status, a name, and sometimes an
   error of wildly varying length, which is the shape tables are worst at. It
   also means no separate mobile treatment — this reads the same at 390px and
   1400px. */
.tests { list-style: none; margin: 0; padding: 0; }

.test {
  padding: 0.6rem 0 0.6rem 0.7rem;
  border-left: 3px solid var(--line);
  margin-bottom: 0.5rem;
}
.test:last-child { margin-bottom: 0; }

/* Colour on the rail rather than the background: a page of thirteen tinted
   blocks is harder to read than a page with three marked edges. */
.test-failed { border-left-color: var(--bad-ink); }
.test-flaky { border-left-color: var(--warn-ink); }
.test-passed { border-left-color: var(--good-ink); }

.test-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.45rem;
}

.test-name { font-weight: 500; }

.test-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.7rem;
  font-size: 0.78rem;
  margin-top: 0.15rem;
}

.test-error {
  margin: 0.35rem 0 0;
  font-size: 0.8rem;
  color: var(--bad-ink);
  overflow-wrap: anywhere;
}

.skipped-fold { margin-top: 0.75rem; }
.skipped-fold > summary {
  cursor: pointer;
  font-size: 0.85rem;
  color: var(--muted);
  padding: 0.3rem 0;
}
.skipped-fold[open] > summary { margin-bottom: 0.4rem; }

/* The gate's own report, embedded. Tall because it is a full application and a
   short frame makes it useless; the viewport unit keeps it sane on a phone,
   where 900px of iframe would be the whole page and then some. */
.report {
  width: 100%;
  height: min(70vh, 640px);
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
}

/* One check and its findings on a line, wrapping rather than widening the cell —
   several findings on one check is the normal case, not the exception. */
.check {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.35rem;
  margin-bottom: 0.25rem;
}
.check:last-child { margin-bottom: 0; }

/* ===== Platform components, told apart from what runs on them =====

   Two devices, chosen by what kind of list it is.

   A *roster* — apps, gates, deployments — has no inherent order, so the tier
   becomes a heading and the groups sit apart. A *timeline* — runs, events — is
   ordered by time, and splitting it in two destroys the property that makes it
   a timeline; there the row carries `.pill.platform` instead.

   Marked with a rule and a heading rather than a coloured panel, because colour
   in this portal already means outcome — good, warn, bad. A third meaning on
   the same channel would make a perfectly healthy platform read as a warning. */
.tier { margin-bottom: 2rem; }
.tier:last-child { margin-bottom: 0; }

.tier-head {
  font-size: 0.8rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted);
  font-weight: 600;
  margin: 0 0 0.5rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--line);
}

.tier-head .muted { font-weight: 400; }

/* The heavier rule on the platform group, so the eye lands there first. */
.tier-platform .tier-head { border-bottom-color: var(--ink); color: var(--ink); }

.tier .note { margin-top: 0; }

/* For timelines, where grouping is not available. Deliberately quiet: it marks
   a row, it does not raise an alarm about one. */
.pill.platform {
  background: transparent;
  border: 1px solid var(--line);
  color: var(--muted);
  letter-spacing: 0.04em;
}

/* ===== Tables that stop being tables on a phone =====

   `.table-wrap` scrolls sideways, which is the right escape hatch for a genuinely
   wide table and the wrong primary layout for the thing you check from your phone.
   A four-column table in a 390px viewport is a scrollbar and one and a half
   columns; the ten-column gates table is worse.

   So below the breakpoint each row becomes a card and each cell labels itself from
   `data-label`. One template, two layouts — the alternative was a second block of
   markup per page, and two renderings of one truth drift the moment somebody edits
   only the one they were looking at.

   The breakpoint is 720px rather than the 899px the sidebar uses. Between the two
   the drawer has already freed the full width, and a table that fits should stay a
   table: cards are a concession, not an improvement.

   Opt in with `.cards-sm` rather than applying to every table, because a two-column
   table is already fine and would only get taller. */
@media (max-width: 720px) {
  /* The scroller has nothing to scroll once rows are blocks, and leaving it
     armed puts a stray horizontal scrollbar under the cards. */
  .table-wrap:has(.cards-sm) {
    overflow-x: visible;
    border: none;
    background: none;
  }

  .cards-sm,
  .cards-sm tbody,
  .cards-sm tr,
  .cards-sm td {
    display: block;
    width: 100%;
  }

  /* Headers are the labels, and they move onto each cell below. */
  .cards-sm thead { display: none; }

  .cards-sm tr {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 0.65rem 0.8rem;
    margin-bottom: 0.6rem;
  }
  .cards-sm tr:last-child { margin-bottom: 0; }

  .cards-sm td {
    border: none;
    padding: 0.2rem 0;
  }

  .cards-sm td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 0.68rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 0.05rem;
  }

  /* The first cell is what the card is *about*, so it reads as a heading rather
     than as another labelled field. */
  .cards-sm td.card-title {
    font-size: 1rem;
    font-weight: 600;
    padding-bottom: 0.35rem;
  }
  .cards-sm td.card-title::before { content: none; }

  /* An empty cell would otherwise render as a label with nothing under it —
     common in these tables, where a trailing cell holds an optional action. */
  .cards-sm td:empty { display: none; }

  /* Long identifiers — image refs, URLs, shas — must wrap rather than push the
     card wider than the screen. */
  .cards-sm td.mono,
  .cards-sm td.payload {
    overflow-wrap: anywhere;
  }
}

/* ===== The handful of things that used to be inline style attributes =====

   Moved here so the portal's CSP can be `style-src 'self'` with no
   `unsafe-inline` — which is what makes the header worth sending at all, since
   a policy that permits inline styles permits an injected one. Named for what
   they are for rather than for what they do, so a second use reads correctly. */

/* Several small forms sitting on one line as a row of buttons, rather than each
   claiming a block of its own. */
form.inline { display: inline; }

/* An editor that fills the column it is in. */
.fill { width: 100%; }

/* Stored text shown as written — a definition revision, a captured log — where
   the line breaks are part of the content. */
.wrap { white-space: pre-wrap; }

/* The quiet half of a pair of actions: the one that does not do the thing. */
.button-link.secondary { background: var(--line); color: var(--ink); margin-left: 0.5rem; }

/* What a gate checked, beside the pill saying how it went. Small and quiet:
   the pill is the answer and this is the working, and a table row that shouts
   both is a table nobody scans. */
.summary { display: block; font-size: 0.75rem; margin-top: 0.2rem; }

/* Findings, named rather than counted — the reason they travel home at all.
   Unstyled markers, because the severity pill already opens each line. */
.findings { list-style: none; margin: 0.35rem 0 0; padding: 0; font-size: 0.8rem; }
.findings li { margin: 0.15rem 0; }

.counts { display: flex; gap: 1.25rem; flex-wrap: wrap; margin: 0 0 1rem; }
.count { font-size: 0.85rem; color: var(--muted); }
.count strong { color: var(--ink); font-size: 1rem; }

.empty, .note { color: var(--muted); font-size: 0.875rem; margin: 0 0 1rem; }
.note.good { color: var(--good-ink); }
.note.bad { color: var(--bad-ink); }
.muted { color: var(--muted); }
.error { color: var(--bad-ink); }

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

.card {
  display: block;
  padding: 0.8rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  text-decoration: none;
  color: inherit;
}

.card:hover { border-color: var(--accent); }

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

.card .value {
  font-size: 1.35rem;
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0.15rem 0 0.1rem;
}

.card .sub { font-size: 0.8rem; color: var(--muted); }

/* Two columns that become one on a narrow screen. */
.split {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 1.5rem;
}

/* ===== Waiting-for-you list ===== */

.todo { list-style: none; margin: 0 0 1rem; padding: 0; }

.todo li {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
  margin-bottom: 0.4rem;
  font-size: 0.9rem;
}

.todo li.bad { border-left: 3px solid var(--bad-ink); }
.todo li.warn { border-left: 3px solid var(--warn-ink); }
.todo li a { color: var(--accent); }
.todo .grow { flex: 1 1 auto; }

/* ===== Forms ===== */

input, select, textarea {
  width: 100%;
  padding: 0.3rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg);
  color: var(--ink);
  font-size: 0.9rem;
}

input[type="checkbox"] { width: auto; }

/* A dropdown is never improved by being wider than its longest option. A text
   field can use every pixel it is given; "Stand-alone app" cannot, and stretched
   to the full width of the page it read as an unfinished form. */
select { max-width: 24rem; }

button {
  margin-top: 1rem;
  padding: 0.45rem 1.1rem;
  border: none;
  border-radius: 5px;
  background: var(--accent);
  color: var(--bg);
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

button.danger { background: var(--bad-ink); }

button.quiet {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--line);
}

code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85em;
  background: var(--line);
  padding: 0.1rem 0.3rem;
  border-radius: 3px;
}

.button-link {
  display: inline-block;
  padding: 0.4rem 0.9rem;
  border-radius: 5px;
  background: var(--accent);
  color: var(--bg);
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: none;
}

/* Secrets and commands are copied by hand — they must not wrap ambiguously. */
.token {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.82rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: 5px;
  padding: 0.7rem 0.9rem;
  overflow-x: auto;
  white-space: pre;
  user-select: all;
}

td label { font-weight: 600; }

/* ===== Errors in a table ===== */

/* The message used to be dumped into a full-width row under every failed run,
   which doubled the height of the table and put the least-read text in the
   most prominent position. A <details> keeps it one click away and needs no
   JavaScript to open. */
.outcome > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
}

.outcome > summary::-webkit-details-marker { display: none; }
.outcome[open] > summary { margin-bottom: 0.35rem; }

.outcome .why {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  color: var(--bad-ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-width: 420px;
  margin: 0;
}

/* The caret only appears when there is something behind it. */
.outcome > summary::after { content: " ⌄"; color: var(--muted); font-size: 0.7rem; }
.outcome[open] > summary::after { content: " ⌃"; }

/* ===== Dialog ===== */

dialog {
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  color: var(--ink);
  padding: 1.25rem;
  max-width: 520px;
  width: calc(100vw - 2rem);
  box-shadow: var(--shadow);
}

dialog::backdrop { background: rgba(0, 0, 0, 0.45); }
dialog h2 { margin-top: 0; }

/* An editor, not a confirmation. 520px is right for "are you sure"; a prompt of
   two hundred lines needs the width to show where its own lines wrap, because
   that is how the person writing it reads its shape. */
dialog.wide { max-width: 56rem; }
dialog.wide textarea { min-height: 60vh; }

/* A row of controls that belong together, one of which is a form of its own —
   without this they stack, because a form is a block. */
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-3) 0;
}

.actions form { margin: 0; }
.actions button { margin-top: 0; }

button.secondary { background: var(--line); color: var(--ink); }
button.small { padding: 0.25rem 0.7rem; font-size: 0.85rem; }

/* ===== A document, rendered =====

   A definition is prose someone wrote for someone to follow, so it is set as
   prose: a measure that does not run the width of a monitor, and headings that
   sit under this page's own rather than competing with them.

   The width is on the text and not on the section, so the tables and controls
   around it still use the room they need. */
.prose {
  max-width: 46rem;
  margin: var(--space-4) 0;
}

.prose h3, .prose h4, .prose h5, .prose h6 {
  margin: 1.6rem 0 0.5rem;
  letter-spacing: -0.01em;
}

.prose h3 { font-size: 1.15rem; }
.prose h4 { font-size: 1rem; }
.prose h5, .prose h6 { font-size: 0.9rem; color: var(--muted); }
.prose p { margin: 0 0 0.85rem; }
.prose ul, .prose ol { margin: 0 0 0.85rem; padding-left: 1.35rem; }
.prose li { margin-bottom: 0.3rem; }
.prose hr { border: none; border-top: 1px solid var(--line); margin: 1.5rem 0; }

.prose code {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  /* Tight horizontally: 0.3rem left a gap before a following full stop that
     read as a typo rather than as padding. */
  padding: 0.05rem 0.22rem;
  font-size: 0.9em;
}

.prose pre.code {
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: var(--space-3);
  overflow-x: auto;
}

.prose pre.code code { background: none; border: none; padding: 0; }

.prose blockquote {
  margin: 0 0 0.85rem;
  padding-left: var(--space-3);
  border-left: 3px solid var(--line);
  color: var(--muted);
}

/* ===== Settings =====

   A definition list rather than a table: each row is a label, an explanation
   and one control, and a table would put the explanation in a cell that has to
   be as wide as the longest one.

   The label column is fixed so every control lines up down the page — a form
   whose inputs step left and right as the labels change length is harder to
   scan than one with a ragged right edge. */
.settings { margin: 0 0 var(--space-4); }

.setting {
  display: grid;
  grid-template-columns: minmax(12rem, 22rem) minmax(0, 1fr);
  gap: var(--space-3) var(--space-4);
  align-items: start;
  padding: var(--space-3) 0;
  border-top: 1px solid var(--line);
}

.setting dt { margin: 0; }
.setting dd { margin: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.setting label { font-weight: 600; display: block; }
.setting .help { display: block; font-size: 0.85rem; color: var(--muted); margin-top: 0.2rem; }
.setting input { max-width: 22rem; }
.setting .unit { font-size: 0.85rem; color: var(--muted); }

/* Where the value came from, said in words next to it. A page that shows an
   inherited value and an override identically makes clearing a box a leap of
   faith. */
.setting .source { flex-basis: 100%; font-size: 0.8rem; color: var(--muted); }
.setting.overridden label::after {
  content: "override";
  margin-left: 0.5rem;
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--warn-ink);
}

@media (max-width: 700px) {
  .setting { grid-template-columns: 1fr; }
}

/* ===== Meters =====

   One box per figure, wide enough that the bar is worth drawing. Three or four
   across a laptop: a host has a CPU, a memory and one or two disks, and they are
   read together — "is this box in trouble" is a question about all of them at
   once, not a list to scroll. */
.meters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.meter {
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  padding: var(--space-3) var(--space-4);
}

.meter-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.meter-label { font-weight: 600; letter-spacing: -0.01em; }
.meter-value { font-variant-numeric: tabular-nums; font-size: 1.1rem; }

.meter-track {
  height: 0.5rem;
  border-radius: 999px;
  background: var(--bg);
  border: 1px solid var(--line);
  overflow: hidden;
}

.meter-fill {
  display: block;
  height: 100%;
  background: var(--accent);
  border-radius: 999px;
}

/* Colour only at the point where it means something. A bar that shades from
   green through amber at every value makes 40% look like a warning. */
.meter-fill.warn { background: var(--warn-ink); }
.meter-fill.bad { background: var(--bad-ink); }

.meter-detail {
  margin: var(--space-2) 0 0;
  font-size: 0.8rem;
  color: var(--muted);
}

/* Three figures in a column, for the list where the bars would not fit. */
.usage { display: flex; flex-wrap: wrap; gap: 0.5rem; font-size: 0.8rem; }
.usage .hot { color: var(--bad-ink); font-weight: 600; }

.definition { margin-bottom: 3rem; }
.definition h2 .muted { font-weight: 400; font-size: 0.85rem; }
.history { margin-top: var(--space-4); }

/* The global button rule adds 1rem above, which is right after a paragraph and
   wrong inside a table cell — it made the row carrying the button taller than
   every other row. */
.history td button { margin-top: 0; }
.history td form { margin: 0; }

.choice {
  display: flex;
  gap: 0.55rem;
  align-items: flex-start;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}

.choice:last-of-type { border-bottom: none; }
.choice input { margin-top: 0.25rem; }
.choice .name { font-weight: 600; font-size: 0.9rem; }
.choice .why { font-size: 0.8rem; color: var(--muted); }

.dialog-actions { display: flex; gap: 0.6rem; align-items: center; }
.dialog-actions button { margin-top: 0; }

/* A block that will hold the commit list once something generates one. */
.placeholder {
  border: 1px dashed var(--line);
  border-radius: 6px;
  padding: 0.7rem 0.85rem;
  color: var(--muted);
  font-size: 0.82rem;
  margin: 0.75rem 0;
}

/* ===== Transcript ===== */

.transcript {
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--panel);
  max-height: 70vh;
  overflow: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
}

.turn { padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--line); }
.turn:last-child { border-bottom: none; }
.turn .who { color: var(--muted); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; }
.turn pre { margin: 0.25rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }

/* ===== Mobile ===== */

@media (max-width: 899px) {
  .menu-btn { display: inline-flex; }

  /* The albumsite drawer: off-canvas, slid in, with the page behind it locked
     and an overlay that closes it. */
  .sidebar {
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
    width: min(280px, 82vw);
    flex-basis: auto;
    z-index: 70;
    transform: translateX(-110%);
    transition: transform 0.22s ease;
    box-shadow: var(--shadow);
    padding-top: 1rem;
  }

  .sidebar.open { transform: translateX(0); }

  .sidebar-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.45);
    z-index: 60;
  }

  body.sidebar-open { overflow: hidden; }
  body.sidebar-open .sidebar-overlay { display: block; }

  main { padding: 1.25rem 1rem 3rem; }
  .cards { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); }
}

@media (prefers-reduced-motion: reduce) {
  .sidebar { transition: none; }
}

/* A pick-one form. The agents form beside it is two short checkboxes and flows
   fine inline; these labels are sentences, and inline they wrapped away from
   their own radio button. Stacked, each option is one line you can hit with a
   thumb. */
.choice {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.4rem;
  margin-bottom: 0.5rem;
}

.choice label {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  cursor: pointer;
}

.choice button { margin-top: 0.35rem; }

/* ===== A command to run elsewhere =====

   Wraps rather than scrolls: this is read before it is run as root, and a line
   that runs off the edge of a phone is one you cannot check. */
.copy-command {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 0.6rem 0.7rem;
  margin: 0.5rem 0 1rem;
}

.copy-command code {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: 0.82rem;
  background: none;
  padding: 0;
}

.copy-button { flex: 0 0 auto; }

/* The deployer is listed among the deployments but is not one of them. */
.deployer-row td { border-top: 1px solid var(--line); }

/* ===== The roster =====

   One line per app, three columns, no prose. What this page is asked is "is
   production all right, and what is it running"; everything else has a page of
   its own with room for it.

   The line is an anchor rather than a row with a link in it, so the click
   target is the whole line and not the few characters of a name. */
.app-lines {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  overflow: hidden;
  background: var(--panel);
}

.app-line + .app-line { border-top: 1px solid var(--line); }

/* Name left, the two facts about it together on the right, and a chevron on the
   edge. The middle column used to be a fixed 10rem with the name taking every
   remaining pixel, which on a wide screen left the version stranded in the
   middle of a field of nothing — related things reading as unrelated. */
.app-line > a {
  display: grid;
  grid-template-columns: minmax(8rem, 1fr) auto minmax(9rem, 13rem) 0.75rem;
  align-items: center;
  gap: var(--space-4);
  padding: 0.7rem var(--space-4);
  text-decoration: none;
  color: inherit;
}

/* A row is a touch target as well as a line of text. 0.7rem top and bottom puts
   it a shade over 40px, which is the smallest a pointer target should be, and
   is the difference between a list and a wall of text. */
.app-line > a:hover { background: var(--bg); }
.app-line.unmanaged > a { opacity: 0.62; }
.app-line-name { font-weight: 600; letter-spacing: -0.01em; }
.app-line-version { color: var(--muted); font-size: 0.85rem; text-align: right; }

/* The affordance for a row that is entirely a link. Aria-hidden: it says
   "clickable", which a screen reader already knows from the anchor. */
.app-line-go { color: var(--line); text-align: right; }
.app-line > a:hover .app-line-go { color: var(--muted); }
.app-line-health {
  font-size: 0.85rem;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  justify-self: start;
}

/* A dot rather than a coloured word: the state is read at a glance and the
   word is there for anyone who cannot distinguish the colours. */
.dot {
  /* inline-block, not inline: width and height do nothing to an inline box, so
     every dot outside a flex parent — the ones inside pills — was zero by zero
     and invisible. It worked everywhere it was first used because those parents
     happened to be flex containers. */
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 999px;
  background: var(--muted);
  flex: none;
}

.dot-ok { background: var(--good-ink); }
.dot-bad { background: var(--bad-ink); }
.dot-warn { background: var(--warn-ink); }
.dot-muted { background: var(--line); border: 1px solid var(--muted); }

/* Narrow: the version drops out before the state does. Which version production
   runs is a detail; whether it is up is the reason the page was opened. */
@media (max-width: 620px) {
  .app-line > a { grid-template-columns: 1fr auto 0.75rem; gap: var(--space-3); padding: 0.7rem var(--space-3); }
  .app-line-version { display: none; }
}

/* ===== Environment cards =====

   The table these replace had eight columns and scrolled sideways on anything
   narrower than a laptop, which put "is production up" behind a scrollbar.

   Every card has the same five rows whether or not it has anything to say in
   them. A card that grows a line when a job starts would shift every card below
   it on a page someone is reading. */
/* Set explicitly rather than inherited: the global h2 rule gives 2rem above,
   but `section:first-of-type h2` cuts it to 0.5rem — and because every group
   lives inside one section, that shorter margin applied to all of them and the
   environments ran together. */
.env-group { margin-bottom: 1.75rem; }
.env-group:last-child { margin-bottom: 0; }
.env-group h2 {
  text-transform: lowercase;
  margin: 0 0 var(--space-2);
  color: var(--muted);
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
}

/* Beside the environment it belongs to, and empty when nothing is running. It
   holds its line either way — it is a heading that is always there, which is
   what the cards were reserving a blank row each to imitate. */
.env-note {
  font-size: 0.8rem;
  font-weight: 400;
  text-transform: none;
  color: var(--warn-ink);
}

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
  gap: var(--space-3);
  /* Wide enough for the longest line the card holds — "Build #316 (a1b2c3d) |
     24.08.26 18:49" beside two buttons. Narrower, that line wrapped and left
     the divider dangling at the end of a row. */
  grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
  max-width: 70rem;

  /* Cards size to their own content rather than stretching to the tallest in
     the row. Stretching plus a footer pinned with margin:auto is what put a
     hole in the middle of every card that had less to say than its neighbour. */
  align-items: start;
}

/* Two columns rather than a header above a body. The right-hand side is taller
   than one line and, stacked, its height pushed the left-hand side down — a
   band of nothing under the component name, and a card as tall as its button
   instead of as tall as what it had to say. */
.dcard {
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  padding: var(--space-3) var(--space-4);
  display: flex;
  justify-content: space-between;
  align-items: start;
  gap: var(--space-3);
}

.dcard-facts {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  min-width: 0;
  /* Smaller than the page around it: this is reference data read in a glance
     across several cards, not prose. */
  font-size: 0.85rem;
}

/* Two facts that are read together, with a divider that is quieter than either
   of them. */
.dcard-line {
  margin: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.dcard-line .sep { color: var(--line); }
.dcard-line .muted { font-size: 0.95em; }

/* Date over time, so the pair takes the width of the wider half rather than of
   both. The card is not tall enough to care and is not wide enough not to. */
.deployed-at { display: inline-flex; flex-direction: column; line-height: 1.15; }
.deployed-at .d { font-size: 0.85em; color: var(--muted); }
.deployed-at .t { color: var(--muted); }

/* A restart has nothing to name, so its button is one line and should not
   reserve a second. */
button.deploy:has(> :only-child) { justify-content: center; }

/* The host and, under it, the one action for this component. Stacked in the
   corner rather than laid across the foot of the card: it acts on one component
   of one environment, and a full-width bar gave it the weight of a decision
   about the whole card. There was room here the card was not using. */
.dcard-act {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.35rem;
  text-align: right;
  flex: none;
}

.dcard-act form { margin: 0; }
.dcard-act button { margin-top: 0; white-space: nowrap; }

/* Present on every card whether or not it can be pressed, so a row of cards is
   the same height regardless of which of them has a promotion outstanding.
   Faded rather than hidden: the absence of a control is ambiguous — nothing to
   send, or not that kind of card — and a disabled one is not. */
button.deploy {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.05rem;
  padding: 0.25rem 0.6rem;
  line-height: 1.15;
  font-size: 0.75rem;
  min-width: 6.5rem;
}

.deploy-verb { font-weight: 600; }
.deploy-sha { font-size: 0.7rem; opacity: 0.85; }

/* The separator never ends a line: if the pair has to wrap, it wraps as a pair
   rather than leaving a divider hanging off the end of a row. */
.dcard-line { white-space: nowrap; }
.dcard-line > * { flex: none; }

button.deploy:disabled {
  background: var(--line);
  color: var(--muted);
  cursor: default;
  opacity: 0.65;
}

/* Three sizes, and they rank the three facts. The version is what the card is
   for — which of the things we build is running here — so it is the largest and
   the only one in ink; the component names the card; the host qualifies it. */
.dcard-component { font-weight: 600; letter-spacing: -0.01em; }
.dcard-host { font-size: 0.8rem; }
.dcard-version {
  margin: 0;
  font-size: 0.95rem;
  color: var(--ink);
}

.dcard-state {
  margin: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.85rem;
  color: var(--muted);
}

.dcard-manual { border-style: dashed; }

/* ===== A check, as a card =====

   Wider than a deployment card because the rows inside are lists — the checks a
   build ran, the counts a suite reported — and a narrow column wraps every one
   of them onto three lines. Two or three across a laptop rather than four.

   The head is the link and the rest of the card is not: the body holds links of
   its own (the report, the scan run), and a card that is entirely a link
   swallows them. */
.gcard {
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  padding: var(--space-3) var(--space-4);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.gcard-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  text-decoration: none;
  color: inherit;
  margin: calc(var(--space-3) * -1) calc(var(--space-4) * -1) 0;
  padding: var(--space-3) var(--space-4) var(--space-2);
  border-radius: 10px 10px 0 0;
}

.gcard-head:hover { background: var(--bg); }
.gcard-app { font-weight: 600; letter-spacing: -0.01em; }
.gcard-app .muted { font-weight: 400; }
.gcard-go { margin-left: auto; color: var(--line); }
.gcard-head:hover .gcard-go { color: var(--muted); }

.gcard-state {
  margin: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: 0.85rem;
}

.gcard-state .label {
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

.gcard-when { color: var(--muted); }
.gcard-summary { margin: 0; font-size: 0.85rem; }
.gcard-links { margin: 0; display: flex; gap: var(--space-3); font-size: 0.85rem; }

/* The checks a build ran, one pill each and wrapping. */
.checkline { margin: 0; display: flex; flex-wrap: wrap; gap: 0.35rem; font-size: 0.85rem; }

/* Wider tracks than the deployment cards get: these hold lists. */
.tier .cards { grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); }

.finding-group { margin-bottom: var(--space-4); }
.finding-group h3 { display: flex; align-items: baseline; gap: var(--space-2); margin-bottom: var(--space-2); }
.finding-group h3 .muted { font-weight: 400; font-size: 0.85rem; }

/* ===== The build =====

   One line, above the environments, about the only version on this page that
   may not exist yet. The whole line is the link, and it goes to GitHub — this
   is the one place in the portal where the answer is somewhere else. */
.builds { margin-bottom: var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }

.build-line {
  display: flex;
  align-items: baseline;
  /* One line. It wraps only when the viewport genuinely cannot hold it, rather
     than because a stacked timestamp made it two lines tall by default. */
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  padding: 0.65rem var(--space-4);
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--panel);
  text-decoration: none;
  color: inherit;
}

.build-line:hover { background: var(--bg); }
.build-state { font-weight: 600; }
.build-number { font-weight: 600; font-variant-numeric: tabular-nums; }
.build-sha { font-size: 0.9rem; color: var(--muted); }
.build-when { font-size: 0.8rem; color: var(--muted); white-space: nowrap; }
.build-on { display: flex; gap: 0.3rem; flex-wrap: wrap; font-size: 0.8rem; }
.build-why { font-size: 0.85rem; }
.build-go { margin-left: auto; color: var(--line); }
.build-line:hover .build-go { color: var(--muted); }

.run-lines { list-style: none; margin: 0; padding: 0; }
.run-lines li + li { border-top: 1px solid var(--line); }
.run-lines a { display: flex; gap: var(--space-3); align-items: center; padding: 0.35rem 0; text-decoration: none; color: inherit; }
.run-lines a:hover { color: var(--accent); }

/* Two lines of title and no more: a list of issues is for finding one, and a
   wrapped paragraph per row turns twenty issues into a page of scrolling. */
.issue-title {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* A run that has not finished, and the one control that ends it. Set apart
   from the record below it because it is the only part of this page that is
   about now rather than about what happened. */
.live-run {
  border: 1px solid var(--warn-ink);
  border-radius: 10px;
  background: var(--warn-bg);
  color: var(--warn-ink);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
}

.live-run .running { display: flex; align-items: center; gap: var(--space-2); margin: 0; }
.live-run form { margin: var(--space-3) 0 0; }
.live-run button { margin-top: 0; }
.live-run .note { margin: var(--space-2) 0 0; color: inherit; opacity: 0.85; }

/* A run that has not finished yet. The dot beats rather than the row flashing:
   the row is text somebody is reading, and one small moving thing says "this is
   now" without making the rest hard to read. */
@keyframes beat {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}

.dot.beating { animation: beat 1.4s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  .dot.beating { animation: none; }
}

.runs .live td { background: color-mix(in srgb, var(--accent) 6%, transparent); }
.runs .pill .dot { margin-right: 0.35rem; }

tr.clickable { cursor: pointer; }
tr.clickable:hover { background: var(--bg); }

/* ===== Configuration =====

   Two columns and an action, sized so nothing needs a sideways scrollbar: a
   variable name and its value are both short, and the page that shows them is
   read far more often than it is edited. */
table.kv { width: 100%; table-layout: fixed; }
table.kv td { padding: 0.3rem 0.5rem; vertical-align: top; word-break: break-word; }
table.kv .kv-name { width: 40%; }
table.kv .kv-act { width: 2.5rem; text-align: right; }
table.kv .kv-act form { display: inline; }

/* Half and half, on one line. The pair used to stack, which made adding two
   variables look like four decisions. */
.config-add {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-2);
}

.config-add input { width: 100%; }
.config-add button { padding: 0.25rem 0.7rem; font-weight: 600; }
.config-add-error { grid-column: 1 / -1; color: var(--bad-ink); font-size: 0.85rem; }

@media (max-width: 560px) {
  .config-add { grid-template-columns: 1fr auto; }
  .config-add input[name="value"] { grid-column: 1 / -1; }
}

.grants .choice { display: block; margin-bottom: var(--space-2); }

/* ===== Platform configuration =====

   This table earns its horizontal scroll: the keys are long, the values are
   the thing being edited, and squeezing both into the viewport made the inputs
   too small to read what was in them.

   So the input gets the room and the defaults column gives it up — truncated to
   a dozen characters, with the whole value one click away. */
.config-table input[type="text"],
.config-table input:not([type]) {
  width: 100%;
  min-width: 22rem;
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 0.9rem;
  padding: 0.35rem 0.5rem;
}

.config-table td, .config-table th { font-size: 0.85rem; }
.config-table .config-key { white-space: nowrap; }

.config-default {
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.78rem;
  color: var(--muted);
  cursor: pointer;
  border: none;
  background: none;
  font-family: var(--mono, ui-monospace, monospace);
}

.config-default:hover { text-decoration: underline; }

/* ------------------------------------------------------------- status page */

/*
 * The public page. It renders no portal chrome and links this stylesheet only
 * for the tokens and the dots, so everything it needs beyond that is here.
 */
.status-page { padding: 2rem 1rem; }

.status { max-width: 40rem; margin: 0 auto; }

.status h1 { margin-bottom: 1.5rem; }

.status .stale { border-left: 3px solid var(--bad-ink); padding-left: 0.75rem; }

.status .message { margin: 1rem 0 1.5rem; padding: 0.75rem 1rem; border-radius: 6px; background: var(--panel); }
.status .message.degraded { border-left: 3px solid var(--warn-ink); }
.status .message.outage { border-left: 3px solid var(--bad-ink); }

.status .services { list-style: none; padding: 0; margin: 0; }

.status .services li {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: baseline;
  gap: 0.5rem 0.75rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--line);
}

.status .services .name { font-weight: 600; }
.status .services .state { text-align: right; }
/* Under the name, spanning the two columns after the dot. */
.status .services .when { grid-column: 2 / -1; color: var(--muted); font-size: 0.875rem; }

.status .as-of { margin-top: 1.5rem; color: var(--muted); font-size: 0.875rem; }

/* A year of days, one cell each, oldest at the left. */
.status .year {
  grid-column: 1 / -1;
  display: flex;
  gap: 1px;
  margin: 0.5rem 0 0.25rem;
  /* The cells shrink to fit rather than scrolling: a year that needs a
     horizontal scrollbar on a phone is a year nobody looks at. */
  min-width: 0;
}

.status .year .day {
  flex: 1 1 0;
  min-width: 1px;
  height: 26px;
  border-radius: 1px;
  background: var(--line);
}

.status .year .day.ok { background: var(--good-ink); }
.status .year .day.warn { background: var(--warn-ink); }
.status .year .day.bad { background: var(--bad-ink); }
/* Not grey-as-in-down. Nothing was recorded, and the cell says so by being
   nearly absent rather than by being a colour on the scale. */
.status .year .day.none { background: var(--line); opacity: 0.4; }

.status .uptime {
  grid-column: 1 / -1;
  margin: 0;
  color: var(--muted);
  font-size: 0.875rem;
}
