/* The console's layout and the few components Bootstrap does not have.
 *
 * Three things this file is responsible for, because getting them wrong is what
 * made the first attempt hard to read:
 *
 *   1. A horizontal bar for sections, a left column for whatever filters the page
 *      you are on, and a bounded content column. Ops data wants width, but not
 *      2000px of it: a line of text that long is genuinely hard to scan back from.
 *   2. Column discipline in tables. Without declared widths, a long "waiting on"
 *      cell pushes the numbers to the far edge and leaves a dead zone in the
 *      middle, which is what made the board look broken.
 *   3. One spacing scale, used everywhere, so cards and rows line up on a grid
 *      instead of each picking their own padding.
 */

:root {
  --topbar-h: 3.5rem;
  /* The bar plus the page header. stack.js overwrites this with the measured height as
     soon as it runs; the value here is the fallback for the moment before that, and for
     the no-JavaScript case. It is deliberately a little generous, because pinning
     something too low leaves a gap and pinning it too high hides it. */
  --stack-h: calc(var(--topbar-h) + 7rem);
  --rail-w: 14.5rem;
  /* Fluid up to here. 82rem left about 80px unused down each side of a 1470px
     screen, which is the common case, and the cap was written for reading prose
     while the pages that hit it are tables and cards. It still caps, because a
     paragraph running the full width of a 4K monitor is genuinely hard to scan
     back from. */
  --content-max: 110rem;
  --gutter: 1.75rem;

  /* One brand colour, used for the mark and the active nav marker and nothing
     else. It deliberately stays off buttons and status: amber, green and red mean
     something on this console, and a brand colour competing with them is how a
     failed record stops standing out. */
  --brand: #4b3fd4;

  /* An 8px rhythm. Everything vertical is a multiple of one of these. */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.5rem;
  --s6: 2rem;

  --ins-bg: rgba(25, 135, 84, 0.22);
  --del-bg: rgba(220, 53, 69, 0.22);

  --scroll-thumb: rgba(120, 130, 145, 0.55);
  --scroll-thumb-hover: rgba(120, 130, 145, 0.85);
}

[data-bs-theme="dark"] {
  /* A surface half a step off the body, so the bar and the sub-nav read as a
     different plane without a hard colour change. */
  --surface: #16181c;
  --surface-2: #1c1f24;
  --hairline: rgba(255, 255, 255, 0.08);
  --brand: #7b6ff0;
}

[data-bs-theme="light"] {
  --surface: #f7f8fa;
  --surface-2: #ffffff;
  --hairline: rgba(0, 0, 0, 0.09);
  --ins-bg: rgba(25, 135, 84, 0.16);
  --del-bg: rgba(220, 53, 69, 0.14);
}

body {
  min-height: 100vh;
}

/* ---------------------------------------------------------------- shell */

.app {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.app-body {
  display: flex;
  align-items: stretch;
  flex: 1 1 auto;
  min-height: 0;
}

.app-main {
  flex: 1 1 auto;
  min-width: 0;          /* lets tables inside actually shrink */
  display: flex;
  flex-direction: column;
}

/* One column width for every page, so headers, cards and tables share edges. */
.wrap {
  width: 100%;
  max-width: var(--content-max);
  padding-inline: var(--gutter);
  margin-inline: auto;
}

.page-head {
  border-bottom: 1px solid var(--hairline);
  /* Tighter at the bottom than the top: the rule already separates it from the
     content, so symmetrical padding just pushed everything down the screen. */
  padding-block: var(--s4) var(--s3);
  background: var(--bs-body-bg);
  position: sticky;
  /* Below the bar rather than at the top of the viewport, so the two stick
     without overlapping. */
  top: var(--topbar-h);
  z-index: 20;
}

/* Actions in the header line up on one baseline whatever they are: a button, a
   metric, or both. Without this a button next to a two-line metric sat centred
   against it and nothing shared an edge. */
.head-actions {
  display: flex;
  align-items: flex-end;
  gap: var(--s5);
  flex: 0 0 auto;
}

.min-w-0 {
  min-width: 0;
}

.page-head h1 {
  font-size: 1.5rem;
  font-weight: 600;
  margin: 0;
  letter-spacing: -0.01em;
}

.page-head .sub {
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
  margin: var(--s1) 0 0;
}

.page-body {
  padding-block: var(--s4) var(--s6);
  flex: 1 1 auto;
}

/* Jumping to a card must not put it under the bar and the page header, which are both
   sticky. Without this the heading you clicked is the one thing you cannot see. The
   height comes from --stack-h, measured rather than guessed: the guess that used to be
   here was short, so the target still landed partly under the header. */
[id="pipeline"], [id="revisions"], [id="deployments"], [id="ask"],
[id="inputs"], [id="infrastructure"], [id="cost"], [id="timeline"] {
  scroll-margin-top: calc(var(--stack-h) + 0.5rem);
}

html {
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

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

.topbar {
  position: sticky;
  top: 0;
  z-index: 30;                       /* above .page-head, which sticks below it */
  height: var(--topbar-h);
  background: var(--surface);
  border-bottom: 1px solid var(--hairline);
}

.topbar-in {
  display: flex;
  align-items: center;
  gap: var(--s5);
  height: 100%;
}

/* ---------------------------------------------------------------- wordmark */

.brand {
  display: flex;
  align-items: center;
  gap: var(--s2);
  text-decoration: none;
  color: var(--bs-body-color);
  flex: 0 0 auto;
}

.brand-mark {
  width: 1.375rem;
  height: 1.375rem;
  flex: 0 0 auto;
}

/* The name carries the character, because there is no webfont to do it: this
   console has to render on a box with no outbound internet, which is the same
   reason bootstrap is vendored. Tight tracking and a two-tone split, echoing the
   two squares in the mark. */
.brand-name {
  font-size: 1.0625rem;
  font-weight: 500;
  letter-spacing: -0.03em;
  white-space: nowrap;
}

.brand-name b {
  font-weight: 700;
  color: var(--brand);
}

.brand-tag {
  font-size: 0.6875rem;
  color: var(--bs-secondary-color);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  white-space: nowrap;
}

/* ---------------------------------------------------------------- sections */

.topnav {
  display: flex;
  align-items: center;
  gap: var(--s1);
  min-width: 0;
  height: 100%;
}

.topnav-item {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  height: 100%;
  padding-inline: var(--s3);
  border: 0;
  background: none;
  color: var(--bs-secondary-color);
  text-decoration: none;
  font-size: 0.875rem;
  font-weight: 500;
  white-space: nowrap;
  /* Reserves the marker's height at rest, so nothing shifts on hover. */
  box-shadow: inset 0 -2px 0 transparent;
}

.topnav-item:hover {
  color: var(--bs-body-color);
  box-shadow: inset 0 -2px 0 var(--hairline);
}

.topnav-item.on {
  color: var(--bs-body-color);
  font-weight: 600;
  box-shadow: inset 0 -2px 0 var(--brand);
}

.topnav-item .n {
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--bs-secondary-color);
}

/* The one count that is repeated outside its section. Everything else waits for
   somebody to open Records; this is the reason to open it. */
.topnav-badge {
  min-width: 1.125rem;
  padding-inline: 0.3125rem;
  border-radius: 999px;
  background: var(--bs-warning);
  color: #000;
  font-size: 0.6875rem;
  font-weight: 700;
  line-height: 1.125rem;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.caret {
  width: 0.625rem;
  height: 0.375rem;
  opacity: 0.6;
  flex: 0 0 auto;
}

.ext {
  width: 0.6875rem;
  height: 0.6875rem;
  opacity: 0.5;
  flex: 0 0 auto;
}

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

/* Total spend, pushed to the right-hand end beside the account. Two lines of small
   type rather than a metric block: it is a background fact, not something to read on
   the way to doing anything. */
.topbar-spend {
  margin-left: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  line-height: 1.1;
  padding-inline: var(--s3);
  text-decoration: none;
  color: var(--bs-secondary-color);
  flex: 0 0 auto;
}

.topbar-spend:hover {
  color: var(--bs-body-color);
}

.topbar-spend .n {
  font-size: 0.75rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.topbar-spend .u {
  font-size: 0.6875rem;
  font-variant-numeric: tabular-nums;
}

/* The spend block now takes the slack, so the account menu no longer needs to. */
.topbar-spend ~ .topbar-me {
  margin-left: 0;
}

@media (max-width: 48rem) {
  .topbar-spend {
    display: none;
  }
}

.topbar-me {
  margin-left: auto;
  height: 100%;
  flex: 0 0 auto;
}

.me-item {
  height: 100%;
}

.avatar {
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--brand);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 700;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}

.menu-me {
  min-width: 12rem;
}

.me-head {
  padding: var(--s2) var(--s4) var(--s1);
  font-size: 0.875rem;
}

.me-head .role {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* The sign-out button is a form submit, so it needs telling it is a menu row. */
.menu-me form {
  margin: 0;
}

.menu-me .dropdown-item {
  width: 100%;
  text-align: left;
  border: 0;
  background: none;
}

/* ---------------------------------------------------------------- the rail
 *
 * The left column of a section, beside the page rather than inside it. It is not
 * global navigation: the queues filter one page and the anchors belong to one
 * record, so each page supplies its own. A page that supplies none renders no
 * element, `.app-body` is left with a single child, and the content takes the whole
 * width without needing a flag or `:has()`.
 */

.subnav {
  flex: 0 0 var(--rail-w);
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: var(--s4) var(--s2);
  background: var(--surface);
  border-right: 1px solid var(--hairline);
  /* Its own scroll, so a long list of queues never pushes the page down and never
     scrolls away from the content it filters. */
  position: sticky;
  top: var(--topbar-h);
  height: calc(100vh - var(--topbar-h));
  overflow-y: auto;
}

.subnav-title {
  font-size: 0.6875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--bs-secondary-color);
  padding: 0 var(--s3) var(--s2);
}

/* A rail row is the same shape as a top-bar item so the two read as one system:
   hover fills, current carries the brand marker. */

/* An optional dot, a label that takes the slack, and a count in a fixed column so
   the numbers line up vertically instead of floating wherever the label ends. */
.subnav-item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) 2rem;
  align-items: center;
  gap: var(--s2);
  padding: 0.4375rem var(--s3);
  border-radius: 0.375rem;
  color: var(--bs-body-color);
  text-decoration: none;
  font-size: 0.8125rem;
  line-height: 1.3;
}

/* A row with no dot should not leave a hole where one would have been. */
.subnav-item > .t:first-child {
  grid-column: 1 / 3;
}

.subnav-item .t {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.subnav-item .n {
  grid-column: 3;
  text-align: right;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--bs-secondary-color);
}

.subnav-item .ext {
  grid-column: 3;
  justify-self: end;
}

.subnav-item:hover {
  background: var(--surface);
}

.subnav-item.on {
  background: var(--surface);
  font-weight: 600;
  box-shadow: inset 2px 0 0 var(--brand);
}

.subnav-item.on .n,
.subnav-item .n.hot {
  color: var(--bs-body-color);
}

.subnav-rule {
  margin: var(--s2) var(--s3);
  border-color: var(--hairline);
  opacity: 1;
}

/* A semantic dot instead of a differently-coloured button per state. Five outlined
   colours in a row read as decoration; five dots read as a legend. */
.dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  flex: 0 0 auto;
  background: var(--bs-secondary);
}

.dot-warning { background: var(--bs-warning); }
.dot-info { background: var(--bs-info); }
.dot-primary { background: var(--bs-primary); }
.dot-success { background: var(--bs-success); }
.dot-danger { background: var(--bs-danger); }

/* ---------------------------------------------------------------- record grid
 *
 * Four cards in two pairs: the bucket beside the actions that fill it, the revision
 * list beside the box that asks for a change. DOM order already matches, so plain
 * row flow places them; nothing needs explicit grid coordinates.
 */

.record-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--s4);
  align-items: start;
  margin-bottom: var(--s4);
}

/* The htmx swap wrapper draws no box, so the two cards it holds are grid items of
   the row above rather than a single cell. It has to stay in the DOM: one poll
   refreshes the pipeline and the revisions together, and splitting them into two
   requests would be two chances to show a pipeline from one moment beside a revision
   list from another. */
#live-cards {
  display: contents;
}

/* Visual order set here rather than in the markup, because the DOM order is not free:
   the revisions card has to stay inside the polled wrapper, and the chat has to stay
   outside it or a swap would replace a half-typed instruction. `order` on grid items
   separates the two concerns.
 *
 *   bucket  |  pipeline
 *   chat    |  revisions
 *
 * Collapsed to one column the same numbers give actions, then chat, then history,
 * which is also the right reading order. */
.slot-bucket { order: 1; }
#live-cards > #pipeline { order: 2; }
#ask { order: 3; }
#live-cards > #revisions { order: 4; }

/* The pair on the second row matches height. The grid is `align-items: start`, so
   each card was its own height and the chat ended a long way short of the revision
   list beside it.
 *
 * The extra height goes into the textarea rather than into blank card, because this
 * is the box people spend their time in and three rows was already a squeeze for a
 * paragraph of instruction. */
#ask,
#live-cards > #revisions {
  align-self: stretch;
}

#ask > .card-body,
#ask .chat-form,
#ask .chat-input {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

#ask .chat-form,
#ask .chat-input {
  flex: 1 1 auto;
}

#ask .chat-input textarea {
  flex: 1 1 auto;
  min-height: 5.5rem;
  resize: vertical;
}

/* A flex column stretches its children across the cross axis, which turned the
   submit into a full-width bar. It is a button, not a banner. */
#ask .chat-form > .btn {
  align-self: flex-start;
}

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

/* ---------------------------------------------------------------- working
 *
 * A spinner sized to sit inside a button or a pill without changing its height.
 */

.spinner {
  display: inline-block;
  width: 0.75rem;
  height: 0.75rem;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  vertical-align: -0.125rem;
}

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

@media (prefers-reduced-motion: reduce) {
  .spinner {
    animation-duration: 2.4s;
  }
}

/* ---------------------------------------------------------------- disclosure
 *
 * A `<details>` styled to belong here. Long explanations were sitting above the
 * controls they explain, which is what made the record page read as a document: on
 * one page the reason a conversion is needed took four paragraphs before you reached
 * the button that runs it.
 *
 * No JavaScript. `<details>` is keyboard operable, findable by browser search when
 * open, and survives the page being rendered with scripts blocked.
 */

.disclose > summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
  padding: var(--s2) 0;
  user-select: none;
}

.disclose > summary::-webkit-details-marker {
  display: none;
}

/* The marker is drawn rather than inherited, so it is the same triangle in every
   browser and rotates rather than swapping glyph. */
.disclose > summary::before {
  content: "";
  width: 0.375rem;
  height: 0.375rem;
  flex: 0 0 auto;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 0.15s ease;
}

.disclose[open] > summary::before {
  transform: rotate(45deg);
}

.disclose > summary:hover {
  color: var(--bs-body-color);
}

.disclose-body {
  padding-block: var(--s2) var(--s1);
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

.disclose-body > p {
  max-width: 52rem;
}

.disclose-body > p:last-child {
  margin-bottom: 0;
}

/* When the disclosure *is* the card, it gets the card's frame and padding. */
.disclose-card {
  border: 1px solid var(--bs-border-color);
  border-radius: var(--bs-border-radius);
  background: var(--bs-card-bg, transparent);
  padding: var(--s1) var(--s4) var(--s2);
}

/* A button sitting inside a sentence. Without this the padding of a real button
   pushes the line apart wherever it appears. */
.btn-inline {
  --bs-btn-padding-y: 0;
  --bs-btn-padding-x: 0.125rem;
  --bs-btn-font-size: inherit;
  vertical-align: baseline;
  text-decoration: underline;
}

@media (prefers-reduced-motion: reduce) {
  .disclose > summary::before {
    transition: none;
  }
}

/* ---------------------------------------------------------------- scroll panes
 *
 * A record with twelve revisions rendered twelve rows, so reaching the cards below
 * meant scrolling past all of them, and every card had a different height for no
 * reason other than how much history happened to exist. A list gets a height and
 * its own scrollbar: the page stays the same length whether a record has two
 * revisions or two hundred.
 *
 * The header stays put while the body scrolls, because a scrolled list with no
 * visible title is the thing that makes a console feel like a file dump.
 */

.pane {
  max-height: 26rem;
  overflow-y: auto;
  /* Chaining stays ON, deliberately. This was `overscroll-behavior: contain`, which
     is the opposite of what a pane inside a long page wants: the wheel stopped dead
     once the list ended and the page would not move again until the pointer left the
     card. `contain` is for a scroller that owns the viewport, like a modal or a chat
     panel. Here the pane is one card among many, so reaching its end must carry on
     scrolling the page underneath. */
  overscroll-behavior: auto;
}

.pane-sm {
  max-height: 17rem;
}

.pane-lg {
  max-height: 34rem;
}

/* A sticky header inside a scrolling card. It needs its own background, or the rows
   show through as they pass underneath. */
.card > .card-header {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--surface);
}

/* The scrollbar is visible at rest, not on hover. A pane whose thumb only appears
   when the pointer is inside it looks like a list that was cut off: the thumb is the
   only thing saying there is more below, so hiding it defeats the point.
   Firefox gets the standard property, WebKit the pseudo-elements. */
.pane {
  scrollbar-width: thin;
  scrollbar-color: var(--scroll-thumb) transparent;
}

.pane::-webkit-scrollbar {
  width: 0.5rem;
}

.pane::-webkit-scrollbar-track {
  background: transparent;
}

.pane::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: content-box;
}

.pane:hover::-webkit-scrollbar-thumb {
  background: var(--scroll-thumb-hover);
  background-clip: content-box;
}

/* ---------------------------------------------------------------- buttons
 *
 * One geometry for every button in the console. The templates had fourteen
 * different shapes: `btn-sm` next to full size, `btn-link` for actions that then
 * looked like body text, and three sizes of the same idea on one row. Normalising
 * here rather than in forty templates means a page cannot drift again, and a
 * template that says `btn-sm` or nothing gets the same button either way.
 *
 * Bootstrap 5.3 drives buttons from custom properties, so this sets those rather
 * than fighting its selectors with !important.
 */

.btn,
.btn-sm,
.btn-lg {
  --bs-btn-padding-y: 0.375rem;
  --bs-btn-padding-x: 0.75rem;
  --bs-btn-font-size: 0.8125rem;
  --bs-btn-font-weight: 500;
  --bs-btn-border-radius: 0.375rem;
  --bs-btn-line-height: 1.25rem;
  white-space: nowrap;
}

/* The workhorse. Bootstrap's outline-secondary is grey text on a grey border, which
   on the dark theme is almost exactly how it draws a *disabled* button: every
   "Edit inputs", "Diff" and "Download" read as unavailable. Label colour comes from
   the body instead, and disabled state is left to opacity, which is the only thing
   that should signal it. */
.btn-outline-secondary {
  --bs-btn-color: var(--bs-body-color);
  --bs-btn-border-color: var(--bs-border-color);
  --bs-btn-hover-color: var(--bs-body-color);
  --bs-btn-hover-bg: var(--surface-2);
  --bs-btn-hover-border-color: var(--bs-secondary-color);
  --bs-btn-active-color: var(--bs-body-color);
  --bs-btn-active-bg: var(--surface-2);
  --bs-btn-active-border-color: var(--bs-secondary-color);
  --bs-btn-disabled-color: var(--bs-secondary-color);
  --bs-btn-disabled-border-color: var(--bs-border-color);
}

/* Disabled has to be unmistakable now that outline buttons are legible. Bootstrap's
   0.65 opacity was enough when the enabled state was already dim; against a properly
   contrasted label it left "Write DNS" looking pressable when it is not. Opacity is
   the whole signal, so it carries the weight, and the cursor confirms it. */
.btn:disabled,
.btn.disabled,
fieldset:disabled .btn {
  opacity: 0.4;
  cursor: not-allowed;
  box-shadow: none;
}

/* Tertiary actions. A real button that only shows its edges when you approach it,
   so "Check again" reads as something you can press without competing with the
   primary action beside it. `btn-link` is redirected here because a bare underlined
   link in a row of buttons does not look pressable at all. */
.btn-subtle,
.btn-link {
  --bs-btn-color: var(--bs-body-color);
  --bs-btn-bg: transparent;
  /* A visible edge at rest, not on hover. Borderless-until-hover was the same
     mistake as the underlined link it replaced: if it does not look pressable
     before you reach it, it is not a button. */
  --bs-btn-border-color: var(--hairline);
  --bs-btn-hover-color: var(--bs-body-color);
  --bs-btn-hover-bg: var(--surface-2);
  --bs-btn-hover-border-color: var(--hairline);
  --bs-btn-active-bg: var(--surface-2);
  --bs-btn-active-border-color: var(--hairline);
  text-decoration: none;
}

/* An action that leaves the console, so the icon is part of the label. */
.btn .ext {
  margin-left: 0.25rem;
  opacity: 0.7;
  vertical-align: -0.0625rem;
}

/* Actions that belong together sit together, with one gap value rather than each
   template picking its own margin. */
.actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
}

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

/* A block of data has a width it wants, and it is not "all of it". Seven columns
   and three rows stretched across 1200px reads as broken: the numeric columns
   become dashes separated by dead space, and the eye has to travel the whole screen
   to get from a record to its activity time.
 *
 * So the page is fluid and the data is not. Empty space to the right of a dense
 * table looks deliberate; a stretched table looks like a bug. Cards on the record
 * page have no cap, because they have content to fill it. */
.data-block {
  max-width: 74rem;
}

/* Declared widths via colgroup do the real work; these keep the cells honest. */
.table-fixed {
  table-layout: fixed;
  width: 100%;
  margin: 0;
}

.table-fixed th,
.table-fixed td {
  padding: 0.625rem var(--s3);
  vertical-align: middle;
  overflow-wrap: anywhere;
}

.table-fixed thead th {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--bs-secondary-color);
  white-space: nowrap;
}

.num,
.tabular {
  font-variant-numeric: tabular-nums;
}

th.num,
td.num {
  text-align: right;
}

/* Paragraphs inside a card stop at a readable measure even when the card is wide.
   Now that the page is fluid, an explanation in a full-width card ran past 1100px,
   which is roughly 160 characters and genuinely hard to scan back from. Tables,
   forms and rows are unaffected.
 *
 * In rem rather than ch on purpose. `ch` is the better measure in isolation, but it
 * resolves against each element's own font size, so a card mixing 1rem and
 * 0.8125rem paragraphs got a different right edge on every one and the block read
 * as ragged. One column edge beats a per-paragraph optimum here. */
.card-body > p,
.card-body > .prose {
  max-width: 52rem;
}

.cell-title {
  font-weight: 600;
  text-decoration: none;
}

.cell-sub {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  margin-top: 0.125rem;
}

/* Badges: one size, so a column of them has a straight left and right edge. */
.state {
  display: inline-block;
  min-width: 6.5rem;
  text-align: center;
  font-size: 0.6875rem;
  font-weight: 600;
  padding: 0.1875rem var(--s2);
  border-radius: 999px;
  white-space: nowrap;
}

/* A one-line finding about the record, such as whether the domain already serves
   something. It was bare small text sitting above the first card, which read as an
   orphaned sentence rather than as a status. */
.strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  margin-bottom: var(--s4);
  border: 1px solid var(--hairline);
  border-left: 2px solid var(--bs-secondary);
  border-radius: 0.375rem;
  background: var(--surface);
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
}

.metric {
  display: flex;
  flex-direction: column;
  gap: 0.125rem;
}

.metric .label {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--bs-secondary-color);
}

.metric .value {
  font-size: 1.5rem;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.metric .foot {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* Wide content scrolls inside its own box. The page never scrolls sideways. */
.scroll-x {
  overflow-x: auto;
}

.sha {
  font-family: var(--bs-font-monospace);
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* ---------------------------------------------------------------- timeline */

.timeline {
  list-style: none;
  margin: 0;
  padding-left: 1.5rem;
  position: relative;
}

.timeline::before {
  content: "";
  position: absolute;
  left: 0.3125rem;
  top: 0.4rem;
  bottom: 0.4rem;
  width: 2px;
  background: var(--hairline);
}

.timeline > li {
  position: relative;
  padding-bottom: var(--s4);
}

.timeline > li:last-child {
  padding-bottom: 0;
}

.timeline > li::before {
  content: "";
  position: absolute;
  left: -1.375rem;
  top: 0.4rem;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: 50%;
  background: var(--bs-body-bg);
  border: 2px solid var(--bs-secondary);
}

.timeline > li.is-running::before { border-color: var(--bs-info); animation: pulse 1.4s ease-in-out infinite; }
.timeline > li.is-succeeded::before { border-color: var(--bs-success); background: var(--bs-success); }
.timeline > li.is-failed::before { border-color: var(--bs-danger); background: var(--bs-danger); }
.timeline > li.is-interrupted::before { border-color: var(--bs-warning); background: var(--bs-warning); }
.timeline > li.is-noop::before,
.timeline > li.is-skipped::before { border-style: dashed; }

@keyframes pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(13, 202, 240, 0.5); }
  50% { box-shadow: 0 0 0 5px rgba(13, 202, 240, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .timeline > li.is-running::before { animation: none; }
}

/* ---------------------------------------------------------------- logs */

.logstream {
  max-height: 20rem;
  overflow-y: auto;
  font-family: var(--bs-font-monospace);
  font-size: 0.75rem;
  line-height: 1.55;
  background: var(--surface);
}

.logstream .lvl-warning { color: var(--bs-warning-text-emphasis); }
.logstream .lvl-error { color: var(--bs-danger-text-emphasis); }
.logstream .lvl-debug { color: var(--bs-secondary-color); }

/* ---------------------------------------------------------------- diff */

.diff ins {
  background: var(--ins-bg);
  text-decoration: none;
  border-radius: 2px;
  padding: 0 2px;
}

.diff del {
  background: var(--del-bg);
  border-radius: 2px;
  padding: 0 2px;
}

.row-added { box-shadow: inset 3px 0 0 var(--bs-success); }
.row-removed { box-shadow: inset 3px 0 0 var(--bs-danger); }
.row-changed { box-shadow: inset 3px 0 0 var(--bs-warning); }

.hand-edited { box-shadow: inset 3px 0 0 var(--bs-info); }

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

@media (max-width: 62rem) {
  /* The rail becomes a strip above the content: at this width a 14.5rem column
     leaves the table too narrow to read, and the rows are short enough to wrap. */
  .app-body {
    flex-direction: column;
  }

  .subnav {
    flex: 0 0 auto;
    width: 100%;
    height: auto;
    flex-direction: row;
    flex-wrap: wrap;
    gap: var(--s2);
    position: static;
    border-right: 0;
    border-bottom: 1px solid var(--hairline);
    padding: var(--s3) var(--gutter);
  }

  .subnav-title {
    width: 100%;
    padding-bottom: 0;
  }

  /* Rows shrink to their content when they sit in a row rather than a column, so
     the fixed count column would leave a gap after every label. */
  .subnav-item {
    grid-template-columns: auto auto auto;
  }

  .subnav-rule {
    display: none;
  }

  :root {
    --gutter: 1rem;
  }
}

/* The bar itself never wraps: the brand tag and the username are the two things
   that can go without losing a control. */
@media (max-width: 40rem) {
  .topbar-in {
    gap: var(--s3);
  }

  .brand-tag {
    display: none;
  }
}

/* ---------------------------------------------------------------- forms
 *
 * A panel is a card with a proper head: a title, a line saying what the group is
 * for, and a body on the same rhythm as everything else. Bootstrap's card-header is
 * a single line of text, which is why the first version of the create form had four
 * cards that all looked like table headers.
 */

.panel {
  border: 1px solid var(--hairline);
  border-radius: 0.5rem;
  background: var(--bs-body-bg);
  overflow: hidden;
}

.panel-head {
  padding: var(--s4) var(--s4) 0;
}

.panel-head h2 {
  font-size: 0.9375rem;
  font-weight: 600;
  margin: 0;
  letter-spacing: -0.005em;
}

.panel-head p {
  font-size: 0.8125rem;
  color: var(--bs-secondary-color);
  margin: 0.125rem 0 0;
  max-width: 60ch;
}

.panel-body {
  padding: var(--s4);
}

/* One field. The gap between fields is the field's own, so a panel body does not
   need to know how many it holds. */
.field + .field,
.field + .why,
.panel-body > .why {
  margin-top: var(--s4);
}

.field .form-label {
  font-size: 0.8125rem;
  font-weight: 500;
  margin-bottom: 0.3125rem;
  display: flex;
  align-items: baseline;
  gap: 0.375rem;
}

.field .req {
  color: var(--bs-danger);
  font-weight: 600;
}

.field .opt,
.form-label .opt {
  font-size: 0.6875rem;
  font-weight: 400;
  color: var(--bs-secondary-color);
  text-transform: lowercase;
}

.field-hint {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  margin-top: 0.3125rem;
  max-width: 62ch;
}

.field-error {
  font-size: 0.75rem;
  color: var(--bs-danger-text-emphasis);
  margin-top: 0.3125rem;
}

.field-hint.over {
  color: var(--bs-warning-text-emphasis);
}

/* Long explanations belong here: available on demand, out of the way otherwise. The
   first version put three lines of prose under every input, which made six fields
   read as twenty. */
.why {
  border-top: 1px solid var(--hairline);
  padding-top: var(--s3);
  font-size: 0.75rem;
}

.why summary {
  cursor: pointer;
  color: var(--bs-secondary-color);
  font-weight: 500;
}

.why summary:hover {
  color: var(--bs-body-color);
}

.why p {
  color: var(--bs-secondary-color);
  margin: var(--s2) 0 0;
  max-width: 68ch;
}

/* Files chosen but not yet uploaded. A file input shows only a count, and "3 files"
   is not enough to notice you picked the wrong three. */
.picked {
  list-style: none;
  margin: var(--s2) 0 0;
  padding: 0;
  font-size: 0.75rem;
}

.picked li {
  display: flex;
  gap: var(--s2);
  padding: 0.3125rem var(--s2);
  border: 1px solid var(--hairline);
  border-radius: 0.25rem;
  margin-bottom: 0.25rem;
  background: var(--surface);
}

.picked .size {
  margin-left: auto;
  color: var(--bs-secondary-color);
}

.form-foot {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-top: var(--s5);
  padding-top: var(--s4);
  border-top: 1px solid var(--hairline);
}

.form-foot p {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

/* The numbered sequence after the form. Numbered because these really are ordered:
   nothing can be published before it is approved. */
.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
}

.steps li {
  display: flex;
  gap: var(--s3);
}

.steps .n {
  flex: 0 0 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  border: 1px solid var(--hairline);
  display: grid;
  place-items: center;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--bs-secondary-color);
}

.steps strong {
  font-size: 0.8125rem;
  display: block;
}

.steps p {
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
  margin: 0.125rem 0 0;
}

/* A textarea that is going to hold a page of instructions should look like it. */
#id_instructions {
  min-height: 11rem;
  line-height: 1.6;
}

/* ================================================================ palette 2
 *
 * The console shipped on Bootstrap's stock blue and green. Both are recognisable as
 * defaults rather than as a decision, and next to the neutral chrome they read as
 * older than the rest of the page. This block restates the semantic colours and adds
 * the depth treatment. It is colour and surface only: no geometry, no layout, no
 * component behaviour.
 *
 * The choices, and the reasoning:
 *
 *   primary  indigo. It is the console's existing brand family, so the mark and the
 *            nav marker still agree with the buttons, but a step brighter so a
 *            primary action carries on a dark ground.
 *   success  emerald rather than Bootstrap's forest green, which is dark enough that
 *            white text on it looks heavy and the button reads as disabled.
 *   danger   rose. Bootstrap's red sits very close to its orange warning; at a glance
 *            on a busy board they were hard to tell apart.
 *   warning  amber, unchanged in spirit, warmed slightly to separate it from danger.
 *   info     cyan, so "running" never competes with "primary action".
 *
 * Semantic colours stay semantic: the brand is on the mark and the nav, and nowhere
 * near status, because a brand colour competing with green and red is how a failed
 * record stops standing out.
 */

:root {
  --p-500: #5b5bd6;
  --p-600: #4f4bc9;
  --p-700: #4340b0;
  --s-500: #0f9d76;
  --s-600: #0c8765;
  --d-500: #e5484d;
  --d-600: #d13639;
  --w-500: #e0891a;
  --i-500: #0d9ec7;

  --brand: var(--p-500);

  /* Two roles, deliberately separate.
   *
   * The -500 hues are for TEXT, badges and borders: they have to carry on the page
   * background, which is why the dark theme lifts them.
   *
   * The -fill hues are for a SOLID button with a white label, and there the same lift
   * is wrong: measured against white, the lifted green came out at 1.85:1 and the
   * lifted red at 2.90:1, well under the 4.5 a 13px label needs. So the fills stay
   * deep in both themes and only the text hues move. */
  --p-fill: #5b5bd6;
  --p-fill-hover: #4f4bc9;
  --p-fill-active: #4340b0;
  --s-fill: #0b7a5c;
  --s-fill-hover: #0a6e53;
  --d-fill: #d13639;
  --d-fill-hover: #c62b2f;
  --w-fill: #96590d;
  --i-fill: #097996;

  --bs-primary: var(--p-500);
  --bs-primary-rgb: 91, 91, 214;
  --bs-success: var(--s-500);
  --bs-success-rgb: 15, 157, 118;
  --bs-danger: var(--d-500);
  --bs-danger-rgb: 229, 72, 77;
  --bs-warning: var(--w-500);
  --bs-warning-rgb: 224, 137, 26;
  --bs-info: var(--i-500);
  --bs-info-rgb: 13, 158, 199;

  --bs-link-color: var(--p-600);
  --bs-link-hover-color: var(--p-700);
  --bs-link-color-rgb: 79, 75, 201;

  /* Ink for a label sitting on the matching -bg-subtle ground. On white the subtle
     ground is pale, so the text has to be the deep fill hue to clear 4.5:1; in the dark
     theme the ground is dark and the same hue would be unreadable, so it flips to the
     lifted -500 there. Used by the effect tags on the mail actions. */
  --w-ink: var(--w-fill);
  --i-ink: var(--i-fill);
}

[data-bs-theme="dark"] {
  /* Lifted, because the same indigo that reads as confident on white goes muddy on a
     near-black ground. */
  --p-500: #7b78f0;
  --p-600: #6d6ae8;
  --p-700: #605dd8;
  --s-500: #3dd6a3;
  --s-600: #2fc493;
  --d-500: #ff6369;
  --d-600: #f04a50;
  --w-500: #ffb224;
  --i-500: #3ec9e8;

  --bs-link-color: #9c99ff;
  --bs-link-hover-color: #b3b1ff;
  --bs-link-color-rgb: 156, 153, 255;

  /* Fills barely move between themes: a solid button carries its own ground, so what
     matters is the label on it, not the page behind it. Nudged up only enough to stop
     them reading as holes punched in a dark page. */
  --p-fill: #5f5cea;
  --p-fill-hover: #5350de;
  --p-fill-active: #4845c9;
  --s-fill: #0b7a5c;
  --s-fill-hover: #0a6e53;
  --d-fill: #d13639;
  --d-fill-hover: #c62b2f;
  --w-fill: #a5630f;
  --i-fill: #097996;

  --w-ink: var(--w-500);
  --i-ink: var(--i-500);
}

/* Subtle and emphasis pairs, which the badges and alerts are built from. Derived
   from the one colour above rather than listed twice, so a palette change is a
   one-line edit and the pairs cannot drift out of step. */
:root,
[data-bs-theme="dark"],
[data-bs-theme="light"] {
  --bs-primary-bg-subtle: color-mix(in srgb, var(--p-500) 14%, transparent);
  --bs-primary-border-subtle: color-mix(in srgb, var(--p-500) 34%, transparent);
  --bs-primary-text-emphasis: var(--p-500);

  --bs-success-bg-subtle: color-mix(in srgb, var(--s-500) 14%, transparent);
  --bs-success-border-subtle: color-mix(in srgb, var(--s-500) 34%, transparent);
  --bs-success-text-emphasis: var(--s-500);

  --bs-danger-bg-subtle: color-mix(in srgb, var(--d-500) 14%, transparent);
  --bs-danger-border-subtle: color-mix(in srgb, var(--d-500) 34%, transparent);
  --bs-danger-text-emphasis: var(--d-500);

  --bs-warning-bg-subtle: color-mix(in srgb, var(--w-500) 16%, transparent);
  --bs-warning-border-subtle: color-mix(in srgb, var(--w-500) 34%, transparent);
  --bs-warning-text-emphasis: var(--w-500);

  --bs-info-bg-subtle: color-mix(in srgb, var(--i-500) 14%, transparent);
  --bs-info-border-subtle: color-mix(in srgb, var(--i-500) 34%, transparent);
  --bs-info-text-emphasis: var(--i-500);

  --bs-secondary-bg-subtle: color-mix(in srgb, var(--bs-body-color) 7%, transparent);
  --bs-secondary-border-subtle: color-mix(in srgb, var(--bs-body-color) 14%, transparent);
}

/* ---------------------------------------------------------------- solid buttons
 *
 * Bootstrap 5.3 drives buttons from per-variant custom properties, so setting
 * --bs-primary alone changes nothing about .btn-primary. Each variant is restated.
 *
 * The depth is two things and no more: a one-stop gradient so the surface is not
 * flat, and a shadow tinted with the button's own colour so it sits on the page
 * rather than floating above it in grey. A hairline inset at the top is what reads
 * as glass; a real blur behind an opaque button would do nothing.
 */

.btn-primary,
.btn-success,
.btn-danger,
.btn-warning,
.btn-info {
  --bs-btn-color: #fff;
  --bs-btn-hover-color: #fff;
  --bs-btn-active-color: #fff;
  --bs-btn-disabled-color: #fff;
  border: 1px solid transparent;
  background-image: linear-gradient(
    180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0) 60%
  );
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 1px 2px rgba(16, 24, 40, 0.18);
  transition: box-shadow 0.15s ease, transform 0.05s ease, background-color 0.15s ease;
}

.btn-primary:hover,
.btn-success:hover,
.btn-danger:hover,
.btn-warning:hover,
.btn-info:hover {
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.28),
    0 2px 8px -1px var(--btn-glow, rgba(16, 24, 40, 0.35));
}

.btn-primary:active,
.btn-success:active,
.btn-danger:active,
.btn-warning:active,
.btn-info:active {
  transform: translateY(0.5px);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
}

.btn-primary {
  --bs-btn-bg: var(--p-fill);
  --bs-btn-border-color: var(--p-fill);
  --bs-btn-hover-bg: var(--p-fill-hover);
  --bs-btn-hover-border-color: var(--p-fill-hover);
  --bs-btn-active-bg: var(--p-fill-active);
  --bs-btn-active-border-color: var(--p-fill-active);
  --bs-btn-disabled-bg: var(--p-fill);
  --bs-btn-disabled-border-color: var(--p-fill);
  --btn-glow: color-mix(in srgb, var(--p-fill) 45%, transparent);
}

.btn-success {
  --bs-btn-bg: var(--s-fill);
  --bs-btn-border-color: var(--s-fill);
  --bs-btn-hover-bg: var(--s-fill-hover);
  --bs-btn-hover-border-color: var(--s-fill-hover);
  --bs-btn-active-bg: var(--s-fill-hover);
  --bs-btn-disabled-bg: var(--s-fill);
  --btn-glow: color-mix(in srgb, var(--s-fill) 45%, transparent);
}

.btn-danger {
  --bs-btn-bg: var(--d-fill);
  --bs-btn-border-color: var(--d-fill);
  --bs-btn-hover-bg: var(--d-fill-hover);
  --bs-btn-hover-border-color: var(--d-fill-hover);
  --bs-btn-active-bg: var(--d-fill-hover);
  --bs-btn-disabled-bg: var(--d-fill);
  --btn-glow: color-mix(in srgb, var(--d-fill) 45%, transparent);
}

.btn-warning {
  --bs-btn-bg: var(--w-fill);
  --bs-btn-border-color: var(--w-fill);
  --bs-btn-hover-bg: var(--w-fill);
  --btn-glow: color-mix(in srgb, var(--w-fill) 45%, transparent);
}

.btn-info {
  --bs-btn-bg: var(--i-fill);
  --bs-btn-border-color: var(--i-fill);
  --bs-btn-hover-bg: var(--i-fill);
  --btn-glow: color-mix(in srgb, var(--i-fill) 45%, transparent);
}

/* Outline variants inherit the new hues rather than the old ones. */
.btn-outline-primary {
  --bs-btn-color: var(--p-500);
  --bs-btn-border-color: color-mix(in srgb, var(--p-500) 45%, transparent);
  --bs-btn-hover-bg: color-mix(in srgb, var(--p-500) 12%, transparent);
  --bs-btn-hover-color: var(--p-500);
  --bs-btn-hover-border-color: var(--p-500);
  --bs-btn-active-bg: color-mix(in srgb, var(--p-500) 20%, transparent);
  --bs-btn-active-color: var(--p-500);
  --bs-btn-disabled-color: var(--p-500);
}

.btn-outline-danger {
  --bs-btn-color: var(--d-500);
  --bs-btn-border-color: color-mix(in srgb, var(--d-500) 45%, transparent);
  --bs-btn-hover-bg: color-mix(in srgb, var(--d-500) 12%, transparent);
  --bs-btn-hover-color: var(--d-500);
  --bs-btn-hover-border-color: var(--d-500);
  --bs-btn-active-bg: color-mix(in srgb, var(--d-500) 20%, transparent);
  --bs-btn-active-color: var(--d-500);
  --bs-btn-disabled-color: var(--d-500);
}

/* Focus is a ring in the primary hue, on every control, replacing Bootstrap's stock
   blue halo. Keyboard users get the same language as the rest of the page. */
.btn:focus-visible,
.form-control:focus,
.form-select:focus,
.form-check-input:focus {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--p-500) 28%, transparent);
  border-color: color-mix(in srgb, var(--p-500) 60%, var(--hairline));
  outline: none;
}

.form-check-input:checked {
  background-color: var(--p-500);
  border-color: var(--p-500);
}

/* ---------------------------------------------------------------- glass
 *
 * Used where something floats over content that scrolls underneath, and nowhere
 * else. A blurred panel over a solid background is just a tinted panel with a
 * performance cost, and behind dense tables it hurts reading. So: the top bar, and
 * sticky table headers.
 */

.topbar {
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  backdrop-filter: saturate(150%) blur(14px);
  -webkit-backdrop-filter: saturate(150%) blur(14px);
  border-bottom: 1px solid var(--hairline);
}

@supports not (backdrop-filter: blur(1px)) {
  .topbar { background: var(--surface); }
}

.card > .card-header {
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  backdrop-filter: saturate(140%) blur(10px);
  -webkit-backdrop-filter: saturate(140%) blur(10px);
}

/* The active nav marker picks up the new primary automatically through --brand, but
   the badge on it was drawn against the old blue. */
.topnav-badge {
  background: color-mix(in srgb, var(--w-500) 22%, transparent);
  color: var(--w-500);
  border: 1px solid color-mix(in srgb, var(--w-500) 34%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .btn-primary, .btn-success, .btn-danger, .btn-warning, .btn-info {
    transition: none;
  }
}

/* ---------------------------------------------------------------- page progress
 *
 * A 2px bar under the top bar while a page is on its way. See progress.js for why it
 * waits before appearing and why it never reaches the end on its own.
 *
 * Transform rather than width: a width animation lays out on every frame, and this
 * runs during navigation, which is exactly when the main thread is busiest.
 */

/* ---------------------------------------------------------------- the page loader
 *
 * In the middle, not a hairline under the bar. The bar was 2px at the top of a 1500px
 * window while the eye was on whatever had just been clicked, usually most of a screen
 * away, so on a slow page it read as nothing happening at all.
 *
 * Never blocks. `pointer-events: none` on the layer and the card, so even if this were
 * somehow left on screen the page underneath is still fully usable. That matters: it has
 * already been left on screen once, by a submit that got cancelled after this had
 * started.
 */
.page-loader {
  position: fixed;
  inset: 0;
  z-index: 1080;
  display: grid;
  place-items: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.16s ease-out;
  /* A whisper of a scrim: enough that the page reads as busy, not enough to hide what
     you were looking at or to imply a modal. 38% with a blur was the first attempt and
     it dimmed a dark page almost to black, which reads as a failure rather than a wait. */
  background: color-mix(in srgb, var(--bs-body-bg) 16%, transparent);
}

.page-loader.on {
  opacity: 1;
}

.page-loader-card {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.75rem 1.125rem;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18), 0 12px 32px -12px rgba(0, 0, 0, 0.45);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--bs-body-color);
  pointer-events: none;
  transform: translateY(0.35rem) scale(0.98);
  transition: transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
}

.page-loader.on .page-loader-card {
  transform: translateY(0) scale(1);
}

.page-loader-spin {
  width: 1rem;
  height: 1rem;
  flex: 0 0 auto;
  border-radius: 999px;
  /* A ring with one lit quarter, so the direction of travel is legible at 16px. */
  border: 2px solid color-mix(in srgb, var(--p-500) 28%, transparent);
  border-top-color: var(--p-500);
  animation: page-loader-spin 0.7s linear infinite;
}

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

.page-loader-text {
  letter-spacing: 0.01em;
}

/* A spinner is motion for its own sake to somebody who asked for less of it, so the ring
   pulses in place instead. Still moving, still clearly not finished. */
@media (prefers-reduced-motion: reduce) {
  .page-loader {
    transition: opacity 0.1s linear;
  }

  .page-loader-card {
    transform: none;
    transition: none;
  }

  .page-loader-spin {
    animation: page-loader-pulse 1.1s ease-in-out infinite;
    border-color: color-mix(in srgb, var(--p-500) 55%, transparent);
    border-top-color: var(--p-500);
  }

  @keyframes page-loader-pulse {
    0%, 100% { opacity: 0.35; }
    50% { opacity: 1; }
  }
}

/* How far through a running step is.
 *
 * Deliberately thin and deliberately short of full. This is elapsed against the median
 * of past runs, not real progress: nothing in an agent call or a detached installer
 * reports how far it has got. So the bar is a hint about pace, sized like a hint, and it
 * stops at 90% because the only thing that means "finished" is the step finishing.
 *
 * Amber once it passes the typical duration, which is the moment the estimate stops
 * being useful and the honest thing is to say so rather than to keep the bar creeping. */
.mf-steptime {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  font-size: 0.75rem;
  color: var(--bs-secondary-color);
}

.mf-steptime-bar {
  flex: 1 1 8rem;
  min-width: 6rem;
  max-width: 22rem;
  height: 3px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--bs-secondary-color) 24%, transparent);
  overflow: hidden;
}

.mf-steptime-bar i {
  display: block;
  height: 100%;
  width: 2%;
  border-radius: inherit;
  background: var(--brand);
  transition: width 0.9s linear;
}

.mf-steptime.is-over .mf-steptime-bar i {
  background: var(--bs-warning, #e0891a);
}

.mf-steptime.is-over {
  color: var(--bs-warning-text-emphasis, var(--bs-warning));
}

@media (prefers-reduced-motion: reduce) {
  .mf-steptime-bar i { transition: none; }
}
