/* Fleetmation — the visual system (Phase 23a).
 *
 * Reference: the STRIPE DASHBOARD product UI, explicitly not stripe.com
 * marketing. One sans, no display face. One accent used once, for links, the
 * active control and focus. White cards with 1px hairline borders on a cool grey
 * page. Small type with tabular numbers. Badges are coloured text on their own
 * tint, never a saturated fill. No gradients, no shadows, no glass — and that
 * prohibition is tested, in tests/test_visual_system.py, because it is what stops
 * this drifting into the median of the internet.
 *
 * ONE DELIBERATE DEVIATION: money follows accounting convention rather than web
 * convention — right-aligned, tabular figures, a rule above a total. No
 * general-purpose reference supplies that; it comes from the domain the office
 * already works in all day.
 *
 * THIS IS NOT A DENSITY EXERCISE. Cells were padded 6.4px vertically before this
 * phase, which is TIGHTER than the 8px scale, so rows here are slightly TALLER
 * than they were and fewer trips fit on a screen. That is the human's decision of
 * 2026-09-24, and `test_no_cell_padding_is_tighter_than_it_was_before_this_phase`
 * exists because it is exactly the decision that erodes later when somebody wants
 * one more visible row.
 *
 * Components consume tokens and never restate a value. Every literal colour lives
 * in the :root block below and nowhere else — also tested.
 */

:root {
  /* --- Type ---------------------------------------------------------------
     One modular scale, ratio 1.2, from a 14px base. In `rem`, so a reader who
     has changed their browser's font size still gets what they asked for. */
  --fs-n1: 0.75rem;
  --fs-0: 0.875rem;
  --fs-1: 1.05rem;
  --fs-2: 1.26rem;
  --fs-3: 1.5rem;
  --fs-4: 1.8rem;
  --lh-tight: 1.2;
  --lh-body: 1.5;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
    Arial, sans-serif;

  /* --- Space --------------------------------------------------------------
     An 8px scale with one 4px half-step. In `px`, NOT `rem`, deliberately: the
     base type drops from 16px to 14px, so `0.5rem` means 8px before this phase
     and 7px after — a value that had quietly slipped below the padding floor
     while still reading as half a step. px makes the scale mean what it says. */
  --space-half: 4px;
  --space-1: 8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 32px;
  --space-5: 48px;
  --space-6: 64px;

  --page: 1200px;
  --measure: 72ch;
  --radius: 6px;

  /* --- Surface and ink ---------------------------------------------------- */
  --page-bg: #f6f8fa;
  --surface: #ffffff;
  --surface-2: #eef1f5;
  --ink-1: #30313d;
  --ink-2: #596171;
  /* #616873, not Stripe's #6b7280: measured at 4.27:1 on --surface-2, which
     fails AA. This is 4.96:1 at worst across all three surfaces. */
  --ink-3: #616873;
  --rule: #e3e8ee;
  --rule-strong: #d5dbe1;

  /* --- Accent ------------------------------------------------------------
     Stripe's indigo is a BRAND colour, safe on white (4.70:1) and failing on
     both tinted surfaces (4.41 and 4.15). So it is kept for non-text use —
     the focus ring and the active indicator, where 3:1 suffices — and
     --accent-ink is the text version, 5.55:1 at worst. Measured, not eyeballed. */
  --accent: #635bff;
  --accent-ink: #4f46e5;
  --accent-wash: #f1f0ff;

  /* --- Status -------------------------------------------------------------
     Reserved meanings, never used for decoration, always paired with a word so
     the meaning survives a monochrome screen and a reader who does not
     distinguish red from green. These are the values the office has been reading
     since 2026-08-25 and they are unchanged: all five measure between 8.5:1 and
     10.5:1 on their own wash, so there was nothing to fix and changing them
     would have retrained the office for no reason. */
  --good-ink: #1e5228;
  --good-wash: #f1f9f2;
  --good-rule: #2f7a3d;
  --warn-ink: #4a3a00;
  --warn-wash: #fff8e6;
  --warn-rule: #d9b45a;
  --bad-ink: #6b2205;
  --bad-wash: #fdf3f0;
  --bad-rule: #8a2b06;
  --info-ink: #1b4a66;
  --info-wash: #eff6fb;
  --info-rule: #2a6f97;
  --muted-ink: #444444;
  --muted-wash: #f6f6f6;
  --muted-rule: #999999;
  --unknown-ink: #6b5b00;
  --unknown-wash: #fff8dc;
  /* Support mode is a STATE, not a failure. It was the same red as "this data
     cannot be right", which taught the eye to read a superadmin working normally
     as something being wrong. A deep slate-violet is unmistakable, sits apart
     from every status colour, and carries white text at 11.63:1. */
  --support-bar: #3d3450;
}

/* --- Page ------------------------------------------------------------------ */

body {
  font-family: var(--sans);
  font-size: var(--fs-0);
  line-height: var(--lh-body);
  margin: 0;
  color: var(--ink-1);
  background: var(--page-bg);
}

main {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--space-3);
}

h1, h2, h3 {
  line-height: var(--lh-tight);
  color: var(--ink-1);
}

h1 { font-size: var(--fs-4); margin: 0 0 var(--space-1); }
h2 { font-size: var(--fs-2); margin: var(--space-4) 0 var(--space-2); }
h3 { font-size: var(--fs-1); margin: var(--space-3) 0 var(--space-1); }

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

/* One accent, used once. Focus is the same colour as the active control because
   they are the same idea: this is the thing you are operating. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The skip link is offscreen until focused — keyboard users get it first, mouse
   users never see it. */
.skip {
  position: absolute;
  left: -9999px;
}
.skip:focus {
  left: var(--space-2);
  top: var(--space-1);
  background: var(--surface);
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  z-index: 2;
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- Buttons ---------------------------------------------------------------
   Every button in the application was browser default chrome (office,
   2026-09-24) — grey, system-font, out of scale with everything around it.

   TWO WEIGHTS ONLY. The default is a quiet control on a white surface: most
   buttons here are "log out", "download", "dismiss". `--primary` is the one
   action a screen exists for — capture the invoices, fetch the month, verify the
   trip — and there should never be two on a screen.

   White on --accent-ink measures 6.29, comfortably past AA. White on Stripe's
   own --accent is 4.70, which passes but only just, and it is kept for borders
   and focus where no text sits on it. */

button,
.button {
  font: inherit;
  font-size: var(--fs-0);
  line-height: var(--lh-tight);
  color: var(--ink-1);
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--space-1) var(--space-2);
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}

button:hover,
.button:hover {
  background: var(--surface-2);
  border-color: var(--ink-3);
}

button[disabled] {
  color: var(--ink-3);
  background: var(--surface-2);
  cursor: not-allowed;
}

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

/* A control that lives inside a sentence, where a full button would break the
   line — "· Verify this trip" among the corrections. */
form.inline button {
  padding: 0 var(--space-1);
}

/* --- Form controls ---------------------------------------------------------
   Inputs and selects were browser default while the buttons beside them were
   not, so a filter row read as two different toolkits bolted together. */

input[type="text"],
input[type="number"],
input[type="date"],
input[type="email"],
input[type="search"],
select,
textarea {
  font: inherit;
  font-size: var(--fs-0);
  color: var(--ink-1);
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--space-1);
}

label { color: var(--ink-1); }

/* A filter row is a GRID, not a sentence. Written inline it wrapped wherever the
   window happened to end — on the document library, "Account" sat on one line
   and its own dropdown on the next (office, 2026-09-24). Each control is now a
   stacked label and field in its own cell, so they line up however wide the
   window is. */
.controls {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, max-content));
  gap: var(--space-2);
  align-items: end;
  margin-bottom: var(--space-3);
}

.controls label {
  display: flex;
  flex-direction: column;
  gap: var(--space-half);
  color: var(--ink-2);
  font-size: var(--fs-n1);
}

/* A tick box reads along the line, not above it. */
.controls label:has(input[type="checkbox"]) {
  flex-direction: row;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--fs-0);
  color: var(--ink-1);
}

/* The three-up row on the landing page. */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.kicker {
  margin: 0 0 var(--space-half);
  color: var(--ink-2);
  font-size: var(--fs-n1);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

/* --- Page head ------------------------------------------------------------- */

.page-head {
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule);
}
.page-head .standfirst {
  margin: 0;
  color: var(--ink-2);
  max-width: var(--measure);
}

/* Two things read side by side, each in its own card: on the trip screen, what
   was hauled and what was billed for it. `auto-fit` rather than a fixed pair, so
   the panes stack on a narrow window without a second rule to say so, and
   `align-items: start` so the shorter card keeps its own height instead of
   stretching to match its neighbour. */
.panes {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
  gap: var(--space-3);
  align-items: start;
}

/* --- Card ------------------------------------------------------------------ */

.card {
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-2);
  margin-bottom: var(--space-3);
}
.card > h2:first-child,
.card > h3:first-child { margin-top: 0; }

/* --- Sign-in screens -------------------------------------------------------
   A narrow centred column, not a page of application chrome. These were wearing
   `.controls` — the filter-row grid — which lays fields out SIDE BY SIDE across
   the full width, so "Email" and "Password" sat in a row with the button beside
   them and the labels squashed above (office, 2026-09-25).

   A form someone fills in top to bottom wants one column. */

.auth {
  max-width: 26rem;
  margin: var(--space-5) auto;
}

.auth .page-head {
  border-bottom: 0;
  text-align: center;
  margin-bottom: var(--space-3);
}
.auth .page-head .standfirst { margin-inline: auto; }

.auth form {
  display: grid;
  gap: var(--space-2);
}

/* Django renders each field in its own <p>. Stacked, full width, label above. */
.auth form p {
  display: grid;
  gap: var(--space-half);
  margin: 0;
}

.auth label {
  color: var(--ink-2);
  font-size: var(--fs-n1);
}

.auth input {
  width: 100%;
  box-sizing: border-box;
}

.auth button {
  width: 100%;
  padding: var(--space-1) var(--space-2);
}

/* The way out, under the card rather than in it — it is not part of the form. */
.auth__aside {
  margin-top: var(--space-2);
  text-align: center;
  font-size: var(--fs-n1);
}

/* Django puts the help text for a password field in a <ul>; left as a list would
   bullet four lines of advice nobody asked for at the widest point of the page. */
.auth .helptext,
.auth form ul {
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--ink-2);
  font-size: var(--fs-n1);
}

/* --- Tables ----------------------------------------------------------------
   A wide table scrolls inside its own card rather than pushing the page
   sideways: several screens here run to 29 columns. */

.table-wrap {
  overflow-x: auto;
  background: var(--surface);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  margin-bottom: var(--space-3);
}

table {
  border-collapse: collapse;
  width: 100%;
}

caption {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

th, td {
  text-align: left;
  /* 8px/16px — up from 6.4px/9.6px, never down. See the header of this file. */
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--rule);
}

thead th {
  background: var(--surface-2);
  color: var(--ink-2);
  font-size: var(--fs-n1);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

tbody tr:hover { background: var(--surface-2); }

/* --- Numbers --------------------------------------------------------------
   The deviation from the reference, and the point of the whole exercise: 166
   cells in this application rendered money left-aligned in a proportional face,
   so decimal points lined up nowhere and a column could not be scanned for
   magnitude — every figure had to be read. */

.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum";
  white-space: nowrap;
}

/* A total is separated by a rule above it, the way a ledger does it — not by
   bold text alone, which does not survive a monochrome print. */
.total th, .total td {
  border-top: 2px solid var(--rule-strong);
  border-bottom: none;
  font-weight: 600;
}

/* --- Facts ----------------------------------------------------------------
   Label/value metadata. This exists because 103 rows across the app were
   `<tr><th>Label</th><td>value</td></tr>` — a definition list wearing a table,
   which a screen reader announces as a grid. 23b converts them. */

.facts {
  display: grid;
  grid-template-columns: max-content minmax(0, auto);
  gap: var(--space-half) var(--space-2);
  margin: 0;
  /* Held to a readable width instead of stretching across 1200px. A label at one
     edge and its value at the other is two columns of unrelated text (office,
     2026-09-24). */
  justify-content: start;
}
.facts dd { min-width: 0; }
/* A definition list is NOT a column to scan, so a value in one is never
   right-aligned — that is what threw every value to the far side of the screen.
   Tabular figures are still wanted; the alignment is not. */
.facts dd.num { text-align: left; }
.facts dt { color: var(--ink-2); }
.facts dd { margin: 0; }

/* --- Review: the paper beside the reading ---------------------------------- */

.review {
  display: grid;
  grid-template-columns: minmax(20rem, 2fr) 3fr;
  gap: var(--space-3);
  align-items: start;
  margin-bottom: var(--space-3);
}

.review__reading { min-width: 0; }

/* Paging the queue, on the same band as what the document IS: previous at the
   far left, the metadata between, next at the far right. The two controls hold
   their corners whether or not there is a document either side, so the metadata
   does not shuffle sideways as the clerk moves through the library. */
.doc-paging {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.doc-paging > .facts--strip { flex: 1 1 20rem; }
.doc-paging__back,
.doc-paging__forward { flex: 0 0 auto; min-height: 1px; }
.doc-paging__forward { margin-left: auto; }

/* What the file IS, as one line above both columns rather than a list down the
   left of them. In a column it cost the card a screenful of height and pushed it
   below the top of the paper it is read against (office, 2026-09-26). */
.facts--strip {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--rule);
  margin-bottom: var(--space-3);
}
.facts--strip dt { flex: 0 0 auto; }
.facts--strip dt::after { content: ":"; }
.facts--strip dd { flex: 0 0 auto; margin-right: var(--space-2); font-weight: 500; }

.paper {
  width: 100%;
  /* It used to follow the page down, because the left column ran on past the
     fields to a table of every invoice in the file. That table is gone — the card
     pages instead — so the two columns are now a similar length and a frame
     scrolling independently of the page is just two scrollbars where one will do
     (office, 2026-09-25).

     Taller since 2026-09-26: with the metadata out of the column the split opens
     high on the page, so the frame has the room, and every centimetre of it is a
     scroll the clerk does not make inside the PDF viewer. */
  height: calc(100vh - 5rem);
  min-height: 42rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
  background: var(--surface);
}

/* The card is what the paper is read against, so it gets the same top and the
   same visual weight — not a box floating beside a much larger one. */
.review__reading > .card { margin-top: 0; }

/* On a narrow window the two stack, and the fields come first: the PDF is the
   reference, the fields are the work. */
@media (max-width: 60rem) {
  .review { grid-template-columns: 1fr; }
  .paper { height: 30rem; min-height: 0; }
}

/* The invoice pager on a multi-invoice document. Sits under the card it moves,
   so the relationship is obvious without a label saying so. */
.pager {
  display: flex;
  gap: var(--space-1);
  justify-content: flex-end;
  margin: var(--space-2) 0 0;
}

/* --- Badge ---------------------------------------------------------------- */

.badge {
  display: inline-block;
  border-radius: var(--radius);
  padding: 0 var(--space-half);
  font-size: var(--fs-n1);
  white-space: nowrap;
  background: var(--surface-2);
  color: var(--ink-2);
}
.badge--good { background: var(--good-wash); color: var(--good-ink); }
.badge--warn { background: var(--warn-wash); color: var(--warn-ink); }
.badge--bad { background: var(--bad-wash); color: var(--bad-ink); }
.badge--accent { background: var(--accent-wash); color: var(--accent-ink); }

/* --- Empty state ---------------------------------------------------------- */

.empty {
  color: var(--ink-2);
  background: var(--surface-2);
  border: 1px dashed var(--rule-strong);
  border-radius: var(--radius);
  padding: var(--space-3);
  text-align: center;
}

/* --- Navigation and banners ----------------------------------------------
   Tokenised here, restructured in 23c: 33 links in one flat row is the problem,
   and it is a structural one rather than a styling one. */

/* SCOPED to the site navigation, and that scoping is load-bearing. A bare `nav`
   selector also caught `_pagination.html`, which is a `<nav class="pagination">`:
   the page controls rendered as a full-width grey bar, and once `nav a` dropped
   the underline they read as chrome rather than as links. The office reported the
   page selector as MISSING from the documents screen (2026-09-24) — it was
   rendering the whole time and had stopped looking like anything.

   The bare selector predates Phase 23; removing the underline is what made it
   unusable. `<nav>` is the correct element for both, so the fix is the selector. */
.site-nav {
  background: var(--surface);
  border-bottom: 1px solid var(--rule);
  padding: var(--space-1) var(--space-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}
/* The name, and then everything else pushed to the far edge. */
.brand {
  font-weight: 600;
  font-size: var(--fs-1);
  color: var(--ink-1);
  text-decoration: none;
  margin-right: auto;
}
.site-nav__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}
.site-nav a { color: var(--ink-2); text-decoration: none; }
/* Kept as the guard it became: the bar must never go back to a huddle on the
   left. The brand now does the pushing, so this targets the links block. */
.site-nav > form:last-child { margin-left: auto; }
.site-nav a:hover { color: var(--accent-ink); text-decoration: underline; }

/* An empty table says so, rather than leaving a blank grid that reads as a
   loading failure. Muted and centred so it is plainly an answer and not a row. */
.empty-row td {
  color: var(--ink-2);
  text-align: center;
  padding: var(--space-3) var(--space-2);
}

/* --- Menu groups ----------------------------------------------------------
   Six `<details>` instead of thirty-three flat links. No JavaScript: this is the
   browser's own disclosure widget, so it works with the keyboard, works before
   any script could have run, and works when scripts are off.

   The group holding the current page is rendered `open` server-side, so the
   section you are working in is always already expanded. */

.nav-group {
  position: relative;
  /* Extends the hoverable area to meet the list below it. */
  padding-bottom: var(--space-half);
}

.nav-group > .nav-label {
  cursor: default;
  display: inline-block;
  padding: var(--space-half) var(--space-1);
  border-radius: var(--radius);
  color: var(--ink-2);
}
.nav-group > .nav-label::after { content: " ▾"; color: var(--ink-3); }
.nav-group:hover > .nav-label,
.nav-group:focus-within > .nav-label { background: var(--surface-2); color: var(--accent-ink); }

/* The group you are working in, marked without having to be open. */
.nav-group.nav-group--current > .nav-label { color: var(--accent-ink); font-weight: 600; }

/* Floated over the page rather than pushing it down — a menu that reflows the
   screen underneath it moves the row you were about to click. */
.nav-group > ul { display: none; }
.nav-group:hover > ul,
.nav-group:focus-within > ul {
  display: block;
  position: absolute;
  z-index: 1;
  top: 100%;
  left: 0;
  min-width: 14rem;
  /* No gap. See the note above: a margin here is dead ground the pointer has to
     cross, and crossing it closes the menu. */
  margin: 0;
  padding: var(--space-1);
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius);
}
.nav-group li { margin: 0; }
.nav-group a {
  display: block;
  padding: var(--space-half) var(--space-1);
  border-radius: var(--radius);
  color: var(--ink-1);
  text-decoration: none;
}
.nav-group a:hover { background: var(--surface-2); color: var(--accent-ink); }

/* Where you are now. Not colour alone — the weight carries it on a monochrome
   screen and for a reader who cannot distinguish the tint. */
.nav-group a[aria-current="page"] {
  background: var(--accent-wash);
  color: var(--accent-ink);
  font-weight: 600;
}

/* Page controls. Links that look like links, because that is what they are — and
   the count beside them is how a clerk knows there IS a page two. */
.pagination {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--rule);
}
.pagination a { color: var(--accent-ink); }
.pagination span { color: var(--ink-2); }

/* Everything above the content, stuck to the top as one piece (office,
   2026-09-24). These screens run to hundreds of rows, and losing the navigation
   at row 40 means scrolling back to the top to go anywhere. Stuck as a GROUP
   rather than element by element: otherwise each would need to know the height
   of the one above it, and the support banner wraps to two lines when narrow.
   z-index keeps it over the table it covers. */
.chrome {
  position: sticky;
  top: 0;
  z-index: 3;
}

.support-banner {
  background: var(--support-bar);
  color: var(--surface);
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-n1);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  justify-content: space-between;
}
.support-banner form { margin: 0; }
/* Still unmissable — a superadmin must never forget whose data they are in — but
   no longer the one element on the page that ignores the type scale. The button
   reads as a control on the dark bar rather than as browser default chrome. */
.support-banner button {
  font: inherit;
  color: var(--surface);
  background: transparent;
  border: 1px solid var(--surface);
  border-radius: var(--radius);
  padding: var(--space-half) var(--space-1);
  cursor: pointer;
}
.support-banner button:hover { background: var(--ink-1); }

.grace-banner {
  background: var(--warn-wash);
  color: var(--warn-ink);
  border-bottom: 1px solid var(--warn-rule);
  padding: var(--space-1) var(--space-2);
}

.back { margin: 0 0 var(--space-2); }

/* --- Notices --------------------------------------------------------------
   Flags and warnings were reading as body text and being scrolled past (office,
   2026-08-25). The markup was already right — `role="alert"` marks every warning
   and Django's message framework already emits `class="error"` and friends — and
   what was missing was any rule at all.

   Three weights, so severity is visible before the words are read: `.alarm` (red)
   is data that cannot be right; `role="alert"` (amber) is something to check;
   `success` (green) is a thing that worked. Colour is never the only signal —
   each keeps its border and its wording. */

[role="alert"], .messages li {
  border: 1px solid var(--warn-rule);
  border-left-width: var(--space-half);
  background: var(--warn-wash);
  color: var(--warn-ink);
  padding: var(--space-1) var(--space-2);
  margin: 0 0 var(--space-1);
  border-radius: var(--radius);
}
.messages { list-style: none; padding: 0; margin: 0 0 var(--space-2); }
.messages li.success {
  border-color: var(--good-rule); background: var(--good-wash); color: var(--good-ink);
}
.messages li.info {
  border-color: var(--info-rule); background: var(--info-wash); color: var(--info-ink);
}
/* Amber is already the default above; named anyway, so every tag the message
   framework can emit has a rule somebody can find and change. */
.messages li.warning {
  border-color: var(--warn-rule); background: var(--warn-wash); color: var(--warn-ink);
}
.messages li.debug {
  border-color: var(--muted-rule); background: var(--muted-wash); color: var(--muted-ink);
}
.messages li.error, [role="alert"].alarm {
  border-color: var(--bad-rule); background: var(--bad-wash); color: var(--bad-ink);
}

/* One flag code among plain cells is exactly what gets scrolled past, so it is a
   chip rather than a word. */
.flag {
  display: inline-block;
  background: var(--bad-wash);
  color: var(--bad-ink);
  border: 1px solid var(--bad-rule);
  border-radius: var(--radius);
  padding: 0 var(--space-half);
  margin: 0 var(--space-half) var(--space-half) 0;
  font-size: var(--fs-n1);
  white-space: nowrap;
}

/* Data that cannot be right — loud on purpose, and never filtered away.
   Declared after the notice rules so it keeps winning where a warning carries
   both. */
.alarm {
  border: 2px solid var(--bad-rule);
  background: var(--bad-wash);
  padding: var(--space-2);
  margin-bottom: var(--space-3);
  border-radius: var(--radius);
}
.alarm h2 { margin-top: 0; color: var(--bad-rule); }

/* Raw payload dumps — extraction errors and model replies. Wrapped rather than
   scrolled sideways, and capped so one failed document cannot own the screen. */
.payload {
  white-space: pre-wrap;
  word-break: break-word;
  background: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  padding: var(--space-1);
  font-size: var(--fs-n1);
  max-height: 30rem;
  overflow: auto;
}

/* --- Operations dashboard (Phase 18) --------------------------------------
   Each panel is its own block so a reader can tell where one source of figures
   ends and the next begins. */

section.panel {
  border-top: 2px solid var(--rule);
  padding-top: var(--space-1);
  margin-bottom: var(--space-4);
}
section.panel .freshness { color: var(--ink-3); margin-bottom: var(--space-half); }

/* Health reads as three states, not two: "unknown" is the absence of evidence,
   which is neither a claim that a component works nor that it does not. */
.unknown {
  color: var(--unknown-ink);
  background: var(--unknown-wash);
  padding: 0 var(--space-half);
  border-radius: var(--radius);
}
.down { color: var(--bad-rule); }

/* --- Inline form, for an action that belongs in a sentence ---------------- */

form.inline { display: inline; }
