/* Copyright (c) 2026, SCVGN / Michael
   Licensed under the BSD 2-Clause License. See LICENSE in the project root.

   TMDCS visual system — the whole of it. One stylesheet, no CDN, no
   JavaScript, dark only. Implements docs/design-language.md and
   docs/spec-v0.1-alpha.md section 4.

   Contents
     1  Tokens                    7  Flashes and banners
     2  Reset and base            8  Chips, badges, states
     3  Layout                    9  Tables — the report
     4  Nav and tabs             10  Toolbar and filters
     5  Cards                    11  Login card
     6  Buttons and forms        12  Styleguide helpers
                                 13  Mode-dependent controls
                                 14  Allowances and conventions
*/

/* ------------------------------------------------------------------ 1 */
/* Palette taken verbatim from restrictions/static/css/app.css. Do not
   retune. The ramps below are the corrected v0.1 tokens: they were
   re-measured against the surfaces they actually sit on, not against
   --panel, on which no chip is ever drawn. */
:root {
  /* fleet palette — verbatim */
  --bg: #14161a;
  --panel: #1d2026;
  --text: #d6d9de;
  --accent: #4f9cf9;
  --danger: #e05555;

  /* fleet supporting values */
  --muted: #8b95a3;
  --border: #2a2e35;
  --input-border: #333;
  --on-accent: #0b0d10;
  --flash-error-bg: #3a1f22;
  --flash-info-bg: #1f2c3a;
  --code-bg: #26313f;

  /* TMDCS surfaces the fleet has no equivalent of */
  --rule-v: #23262c;        /* vertical column rule, quieter than --border */
  --head-bg: #242830;       /* sticky table head */
  --row-zebra: #20242b;
  --row-hover: #262b33;
  --row-ineffective: #1a1d22;
  --row-error: #2e2126;
  --row-error-hi: #3c282e;  /* error row, hovered — the lightest real ground */
  --row-warn: #2e2a1f;      /* a line with an ACKNOWLEDGED finding */
  --row-warn-hi: #3c3628;
  --row-total: #242830;
  --input-bg: #171a1f;
  --placeholder: #757575;   /* an empty box's hint: see section 6 */

  /* ELEMENT TYPE (5). Colour is the THIRD cue: every chip carries its
     3-letter code AND a distinct glyph, so the ramp is never load-bearing
     on its own. --t-repetitive is deliberately near-neutral because most
     rows are Repetitive and colouring the majority is noise. */
  --t-repetitive: #aeb9c9;   /* circle   */
  --t-occasional: #19b6d8;   /* diamond  */
  --t-contingency: #efab3f;  /* square   */
  --t-foreign: #b281f0;      /* triangle */
  --t-ineffective: #8c95a0;  /* ring     */

  /* The chip grounds those five are measured on. #332e44 is the surface
     --t-foreign was solved against (4.51:1); do not lighten it. */
  --chip-repetitive-bg: #2a2e35;
  --chip-occasional-bg: #152f3a;
  --chip-contingency-bg: #38301f;
  --chip-foreign-bg: #332e44;
  --chip-ineffective-bg: #292d32;

  /* ELEMENT CATEGORY (3). VA/ENVA/NVA text is always present; the second
     cue is fill weight — boxed / bare / bordered — not hue. #3c2b34 is the
     surface --c-nva was solved against (4.53:1). */
  --c-va: #8ae8b0;
  --c-enva: #7fb0e8;
  --c-nva: #ec6f87;
  --chip-va-bg: #1d3529;
  --chip-enva-bg: #1f2c3a;
  --chip-nva-bg: #3c2b34;

  /* type */
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  --radius: 8px;
  --shadow-card: 0 8px 30px rgba(0, 0, 0, 0.35);

  /* sticky column geometry — the left offset of .col-desc is the width of
     .col-el, so these two must stay in step. Description is 11rem since
     2026-09-29 (it was 14rem), one of the three savings that fit the report
     in its pane at 1440 in the order Michael reads it: section 9, THE REPORT
     FITS ITS PANE. */
  --col-el-w: 4.5rem;
  --col-desc-w: 11rem;
}

/* ------------------------------------------------------------------ 2 */
*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  color-scheme: dark;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 system-ui, sans-serif;
}

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

a:hover,
a:focus-visible {
  text-decoration: underline;
}

/* A LINK THAT IS A WHOLE LIST ITEM IS A TARGET 24px TALL. A list of links —
   the styleguide's contents, the reports a delete screen names, the studies a
   blocked report points at — is a column of targets, and an inline link is
   only as tall as its glyphs: 18px, with the next one 22.5px down, too close
   for the WCAG 2.5.8 spacing exemption on a phone. As an inline block it is
   the line's 22.5px, and a pixel of padding above and below makes 24.5; the
   negative margin hands the line those two pixels back, so the list is not a
   pixel taller. `:only-child` is as near as a selector comes to "the link is
   the item", and every list of links here is written `<li><a>…</a></li>`. It
   counts elements, not text, so a sentence whose one element is a link would
   match too and its link would stop breaking across lines; no list item in
   the application is written that way. (0,1,2) against nothing. */
li > a:only-child {
  display: inline-block;
  padding-block: 1px;
  margin-block: -1px;
}

h1 {
  font-size: 1.5rem;
  margin: 0 0 1rem;
}

h2 {
  font-size: 1.2rem;
  margin: 2rem 0 0.75rem;
}

h3 {
  font-size: 1rem;
  margin: 1.5rem 0 0.5rem;
}

p {
  margin: 0 0 0.75rem;
}

hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 1.5rem 0;
}

code,
kbd,
samp {
  font-family: var(--mono);
  font-size: 0.9em;
  background: var(--code-bg);
  border: 1px solid var(--accent);
  border-radius: 4px;
  padding: 0.05rem 0.35rem;
}

pre {
  font-family: var(--mono);
  background: var(--code-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  overflow-x: auto;
}

pre code {
  background: none;
  border: 0;
  padding: 0;
  white-space: inherit;
}

/* A CODE CHIP IS ONE BOX. An inline `<code>` with a border breaks like any
   other text, and broken it is drawn as two half-boxes, each open on one side
   — "SCVGN" at the end of one line and "TMDCS" at the start of the next, on
   the two-factor enrolment and in the styleguide (2026-09-29). What a chip
   holds is a name, a token or a value, a few words at most, and it is held on
   one line. The enrolment's setup key is the exception: thirty-two characters
   with no space, typed from the screen, and it may break anywhere rather than
   run out of a phone's card. It is the code that stands alone on its line in
   the "Can't scan it?" panel — the account and issuer after it share theirs
   — and it is named that way rather than by a class because the tests and
   the audit's seeding read the key off its markup as it stands. (0,0,1) and
   (0,2,3). Code inside a `<pre>` is a block of lines, not a chip, and `pre
   code` above hands it the `<pre>`'s own `white-space` back at (0,0,2), or
   the rule here would fold its lines into one. */
code {
  white-space: nowrap;
}

details.manual-key > p > code:only-child {
  white-space: normal;
  overflow-wrap: anywhere;
}

.muted {
  color: var(--muted);
}

/* Struck out and still there: a quantity member whose element type carries no
   standard time, so `cycle_count()` excludes its occurrences. It IS a member —
   an imported project file can declare one, and an element can be reclassified
   long after the box was ticked — so it is shown and explained rather than
   hidden, and removing it stays the analyst's decision. */
.struck {
  color: var(--muted);
  text-decoration: line-through;
}

.small {
  font-size: 0.82rem;
}

.nowrap {
  white-space: nowrap;
}

.mono {
  font-family: var(--mono);
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* `padding: 0` since section 9 gave `caption` padding of its own: most
   captions in the application are hidden this way, and a padded box is not
   the one pixel this rule means. (0,1,0) against `caption`'s (0,0,1). */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ------------------------------------------------------------------ 3 */
.site-header {
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}

.site-header > .bar {
  max-width: 1280px;
  margin: 0 auto;
  padding: 0.6rem 1rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.25rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--text);
  font-weight: 600;
  letter-spacing: 0.02em;
}

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

.brand span {
  color: var(--accent);
}

.brand .logo {
  height: 28px;
  width: auto;
}

/* Ship the light rule even though v0.1 is dark-fixed: adding a theme
   toggle later then costs no CSS. */
.logo-light {
  display: none;
}

.logo-dark {
  display: inline;
}

[data-theme="light"] .logo-light {
  display: inline;
}

[data-theme="light"] .logo-dark {
  display: none;
}

main {
  width: 100%;
  max-width: 960px;
  margin: 0 auto;
  padding: 1.5rem 1rem;
  flex: 1 0 auto;
}

main.auth {
  max-width: 420px;
  padding-top: 3rem;
}

main.wide {
  max-width: 1280px;
}

main.full {
  max-width: none;
}

.site-footer {
  background: var(--panel);
  border-top: 1px solid var(--border);
  color: var(--muted);
  text-align: center;
  font-size: 0.82rem;
  padding: 0.9rem 1rem;
}

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  margin-bottom: 1rem;
}

/* THE HEAD SPACES ITS HEADING, whichever level it is. `.page-head h1` alone
   stood here, and a tab's own title is an `<h2>` — the project's name is the
   page's `<h1>`, above the strip — so Studies, Elements, Operators, Areas,
   Quantities, Joblets, the Study Grid, every study page and the import review
   kept the h2's own `2rem 0 0.75rem`. In a flex row that margin does not
   collapse into anything, and those titles started 52px under the tab strip
   while Reports and Issued, written as `<h1>`s, started 20px under it: every
   tab change moved the title 32px and changed its size (2026-09-29, the audit
   Michael asked for — "so that there is no more supises on lake of gaps").
   The templates now agree on the level (a tab's title is an `<h2>`) and this
   agrees on the margin. The head's own `margin-bottom` and `gap` space it.
   (0,1,1), as `.page-head h1` was, against `h1`'s and `h2`'s (0,0,1). */
.page-head > :is(h1, h2) {
  margin: 0;
}

/* A LINE THAT BELONGS TO THE HEADING GOES UNDER IT. A paragraph in the head
   is a flex item beside the heading, and `space-between` sends it to the far
   edge — right for a count or a version, which is what the study pages and
   the styleguide put there. An observation line's element description is
   not that: it is what "Line 1 · 42:19 5" is, and at 1440 it stood 900px to
   the right of the code it describes (the audit, 2026-09-29). `.page-sub`
   takes a line of its own under the heading, the head's 0.5rem row gap
   above it and no paragraph margin under it — in a flex row that margin adds
   to the head's own. (0,2,0), against `p`'s (0,0,1). */
.page-head > .page-sub {
  flex-basis: 100%;
  margin: 0;
}

.stack > * + * {
  margin-top: 1rem;
}

/* An action row brings its own vertical spacing, and the containers that would
 * rather it did not are named in the rule below rather than guessed at per
 * screen.
 *
 * `.row-actions` carries `gap` — which is spacing BETWEEN the buttons — and,
 * until 2026-08-30, no margin whatsoever, so the row hugged the button and
 * whatever sat above it touched that. Michael, 2026-08-17, diagnosing it from
 * the rendered page better than my first fix did:
 *
 *     I can see that the padding and border are applied on the text field of
 *     the button, not the button filed it self, that why both containers from
 *     top and buttom are touching the button container.
 *
 * Exactly so: the visible padding belongs to `.btn`, not to its container.
 *
 * THAT WAS FIXED TWICE AT THE INSTANCE AND REPORTED A THIRD TIME ANYWAY.
 * `form + .card` spaced the section BELOW a submit row; `.card + .row-actions`
 * then spaced the card ABOVE one. Both are adjacent-sibling rules, so both
 * answer for exactly one shape of neighbour — and the Log's pagination row
 * follows a `.table-wrap`. Michael, 2026-08-30, note the word "again":
 *
 *     button '<- Newer' 'Page 1 of 4' and 'Older ->' again have no padding and
 *     touch the top and bottom tables, class="row-actions".
 *
 * `.card + .row-actions` is gone, and enumerating every neighbour it could
 * have had instead — `.table-wrap`, `.table-scroll`, `.card-grid`, `p`, the
 * next `.card` BELOW — is the same rule written six times and one more
 * neighbour away from a seventh report. The margin belongs to the row.
 *
 * 1.5rem, unchanged from `.card + .row-actions` and matching `form + .card`,
 * so a submit row sits evenly between what it saves and what comes next. Top
 * AND bottom because Michael named both sides; in normal flow the two collapse
 * with the neighbour's own margin, so nothing doubles.
 *
 * WHAT IT COSTS: a screen whose action row is the last thing in `main` ends
 * 1.5rem further above the footer, because a last child's margin cannot
 * collapse out through `main`'s own padding. Seven of them, counted by
 * rendering every screen rather than by reading the templates: the password
 * change, the sign-out confirmation, the 2FA enrolment, a new element, a new
 * report, the per-element relaxation guide, and the study delete confirmation.
 * Accepted. It is whitespace at the end of a page, and the alternative is
 * `:last-child` bookkeeping over the sixty-eight action rows the templates
 * hold — a rule that has to be right about every one of them to be right at
 * all, in exchange for a gap nobody can see. */
.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  margin-top: 1.5rem;
  margin-bottom: 1.5rem;
}

/* A ROW INSIDE SOMETHING THAT SPACES ITS OWN CHILDREN TAKES ITS SPACING FROM
 * THAT. This list is not a set of screens, it is every container in this sheet
 * that lays a `.row-actions` out itself, and each of them says so in its own
 * section: `.page-head`, `.toolbar`, `.toolbar-line` (the Log's Filter and
 * Clear, since 2026-09-29), `.finding`, `.report-head` and `.card` are
 * `display: flex` with a `gap` — where a margin does not collapse, it ADDS
 * to the gap — a `.row-actions` inside a `.row-actions` is a flex item of one
 * (the acknowledgement forms in a validation finding), and a `td` is a table
 * cell whose own padding is the spacing.
 *
 * Both halves of the old comment's scoping argument survive here and are the
 * reason this is a list and not a blanket rule: "a `.row-actions` used as a
 * card's own footer relies on having no margin, and several screens open with
 * one directly under the page heading where a gap would be wrong".
 *
 * Specificity (0,2,0) — two classes — except `td > .row-actions` at (0,1,1).
 * Both strictly beat the (0,1,0) above, so this is not a tie settled by source
 * order, which is the failure `input.input-narrow` in section 14 was written
 * to avoid.
 *
 * `>` and not a descendant combinator, deliberately. A `.row-actions` DEEP
 * inside a card is not laid out by the card: on issues/confirm.html the Issue
 * button sits in a plain `<form>` inside one, in ordinary block flow, and the
 * "an issued report is frozen" paragraph under it was welded to the button in
 * exactly the way Michael has now reported three times. */
.page-head > .row-actions,
.card > .row-actions,
.toolbar > .row-actions,
.toolbar-line > .row-actions,
.finding > .row-actions,
.report-head > .row-actions,
.row-actions > .row-actions,
td > .row-actions {
  margin-top: 0;
  margin-bottom: 0;
}

/* THE PAGE HEAD'S ACTION ROW MAY BE NARROWER THAN ITS CONTENT, so that what is
 * in it can shrink. The project list's row holds the search form, and since
 * 2026-09-29 that form keeps its box and its button on one line (section 6, A
 * SEARCH BOX SIZES ITSELF), so its content is a box thirty characters wide
 * plus the button: 383px, in a head that is 366px on a phone. A flex item
 * refuses by default to be narrower than its content, so the row would have
 * stayed 383px and scrolled the page sideways — the first measurement of the
 * fix did exactly that, 396px wide. With this the row takes the head's width
 * and the box inside it gives way, which is Michael's 2026-09-14 request kept
 * at the one width nobody had drawn: "Could the search button be nexto to the
 * input field". (0,2,0), and nothing else sets a minimum width on it. */
.page-head > .row-actions {
  min-width: 0;
}

/* The Actions cell on Accounts: one horizontal line, in a fixed order.
 *
 * Michael, 2026-08-23:
 *
 *     please make sure that the 'actions' section are always in horizontal
 *     line, not squished together please, set role should be last.
 *
 * It was three stacked blocks — a wrapping `.row-actions` of six buttons, then
 * Rename, then Set role — so a row read as a pile rather than a line, and the
 * wrap point moved with the account's state (Unlock only appears when locked,
 * Reset 2FA only when a factor is armed). Two rows of the same table therefore
 * disagreed about their own shape, which is the same complaint he made about
 * the allowance guide.
 *
 * `nowrap` is safe here and only here: the table already sits in a
 * `.table-wrap` with `overflow-x: auto`, so a wide row scrolls the table
 * instead of breaking the page. `flex-shrink: 0` is the half that stops the
 * squeezing — without it flexbox happily compresses a button until its label
 * wraps mid-word, which is what "squished together" looks like. */
/* WRAPS, since 2026-08-30.  Michael: "the 'set' box is out of view, please
   shrink some of the columns so that the view shows all buttons".  This row
   holds up to eight controls, two of which carry their own input or select, and
   `nowrap` made them one line however narrow the viewport — so the last of them,
   Set role, sat past the right edge of `.table-wrap` behind a horizontal scroll
   nobody looks for on a table whose other columns all fit.
   The inner `.form-inline` keeps its `nowrap` below, and that is the whole
   trick: Rename stays glued to its box and Set role to its select, so wrapping
   breaks the row BETWEEN controls and never through one. */
/* AND ON A PHONE THE ROW IS NOT A COLUMN, since 2026-09-29. The table is
   wider than a phone whatever this cell does, and scrolls in its `.table-wrap`;
   left to shrink, the cell was squeezed to its widest single control and the
   buttons stacked one per line, 174px a row. At least 24rem — the widest
   control and a couple beside it — they wrap in threes and a row is 133px.
   At 1024 and 1440 the cell is 552 and 808px, and nothing moves. (0,1,0) and
   the only `min-width` on it: the same wrapping ("between controls and never
   through one", above), across a wider cell, on the one width where the table
   scrolls anyway (section 9, ON A PHONE A TABLE SCROLLS). */
.account-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem 0.5rem;
  min-width: 24rem;
}

/* And in the same threes on a phone, where the buttons and boxes in the row
   are 16px so that Safari does not zoom on the Rename box (section 6, ON A
   PHONE A CONTROL'S TEXT IS 16px): each is 16/15 of its width, and in 24rem
   the rows fell to fours, 161px each. 25.6rem is 24rem at that ratio
   (the second round of the audit, 2026-09-29). The same selector, later,
   inside the query. */
@media (max-width: 640px) {
  .account-actions {
    min-width: 25.6rem;
  }
}

.account-actions > * {
  flex: 0 0 auto;
}

/* Rename and Set role are `.form-inline`, which wraps and bottom-aligns by
 * default. Inside this row they are single items on one line like the rest. */
.account-actions .form-inline {
  flex-wrap: nowrap;
  align-items: center;
  gap: 0.4rem;
}

.account-actions .btn {
  white-space: nowrap;
}

/* The other half of the same instruction — "the 'user' probably gould loose a
   couple of pixels".  A `<td>` is sized by its content, and this one holds two
   muted sub-lines under the name, so it took a column far wider than a username
   out of the Actions cell beside it.  `width: 1%` is the table-layout idiom for
   "as narrow as your content allows": the browser distributes the shortfall
   across the remaining columns, and Actions is the only one here that wants it. */
.accounts-table .col-user {
  width: 1%;
}

.accounts-table .col-user .small {
  white-space: nowrap;
}

/* And the username is one line, since 2026-09-29: a name, which on a phone
   broke "layout-" / "admin" at its hyphen in the squeezed column (section 9,
   ON A PHONE A TABLE SCROLLS). (0,3,0), against nothing. */
.accounts-table .col-user .mono {
  white-space: nowrap;
}

/* ------------------------------------------------------------------ 4 */
.nav {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  align-items: center;
}

.nav a {
  color: var(--text);
  padding: 0.15rem 0;
  border-bottom: 2px solid transparent;
}

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

.nav a.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.nav .who {
  color: var(--muted);
  font-size: 0.82rem;
}

/* Project tabs. Every tab is a real <a href> to a real route — there is no
   JavaScript anywhere in TMDCS, so tabs are bookmarkable and Back works.

   THE CURRENT TAB IS UNDERLINED, since 2026-09-29, on Michael's choice of the
   "Underline style" out of the three the audit put to him. It was a box open
   at the bottom — the panel's ground, a border on three sides, pulled a pixel
   down onto the strip's rule so the box and the page below it read as one
   sheet. That shape is only true on the LAST row of a strip, and the strip
   wraps: the engineer's twelve tabs do not fit a line at 1024 (Joblets and
   Import went down alone), and a phone takes four rows. On any other row the
   open-bottomed box floated above text 3px below it, joined to nothing. An
   underline marks the tab it is under wherever that tab lands, which is why
   the header's own nav has always used one (`.nav a.active`, above): the same
   2px of accent, under the same kind of link.

   The rows are spaced by the strip's `row-gap`, 0.5rem, and every tab gives
   the line the same pixel back (`margin-bottom: -1px`), so each row is as far
   from the next as any other and the last row's underline lies on the
   strip's rule rather than a pixel above it. Measured in Chromium on the
   engineer's strip: one row at 1440, two at 1024, four at 390, every row
   38.9px tall and 7px from the next, and a tab's title 20px under the strip
   on every tab (22px where the head carries a button, which it sits beside
   on a baseline). No tab draws a ground or a box
   now, so a tab on one row can no longer touch the text of the row under it.
   (0,1,1) and (0,2,1), the specificity the shape had. */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.25rem;
  border-bottom: 1px solid var(--border);
  margin-bottom: 1.25rem;
}

.tabs a {
  color: var(--muted);
  padding: 0.45rem 0.8rem;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.tabs a:hover {
  color: var(--text);
  text-decoration: none;
}

.tabs a.active {
  color: var(--text);
  border-bottom-color: var(--accent);
}

/* ------------------------------------------------------------------ 5 */
.card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.5rem;
  /* Named container so the lists inside can respond to the CARD's width
   * rather than the viewport's — the same list is used in a full-width panel
   * and in a third of a row, and only the card knows which. */
  container: card / inline-size;
  /* A column whose `gap` is the spacing between everything in the card, so
   * no child's own margin has to be right about its neighbour. A margin does
   * NOT collapse into a flex gap, it adds to it — which is why the rules below
   * take the margins off the children that bring one (the hint, the list, the
   * table wrappers, whatever comes last).
   *
   * This comment used to say the column was there so a trailing hint could sit
   * at the foot of a card STRETCHED to the height of its tallest neighbour.
   * Cards are no longer stretched — see `.card-grid` — and the hint rule that
   * pushed it down went with the stretch. */
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 0;
  /* A BOX BRINGS ITS OWN BOTTOM MARGIN — the same 1rem `.banner`, `.toolbar`
   * and `.flash` have always carried. See "THE MARGIN BELONGS TO THE BOX"
   * below for why, and for where it is taken off again. */
  margin-bottom: 1rem;
}

/* The card's own children must not force it wider than its grid track. */
.card > * {
  min-width: 0;
}

/* THE MARGIN BELONGS TO THE BOX. Michael, 2026-09-29, asking for the whole
 * application to be gone over:
 *
 *     so that there is no more supises on lake of gaps, or input fields been
 *     weird dimentions etc.
 *
 * The gaps had one cause. This section spaced its boxes with adjacent-sibling
 * rules — `.card + .card`, `form + .card`, then a list of five more under them,
 * `.card + .table-wrap` and its cousins — and an adjacent-sibling rule answers
 * for exactly one shape of neighbour. That is the lesson section 3 wrote down for
 * `.row-actions` after the third report ("The margin belongs to the row"),
 * and this section went on writing the rule one neighbour at a time: the list
 * grew by `.table-scroll` on 2026-09-28 because the KPI card touched the pane
 * above it. Rendering all 180 screens at three widths found the neighbours the
 * list still did not name — a banner under a card (the issued document, the
 * comparison), a card under an empty state (Joblets), and some sixty
 * footnotes flush against the card or table they footnote.
 *
 * `.banner`, `.toolbar` and `.flash` were never in any of those reports,
 * because each of them carries `margin-bottom: 1rem` of its own. The four
 * boxes that did not — `.card`, `.table-wrap`, `.table-scroll` (section 9)
 * and `.empty` — now do, and the list of five is gone: whatever follows a
 * box, it follows it 1rem down, because in block flow two margins collapse and
 * the larger wins. That is also how the bigger breaks keep their size — an
 * h2's 2rem, an action row's 1.5rem, `form + .card`'s 1.5rem — with no rule
 * here having to know about them. (`.card + .card` stays, with a different
 * job: see TWO FORMS THAT ARE BOXES OF THEIR OWN, below.)
 *
 * WHERE THE MARGIN IS TAKEN OFF AGAIN, and each is a container that spaces its
 * own children or an end nothing follows:
 *   - Inside a card. `.card` is a flex column with a gap (above), and a margin
 *     in a flex container adds to the gap instead of collapsing with it: a
 *     table in a card would sit 28px above the next thing and 12px below the
 *     last. A banner in a card (the composition's caveats) is the same case.
 *   - Last in its container. A last child's margin cannot collapse out
 *     through a padded parent — `main`, a card — so it would be 16px of dead
 *     space at the foot of the page or the box. (Section 3 accepted exactly
 *     that for `.row-actions` and says why; here the count is not seven screens
 *     but every screen that ends in a table, and `:last-child` is the whole
 *     of the cost.)
 *   - In a card grid, whose own gap spaces the cards — `.card-grid` below.
 *
 * Specificity (0,2,0) against the boxes' own (0,1,0), so each is a strict win
 * and not a tie settled by source order; `tests.test_layout` reverses the
 * sheet and asks. */
.card > .table-wrap,
.card > .table-scroll,
.card > .empty,
.card > .banner,
.card:last-child,
.table-wrap:last-child,
.table-scroll:last-child,
.empty:last-child {
  margin-bottom: 0;
}

/* A PADDED BOX'S INSET IS ITS PADDING, and nothing inside it may add to the
 * bottom of it. A margin cannot collapse through padding, so the last child's
 * own margin was drawn inside the box: a paragraph's 0.75rem, a list's 1em, a
 * field's 1rem, a finding's 0.75rem — twelve to sixteen pixels more space
 * under the last line of a card, an empty state or a banner than above the
 * first. The same screens drew it five ways, which is what made it visible.
 *
 * (0,2,0), and it beats every margin it meets there: `p`, `ul` at (0,0,1),
 * `.field`, `.finding`, `.row-actions` at (0,1,0). ONE TIE, stated rather
 * than hidden: `.login-card .field` in section 11 is also (0,2,0) and sits
 * later in the file, so a `.field` that were ever the LAST child of the login
 * card would keep its 1rem. None is — every login form ends in its button —
 * and `tests.test_layout` resolves the last child of every card, empty state
 * and banner on every screen, so the day one is, it fails rather than drifts.
 *
 * The banner had half of this already, twice: `.banner-title + p` and
 * `.banner p:last-child` both zeroed a paragraph's margin. The first was also
 * wrong — it fired on a title's sentence that was NOT last, and so welded the
 * blocked study's "Show me the errors" button to the sentence above it. Both
 * are gone; this is the one they were approximating. */
.card > :last-child,
.empty > :last-child,
.banner > :last-child {
  margin-bottom: 0;
}

/* A list in a card is spaced by the card, not by the 1em above and below it
 * that the browser gives every list — which, added to the gap, put 39px
 * between "What happens next" and its first step. The indent stays: it is
 * what makes a list a list. (0,1,1) against the browser's own. */
.card > ul,
.card > ol {
  margin-top: 0;
  margin-bottom: 0;
}

/* A hint that is a direct child of a card is spaced by the card's gap, like
 * every other child. `.field-hint` brings `margin-top: 0.3rem` so it can sit
 * just under its control inside a `.field`, and as a `<p>` it brings the
 * paragraph's 0.75rem below; in the card's flex column both ADD to the gap, so
 * a table's footnote sat 17px under the table and two hints in a row were 24px
 * apart one way and 29px the other. (0,2,0) against `.field-hint` (0,1,0) and
 * `p` (0,0,1). */
.card > .field-hint {
  margin-top: 0;
  margin-bottom: 0;
}

/* A card that follows a form gets at least the break a card following a card
 * does.
 *
 * Michael, 2026-08-17, on the allowance guide:
 *
 *     The button SAVE THE TEMPLATE no margin and padding between the
 *     'contingency' and 'assest on element at a time' Could you add some
 *     space please.
 *
 * `.card + .card` above spaces the cards INSIDE a form, so the rhythm looks
 * right all the way down — and then stops dead at `</form>`, because the next
 * card's previous sibling is the form, not a card. The submit button therefore
 * sat flush against the heading of the next section, which reads as though the
 * button belongs to it.
 *
 * 1.5rem rather than the 1rem between cards: this is a bigger seam than one
 * card to the next — it is the end of everything the button will save — and
 * 1.5rem was this sheet's larger break (`.card-grid`'s, then; the action row's
 * still), so it is the existing rhythm rather than a new number.
 *
 * 2026-09-29: the `.card + .card` this comment named is gone — the card's own
 * bottom margin spaces the cards inside the form now (THE MARGIN BELONGS TO THE
 * BOX, above). This rule stays, and is the one sibling rule here that is not an
 * instance patch: it states a DIFFERENT number for a different seam, and in
 * block flow it wins by collapsing with whatever margin ends the form. */
form + .card {
  margin-top: 1.5rem;
}

/* TWO FORMS THAT ARE BOXES OF THEIR OWN, and so do not end a save the way
 * `form + .card` above means. That rule is for a bare `<form>` wrapped round
 * cards, whose end is the end of everything its button saves. These are not
 * that, and the card after either of them gets the ordinary 1rem break:
 *   - A form that IS a card — the element and quantity editors, the upload —
 *     closes its own border round what its button saves, so the card under it
 *     is simply the next card. `.card + .card` said so at (0,2,0) and always
 *     won over (0,1,1); until 2026-09-29 it was also the rule that spaced
 *     cards at all, and that job is now the upper card's own margin (THE
 *     MARGIN BELONGS TO THE BOX, above). In block flow the two 1rems collapse.
 *   - A GET filter bar saves nothing. The toolbar on the issue comparison is a
 *     `<form>`, so `form + .card` handed the card under it 24px, against 16px
 *     under the same bar on every screen where it is a `<div>`. It is pinned
 *     by `tests.test_compare`, over a drawn comparison: the layout sweep
 *     issues nothing, so the comparison it draws is the empty one, with no
 *     bar and no card, and the review of the fixes found that removing this
 *     selector failed no test.
 * (0,2,0) each, strictly over `form + .card`'s (0,1,1). Neither may reach
 * into a card grid, whose gap is the whole of the spacing: `.card-grid` below
 * says how that is stopped.
 *
 * THE SEAM LIST THAT STOOD HERE IS GONE. `.card + .table-wrap`,
 * `.table-wrap + .card`, `.card + .table-scroll`, `.table-scroll + .card` and
 * `.card + form` were written 2026-09-04 and 2026-09-28, each after Michael
 * found two boxes touching — "all 'tables' and touching eatch other" on
 * Allowances, then "there is no gap between the observation table on the
 * report view and the KPI calculation table". Every pair it named is now
 * spaced by the upper box's own margin (THE MARGIN BELONGS TO THE BOX, above),
 * along with the pairs it never named; `tests.test_layout` resolves the upper
 * box's 1rem on every one of them, on every screen. That is the floor of each
 * seam and not its size: the two margins collapse to the larger, so a lower
 * box's margin-top can widen a seam and nothing there would say so. The
 * three rules that set one are resolved where they are drawn: a card after a
 * card and after a bare form in `tests.test_view_only_controls`, the card
 * under the comparison's bar in `tests.test_compare`. */
.card + .card,
.toolbar + .card {
  margin-top: 1rem;
}

/* `font-weight` is here because a card title is not always a heading. The
 * issue screens draw theirs as a `<p class="card-title">`, which took the
 * paragraph's 400 while every `<h2>` and `<h3>` title beside it was bold — the
 * issued document shows both kinds. 700 is what the headings were giving. */
.card-title {
  margin: 0 0 0.75rem;
  font-size: 1.05rem;
  font-weight: 700;
}

/* A SECOND TITLE IN A CARD OPENS A SECTION, so it sits nearer what it heads
 * than what it follows. Inside the card's flex column a title gets the 12px
 * gap above it and its own 0.75rem PLUS the gap below — 24px — so "By value
 * stream" in the composition card, and "House rules" on the settings form,
 * read as footers of the table above them. 1rem more above puts it 28px under
 * the last section and 24px over its own.
 *
 * `:not(input[type="hidden"])` because a card's FIRST title is not always its
 * first child: a form card opens with the CSRF token, and a `:first-child`
 * test would move every such title 16px down from the card's top edge. A
 * title with nothing but hidden inputs before it is still the first thing
 * drawn. (0,3,1) — against nothing: `.card-title` is the only other rule that
 * sets a title's top margin, at (0,1,0). */
.card > :not(input[type="hidden"]) ~ .card-title {
  margin-top: 1rem;
}

/* AND WHAT A SECOND TITLE FOLLOWS ENDS WHERE ITS LAST LINE ENDS, so that 1rem
 * is the whole of the break. A margin does not collapse in the card's flex
 * column, and the title's own was added to whatever the thing before it
 * brought: the composition card's "By element type" sat 40px under the
 * paragraph introducing it (the paragraph's 0.75rem, the gap, the title's
 * 1rem) and "By value stream" 28px under the table before it, which brings
 * none — two sub-headings of one card at two distances (the second round of
 * the audit, 2026-09-29). With this, every section a title closes is 28px
 * above it, as the rule above says. It is the same statement as `.card >
 * :last-child`, for the end of a section rather than the end of the card.
 * A paragraph and a field are the two things in a card that bring a foot of
 * their own: a box's is taken off in a card already (THE MARGIN BELONGS TO THE
 * BOX, above), and a box is spaced by that, never by what stands after it.
 * `:has()`, so a browser without it keeps the margin it had. (0,3,0) —
 * `:is()` and `:has()` count as their arguments — against `p`'s (0,0,1) and
 * `.field`'s (0,1,0), and over the (0,2,0) `.login-card .field`. */
.card > :is(p, .field):has(+ .card-title) {
  margin-bottom: 0;
}

/* 260px was too narrow for the content that actually goes in these cards: a
 * label column plus a value inside 1.5rem of padding left about 210px, which is
 * where the vertical-fragment wrapping came from. 22rem is the narrowest card
 * that holds a label and its value side by side with room to spare; the
 * container query on `.kv` below stacks them only in a card much narrower
 * than that.
 *
 * `auto-fit` rather than `auto-fill` so a lone card in the last row grows to
 * fill the space instead of sitting at minimum width beside empty tracks —
 * that is the "blocks are different sizes" effect.
 *
 * `align-items: start` — EVERY CARD IS AS TALL AS WHAT IT HOLDS. It was
 * `stretch`, with a trailing hint pushed to the foot of the stretched card so
 * that neighbouring footers lined up. That held while neighbours were alike,
 * and the grid does not choose its neighbours: the study header's clock card
 * was stretched to the height of the form beside it and its note stranded
 * 834px below the three times it explains, and the one-line House rules card
 * on the project Overview was drawn as tall as Contents — a big empty box
 * that reads as something failed to load. Ragged bottoms are the honest
 * shape of cards that hold different amounts; the tops still line up, which
 * is the edge the eye reads a row by. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(22rem, 1fr));
  gap: 1rem;
  align-items: start;
  /* A grid has no bottom margin of its own, so whatever followed it — the
   * cycle-quantity toolbar on the study summary — sat flush against the cards
   * with no gap at all. Match the gap between the cards so the rhythm is even
   * in both directions: 1rem. It said so here and was 1.5rem, so the toolbar
   * under the study summary's cards sat 24px down where every other seam on
   * that screen is 16px. */
  margin-block-end: 1rem;
}

.card-grid:last-child {
  margin-block-end: 0;
}

/* A ROW OF LISTS ENDS ON ONE LINE. The argument above is about neighbours that
 * are not alike, and the grid that is marked `.even` holds three that are: the
 * study summary's Recording, Clock and Study header, a key/value list each
 * about the one study. Drawn at their own heights the three ended at 591, 619
 * and 684px at 1440, and on the blocked study two of them 5px apart, which
 * reads as a slip rather than as a shape (the second round of the audit,
 * 2026-09-29). Stretched, the row ends on one line again, and the extra
 * height is at the foot of the shorter lists, under whatever each ends with.
 * A grid is `.even` in its template, where what its cards hold is known;
 * every other grid keeps `start`. (0,2,0) against `.card-grid`'s (0,1,0). */
.card-grid.even {
  align-items: stretch;
}

/* THE GRID SPACES ITS CARDS, AND ONLY THE GRID. A grid item's margins do not
 * collapse — not with each other, not with the gap — so any margin a card
 * brings is drawn inside the grid as well as the gap. `.card + .card` did
 * exactly that until 2026-09-29: every card after the first in a row began
 * 16px lower than the first, the rows were 32px apart against a 16px column
 * gap, and a phone's stacked cards were twice as far apart as the page's other
 * seams. Three margins can reach a card in here, and all three are taken off:
 *   - its own bottom margin (THE MARGIN BELONGS TO THE BOX, above), which would
 *     do the same thing from the other side — (0,2,0) against (0,1,0);
 *   - `form + .card`'s 1.5rem, because the joblet's three cards are
 *     `<form class="card">` and the second and third each follow a form —
 *     (0,2,0) against (0,1,1);
 *   - `.card + .card`'s 1rem, which is (0,2,0) itself and so needs the (0,3,0)
 *     of the second rule rather than a tie settled by where this sits. */
.card-grid > .card {
  margin-top: 0;
  margin-bottom: 0;
}

.card-grid > .card + .card {
  margin-top: 0;
}

/* Definition lists: a label column that sizes to its content, but CAN shrink.
 *
 * `max-content` alone was wrong twice over. A grid item's default `min-width`
 * is `auto`, which refuses to go below the content's intrinsic size, so the
 * label column took whatever its longest label wanted and the whole grid
 * overflowed its card. The longest label in the app is "Time elapsed after the
 * last breakpoint" at 38 characters, and it sits in the study summary's Clock
 * card — so TEBS, the last breakpoint, the finish and TEAF were pushed outside
 * their card, with the neighbouring card painted over the top of them. Four
 * values that were rendered, correct, and invisible.
 *
 * `minmax(0, max-content)` keeps the sizing behaviour and restores the ability
 * to shrink; the wrap rules let a long label use two lines instead of forcing
 * the page sideways, which is what it did at 768px.
 *
 * AND THE VALUE COLUMN HAS A FLOOR, since 2026-09-29: `min(10rem, 50%)`, not
 * 0. A grid hands out free space to the label column FIRST, up to its
 * max-content, and gives the `1fr` column only what is left — so when a card
 * is narrow and a label is long, the value is what gets squeezed. The stacking
 * query below used to fire on every phone and hid that. Lowered, it let the
 * import review's "Time elapsed after the last breakpoint" take 290px of a
 * 332px card and print "real_multi_operator.psf" down a 30px column three
 * letters at a time. With the floor the value keeps 10rem, or half a card
 * narrower than 20rem, and it is the LABEL that wraps onto a second line —
 * a label is read once, the value is what the list is for. Measured with and
 * without the floor on every screen the audit draws, at 1440, 1024 and 390:
 * that list is the only one it moves. Everywhere else the labels are short
 * enough that the value column was already wider than 10rem. */
.kv {
  display: grid;
  grid-template-columns: minmax(0, max-content) minmax(min(10rem, 50%), 1fr);
  gap: 0.35rem 1rem;
  margin: 0;
}

/* Wrap at spaces, NOT mid-word.
 *
 * `overflow-wrap: anywhere` was added here to stop a long label overflowing its
 * card, and it over-corrected: in a card only 260px wide the label column is
 * about 210px, and `anywhere` breaks inside words rather than between them, so
 * "Ignored count towards a cycle" came out as a vertical column of fragments.
 * The `minmax(0, …)` above is what actually fixes the overflow; this only has
 * to let a long label take a second line. `break-word` breaks a word only when
 * that word alone cannot fit, which is the behaviour wanted. */
.kv dt {
  color: var(--muted);
  overflow-wrap: break-word;
}

.kv dd {
  margin: 0;
  overflow-wrap: break-word;
}

/* A NUMBER IN A LIST READS FROM ITS LABEL, NOT FROM THE CARD'S EDGE. `.num`
 * (section 9) right-aligns, which is what lines a column of figures up by
 * their decimal points — and in a list there is no column, only one value
 * beside one label. The templates reuse it on a `<dd>` for the mono digits
 * (Observations and Corrections on the study summary, the Overview's Contents,
 * the import review), and the right-alignment came along: "179" sat at the
 * card's far edge, 240px from its label, while every other value in the same
 * list started where the value column starts. The face stays; only the
 * alignment is the list's. (0,2,0) against `.num`'s (0,1,0). */
.kv .num {
  text-align: left;
}

/* Below this the label column has no room to be a column at all, so stop
 * pretending: stack the label over its value and let both use the full width.
 * A container query rather than a media query, because what matters is how wide
 * the CARD is, not the viewport — the same list appears in a full-width panel
 * and in a third of a row.
 *
 * 16rem, not the 22rem it was. At 22rem the query fired on EVERY card on a
 * phone — a 390px screen leaves a card 20.75rem of content — so "Studies" was
 * stacked over "11" and the Overview's Contents card was drawn nearly twice as
 * tall as its seven short lines need. Side by side is the list's shape; this
 * is the escape for a card too narrow to have two columns at all, and 16rem is
 * still more than the widest label in the application wants on two lines. */
@container card (max-width: 16rem) {
  .kv {
    grid-template-columns: 1fr;
    gap: 0 0;
  }

  .kv dt {
    margin-top: 0.5rem;
  }

  .kv dt:first-child {
    margin-top: 0;
  }
}

/* A LIST WHOSE VALUES NEED THE CARD'S WIDTH STACKS ON A PHONE, since the
 * second round of the audit Michael asked for (2026-09-29: "so that there is
 * no more supises on lake of gaps, or input fields been weird dimentions
 * etc."). The 16rem query above is right for short pairs — "Studies" and
 * "11" — and two lists are not that:
 *   - the relaxation guide's. Its Source, Provenance, Composition and "On A,
 *     E and M" are paragraphs, and beside a 106px label a 400-character note
 *     was given a 210px column, about 25 characters a line: the list grew
 *     from 730 to 945px tall at 390, a newspaper column with a gutter.
 *   - the import review's. Its labels run to 38 characters and its File is a
 *     mono file name with its size and hash, which is how the cards are told
 *     apart; side by side the name got the value column's 10rem floor and
 *     broke inside itself, "real_multi_operat" / "or.psf", with the next
 *     line starting on the separator the template's no-break spaces are
 *     there to keep off it.
 * Each is marked `.kv-long` in its template and is stacked below 24rem, which
 * is every phone's card (20.75rem at 390) and no desktop one: both lists are
 * in cards the width of the page. Measured at 390: the guide's list back to
 * 730px with the paragraphs 332px wide, and each file name whole on its line.
 * (0,2,0) and (0,2,1) inside the query against the list's own (0,1,0) and
 * (0,1,1) — each a strict win, and the 16rem rules above name `.kv` alone. */
@container card (max-width: 24rem) {
  .kv.kv-long {
    grid-template-columns: 1fr;
    gap: 0 0;
  }

  .kv.kv-long dt {
    margin-top: 0.5rem;
  }

  .kv.kv-long dt:first-child {
    margin-top: 0;
  }
}

/* THE EMPTY STATE, and there is one. A list with nothing in it draws this box
 * in place of its table — not a header row over a single "nothing yet" cell,
 * which is what four screens did. On a phone that cell spans the whole
 * scrolled width of a table with nothing to scroll, so the one sentence
 * explaining the screen was cut off at the scroller's edge. The bottom margin
 * is every box's (THE MARGIN BELONGS TO THE BOX, above). */
.empty {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: 2rem 1rem;
  text-align: center;
  color: var(--muted);
  margin-bottom: 1rem;
}

/* And its sentence is set to a reading measure. The box is as wide as the
 * page, and at 1440 its text ran 1165px on one centred line, which is read by
 * turning the head. (0,1,1) against `p`'s (0,0,1); the vertical margins are
 * the ones the paragraph and `.empty > :last-child` already give.
 *
 * BALANCED, since the second round of the audit (2026-09-29). A sentence a
 * little longer than the measure broke with one word left over — "above."
 * alone on the second line of a centred sentence in a 1248px box, on the
 * report with no studies chosen — and on a phone five more ended the same
 * way. `text-wrap: balance` evens the lines of a short centred text, so none
 * ends on a single word, and a browser without it wraps as before. */
.empty > p {
  max-width: 60ch;
  margin-left: auto;
  margin-right: auto;
  text-wrap: balance;
}

/* ------------------------------------------------------------------ 6 */
/* `text-align: center` since 2026-09-29, because a `<button>` centres its
   label and an `<a class="btn">` does not: the browser gives the one and not
   the other. It shows only on a button wider than its label — `.btn-block`,
   or an `<a>` stretched by the flex column of a card — and every error page's
   "Back to TMDCS" is exactly that: the label sat hard against the left edge
   of a full-width button, where the same button written as a `<button>` would
   have centred it. This places the label inside the button and nothing else:
   where the button itself sits in a cell or a line is its container's
   alignment, and a button as wide as its own label looks the same either way.
   Nothing else in the sheet declares a button's `text-align`. */
.btn {
  display: inline-block;
  font: inherit;
  line-height: 1.2;
  padding: 0.45rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  color: var(--text);
  cursor: pointer;
  text-decoration: none;
  text-align: center;
}

.btn:hover {
  border-color: var(--accent);
  color: var(--text);
  text-decoration: none;
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

.btn-primary:hover {
  filter: brightness(1.08);
  color: var(--on-accent);
}

.btn-danger {
  background: var(--danger);
  border-color: var(--danger);
  color: var(--on-accent);
  font-weight: 600;
}

.btn-danger:hover {
  filter: brightness(1.08);
  color: var(--on-accent);
}

.btn-ghost {
  background: transparent;
}

.btn-sm {
  padding: 0.2rem 0.5rem;
  font-size: 0.85rem;
}

.btn-lg {
  padding: 0.6rem 0.9rem;
  font-size: 1.05rem;
}

.btn-block {
  display: block;
  width: 100%;
}

.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}

.field {
  margin-bottom: 1rem;
}

/* A FIELD ENDS WHERE ITS LAST LINE ENDS, and its own 1rem spaces the next one.
 * A hint written as `<p class="field-hint">` kept the paragraph's 0.75rem
 * below it — `.field-hint` sets only the top — and a margin cannot cross the
 * field's end when the field is a flex item or sits in a padded box, so every
 * hinted field ended 12px lower than a plain one: 40px from its hint to the
 * next label, against 28px from a box to the next label, alternating down the
 * element and settings forms. The auth cards write the same hint as a
 * `<div>`, which has no such margin, so the one component was spacing two
 * ways. (0,2,0) against `p` (0,0,1) and `.field > label` (0,1,1). */
.field > :last-child {
  margin-bottom: 0;
}

.field > label,
.form-label {
  display: block;
  margin-bottom: 0.3rem;
  color: var(--muted);
}

/* HOW A TEXT-ENTRY CONTROL LOOKS. The width is NOT here and has not been since
   2026-08-30; the rule below it is where it went and why.

   `input[type="file"]` was in this list until 2026-09-29 and is not a
   text-entry control: it is a box with a button drawn inside it, and it has a
   rule of its own below (A FILE INPUT IS A BOX WITH A BUTTON IN IT) that
   states its whole look, padding included. Left in here as well, the two
   `padding`s would have been a tie at (0,1,1) settled by source order. */
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="date"],
input[type="search"],
select,
textarea {
  font: inherit;
  color: var(--text);
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: 6px;
  padding: 0.4rem 0.6rem;
}

/* A PLACEHOLDER IS ONE GREY, in every engine, since the second round of the
   audit (2026-09-29). Nothing here said what colour it is, so each browser
   used its own: Chromium's #757575 is plainly not a value beside the #d6d9de
   a value is typed in, but WebKit's — Safari's — is `darkGray`, #a9a9a9, and
   there an empty Actor box reading "any", or an allowance row reading
   "12.00", looked filled in. `--placeholder` (section 1) is Chromium's grey,
   the one every form here was read in, stated so Safari draws it too; it is
   3.8:1 on the box's ground and 3.3:1 from a typed value. Not `--muted`,
   which was tried: the labels' #8b95a3 is 2.1:1 from a value, halfway to
   Safari's 1.7:1, and an empty box read nearly filled. `opacity: 1`
   because Firefox fades a placeholder as well as colouring it. A
   pseudo-element, (0,0,1), against the browsers' own sheets and nothing of
   this one. */
::placeholder {
  color: var(--placeholder);
  opacity: 1;
}

/* AND HOW WIDE IT IS BY DEFAULT — a DEFAULT, at (0,0,0), which is the whole
   point of splitting it out. This is the third bug from the one declaration.

   The first two were the Policy CA % box on reports/new.html and the one on
   reports/report.html; the third is the Log's date filters. Michael,
   2026-08-30:

       the from and to labels, please be consistent and place them on the left
       side of the input fileds they link to not on top, all other once in line
       are on the left. This should as well equal them in one straight
       horizontal line.

   THE ARITHMETIC THAT PRODUCED ALL THREE. `input[type="date"]` is (0,1,1) —
   one attribute selector, one type. A utility class is (0,1,0). The class
   count ties at one and the element count then decides, so `width: 100%` beat
   every single-class rule in this sheet REGARDLESS OF SOURCE ORDER. Section 14
   had to write `input[type="text"].input-narrow` and section 10
   `.toolbar input[type="text"]`, both (0,2,1), and both had to name the very
   selector they were beating. A rule no class can override is a rule that gets
   overridden by a copy of itself on the next screen — which is precisely what
   those two are, and why a third screen was owed the same fix.

   `:where()` takes its argument's specificity to zero, so this list is (0,0,0)
   — the lowest an author rule reaches, beaten by anything naming anything.
   That is what "unless somebody says otherwise, fill the field" always meant.

   NOTHING RESOLVES DIFFERENTLY TODAY, which is the reason to do it now rather
   than on the day it is needed. Every rule in this sheet that narrows one of
   these controls already won — but not all of them on specificity, and the
   difference is the whole lesson. A selector LIST is scored PER BRANCH, so the
   old list was never one (0,1,1) rule: its `input[type="date"]` branch was
   (0,1,1) and its bare `select` branch was (0,0,1). `select { width: auto }`
   two rules below is (0,0,1) as well, so that one was a TIE settled by source
   order, and it would have flipped the day somebody moved it. Only
   `.toolbar input[type="text"]` and `input[type="text"].input-narrow`, both
   (0,2,1), beat the date branch outright. That is the state this edit ends:
   against (0,0,0) every one of them is a strict win.
   `tests.test_layout` re-resolves every control on every screen it renders
   against the OLD per-branch selector list and demands the same answer.

   THE APPEARANCE STAYED ABOVE, AT (0,1,1), AND THE SPLIT IS NOT TIDINESS. A
   browser that cannot parse `:where()` drops this whole rule — that is how an
   unknown selector fails — so what sits in here has to be something a form can
   lose and still be a form. A box that sizes itself rather than filling its
   field is that; a box with no background, no border and no padding on a dark
   page is not. `:where()` shipped in Chrome 88, Firefox 78 and Safari 14, all
   of them older than the `:has()` support section 13 argues about at length,
   so by that section's own reckoning nobody is reading this fallback. That is
   a reason to be careful about WHAT is in here, not a reason to skip it. */
:where(
  input[type="text"],
  input[type="password"],
  input[type="email"],
  input[type="number"],
  input[type="date"],
  input[type="search"],
  input[type="file"],
  select,
  textarea
) {
  width: 100%;
}

/* A BOX THAT SAYS HOW WIDE IT IS, IS THAT WIDE. Michael, 2026-09-29, asking
   for the whole application to be gone over:

       so that there is no more supises on lake of gaps, or input fields been
       weird dimentions etc.

   The weird dimensions were one default overruling an attribute. The markup
   already sized its short boxes with `size` — a report name 36 characters, an
   issue reference 48, a rating 5, a per-study value 8, a line range 5, the
   reasons 24 to 40, an account rename 12 — and the default above filled the
   field regardless, so a 5-character rating box was drawn 1198px wide. Three
   places had each written `width: auto` back for their own screen: the
   toolbar (section 10), the search form (below) and `.field-inline` (section
   14). This is the rule those three were copies of: `size` is the HTML
   attribute for "this many characters", and a box that carries one is sized
   by it.

   It also cures the opposite fault. A percentage width contributes nothing to
   an auto-layout table's column minimum, so a box in a table cell was drawn as
   narrow as the column's other content allowed: the new element's allowance
   percent 21px wide on a phone, an operator code 42px. Sized, a box is as wide
   as it says in a cell too, and a table that no longer fits scrolls in its
   `.table-wrap`, which is what the wrapper is for. The templates give every
   box in a table a `size` for that reason.

   `max-width: 100%` so a 72-character box on a phone stops at its field.
   (0,1,1) against the default's (0,0,0), a strict win; it ties with
   `.field-inline input`'s (0,1,1) on `width` and both say `auto`. The three
   older copies stay: each is a strict win of its own that `tests.test_layout`
   asserts, and they agree with this one. */
input[size] {
  width: auto;
  max-width: 100%;
}

/* A PASSWORD IS NOT A PARAGRAPH. A password box has no length to state — a
   passphrase is as long as its owner makes it — so it filled its field, and
   the one password box outside a sign-in card, "Your password" on Security,
   was drawn 878px wide at 1440 where the same words over the same box are
   338px on the enrolment screen beside it (the audit, 2026-09-29). 24rem stops
   it short of a paragraph and leaves every sign-in card's box as it is: those
   are the card's 338px wide, less than this. `:not([size])` so that a
   password box which did state a size would be sized by the rule above and
   not tie with it on `max-width`: (0,2,1) against its (0,1,1). */
input[type="password"]:not([size]) {
  max-width: 24rem;
}

/* AND HOW TALL A ONE-LINE CONTROL IS: the height of a button, stated. Michael,
   2026-09-28, of the report toolbar:

       the input field for policy CA% is now taller then any other input
       field, culd we get that alighned please

   Measured in Chromium, at 1440 and at 390 wide, on that one line: the Policy
   CA % box 37.28px, the Precision select beside it 34.78px, Apply 34.38px.
   Three heights from three line-heights, and only one of the three was chosen:

   * the box takes `font: inherit` above, and that shorthand carries the line
     height along with the font — body's 15px/1.5, a 22.5px line;
   * the select takes the same 1.5 and cannot use it. Blink pins a drop-down's
     line-height to `normal` whatever an author writes (its computed value
     reads `normal` with this rule in force), then pads the inside of it by a
     pixel above and below that no selector reaches. A select's height is not a
     sum an author can write;
   * `.btn` restates `line-height: 1.2` after its own `font: inherit` — the one
     number here somebody picked.

   WHY A HEIGHT AND NOT A LINE-HEIGHT. The obvious cure — the box's line
   brought down to the button's 1.2, or to `normal` — was measured before it
   was written, and it does not work: the box falls to 32.78px while the select
   stays at 34.78px, two pixels SHORT where it had been two and a half tall.
   The difference is the select's two hidden pixels, which belong to the
   engine's theme and not to this sheet, so no rule that moves the box's
   natural height can land on the select's. Both are told the one height
   outright — and the one worth telling them is the button's, because a bar is
   boxes, pickers AND the button that applies them: `.btn`'s 1.2em of line, its
   0.45rem of padding above and below, and a 1px border top and bottom.
   Section 2 makes everything border-box, so that sum IS the drawn height:
   every box and select measures 34.39px beside the button's 34.38px, one sum
   that the engine rounds onto its 1/64px grid by two routes. The box keeps its
   own 0.4rem of padding, a tenth of a rem to spare around a 1.2em line; the
   engines centre the text in the height they are given — and a select has
   none above and below since 2026-09-29 (the rule after this one), because an
   engine that pads inside the height as well cut its text to a sliver. On a
   phone the text is 16px (below, ON A PHONE A CONTROL'S TEXT IS 16px), so the
   one height is 35.59px there, the button's with it.

   `line-height: 1.2` rides along for the boxes, whose inner line would
   otherwise still be 22.5px inside a 19.6px content box. On the select it is
   the value an engine that honours it would use, and Blink ignores it.

   NOT IN THE LIST, each on purpose:
   * `textarea` is a paragraph, not a line. It keeps the 1.5 `font: inherit`
     gives it — the spacing its text is read at — and its `min-height` below.
   * `input[type="file"]` is given the same height by its own rule below,
     which also takes its padding off top and bottom so the button drawn
     inside it fills that height rather than being squeezed by it — the reason
     this comment used to give for leaving it out.
   * A list box — `select[multiple]`, or a `size` above one — is many lines
     tall, and this rule would flatten it to one. There is none in the
     application, and `tests.test_layout` fails the day one is drawn.

   Specificity (0,1,1) for the boxes and (0,0,1) for the select. Nothing else
   in the sheet declares a height or a line-height for either except
   `.login-card` (section 11), which sets its own padding and font size and so
   sizes its own boxes; it says so where it does it. */
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="date"],
input[type="search"],
select {
  line-height: 1.2;
  height: calc(1.2em + 0.9rem + 2px);
}

/* A SELECT'S TEXT IS CENTRED BY ITS HEIGHT, NOT HELD UP BY ITS PADDING, since
   the second round of the audit (2026-09-29). The height above is stated, and
   every engine centres a drop-down's text in the height it is given; the
   0.4rem above and below only has somewhere to go where an engine adds
   padding of its own inside that — the Linux WebKit the audit's second
   browser runs, whose theme puts 5px above and below the text: there it did
   not fit the height, was cut to a sliver, and on a baseline-aligned bar
   (section 10) each label sat 4.2px below the centre of its select. Safari
   discards an author's padding on a drop-down anyway, and in Chromium neither
   the height nor the text moves. The sides keep their 0.6rem. `:not([multiple])`
   because a list box, were one ever drawn, is lines of options that do need
   it. (0,1,1) against the look's bare `select` at (0,0,1). */
select:not([multiple]) {
  padding-block: 0;
}

/* ON A PHONE A CONTROL'S TEXT IS 16px, since the second round of the audit
   (2026-09-29). iPhone Safari zooms the page in when a box, a select or a
   textarea whose text is smaller than 16px takes the focus, and every one
   here was the body's 15px — so tapping a filter or a Code box enlarged a
   layout fitted to 390px, and the reader had to pinch it back out. The
   viewport is left as it is: forbidding the zoom (`maximum-scale=1`) would
   also stop pinch-zoom on Android, which somebody may need to read at all.
   EVERY full-size button goes to 16px with the boxes, because the one-line
   height above is `.btn`'s own sum in `em`: at 16px each is 35.59px and they
   still agree — a button left at 15px would stand a pixel short of the boxes
   it submits. The first cut sized only the buttons on a row that holds a box
   and left the rest at 15px, so one phone screen drew two button heights
   (the report's Apply beside Save, Save as and Export CSV); the independent
   review of 2026-09-29 found it, and the two labels that would have wrapped
   at 16px were shortened instead ("Turn off two-factor", "Clear all EC flags
   (N)"). `.btn-sm` (rows of buttons, never beside a box) and `.btn-lg` (the
   auth card's, already 16.8px) keep their sizes, and so do the auth card's
   boxes, whose own (0,2,1) is more than these.
   `body` is what makes each rule a strict win rather than a tie settled by
   where the query sits: (0,1,2) against the look's (0,1,1) — and the file
   input's own (0,1,1) — and (0,0,2) against a bare `select` or `textarea`
   (0,0,1). The button's is (0,2,1) — `:not()` counts as its most specific
   argument — against `.btn`'s (0,1,0). */
@media (max-width: 640px) {
  body input[type="text"],
  body input[type="password"],
  body input[type="email"],
  body input[type="number"],
  body input[type="date"],
  body input[type="search"],
  body input[type="file"],
  body select,
  body textarea {
    font-size: 16px;
  }

  body .btn:not(.btn-sm, .btn-lg) {
    font-size: 16px;
  }
}

/* A BOX CLASSED `mono` OR `num` IS SET IN THE MONO FACE, which is what the
   templates have been asking for and not getting. The look above starts with
   `font: inherit`, and that shorthand sets the family too, at (0,1,1) — so
   `.mono` and `.num` at (0,1,0) lost it on every box that carries them: the
   element code, the operator and area codes, the rating, the line range and
   the per-study value were all drawn in the body face while the same codes
   and figures are mono everywhere they are printed. Found 2026-09-29 putting
   `mono` on the settings form's Code box, where it would have done nothing
   either. `.num`'s right alignment always applied; only the face was lost.
   (0,2,1) against the look's (0,1,1); a mono box is a little wider for the
   same `size`, every character being the widest. */
input[type].mono,
input[type].num {
  font-family: var(--mono);
}

/* A number box is as wide as its numbers. It has no `size` attribute — HTML
   gives `size` to text boxes only — and the browser sizes it from `min` and
   `max` instead, 55px for Decimal places' 0 to 6. The default above drew that
   one digit 1198px wide on the settings form. (0,1,1) against the default's
   (0,0,0); `max-width` as for a sized box. The `font-family` beside them is
   older and is NOT a strict win: the look's `font: inherit` is (0,1,1) as well
   and this wins it by being written later. Nothing this edit adds depends on
   it, and it is recorded here rather than disguised. */
input[type="number"] {
  font-family: var(--mono);
  text-align: right;
  width: auto;
  max-width: 100%;
}

/* A read-only field must LOOK read-only. The element code on an existing
   element is drawn rather than omitted — its absence read as an oversight and
   sent people hunting for the setting that had hidden it — but a box painted
   like every other box is an invitation to type in it, and the explanation sits
   below the field where it is read second. Muted, dimmed and cursor: default,
   so the answer arrives before the click. The hint text still says why.

   AND SO MUST A DISABLED ONE, since 2026-09-29. The project Code on the
   settings form is `disabled` rather than `readonly` — it is not submitted at
   all — and only `[readonly]` had this look, so it was painted exactly like
   the editable Name box under it. projects/settings.html has said for months
   that "a disabled box reads as 'you have permission to want this'"; this is
   the half of that the stylesheet owed. `not-allowed` rather than `default`
   because a disabled control is one the browser will not let anybody use, and
   that cursor is what says so everywhere else (`.btn:disabled`).

   `input[type]` AND NOT `input`, AND THAT IS A CORRECTION. `input[readonly]`
   was (0,1,1), the same as `input[type="text"]` in the look above, so the
   muted colour and the panel ground won only because this rule is written
   later — a tie settled by source order, which is what `tests.test_layout`
   reverses the sheet to catch. `input[type]` makes both states (0,2,1), a
   strict win over the look and over the file input's own rule below; every
   input in the application carries a `type`. A textarea or a select is
   already strictly ahead: `textarea[readonly]` and `select:disabled` are
   (0,1,1) against the bare element's (0,0,1). */
input[type][readonly],
textarea[readonly],
input[type]:disabled,
select:disabled,
textarea:disabled {
  color: var(--muted);
  background: var(--panel);
  border-style: dashed;
}

input[type][readonly],
textarea[readonly] {
  cursor: default;
}

input[type]:disabled,
select:disabled,
textarea:disabled {
  cursor: not-allowed;
}

/* A FILE INPUT IS A BOX WITH A BUTTON IN IT, and it was the one control the
   browser still drew: a box 38.28px tall beside 34.39px ones, holding a grey
   square-cornered Arial button among `.btn`s. Out of the text-entry list above
   since 2026-09-29, and described whole here instead.

   The box is a one-line control's height — section 6's sum, `.btn`'s line,
   padding and border — with no padding above or below, so the button inside
   it can take that height whole rather than be squeezed by it, which is what
   the height rule's comment used to say would happen. The button is then drawn
   as the box's own left end: the panel ground a `.btn` has, the text colour
   and face of the page, a rule on its right where it meets the file name, and
   the box's own corner radius less its 1px border on the left. Its padding is
   `.btn`'s across. `::file-selector-button` is Chrome 89, Firefox 82 and
   Safari 14.1; a browser without it keeps its own button inside a box that is
   still the right height. (0,1,1), against nothing: this is the only rule that
   names the type since it left the list above. */
input[type="file"] {
  font: inherit;
  color: var(--text);
  background: var(--input-bg);
  border: 1px solid var(--input-border);
  border-radius: 6px;
  line-height: 1.2;
  height: calc(1.2em + 0.9rem + 2px);
  padding: 0 0.6rem 0 0;
}

input[type="file"]::file-selector-button {
  font: inherit;
  line-height: 1.2;
  height: 100%;
  margin: 0 0.6rem 0 0;
  padding: 0 0.85rem;
  color: var(--text);
  background: var(--panel);
  border: 0;
  border-right: 1px solid var(--input-border);
  border-radius: 5px 0 0 5px;
  cursor: pointer;
}

select {
  width: auto;
}

/* A SEARCH BOX SIZES ITSELF. The default above fills the field, and a search
   box has no field: it sits in a `form[role="search"]`, a flex row, beside the
   button that submits it. Filled to 100% it is a flex item whose hypothetical
   size is the whole row, so the row gives it a line of its own and the button
   drops under it — the toolbar's `#f-ca` defect (section 10) on a third
   screen, which section 6's `:where()` was lowered to make cheap to cure.
   Michael, 2026-09-14, of the project search as B9 shipped it:

       Could the search button be nexto to the input field, on the right
       please, same size as the 'ne project' button.

   `form[role="search"]` and not the one screen's id, because the role is what
   makes a form a search: every search form the application grows takes this
   rule. (0,2,2) strictly beats the (0,1,1) the default had before it was
   lowered, so the win is not a tie settled by source order —
   `tests.test_layout` reverses the sheet and asks. How wide "itself" is comes
   from `size` in the markup, an HTML attribute rather than a second number
   here; `max-width: 100%` keeps the box inside a narrow viewport, as the
   toolbar's rule does.

   AND ON A PHONE THE BOX GIVES WAY, NOT THE BUTTON, since 2026-09-29. Thirty
   characters is 295px, and the search form is a `.row-actions`, which wraps:
   at 390 wide the box and its button did not fit one line, so Search dropped
   under the box — the very arrangement the request above was about, back on
   the one screen width nobody had drawn. The form therefore does not wrap, and
   the box may shrink below its thirty characters (`flex: 0 1 auto` with
   `min-width: 0`, since a flex item will not otherwise go below its own
   width) until the button fits beside it. Measured at 390: the box 277px with
   Search on its line, New project on the next. A search box narrower than its
   `size` is still a search box; a button under it is not the one Michael
   asked for. The form may shrink as well, and the row holding it in the page
   head (section 3) — otherwise each refuses to be narrower than its content
   and the page scrolls sideways instead, which the first measurement did.
   `form[role="search"]` is (0,1,1) against `.row-actions`' `flex-wrap` at
   (0,1,0); the box's rule is (0,2,2) and nothing else gives it a `flex` or a
   minimum width. */
form[role="search"] {
  flex-wrap: nowrap;
  min-width: 0;
}

form[role="search"] input[type="search"] {
  width: auto;
  max-width: 100%;
  flex: 0 1 auto;
  min-width: 0;
}

textarea {
  min-height: 6rem;
}

.field-error {
  color: var(--text);
  font-size: 0.85rem;
  margin-top: 0.3rem;
}

.field-error::before {
  content: "\26A0\FE0E";
  color: var(--danger);
  margin-right: 0.35rem;
}

.field-hint {
  color: var(--muted);
  font-size: 0.85rem;
  margin-top: 0.3rem;
}

/* A refused box, marked where it stands. `input[type]` rather than `input`
   since 2026-09-29, when the settings and new-project forms started using it
   (they used to answer a refusal with a page flash only): `input.is-invalid`
   was (0,1,1), the same as the `input[type="text"]` whose `border` shorthand
   it overrides, so the red edge won on source order alone. (0,2,1) wins
   outright; the select and textarea branches already beat their bare-element
   look at (0,0,1). */
input[type].is-invalid,
select.is-invalid,
textarea.is-invalid {
  border-color: var(--danger);
}

/* A ROW OF FIELDS THAT WRAPS IS SPACED LIKE A COLUMN OF FIELDS, since
   2026-09-29. Side by side the fields are 0.75rem apart, which is the gap
   across. Wrapped — the new operator's Code and Description, the add-a-line
   form on Allowances, the element's allowance row, all of them on a phone —
   the same 0.75rem stood between a box and the next field's label, where
   everywhere else on the page a box is 28px above the next label: a field's
   1rem and a card's 0.75rem gap. So a wrapped line is 1.75rem below the one
   above it, the rhythm of the column it has become, and the submit button
   that wraps under the last box is 28px below it as a card's action row is.
   Nothing wraps at desktop width on the screens the audit draws, so this is
   the phone's spacing only.

   A row of boxes in a TABLE CELL is not a column of fields — the guide's
   low/high/note correction, the accounts' Rename — and keeps 0.75rem between
   lines, where the row's own padding is the spacing: `td .form-inline` at
   (0,1,1) over (0,1,0). The accounts' row states its own `gap` at (0,2,0),
   which beats both. */
.form-inline {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1.75rem 0.75rem;
}

td .form-inline {
  row-gap: 0.75rem;
}

.form-inline .field {
  margin-bottom: 0;
}

/* A SELECT IN A ROW OF FIELDS IS AT LEAST AS WIDE AS ITS LABEL. A field in
   the row is as wide as the wider of its label and its control, and a select
   is `width: auto` — as wide as its longest option — so a short option under
   a long label left the select short of its own field and the button after it
   stranded: "Default quantity" over "units", with Set default 44px past the
   end of the select (the audit, 2026-09-29). With this the select ends where
   its field does, and the button is the row's 0.75rem from it. A box that
   states its `size` is left the width it states; this is the select only.

   NOT IN A FIELD THAT CARRIES A HINT, since the second round of the audit
   (2026-09-29). Such a field is as wide as its sentence — New report's Cycle
   quantity, over "The denominator every frequency is written over…" — and
   the select stretched to the sentence: "— none —" in a 714px box, with the
   Fixed count box after it, where the Precision select in the card below is
   75px. That is section 14's rule, A SENTENCE UNDER A CONTROL MAY NOT DECIDE
   THAT CONTROL'S WIDTH, and a field with a hint keeps its select as wide as
   its options. `:has()`, so a browser without it keeps the stretch it had.
   (0,3,1) — `:not()` and `:has()` count as their argument — against nothing
   else that sets a select's minimum width outside a toolbar, whose own
   (0,1,1) `min-width` is for a different row. */
.form-inline .field:not(:has(> .field-hint)) > select {
  min-width: 100%;
}

/* A TICK BOX OR A RADIO AND ITS WORDS: WHEN THE WORDS NEED A SECOND LINE, IT
   STARTS UNDER THE WORDS, NOT UNDER THE BOX. A `<label>` that wraps its control
   is inline text, and inline text breaks wherever a line runs out — the second
   line starting at the label's left edge, under the box. On the import review
   (2026-09-29) "Discard 3 observations" wrapped and its second line began
   under the radio, where it read as a third option.

   A hanging indent: the label is a block of its own, padded on the left by the
   width of the box and the space after it, and its first line is pulled back
   by the same amount so the box sits where it always did. Every later line
   then starts where the first word did. 1.65rem is that width — a tick box
   and its margins are 20px, a radio 21px, and the space after either about
   4px in the system face — so a continued line lands within a pixel of the
   first word, in both.

   A flex row would line the words up exactly and was tried first. It moves no
   glyph, but it makes every element in the label a flex item of its own —
   the muted date after a study name became a block box a line tall — and the
   audit's measurement then read the label's text 2.3px lower than it is
   drawn. This keeps the inline layout the page already had, so the tick box
   sits on exactly the line it sat on.

   `:first-child` because the indent is the width of a control at the START of
   the label, which is how every tick-box label here is written. And the label
   is the box's tap target — clicking the words ticks the box — so it is at
   least the 24px WCAG 2.5.8 asks of one: a 22.5px line and a pixel above and
   below. The "All lines" toggle on the observation list was the one that
   counted, too close to its neighbours for the spacing exemption.

   What this does NOT do is stop the break between the box and the first word
   where a word is wider than what is left of the line: the member picker's
   two-letter codes dropped under their boxes that way, and section 9 answers
   it where it happens, with room and `nowrap`.

   `:has()`, so a browser without it keeps the inline label it had — the
   fallback is the old layout by construction, which is section 13's argument
   for its hide-rules and holds here for the same reason. (0,2,2) — an
   attribute and a structural pseudo-class inside `:has()`, the two types —
   which strictly beats the rules that make a label a block, `.field > label`
   at (0,1,1) and `.form-label` at (0,1,0), and `.field-inline .form-label`'s
   inline at (0,2,0). */
label:has(> input[type="checkbox"]:first-child),
label:has(> input[type="radio"]:first-child) {
  display: inline-block;
  padding: 1px 0 1px 1.65rem;
  text-indent: -1.65rem;
}

/* ------------------------------------------------------------------ 7 */
.flash {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.7rem 1rem;
  margin-bottom: 1rem;
}

.flash-error,
.flash-danger {
  background: var(--flash-error-bg);
  border-color: var(--danger);
}

.flash-info,
.flash-message,
.flash-success {
  background: var(--flash-info-bg);
  border-color: var(--accent);
}

/* The blocking banner: validation errors, and the contingency
   double-count guard. It is loud on purpose — a report you cannot see the
   warning on is a report you cannot defend. */
.banner {
  border: 1px solid var(--accent);
  border-left-width: 4px;
  border-radius: var(--radius);
  background: var(--flash-info-bg);
  padding: 0.85rem 1rem;
  margin-bottom: 1rem;
}

.banner-blocking {
  border-color: var(--danger);
  background: var(--flash-error-bg);
}

.banner-title {
  font-weight: 600;
  margin: 0 0 0.25rem;
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.banner-blocking .banner-title::before {
  content: "\26A0\FE0E";
  color: var(--danger);
}

/* `.banner-title + p` and `.banner p:last-child` stood here, each zeroing a
   paragraph's bottom margin. Both went 2026-09-29 for the one rule in section
   5 that says what they were approximating — nothing inside a padded box adds
   to the bottom of it — and the first went for a defect of its own: it fired
   on the title's sentence when a second paragraph followed, and welded the
   blocked study's "Show me the errors" button to the line above it. */

/* One validation finding: the rule's own wording, then the fix.

   1rem under it since 2026-09-29, the break every box on the page brings
   (section 5, THE MARGIN BELONGS TO THE BOX). It was 0.75rem, so a list of
   findings was spaced 12px apart and the toolbar under the last one 12px
   down, on a screen whose every other seam is 16. */
.finding {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1rem;
  align-items: flex-start;
  justify-content: space-between;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 4px solid var(--danger);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
}

.finding-warn {
  border-left-color: var(--t-contingency);
}

.finding-body {
  flex: 1 1 24rem;
  min-width: 0;
}

.finding-where {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 0.2rem;
}

.finding-rule {
  margin: 0;
}

.finding-detail {
  color: var(--muted);
  margin: 0.15rem 0 0;
}

/* ------------------------------------------------------------------ 8 */
.chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  font-size: 0.8rem;
  line-height: 1.4;
  padding: 0.1rem 0.45rem;
  border-radius: 4px;
  border: 1px solid transparent;
  white-space: nowrap;
}

.chip .glyph {
  font-size: 0.9em;
}

/* Element type — code + glyph + colour, in that order of importance. */
.chip-repetitive {
  color: var(--t-repetitive);
  background: var(--chip-repetitive-bg);
}

.chip-occasional {
  color: var(--t-occasional);
  background: var(--chip-occasional-bg);
}

.chip-contingency {
  color: var(--t-contingency);
  background: var(--chip-contingency-bg);
}

.chip-foreign {
  color: var(--t-foreign);
  background: var(--chip-foreign-bg);
}

.chip-ineffective {
  color: var(--t-ineffective);
  background: var(--chip-ineffective-bg);
}

/* Element category — the second cue is fill weight, never hue:
   VA boxed, ENVA bare, NVA bordered. */
.cat {
  display: inline-block;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  padding: 0.05rem 0.4rem;
  border-radius: 4px;
  border: 1px solid transparent;
  white-space: nowrap;
}

.cat-value_added {
  color: var(--c-va);
  background: var(--chip-va-bg);
}

/* Bare means no fill and no border — NOT no padding. The padding used to go as
   well, so ENVA's text began 6.4px left of VA's and NVA's in the same column
   and a category column read as ragged. The box is still there, invisible, so
   all three words start on one line. */
.cat-essential_non_value_added {
  color: var(--c-enva);
  background: transparent;
}

.cat-non_value_added {
  color: var(--c-nva);
  background: var(--chip-nva-bg);
  border-color: var(--c-nva);
}

/* A badge is set in its own face and weight wherever it is put. Without these
   it took them from its surroundings, so the "ready" pill beside a study's
   `<h2>` and FROZEN beside an issue's `<h1>` were bold, the percentage in the
   comparison's `<h1 class="mono">` headline was bold monospace, and the same
   pills in a table cell were neither. */
.badge {
  display: inline-block;
  font-family: system-ui, sans-serif;
  font-size: 0.78rem;
  font-weight: 400;
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--muted);
}

.badge-ok {
  color: var(--c-va);
  border-color: var(--c-va);
}

.badge-blocked {
  color: var(--text);
  background: var(--flash-error-bg);
  border-color: var(--danger);
}

.badge-edited {
  color: var(--t-contingency);
  border-color: var(--t-contingency);
}

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

/* ------------------------------------------------------------------ 9 */
/* Tables are the point of this application.

   border-collapse: separate is not a style choice. Collapsed borders do
   not travel with position: sticky cells — the rules stay behind while the
   cell scrolls over them — so the report table draws its own borders per
   cell and sets border-spacing to 0. */
/* Both wrappers carry the 1rem every box does, so the footnote, the card or
   the banner under a table is never flush against it. Section 5 says why it
   is a margin on the box rather than a rule per neighbour, and where it is
   taken off again (in a card, and last in a container).

   A SCROLLER IS THE CONTAINING BLOCK OF WHAT IT SCROLLS: `position:
   relative`, since 2026-09-29. A `.visually-hidden` label is absolutely
   positioned (section 2) with no offsets, so it sits at its static place —
   deep inside a wide table, far to the right of the pane — and it is
   positioned against the nearest POSITIONED ancestor. Neither wrapper was
   one, so that ancestor was the page, and a one-pixel label 600px along a
   scrolled table widened the whole document: the reports list, Accounts, the
   per-study quantities and the import review each scrolled sideways on a
   phone into an empty band (609, 440, 435 and 666px wide at 390). Positioned,
   the wrapper holds the label and scrolls it with the table. No `z-index`, so
   no stacking context: the sticky El and Description columns, the sticky
   head and the TOTAL row stack exactly as they did. Nothing else in a
   wrapper is positioned against it. */
.table-wrap {
  position: relative;
  overflow-x: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 1rem;
}

/* The element tables' pane: 70vh, with the head and the TOTAL row stuck to
   its top and bottom while the lines scroll between them. A pane for a table
   READ in part — a report of elements — and not for a list in which every row
   is a decision: the quantity's member picker and the joblet's element list
   are `.table-wrap`s since 2026-09-29, because the pane cut the last four to
   seven of their nineteen rows off at a row boundary, with no footer and, on
   an overlay scrollbar, no sign there was more. */
.table-scroll {
  position: relative;
  overflow: auto;
  max-height: 70vh;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 1rem;
}

/* The one caption drawn on screen (the force-apply preview) was the browser's:
   centred over the table and 3px above its border. It reads as the table's
   heading row, so it starts where the cells do, wears the head's colour and
   weight, and sits on the table's own ground rather than the page's showing
   through the wrapper. Every other caption is `.visually-hidden`, which
   zeroes this padding (section 2) so a hidden caption stays the one pixel it
   is meant to be. */
caption {
  text-align: left;
  padding: 0.5rem 0.75rem;
  color: var(--muted);
  font-weight: 600;
  background: var(--panel);
}

table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
}

th,
td {
  text-align: left;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--border);
  border-right: 1px solid var(--rule-v);
  background: var(--row-bg, var(--panel));
  vertical-align: top;
}

th:last-child,
td:last-child {
  border-right: 0;
}

tbody tr:last-child > * {
  border-bottom: 0;
}

thead th {
  --row-bg: var(--head-bg);
  color: var(--muted);
  font-weight: 600;
  white-space: nowrap;
  border-bottom-color: var(--border);
}

/* Row state is published once, on the <tr>, as --row-bg. Every cell reads
   it — including the sticky ones, which is why zebra, hover, error and
   ineffective each need exactly one rule and not one rule per column. */
tbody tr {
  --row-bg: var(--panel);
}

tbody tr:nth-child(even) {
  --row-bg: var(--row-zebra);
}

tbody tr:hover {
  --row-bg: var(--row-hover);
}

tbody tr.is-ineffective {
  --row-bg: var(--row-ineffective);
}

tbody tr.is-error {
  --row-bg: var(--row-error);
}

tbody tr.is-error:hover {
  --row-bg: var(--row-error-hi);
}

/* Amber, not red, for a line whose finding somebody has already acknowledged.
   Michael, 2026-09-04, asked for "a red backgound under the row where the
   issue is" and pointed at HR-001 / test2 line 22 — whose one finding, it
   turned out, was already acknowledged. Painting that red would re-open on
   every visit a decision the validation screen records as taken; painting it
   nothing would leave his own example invisible. The validation screen already
   draws the two states apart — `error` for open, `warn` plus an "acknowledged"
   pill for accepted — so the list uses the same two words and the same
   distinction. Same lightness family as --row-error, hue moved from red to
   amber, so the two read as siblings and neither shouts over the other. */
tbody tr.is-warn {
  --row-bg: var(--row-warn);
}

tbody tr.is-warn:hover {
  --row-bg: var(--row-warn-hi);
}

/* Numeric columns: right-aligned, monospace, fixed 4 dp, so decimals line
   up down the page. tabular-nums keeps the digit advance constant even
   where the system font would not. */
.num {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* A NUMERIC COLUMN'S HEAD IS A HEAD, NOT A NUMBER. `.num` was written for the
   cells — mono so the digits stack, nowrap so a figure never breaks — and the
   templates put it on the `<th>` as well, for the right alignment, which is
   the half a head wants. The other two came along: every header row mixed two
   faces, the mono ones over the numbers and the sans ones over the text, and a
   long head could not wrap, so it set its column. "Project total" took 141px of
   the project Overview's 353px Cycle definition card and cut the figures
   under it to "22" for 227 on a phone. A head is set in the head's face and
   wraps between its words like any other; it keeps `.num`'s alignment, so it
   still stands over its figures. A unit written after its label is a word
   too, and on a phone, where a percentage column is squeezed to its figures,
   "CA" and "%" took a line each, so the templates hold the two together
   (`<span class="nowrap">CA %</span>`, since the second round of the audit):
   the head wraps between its words and never inside a label and its unit.
   (0,1,2) against `.num`'s (0,1,0) and `thead th`'s (0,0,2). */
thead th.num {
  font-family: inherit;
  white-space: normal;
}

/* ON A PHONE A TABLE SCROLLS; IT DOES NOT ALSO WRAP ITS NAMES. A figure is
   `nowrap`, so a table of them is wider than a phone before any text column is
   counted, and it scrolls in its wrapper whatever the text does. Auto layout
   then squeezed every text column to its narrowest word as well, so the
   project list at 390 read "4349-" / "DC2-" / "layout" down a 60px column, a
   username broke at its hyphen and rows ran 62 to 150px tall — a table that
   both wraps and scrolls, the worst of each. The audit offered two cures and
   the orchestrator chose, on Michael's "fix everything" of 2026-09-29, the one
   that keeps a name whole: the table scrolls, and a name is one line.

   What is a name is marked, not guessed. A row's own head (`th[scope="row"]`
   — the report, the study, the convention) is one line at every width: each
   is a few words, and the one place that could be longer is a table built to
   scroll. A cell whose text is the project's code or name, a study's name,
   an issue's reference, report and issuer, an account's username or the
   Log's actor, is `.ident`, and is one line on a phone only: at desktop
   width the production archive's codes are sentences of 40 characters
   ("Project 4905 - GXO Primark - AMR Putaway"), and held on one line in a
   960px column they would scroll the project list sideways at 1440 too —
   and the Log's actor, held whole at 1024, pushed the Log 92px past its pane
   with its After column out of sight (the second round of the audit,
   2026-09-29). A study's name is the same case: held whole, the production
   copy's Induct studies list was 1285px in its 1246px pane at 1440, and 543
   of that copy's study names are longer than 20 characters, up to 75 (the
   review of the fixes, 2026-09-29, which marked the study list's names and
   the Issued tab's). An image's "2 fields" on the Log is held with them, on a phone
   only, for the same reason (section 14). The Log's hashes and fingerprints
   are not names and stay breakable — one of them unbroken is 500px. (0,1,1)
   and (0,1,1), against `th, td`, which says nothing about wrapping; `thead
   th`'s nowrap is not a row head's. */
th[scope="row"] {
  white-space: nowrap;
}

/* The composition's row head is not a name but a chip and its words — "ENVA
   Essential non-value added" — and held on one line it is 280px: on a phone
   the By value stream table then showed its slices and a cut Minutes column,
   with Share % and Elements, the figures the card is for, out of the 330px
   pane (the second round of the audit, 2026-09-29). It wraps between its
   words again, and the table scrolls only for Elements, as it did. (0,2,1)
   against the row head's (0,1,1). */
.composition th[scope="row"] {
  white-space: normal;
}

@media (max-width: 640px) {
  td.ident,
  .log-image > summary {
    white-space: nowrap;
  }
}

/* A BUTTON IN A CELL IS ONE LINE, AND SO IS A ROW OF THEM. A squeezed column
   broke "Export .pjf" on the joblets list over two lines inside its button,
   and stacked Edit, Export and Delete into a 122px row; on a phone the
   operators' and areas' Save and Delete stacked the same way. The row stays a
   row and the table scrolls, as it already does for its figures. The Accounts
   actions are an `.account-actions` and wrap on purpose (section 3: Michael
   asked for every button to stay in view). (0,1,1) each, against nothing for
   the button — `.btn` does not say how it wraps — and against `.row-actions`'
   `flex-wrap` at (0,1,0) for the row. */
td .btn {
  white-space: nowrap;
}

td > .row-actions {
  flex-wrap: nowrap;
}

/* TABLES STACKED ONE ABOVE ANOTHER SHARE ONE GRID. Four screens draw a family
   of tables with the same head, and each table was laid out on its own, so
   the same column stood at a different place in each: the project list's
   years (Studies at x=605 in 2014 and at 753 in 2026, with the archive open),
   the relaxation guide's twelve sections, the composition's two tables in one
   card (Minutes ending at 826 in one and 916 in the other), the import
   review's per-file element lists. A reader running an eye down a column
   crossed a table boundary and lost it.

   Each family gives its tables a `<colgroup>` and the widths are stated here:
   the columns of figures, dates and short notes the width their longest real
   value needs, in rem, and where a family has two text columns one of them a
   share of the table — so what is left goes to the same column in every
   table: the project's name, the slice's name, the guide's variation, the
   import's status (and on its decision table the three columns that describe
   a new element, after a decision column sized to its two answers). The
   audit proposed `table-layout: fixed` for this; that turns a width into a
   law, and a name longer than its column would then be drawn over the next
   cell. In the ordinary automatic layout a
   stated width is what a column is given whenever its content allows it —
   which is every table here at desktop width — and a column whose content
   cannot fit grows instead, which on a phone is the project list's
   one-line names (above): those tables scroll, each on its own. Measured on
   the production copy with every year open, at 1440 and at 1024: every
   year's six columns start at the same six x positions, and so do the
   guide's sections and the import's lists. (0,2,1) each, and `<col>` has no
   other rule. */
.year-group col.code {
  width: 28%;
}

.year-group col.count {
  width: 6rem;
}

.year-group col.count-wide {
  width: 8.5rem;
}

.year-group col.date {
  width: 7rem;
}

.composition col.minutes {
  width: 9rem;
}

.composition col.share,
.composition col.count {
  width: 7rem;
}

.guide-source col.condition {
  width: 10rem;
}

.guide-source col.pct {
  width: 6rem;
}

.guide-source col.note {
  width: 7rem;
}

/* The superuser's Correct column holds the three boxes it saves and the
   full-size button beside them — Michael's "full size beside boxes" — in one
   line: 103, 103 and 119px of box, three 12px gaps, the 84px button and the
   cell's 24px of padding are 469px, and 30rem leaves a few pixels for an
   engine whose boxes are a little wider. It was 25rem, and at 1440 all 76
   rows put Correct on a line of its own under the boxes, each row 67px
   taller (the second round of the audit, 2026-09-29). */
.guide-source col.correct {
  width: 30rem;
}

/* The import review's code and row count are as narrow as their heads —
   `width: 1%`, the table idiom for "no wider than the content", which is the
   head in every one of these tables — and its decision column holds "Discard
   3 observations" on one line. */
.import-codes col.code,
.import-codes col.count {
  width: 1%;
}

.import-codes col.decision {
  width: 14rem;
}

/* A blank cell is a statement, not missing data. The dash is drawn with
   ::after so copying the table into a spreadsheet yields a genuinely
   empty cell rather than a stray character. */
.blank::after {
  content: "\2014";
  color: var(--muted);
}

/* The frequency is ONE cell showing the PAIR — "263 occurrences per 227
   cycles" — never two columns both headed Freq. */
.freq {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.freq .den {
  color: var(--muted);
}

/* COUNTED / USED — one cell showing the pair, exactly as .freq does, and
   weighted the other way round on purpose. Michael: "the overridden value
   should show on the qty view next to the current value for this study,
   'i1.iePW' 56/58".

   In .freq the numerator is the subject and the denominator recedes. Here
   the RIGHT half is the subject: it is always the denominator the SMV is
   actually divided by, which is the whole reason the pair is worth printing.
   So `counted` is the one that recedes.

   Colour is not load-bearing: the column heading names both halves, the
   slash separates them, the cell carries a title, and a figure nothing has
   overridden is drawn as a plain number rather than as a pair of equals. */
.pair {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pair .counted {
  color: var(--muted);
}

.pair .used {
  color: var(--accent);
}

/* Ineffective elements render 0(+N): observed time and a count, nothing
   else. */
.ineffective-count .plus {
  color: var(--t-ineffective);
}

/* A REPORT TABLE'S FLOOR IS ITS PANE AT 1024, INSIDE THE PANE'S BORDER. The
   floor is what keeps a report's columns on a phone, where it scrolls rather
   than crushing them. 62rem is the page's column at 1024, and it stood here
   with no account of the pane's own 1px border each side: every report table
   whose columns wanted less than the floor was 992px in a 990px pane at 1024
   — the quantities list, the joblet, the observation list, the comparison —
   and on a desktop browser that draws its scrollbars, 2px of overflow is a
   whole horizontal scrollbar under a table that fits (the second round of
   the audit, 2026-09-29). The floor is the inside of the pane. (0,1,0),
   against nothing; `.sg-compact` gives it up in section 12. */
.report-table {
  min-width: calc(62rem - 2px);
}

/* THE REPORT FITS ITS PANE AT DESKTOP WIDTH, IN THE ORDER IT HAS ALWAYS HAD.
   The report and the study summary are fourteen columns, and at 1440 they were
   1509 to 1589px wide in a 1246px pane, so CA%, SMV and % per SMV Total — the
   columns the page exists to print — began past the pane's right edge behind
   a scroll. The audit offered moving SMV up beside Description. Michael,
   2026-09-29, chose otherwise:

       Tighten, keep order

   — the column order he reads an iePro-style report in stays, and the table is
   made narrower in three places, each measured on the widest real report
   (production's Induct report 4, 1565px):
   * the cells are padded 0.4rem across instead of 0.75rem — fourteen columns,
     two sides each, 157px — which still leaves 13px between two columns'
     figures with the column rule between them;
   * Description is 11rem, not 14rem (section 1): 48px, and a description
     longer than that takes a second line in its own cell;
   * a head may take two lines (below), and the heads that set their columns —
     "Observations/Quantity" at 215px over figures a third as wide, "% per SMV
     Total", "BM/Obs", "BM/Cycle" — are given the break they lacked in the
     templates, a `<wbr>` after the slash and the last two words of "% per SMV
     Total" kept together, so none of them runs to a third line.
   Measured in the audit's sweep: the narrowest these tables can now be drawn
   is 1129 to 1235px (Induct report 4 the widest, the study summaries 1163 to
   1177), so at 1440 every one of them fills its 1246px pane with every column
   in view — the Study Grid's ten studies and its combined column too. At 1024
   the pane is 990px, 139 to 245px short of that narrowest width with every
   column already at its longest figure; fitting there would mean cutting
   figures or Description to a few characters, so at 1024 and below the table
   scrolls in its pane as it always has, with El and Description stuck on the
   left. (0,1,1) against `th, td`'s (0,0,1). */
.report-table th,
.report-table td {
  padding-left: 0.4rem;
  padding-right: 0.4rem;
}

/* A REPORT TABLE THAT IS NOT A REPORT KEEPS ITS PADDING. The joblet's element
   list is a `.report-table` for its sticky El and Description and its row
   rules, not for fourteen columns that must fit a pane: it is seven, in a pane
   with room to spare. At the report's 0.4rem the right-aligned position
   number stood 14px from the El code across a rule that barely shows, so a
   row read "1 1" and the head "On device El" (the second round of the audit,
   2026-09-29). The ordinary 0.75rem puts 27px between them, wider than the
   23px from the tick box to its number. (0,2,1) against the report's (0,1,1). */
.report-table.joblet-list th,
.report-table.joblet-list td {
  padding-left: 0.75rem;
  padding-right: 0.75rem;
}

/* A report head wraps between its words. `thead th` keeps a head on one line,
   which is right for a head over text and is what made these columns wider
   than their figures. Two lines at most, by the templates' own breaks above.
   (0,1,2) against `thead th`'s (0,0,2); `thead th.num` above says the same at
   the same weight. */
.report-table thead th {
  white-space: normal;
}

/* A REPORT ROW IS A WHOLE NUMBER OF PIXELS TALL. The body's 15px/1.5 line is
   22.5px, and a type chip on its baseline made the line 23.5: every row was
   40.5px, so every other rule fell on a half pixel. The sticky El and
   Description cells are painted on a layer of their own and round the half
   the other way, and the audit found their row rules a pixel above the rest of
   the row, doubled, or missing — a stepped left edge on every report at 1024
   and on a phone. A 24px line (15px x 1.6) with the chips, badges, category
   tags and small buttons in a row held inside it — centred on the text, a
   pixel given back above and below so the tallest of them, a small button at
   24.7px, no longer grows the line — makes every row 41px and a two-line head
   65. Measured on the report, the study summary, the Study Grid, the
   observation list (production's 1109 lines) and the styleguide: no row off
   the pixel. (0,1,0) for the line, inherited by every cell; (0,2,0) for the
   boxes, against nothing that sets their vertical alignment or margin. */
.report-table {
  line-height: 1.6;
}

.report-table .chip,
.report-table .badge,
.report-table .cat,
.report-table .btn {
  vertical-align: middle;
  margin-top: -1px;
  margin-bottom: -1px;
}

.report-table .col-el {
  width: var(--col-el-w);
  min-width: var(--col-el-w);
  max-width: var(--col-el-w);
  position: sticky;
  left: 0;
  z-index: 2;
  font-family: var(--mono);
}

.report-table .col-desc {
  width: var(--col-desc-w);
  min-width: var(--col-desc-w);
  max-width: var(--col-desc-w);
  position: sticky;
  left: var(--col-el-w);
  z-index: 2;
  border-right: 1px solid var(--border);
  box-shadow: 6px 0 8px -8px rgba(0, 0, 0, 0.75);
}

.report-table thead .col-el,
.report-table thead .col-desc,
.report-table tfoot .col-el,
.report-table tfoot .col-desc {
  z-index: 4;
}

.report-table thead th {
  position: sticky;
  top: 0;
  z-index: 3;
}

/* A PICK LIST'S EL CELL HOLDS A TICK BOX AS WELL AS THE CODE. The quantity
   editor's member picker is a report table whose El column is "In": the box,
   then the code. 4.5rem leaves 48px inside the cell's padding, which holds a
   code on its own and not a box beside one: the line broke between the box
   and the code, so "SS", "CL" and "OP" dropped under their boxes (2026-09-29)
   and each such row grew from 40px to 62px. The box and its code are one
   thing, so the label does not break (`nowrap`), and the column is given the
   room for them: 6.5rem holds the box, the space and five mono characters,
   which is the most `MAX_ELEMENT_CODE` allows — at 4.5rem an unbreakable
   five-letter code would have run into the sticky Description cell instead.
   Set as the custom property on the table, not as a width on the cell,
   because `.col-desc`'s sticky offset is read from the same property and the
   two must stay in step (section 1). (0,2,0) and (0,2,1), against nothing. */
.report-table.picker {
  --col-el-w: 6.5rem;
}

.report-table .col-el > label {
  white-space: nowrap;
}

/* A POSITION NUMBER IS AS WIDE AS THE LARGEST ONE. The joblet's On device
   column prints each chosen element's place on the tablet after its tick box,
   in a right-aligned cell — so the box moved left by a digit's width at row
   10, and again at 100, and an unticked row, with no number at all, drew its
   box at the cell's right edge (the audit, 2026-09-29). Three mono digits of
   room, whether or not there is a number to put in them — `MAX_ELEMENTS` is
   400 — right-aligned inside that room, and every box in the column stands
   in one line. `text-indent: 0` because an inline block is a block container
   and would inherit the hanging indent its label is given in section 6.
   (0,1,0), against nothing. */
.position {
  display: inline-block;
  min-width: 3ch;
  text-align: right;
  text-indent: 0;
}

/* A ROW OF BOXES CENTRES ITS CELLS. Every cell is `vertical-align: top`
   (above), which is right for text: a two-line description and a one-line
   number both start at the top of their row. It is wrong in a row that edits
   in place — the operators and areas lists, the new element's allowance rows,
   the study's per-quantity override — where the cells hold 34px boxes: the
   Observations, Studies and Corrections figures sat 6px above the text in the
   boxes beside them, and the buttons at a third height. Centred, the figures
   and the text in the boxes share a line.

   A row qualifies when a one-line box or a select is a cell's own content, or
   sits on the one action row that is: that is what makes the box the row's
   content rather than one part of a taller cell. The accounts list and the
   allowance-source table hold their boxes deeper, among several lines of
   other content, and read from the top as they did. Tick boxes and radios do
   not count: they sit on a text line already. Without `:has()` a row stays
   top-aligned, which is how it was. (0,4,5) — `:has()` counts as its most
   specific argument — against `th, td`'s (0,0,1). */
tbody tr:has(
  > td > :is(input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), select),
  > td > .row-actions > :is(input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), select)
) > td {
  vertical-align: middle;
}

/* A LINK THAT IS A CODE OR A NUMBER IS A TARGET, NOT A WORD. The observation
   list links each line from its number and from its element code, and on a
   phone the smallest were 9.55 x 18px — the element "4" — and "25" at
   18 x 18, with other targets too near for the WCAG 2.5.8 spacing exemption.
   24px across by at least the line's 22.5px and a pixel above and below: a
   one-character code gains the difference in width, which moves the
   description after it by at most 14px, and the negative margin hands the
   line back the two pixels of padding, so no row grows. (0,1,2) against
   nothing. */
td.col-el > a,
td.col-desc > a {
  display: inline-block;
  min-width: 1.6rem;
  padding-block: 1px;
  margin-block: -1px;
}

/* A TOTAL row is ruled off from the rows it totals, in every table and not
   only the report's. The composition tables had none of the report's rule
   below, so their TOTAL sat directly under an Ineffective row of nearly the
   same ground and read as one more slice. The same 2px rule and 600 weight the
   report's footer has always had; (0,0,2), which the report's own (0,1,2)
   below still refines. */
tfoot th,
tfoot td {
  border-top: 2px solid var(--border);
  font-weight: 600;
}

/* The totals row sticks to the bottom of the scroll pane, as iePro's does.
   Its cells carry their own --row-bg so the sticky left columns match. */
.report-table tfoot th,
.report-table tfoot td {
  --row-bg: var(--row-total);
  position: sticky;
  bottom: 0;
  z-index: 3;
  border-top: 2px solid var(--border);
  border-bottom: 0;
  font-weight: 600;
}

/* ----------------------------------------------------------------- 10 */
.toolbar {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  align-items: center;
}

/* A LINE OF THE BAR READS ON ONE BASELINE, since 2026-09-29, and was
   `align-items: center`. Centring is right while everything on a line is one
   control tall and wrong the moment one item is not: the report's study
   picker is a `<details>`, and opened it is a list of studies some 400px
   tall, so "Studies" and the Cycle quantity select beside it were centred on
   the middle of the list — 180px below the summary line they belong to — and
   on the Study Grid, where the picker opens by default, "Output" floated
   half-way down it. On the baseline, a label sits on the line of the control
   or summary it names, however tall a neighbour grows. Every other line
   lands where it was: a label, a box and a button of one height share their
   text line either way, and the fix sweep of every screen at three widths
   found no control that moved against its neighbours.

   AND IT IS A HANGING INDENT, the shape section 6 gives a tick box and its
   words, for the same reason: when a line wraps, what wraps starts under the
   controls, not under the line's own label. The first `.toolbar-label` is the
   line's name and sits in the first 6rem; the line is padded by that column
   and its gap, and the label is pulled back into the padding by the same
   amount, so its controls start at 7rem and so does every line they wrap
   onto. Before this, a wrapped note ("the element-type filter is a view…",
   the cycle's basis) and the Log's To field restarted flush under "Filters"
   or "Filter", where they read as the start of a line of their own — the
   ragged edge the audit measured on the Log at 1024 and on a phone. A grid
   with a label column would do the same with one more wrapper round every
   line's controls; this needs none, and a line with no leading label (none
   today) simply starts at the indent.

   `:first-child` and `>` because only the line's own name is the column: a
   `.toolbar-label` further along names one control and travels with it
   (`.toolbar-pair`, below). Its `flex` is the column's width outright, where
   `min-width: 6rem` on `.toolbar-label` left it free to grow past the
   indent. (0,2,0) against `.toolbar-label`'s (0,1,0). */
.toolbar-line {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  align-items: baseline;
  width: 100%;
  padding-left: 7rem;
}

/* A LINE'S HEAD IS A HEAD: the labels' size and a head's weight, since the
   second round of the audit (2026-09-29). The filter labels became
   `.form-label`, 15px, and the words heading their lines — Studies, Filters,
   Allowances, the Log's Filter, the comparison's Then and Now — stayed
   13.6px beside them in the same grey, so "Filters" was drawn smaller than
   the "Operator" it groups, most plainly on a phone where it heads a stack of
   them. At the labels' size and 600, the weight `thead th` heads a table
   with, the head of a line reads as the head of what follows it. (0,1,0),
   against nothing that sets its size or weight. */
.toolbar-label {
  color: var(--muted);
  font-weight: 600;
  min-width: 6rem;
}

.toolbar-line > .toolbar-label:first-child {
  flex: 0 0 6rem;
  margin-left: -7rem;
}

/* ON A PHONE THE LABEL TAKES A LINE OF ITS OWN and the controls start at the
   bar's edge. A 7rem indent is a third of the 20.75rem a phone leaves inside
   the bar, and a filter select beside it would be cut to half its option.
   And every label in the bar is then above what it names, the pairs'
   (`.toolbar-pair`, below) as well as the line's: a pair beside its control
   where it fits and above it where it does not — "Cycle quantity" with the
   Fixed count box, the Study Grid's "Output" over a long name — put both
   shapes in one form on one screen, the mix the audit flags on every form.
   Written as a query, not a container query: the bar is always the page's
   full width. (0,2,0) against `.toolbar-pair`'s (0,1,0) on `align-items` and
   `gap`.

   THE LOG'S FIELDS ARE ITS PAIRS, and on a phone they stack the same way,
   since the second round of the audit (2026-09-29). A `.field-inline` is the
   Log's label and box as one item, and beside each other at 390 their boxes
   started at four x positions, one per label's length, in the one bar on a
   phone that kept its labels beside; above, every box starts at its field's
   edge. Above 640px the Log's labels stay beside their boxes, where Michael
   put them (2026-08-30, "on the left side of the input fileds"). Its Filter
   and Clear are one `.row-actions`, so the full-size pair no longer parts —
   Clear alone on a line — and the row sits at the foot of the line, level
   with the last box rather than with a label. No `align-items` on the
   stacked field: the baseline it inherits acts as the start of a column.
   (0,2,0) against `.field-inline`'s (0,1,0) on `flex-direction` and `gap`;
   nothing else sets the row's `align-self`. */
@media (max-width: 640px) {
  .toolbar-line {
    padding-left: 0;
  }

  .toolbar-line > .toolbar-label:first-child {
    flex-basis: 100%;
    margin-left: 0;
  }

  .toolbar-line > .toolbar-pair {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
  }

  .toolbar-line > .field-inline {
    flex-direction: column;
    gap: 0.3rem;
  }

  .toolbar-line > .row-actions {
    align-self: flex-end;
  }

  /* The cycle quantity takes a line of its own, under the study picker. It is
     a label over a select and what the select resolves to, and beside a short
     summary it fitted the picker's line on one report and not on the next —
     the same bar in two shapes, by the data. (0,3,0), against nothing that
     sets a pair's basis. */
  .toolbar-line > .toolbar-pair.cycle-pair {
    flex-basis: 100%;
  }

  /* The study summary's cycle bar is the same label, select and button as a
     `.row-actions` form rather than a toolbar line. At 16px on a phone the
     three no longer fit 332px, and Apply wrapped onto a line of its own at
     the left — the "button alone on a line" the Log's filter had just been
     rid of. The label goes above, as the report bar's does, and the select
     and Apply share the line under it. (0,3,1), against `.row-actions >
     .form-label`'s (0,2,0), which sets nothing on the basis. */
  .toolbar > form.row-actions > .form-label {
    flex-basis: 100%;
  }
}

/* A LABEL AND ITS CONTROL ARE ONE ITEM OF THE LINE. Michael, 2026-09-29,
   asking for the whole application to be gone over:

       so that there is no more supises on lake of gaps, or input fields been
       weird dimentions etc.

   The report's Filters line was a row of separate flex items — Operator, its
   select, Area, its select — and a wrapping flex line breaks between ANY two
   items, so at 390 wide every filter's label ended the line above its own
   select, beside the previous one. The issue comparison did it at 1440: "Now"
   ended the first line and its picker began the second. Wrapped in one
   `.toolbar-pair`, a label and its control are a single item, so a line can
   break only between pairs. `inline-flex` with the line's own baseline, so the
   label reads on the control's text line. A pair wider than the whole line —
   on a phone, "Cycle quantity" with its select and the Fixed count box, or
   the Study Grid's "Output" over a long quantity name — keeps its label whole
   and puts it above the control, as a stacked form would (`flex-wrap`, and
   the 0.3rem `.form-label` leaves under itself there); left to shrink, the
   label broke over two lines beside the select and the select cut its own
   option short. `min-width: 0` and `max-width: 100%` so a control wider
   than the line even then — the comparison's issue names — is cut at the bar
   (`.toolbar select`, below) instead of pushing the page sideways. A
   `.toolbar-label` in a pair names the one control beside it and gives up
   the 6rem the line's own label is given. (0,2,0) for that against
   `.toolbar-label`'s (0,1,0). */
.toolbar-pair {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 0.5rem;
  min-width: 0;
  max-width: 100%;
}

.toolbar-pair > .toolbar-label {
  min-width: 0;
}

/* A CONTROL'S OWN LABEL, IN A ROW, SITS BESIDE IT. `.form-label` is a block
   with a 0.3rem foot for the stacked form it was made for. In a flex row the
   block became an item like any other — the observation list's "Lines [ ] to
   [ ]" stacked into four lines inside the range — and the foot, which a flex
   item's margin does not lose, lifted "reading" and "become" 2.65px above the
   selects they name. Inline, with no foot, in the three places a label is
   drawn beside its control: a `.toolbar-pair`, an action row, and the
   observation list's line range. The report toolbar's filter labels are
   `.form-label` for the same reason, since 2026-09-29, and so the Log's size:
   they were `.small` at 13px against the Log's 15. (0,2,0) against
   `.form-label`'s (0,1,0); the same declarations as `.field-inline
   .form-label` in section 14, which is the Log's version of this pair. */
.toolbar-pair > .form-label,
.row-actions > .form-label,
.range-fields > .form-label {
  display: inline;
  margin-bottom: 0;
}

/* The observation list's line range is one item of its action row — "Lines
   [ ] to [ ]" — which section 13 hides whole when "All lines" is ticked. A
   flex row of its own, on the row's baseline, so its two boxes and their
   labels stay on one line. Section 13's hide-rule is (0,2,1) and wins
   `display` outright over this (0,1,0). */
.range-fields {
  display: inline-flex;
  align-items: baseline;
  gap: 0.5rem;
}

/* A PICK LIST IN A BAR HAS TRACKS OF ITS OWN. The study picker on the report,
   the Study Grid and New report was a `.card-grid` — section 5's layout for
   cards, `minmax(22rem, 1fr)` with 1rem gaps — inside a `<details>` that is a
   shrink-to-fit flex item: auto-fit then had room for one 352px track, so the
   list opened as one column of studies 38px apart, and on a phone the track
   was wider than the 332px bar and, with real study names that did not wrap,
   scrolled the whole page sideways to 450px. A tick-box label is a line of
   text, not a card: 14rem tracks — or the whole width, whichever is less —
   filled as the space allows, and a quarter-line between rows. The labels
   wrap now (they were `.nowrap`): section 6's hanging indent keeps a second
   line under the name. New report draws the same list in a card, whose gap
   spaces it. */
.pick-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 14rem), 1fr));
  gap: 0.25rem 1rem;
}

/* And open, the picker takes the line. A shut `<details>` is its summary's
   width; an open one grows to fill what is left of the line (and wraps to a
   line of its own below 20rem), so its tracks have room to be tracks and the
   controls after it move to the next line, under the indent, instead of
   squeezing the list to one column. Its summary is then a heading over the
   list, a half-line above the first study rather than flush on it — 0.5rem,
   the number `.manual-key[open] > summary` and `.year-group[open] > summary`
   use for the same seam. (0,2,1) and (0,2,2), against nothing. */
.toolbar details[open] {
  flex: 1 1 20rem;
  min-width: 0;
}

.toolbar details[open] > summary {
  margin-bottom: 0.5rem;
}

.toolbar .spacer {
  margin-left: auto;
}

/* THE BUTTON THAT CLOSES A BAR ENDS ITS LINE ON THE RIGHT, wherever that line
   is. The spacer before it pushes it right only while the two share a line,
   and a note that fills the line ahead of them — the Study Grid's Output
   basis, on real data — sends both to the next, where an empty spacer pushes
   nothing: "Show grid" landed at the left under the note, at 1440 on the
   production copy and at 1024 on the seeded one, where the report's Apply
   and the same button everywhere else end the bar on the right (the second
   round of the audit, 2026-09-29). With an auto margin of its own the button
   ends its line on the right on its own; sharing a line with the spacer, the
   two margins split the free space and it still ends at the edge. (0,3,0),
   against nothing that sets a button's margin in a bar. */
.toolbar .spacer + .btn {
  margin-left: auto;
}

/* THE BAR'S TEXT BOXES SIZE THEMSELVES, and the reason they did not is NOT the
   reason the long form's copy of the same field did not.  Michael, 2026-08-29,
   naming the symptom on reports/new.html:

       Policy CA%, the input filed is under the label deu to been max width
       id=f-ca in css: width: 100%

   The sentence is true of the toolbar's `#f-ca` as well and the cause is a
   different one.  On the long form the box carries `input-narrow` and that
   class was losing a specificity contest (section 14).  Here it carries no
   class at all, so nothing was ever trying to narrow it: section 6's
   `input[type="text"]` stretched it to the full width of `.toolbar-line`, and a
   flex item whose hypothetical size is the whole line is given a line of its
   own — which put the box under the label that names it, on a bar whose whole
   point is that a filter and its value read as one phrase.

   Scoped to the bar rather than pinned on that one box, because every other
   control in here already works this way: `select { width: auto }` in section 6
   sizes the quantity, mode and filter pickers to their own options, and these
   two text boxes were the only things left asking for 100%.  The other one is
   the Fixed cycle count, which sits two elements deeper inside `.cycle-mode` —
   a child combinator would have fixed the visible complaint and left its
   neighbour broken, which is the shape of defect this project keeps finding.

   What does the sizing is `size` on the inputs themselves: 6 characters for a
   percentage, 8 for a cycle count.  Specificity (0,2,1) — one class, one
   attribute, one type — against section 6's (0,1,1).  `max-width` so that a bar
   narrower than the box clips it instead of pushing the page sideways.

   SECTION 6 IS (0,0,0) SINCE 2026-08-30 and the contest recounted above is
   history rather than arithmetic anybody has to redo.  This rule is left at
   (0,2,1) all the same: it still wins, it is what the test module asserts on,
   and re-deriving a selector whose behaviour is unchanged is an edit with no
   cause.  `max-width: 100%` is the half that never had an opponent and never
   needed one. */
.toolbar input[type="text"] {
  width: auto;
  max-width: 100%;
}

/* AND THE BAR'S SELECTS STOP AT THE BAR. Section 6 leaves a select at `width:
   auto`, the width of its longest option, and the issue comparison's two
   pickers name each issue in full — "v1 — 4349-DC2-layout/layout-toolbar v1
   (2026-09-28, 10 studies)" — so each was 537px wide, ran out of the bar on a
   phone and scrolled the whole page sideways to 566px. Capped, a select
   longer than the bar shows its option cut at the bar's edge, the failure the
   conventions block chose on purpose for the same reason (section 14): the
   damage stays inside one control. The version the option starts with is
   still read. (0,1,1) against section 6's `select` at (0,0,1).

   AND AT LEAST 9rem, since 2026-09-29. A select is as wide as its longest
   option, so the report's three filters — each reading "all" until one is
   chosen — were 157, 124 and 187px, and different again on the next report,
   whose operators have longer descriptions: three pickers saying the same word
   at three widths. A floor does not make them equal, and nothing short of
   cutting the option text would; it stops the short ones reading as a
   different kind of control from the long ones beside them. `min-width` has
   no other declaration on a select.

   AND WHAT IT CUTS STAYS CUT, IN SAFARI TOO, since the second round of the
   audit (2026-09-29). The cap above works in Chromium because Blink clips a
   drop-down's content to its box on its own (its computed `overflow` reads
   `clip`). WebKit leaves a select at `overflow: visible`, and its theme
   overrides any `overflow` an author writes, so the cut-off option text —
   about 500px of "v1 — 4349-DC2-layout/layout-toolbar v1 (2026-09-28, 10
   studies)" in a 332px box — still counted as the page's overflow: the
   comparison scrolled sideways to 540px at 390 in WebKit, into an empty
   band. Paint containment clips a box's contents to it in every engine. The
   select's own border, outline and focus ring are not its contents, and are
   drawn whole. `contain` has no other declaration in the sheet. */
.toolbar select {
  max-width: 100%;
  min-width: 9rem;
  contain: paint;
}

/* A FIELD IN A BAR IS SPACED BY THE BAR, exactly as one in a `.form-inline` is
   spaced by the row (section 6, same declaration, same reason).

   `.field` carries `margin-bottom: 1rem`, which is right in a stacked form and
   wrong in a flex row: `.toolbar-line` is `align-items: center`, and centring
   is applied to the item's MARGIN box.  A 1rem margin on the bottom and none
   on the top therefore lifts the field's contents 0.5rem above every control
   beside it that has no margin — the Filter button, the Clear link, the
   `.toolbar-label`.  Four boxes half a line higher than the button that
   submits them is the second half of what Michael reported on the Log:

       This should as well equal them in one straight horizontal line.

   The bar already spaces its own children: `.toolbar-line` is `gap: 0.4rem
   1rem` and `.toolbar` `gap: 0.5rem 1.5rem`, and a margin does not collapse
   inside a flex container, it adds.  Specificity (0,2,0) against `.field`'s
   (0,1,0).

   `.toolbar-line` aligns on the baseline since 2026-09-29 (above), where a
   margin no longer lifts anything; the bar's own `align-items: center` still
   centres, and a wrapped line's height still counts the margin. The rule
   stays for both. */
.toolbar .field {
  margin-bottom: 0;
}

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 0.9rem;
  align-items: center;
  color: var(--muted);
  font-size: 0.85rem;
}

/* ----------------------------------------------------------------- 11 */
/* The fleet login page, non-Bootstrap variant: 56px logo centred INSIDE
   the card, muted subtitle, no page heading, large inputs, full-width
   submit. Everything is scoped to .login-card so no other card shifts.

   AND IT IS THE ONE AUTH CARD, since 2026-09-29. Four screens ask for a
   credential — sign in, the second factor, the password change and the
   two-factor enrolment — and they were built two ways: the two sign-in steps
   as this card, with boxes as tall as the large button under them, and the
   other two as plain cards whose 34px boxes sat over the same 41px button.
   The audit Michael asked for ("so that there is no more supises on lake of
   gaps, or input fields been weird dimentions etc.") found the pair. All four
   are this card now, and this is the whole of its spec:
     - the 420px column, `main.auth`, and this shadow;
     - boxes the large button's height, 41.34px, and the card's width;
     - fields 1rem apart, in the block of their `<form>`;
     - one large submit, the width of the card;
     - the brand, where there is one, 56px and a muted line under it — the
       signed-out pages draw it inside the card, as the fleet page does, and
       the two signed-in screens have the header instead.
   The class keeps the login's name because the login is where the shape
   comes from, and the fleet pattern note is written in its terms. */
.login-card {
  box-shadow: var(--shadow-card);
}

.login-card .login-brand {
  text-align: center;
  margin-bottom: 1.5rem;
}

.login-card .login-logo {
  max-height: 56px;
  width: auto;
  max-width: 100%;
}

.login-card .login-subtitle {
  text-align: center;
  color: var(--muted);
  margin-bottom: 1.5rem;
}

.login-card .field {
  margin-bottom: 1rem;
}

/* THE LOGIN BOXES SIZE THEMSELVES, because they set their own padding and font
   size. Section 6's one-line height is `.btn`'s sum written out, and it is only
   right for a box carrying section 6's padding: 0.6rem of it inside
   `calc(1.2em + 0.9rem + 2px)` at 1.05rem type leaves a 15px content box for a
   20px line. `auto` sizes the box from what this rule declares instead — the
   1.2 line section 6 gives it, 0.6rem above and below, the border — and since
   this padding and font size are `.btn-lg`'s, that is `.btn-lg`'s height: the
   boxes now measure the Sign in button under them exactly, 41.34px. They were
   46.38px, on the 1.5 line section 6 has stopped handing out.
   Specificity (0,2,1) against section 6's (0,1,1). */
.login-card input[type="text"],
.login-card input[type="password"] {
  padding: 0.6rem 0.9rem;
  font-size: 1.05rem;
  height: auto;
}

.login-card .btn {
  margin-top: 0.5rem;
}

/* The two-factor enrolment code, and the typed fallback beside it.

   .qr IS NOT THEMED, and must not be. A QR code is read by a camera, not by
   a person: rendered in this page's own palette it would be near-black ink
   on near-black paper and no phone would decode it, so app/web/totp.py draws
   it white-on-black unconditionally and this only sizes it and gives its
   white edge somewhere to stop. The border uses --border like every other
   edge on the page, so the image sits in the card rather than glaring out
   of it.

   .manual-key is scoped rather than styling bare <details>, which is already
   used unstyled by the report and study-grid screens; restyling those from
   here would be a change to two screens nobody asked to change. */
.qr {
  display: block;
  width: 220px;
  height: 220px;
  max-width: 100%;
  margin: 0 auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.manual-key {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--input-bg);
  padding: 0.5rem 0.75rem;
}

.manual-key > summary {
  cursor: pointer;
  color: var(--accent);
  font-size: 0.9rem;
}

.manual-key[open] > summary {
  margin-bottom: 0.5rem;
}

.manual-key p {
  margin: 0.4rem 0;
}

/* Pre-auth pages other than login show the brand block above the card;
   login.html overrides the block empty so the brand sits inside the card.

   ONE BRAND, since 2026-09-29. This block was a second version of it: a 72px
   logo over an uppercase tagline in the accent colour, where the sign-in card
   two clicks away drew a 56px logo over the same words, muted, in sentence
   case. A signed-out visitor who met the rate limit saw a different
   application from the one they had just signed in to. It is the card's brand
   now — the logo's 56px, and the tagline as the card's subtitle, with the
   subtitle's 1.5rem under the logo. Only where it is drawn differs: above a
   card that has a heading of its own (the error pages), rather than inside
   one that has none. (0,1,1) and (0,2,0) as before, against nothing. */
.auth-brand {
  text-align: center;
  margin-bottom: 1.5rem;
}

.auth-brand img {
  max-height: 56px;
  width: auto;
}

.auth-brand .tagline {
  display: block;
  margin-top: 1.5rem;
  color: var(--muted);
}

/* A NEW PASSWORD IS READ OFF THE SCREEN, and it was drawn as a code chip — the
   13.5px face, the accent border — inside a box of its own: a small chip in a
   box, on the one screen whose whole job is that somebody reads twenty-two
   characters aloud without a mistake (the audit, 2026-09-29). The box stays:
   it is what sets the credential apart from the sentences about it. What is
   in it is the credential itself, in the mono face at 1.25rem, with no second
   border. It keeps the chip's `nowrap` (section 2, A CODE CHIP IS ONE BOX):
   twenty-two characters at this size are 264px, inside a phone's card.
   (0,1,0) for the box, against nothing; (0,1,1) for the code, against
   `code`'s (0,0,1). */
.credential {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--input-bg);
  padding: 0.75rem 1rem;
}

.credential code {
  font-size: 1.25rem;
  background: none;
  border: 0;
  padding: 0;
}

/* ----------------------------------------------------------------- 12 */
.sg-section {
  margin-bottom: 2.5rem;
}

.sg-note {
  color: var(--muted);
  font-size: 0.85rem;
  margin: 0 0 0.75rem;
}

.sg-swatches {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
  gap: 0.75rem;
}

.sg-swatch {
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}

.sg-swatch .chipface {
  height: 3rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.85rem;
}

.sg-swatch .meta {
  padding: 0.35rem 0.5rem;
  font-size: 0.8rem;
  color: var(--muted);
  border-top: 1px solid var(--border);
  font-family: var(--mono);
}

.sg-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  align-items: center;
  margin-bottom: 0.75rem;
}

/* THE STYLEGUIDE CARRIES NO `style` ATTRIBUTE, and the classes below are what
   its sixteen became on 2026-09-29. The CSP is `style-src 'self'`
   (app/web/__init__.py), which drops every inline style, so each of them was
   a declaration the browser never applied: the twelve palette faces were
   transparent — a palette with no colour in it — the specimen card, the
   button column and the sign-in replica were the width of the page, and the
   row-states table scrolled with the report's 62rem minimum. The page that
   documents the system was the one page drawing something other than what it
   documents. `tests.test_layout` fails the day a template writes `style=`.

   A swatch's face is the token it names, on the ground it is read on: the two
   text tokens are drawn on the panel, the two filled tokens carry
   `--on-accent`. (0,3,0) each, against `.sg-swatch .chipface`'s (0,2,0),
   which sets neither property. */
.sg-swatch .chipface.swatch-bg {
  background: var(--bg);
  color: var(--text);
}

.sg-swatch .chipface.swatch-panel {
  background: var(--panel);
  color: var(--text);
}

.sg-swatch .chipface.swatch-text {
  background: var(--panel);
  color: var(--text);
}

.sg-swatch .chipface.swatch-muted {
  background: var(--panel);
  color: var(--muted);
}

.sg-swatch .chipface.swatch-accent {
  background: var(--accent);
  color: var(--on-accent);
}

.sg-swatch .chipface.swatch-danger {
  background: var(--danger);
  color: var(--on-accent);
}

.sg-swatch .chipface.swatch-border {
  background: var(--border);
  color: var(--text);
}

.sg-swatch .chipface.swatch-rule-v {
  background: var(--rule-v);
  color: var(--muted);
}

.sg-swatch .chipface.swatch-row-zebra {
  background: var(--row-zebra);
  color: var(--text);
}

.sg-swatch .chipface.swatch-row-error {
  background: var(--row-error);
  color: var(--text);
}

.sg-swatch .chipface.swatch-row-error-hi {
  background: var(--row-error-hi);
  color: var(--text);
}

.sg-swatch .chipface.swatch-row-total {
  background: var(--row-total);
  color: var(--text);
}

/* The specimens' widths: the button column is 22rem, about the inside of a
   sign-in card, which is what "full width" means for the large button in it;
   the replica is the 420px column `main.auth` is (section 3); the form
   specimen is a form's measure.
   (0,1,0) each, against nothing that sets a maximum width on a `.sg-row`, a
   `.card` or a `<div>`. The row-states table is six columns and fits: it
   gives up the report's 62rem minimum, (0,2,0) against `.report-table`'s
   (0,1,0). */
.sg-narrow {
  max-width: 22rem;
}

.sg-form {
  max-width: 34rem;
}

.sg-auth-column {
  max-width: 420px;
}

.report-table.sg-compact {
  min-width: 0;
}

/* THE REPORT SPECIMEN FITS ITS PANE AS THE REPORT DOES. It is the report's
   columns and a Cat column the report does not draw, fifteen in the pane the
   report fits fourteen into (section 9, THE REPORT FITS ITS PANE), and at 1440
   the fifteenth left it 9px wider than the pane: "% per", "SMV Total" and
   "26.77" cut at the pane's edge mid-glyph, on the page that documents the
   rule (the second round of the audit, 2026-09-29). Its Description is a rem
   narrower than the report's, which its one-word descriptions do not notice,
   and gives the Cat column back. On a phone it is the report's own 8rem
   (below). The property rather than a width on the cell, because the sticky
   Description's offset is read from the same one (section 1). (0,2,0),
   against `:root`'s (0,1,0); the row-states table is in a section too, and
   fits with a rem to spare either way. */
.sg-section .report-table {
  --col-desc-w: 10rem;
}

.sg-toc {
  columns: 2 12rem;
  gap: 1.5rem;
  margin: 0 0 2rem;
  padding-left: 1.1rem;
}

/* ------------------------------------------------------------------ */
@media (max-width: 640px) {
  main {
    padding: 1rem 0.75rem;
  }

  .card {
    padding: 1rem;
  }

  .site-header > .bar {
    padding: 0.5rem 0.75rem;
  }

  .nav {
    width: 100%;
    margin-left: 0;
  }

  /* 8rem on a phone, 10rem until 2026-09-29: El and Description are stuck to
     the pane's left edge, and at 10rem they held 233 of a 364px pane — the
     figures scrolled through a strip a third of the screen wide. The
     styleguide's specimens, which state their own at desktop width (above),
     are the report's on a phone: the same selector, later, inside the query. */
  :root {
    --col-desc-w: 8rem;
  }

  .sg-section .report-table {
    --col-desc-w: 8rem;
  }
}

/* ------------------------------------------------------------------ 13 */
/* MODE-DEPENDENT CONTROLS — and the first use of `:has()` in this project.
   Read this before adding another rule to the block.

   THE RULE THESE IMPLEMENT.  Never offer a control the current mode will
   discard; if one arrives anyway, say so.  Michael, 2026-08-24, about the
   Fixed count box sitting beside a cycle quantity that counts itself:

       This is super confusing and prompts err from user when selecting a
       cycle that auto counts and been allowed to input a manual count value.

   THE MODE CONTROL IS ALWAYS THE `<select>` OR RADIO ON THE SAME FORM, never
   the saved row behind it, and that is a correction rather than a starting
   position.  The first version of this work decided the stored modes — a
   quantity's type, an element's type — server-side, on the grounds that a view
   which never renders the control is the honest mechanism.  Driving it settled
   it the other way.  Off the SAVED type, converting a Manual quantity to an
   observation count costs two saves instead of one, and the state between them
   is a live quantity resolving to 0 on every study; an Ineffective element
   cannot be re-typed and auto-rated in one pass at all.  Worse, it left the
   same control gated two ways in one file — the create screen here, the edit
   screen in Jinja — which is the drift this project keeps paying for.  One
   mechanism, one place, and it is this one.

   Server-side gating survives in exactly one instance, the contingency waiver
   on the issue screen, where the mode is not on the form at all: whether the
   guard fired is computed from the report, not chosen by the analyst.

   EVERY RULE IN THIS BLOCK IS A HIDE-RULE, and that is not a style choice.
   A browser without `:has()` support drops the whole selector, so the rule
   never matches and the control stays exactly as visible as it is today.
   Written the other way round — hidden by default, revealed by `:has()` — the
   same browser would leave the control permanently hidden, which is a worse
   failure than the one being fixed.  So: default visible, `:has()` hides.  No
   `@supports` guard is needed, and none should be added, because the fallback
   is the pre-existing behaviour by construction.  The `.mode-note` rules are
   the mirror image and safe for the same reason: an unsupported browser simply
   never shows a sentence that did not exist before either.

   WHICH BROWSERS LAND ON THE FALLBACK, since arguing about it at this length
   without naming it is half an argument.  `:has()` shipped in Safari 15.4
   (March 2022), Chrome and Edge 105 (August 2022) and Firefox 121 (December
   2023).  Firefox is the binding constraint and it is the one to think about
   here: Firefox ESR 115 has no `:has()` at all, and an ESR is exactly what a
   managed corporate desktop is pinned to.  So the population that sees every
   control at once is real, nameable, and probably at GXO.  What they get is
   this application as it shipped before this block existed, plus the server
   half below — never a broken screen, and never a silent discard.

   A CONTROL THAT ALREADY HOLDS A VALUE IS NOT GATED.  `studies/observation.html`
   leaves the class off a line that carries a rating and `projects/element.html`
   off an element that carries an auto rating, because a refusal must have a
   visible control to point at.  A value that can be neither used nor removed
   is a worse trap than a control that should not have been offered, and on a
   create form — where a rejected submission is re-rendered with what was typed
   — hiding it makes the refusal permanent.

   AND THE SERVER STILL CHECKS.  Hiding a control does not stop a crafted POST
   and a hidden input still submits its value, so every one of these has a
   handler that refuses the value or clears it and names the drop.  Nothing
   here is a guard; it is the courtesy in front of one.

   THE HANDLER MUST ALSO KNOW WHAT THE FORM WAS DRAWN WITH.  Three of these
   notices had to learn it the hard way: a hidden input keeps submitting the
   value the screen rendered, so "the analyst typed this" and "the form carried
   this back" look identical unless the handler compares against the stored
   state.  Told apart, they are silence and a sentence; not told apart, every
   ordinary mode change earns a red error blaming the analyst.  See
   `reports._is_the_drawn_value`, `allowances._note_dropped_variation` and
   `imports._note_discarded_fields`. */

/* The whole-study element change. Ticking "All lines" makes the range
   meaningless, so the range goes — a hide-rule like every other one in this
   block, default visible, so a browser without `:has()` simply shows both and
   the handler, which checks the box first, still does what was asked.
   Specificity (0,2,1) — the pseudo-class counts as a class — against nothing
   competing: `.range-fields` has no other rule. */
form:has(input[name="all_lines"]:checked) .range-fields {
  display: none;
}

/* The marker classes.  `.mode-only` wraps a control some modes hide;
   `.mode-note` is the sentence that takes its place, drawn only in the mode
   that hides it.  They carry no appearance of their own — the per-instance
   selectors below are what switch them — so that grepping either name finds
   every mode-dependent control in the application.

   WHEN A GATED CONTROL NEEDS A `.mode-note` AND WHEN IT DOES NOT, because
   answering that per instance is how two screens end up disagreeing.  It needs
   one whenever the control that hid it is not the adjacent sibling — Auto
   rating is three fields below the Type select, the member picker is a
   twenty-row table whose absence is a visible hole, and the import review's
   radio is in a far column of the same row.  It does not when the mode control
   sits right there and says so itself: the Fixed count box is immediately
   right of the select that hides it, which is Michael's own description of
   what he wanted ("Only when a cycle quantity that requires manual input ...
   we should see the fixed count input field on the right from it"), and a
   sentence in its place would be the thing he asked to have removed. */
.mode-note {
  display: none;
  color: var(--muted);
  font-size: 0.85rem;
}

/* THE CYCLE, on reports/new.html and the report toolbar.  "Fixed count" means
   something under `— a fixed count —` and nothing under any other option, so
   it goes when any other option is current.  `:not([value="fixed"])` rather
   than a list of the alternatives: the alternatives are this project's
   quantities and there is no way to enumerate them in a stylesheet. */
.cycle-mode:has(select[name="quantity"] option:checked:not([value="fixed"])) .mode-only {
  display: none;
}

/* Layout, not gating. Wrapping the select and its Fixed count box in one
   element is what gives `:has()` something to scope to, and on the toolbar
   that element became a single flex ITEM of `.toolbar-line` — so the two
   controls it holds lost the 1rem gap they used to have as items in their own
   right. This gives it back, with `.toolbar-line`'s own values so the row
   cannot drift from the rows above and below it. On reports/new.html the same
   class sits on a `.form-inline`, which already lays its fields out, so the
   rule is scoped to the toolbar.

   ON THE BASELINE, since 2026-09-29, as the line is (section 10): the group
   holds the "= N" the choice resolves to as well now, a line of text beside
   the select, and centred it stood 0.45px off the label's text line. The
   select and the Fixed count box are one height, so they line up either way.
   (0,2,0), against nothing. */
.toolbar .cycle-mode {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.4rem 1rem;
  align-items: baseline;
}

/* The inert-settings note is a line of the toolbar, so it takes a whole one
   rather than tucking in beside the Apply button, and it carries no paragraph
   margin inside a flex row that spaces its own children. */
.toolbar > .mode-note {
  flex-basis: 100%;
  margin: 0;
}

/* The same select, at the other end of the same form: with no cycle quantity
   at all there is no denominator.

   It marked dormant settings once — three of them, then Frequency alone after
   migration 0021 retired the SMV method and 0017 the ignored-occurrence rule.
   Migration 0022 retired Frequency too, and a retired setting is the opposite
   treatment: it has no mode in which it would ever go live, so its box is
   removed rather than marked and the note may not name it.  Nothing dormant is
   left, so the sentence this rule reveals is now about the CONSEQUENCE of
   choosing no cycle quantity — BM/Cycle degrades to BM/Obs and the
   Observations/Quantity column comes out blank down its whole length.  Not
   "the only way to reach a blank one", which an earlier draft of this comment
   claimed and which the report table falsifies on every report it draws: an
   Ineffective, Contingency or Foreign row has no standard time and so no ratio
   to print.

   So this is the one `.mode-note` that does not stand in for a hidden control.
   It keeps the class because it is still exactly what the class is for
   mechanically — a sentence drawn only in one mode of the select above it —
   and because grepping `mode-note` must keep finding every mode-dependent
   sentence in the application, which is the whole reason the marker classes
   carry no appearance of their own. */
.cycle-host:has(select[name="quantity"] option:checked[value=""]) .mode-note-nocycle {
  display: revert;
}

/* AUTO RATING, on the element form — create and edit alike.  Ineffective time
   is unrated by definition, so an automatic rating on it is a contradiction
   the calculation is bound to ignore and `auto_rating_field` refuses.  The
   refusal stays; this stops the analyst reaching the refusal.  The view leaves
   the class OFF an element that already carries an auto rating, so the one
   screen that can show the refusal always shows the control that answers it. */
.element-mode:has(select[name="type"] option:checked[value="ineffective"]) .mode-only-rated {
  display: none;
}

.element-mode:has(select[name="type"] option:checked[value="ineffective"]) .mode-note-rated {
  display: revert;
}

/* THE MEMBER PICKER, on both quantity screens.  The member set is the cycle
   definition of an observation count and means nothing on any other type.  Off
   the Type SELECT rather than the saved row, so that converting a Manual
   quantity into an observation count is still one save — see the note at the
   top of this section for what the other way cost. */
.quantity-mode:has(select[name="type"] option:checked:not([value="observation_count"])) .mode-only-members {
  display: none;
}

.quantity-mode:has(select[name="type"] option:checked:not([value="observation_count"])) .mode-note-members {
  display: revert;
}

/* THE RATING, on the observation line editor.  The mode is the element the
   line reads as, which is the select immediately above — so each option
   carries its element's type and the rule reads it off the chosen one.  The
   view leaves this class OFF whenever the line already carries a rating, so a
   line that needs correcting always shows the box that corrects it. */
.line-mode:has(select[name="element_id"] option:checked[data-el-type="ineffective"]) .mode-only-rating {
  display: none;
}

/* ARGUED FROM, on the relaxation guide.  The provenance select is subordinate
   to the percentage stacked above it: with no percentage there is no row to
   hang a justification on.  `:placeholder-shown` is how a stylesheet asks
   whether a text box is empty, and it needs the placeholder the box already
   carries — "none" since 2026-09-29, when the box was sized to a percentage
   and "leave blank if it does not apply" moved to its hint. Any placeholder
   will do; no placeholder at all and the box is never `:placeholder-shown`,
   so the select would never hide. */
.stack:has(input[name^="pct_"]:placeholder-shown) .mode-only-argued {
  display: none;
}

/* THE IMPORT REVIEW ROW.  Discarding a code creates no element, so the
   description, type and category it would have been created with are not
   read.  The note takes their place in the same cells rather than leaving
   three blanks that read as a rendering fault. */
tr:has(input[name^="decision::"][value="discard"]:checked) .mode-only-create {
  display: none;
}

tr:has(input[name^="decision::"][value="discard"]:checked) .mode-note-discard {
  display: revert;
}

/* ------------------------------------------------------------------ 14 */
/* ALLOWANCES AND CONVENTIONS — in line, every field sized by what it holds.

   "One row, every field ALIKE" is what this heading said, and the second half
   of it was the mistake — alike is what an equal grid track gives you, and it
   is the last thing the three controls it then held wanted.  What Michael asked for was them
   IN LINE with each other.  Same line; not the same width.

   Michael asked in August for the widest control, Allowance mode, to take its
   own column and be centred against the rest.  That was right for the block as
   it stood: SEVEN controls, and its options spell out the whole formula
   ("Additive — BM/Cycle × (1 + RA% + CA%)"), which is the distinction the
   vendor's own documentation blurs and is worth its width.

   Migration 0017 retired the two ignore controls, 0021 the SMV method and 0022
   the Frequency.  What was left fitted a row easily, and he asked for the split
   to go: "I made a mistake there before... please revert that and put it in
   line with all the others."
   The split was not reverted because it was wrong — it was reverted because
   the crowding it answered no longer existed.

   AND ON 2026-09-28 THE WIDE CONTROL ITSELF WENT.  Michael: "The allowance mode
   will be decomisioned completely from the entire TMDCS app, we will be using
   only the 'additive' model in the system."  Migration 0027.  The block holds
   Policy CA % and Precision, and nothing below needed changing for it: the row
   is count-free, which is the point of it.  The Allowance-mode select is
   still the worked example in what follows, in the past tense, because it is
   what taught each rule.

   AND THEN THE ROW WAS A MESS ANYWAY.  Michael, 2026-08-29:

       new.html has a mess with the layout of the Allowances and conventions

   `repeat(auto-fit, minmax(12rem, 1fr))` gave three EQUAL tracks, and equal was
   the one thing those three controls were not.  A `<select>` is `width: auto`
   (section 6), which for a select means the width of its widest option — so the
   Allowance-mode box asked for all 37 characters of "Additive — BM/Cycle × (1 +
   RA% + CA%)" and a third of the card is not that, on any card this application
   is read on.  `min-width: 0` below, which is there so the grid cannot be
   forced wider than its card, then let the select OVERFLOW its track and paint
   across the Policy CA % field beside it.  At the same moment Precision, whose
   widest option is four characters ("6 dp"), was handed exactly as much room.  The formula the
   block exists to spell out was the one thing being cut off.

   So the tracks go.  A wrapping flex row asks each field for the width its own
   control needs, hands nothing to a field that has not asked for it, and breaks
   to a second line when the card runs out.  That last part is the whole of what
   `auto-fit` was chosen for — one line on a wide screen, two on a narrow one,
   with no breakpoint deciding it — and it is kept, without a track anywhere
   pretending three unlike controls are alike.

   AND YES, THIS IS THE `form-inline` SHAPE THIS BLOCK WAS BUILT TO GET AWAY
   FROM.  The comment on reports/new.html still says so: a `form-inline`
   "wrapped them at whatever point the widest select happened to fall, which is
   why no two of these blocks looked alike".  That was true of SEVEN controls
   whose widths nothing bounded.  What made the wrapping unpredictable was not
   flex; it was that a field's width was set by the longest SENTENCE in it — see
   the `.field-hint` cap below, which is the part `form-inline` never had.

   AND A WRAPPED LINE IS A FIELD'S DISTANCE DOWN, since 2026-09-29, for the
   reason `.form-inline` is (section 6, A ROW OF FIELDS THAT WRAPS IS SPACED
   LIKE A COLUMN OF FIELDS): on a phone Precision wraps under Policy CA %, and
   0.75rem put its label 12px under the hint above it where every other field
   in the form is 28px from the next. */
.conventions {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1.75rem 1.5rem;
}

/* `flex: 0 1 auto` in full, rather than left to the defaults, because all three
   parts are load-bearing and one of them is not the default: basis `auto` reads
   the field's own content width, grow `0` is what stops a two-character
   Precision box being inflated to a third of the row all over again, and shrink
   `1` is the last resort.

   Last resort and not the ordinary case, which is the part worth knowing:
   wrapping is decided FIRST, from the fields' own widths, so a card that cannot
   hold all three breaks the row rather than squeezing anything — nothing is
   truncated to make a line fit.  Shrink only engages on a line holding a single
   field already wider than the card, which is the phone, and there squeezing is
   exactly what is wanted.  `min-width: 0` stays for that same case: a flex item
   refuses by default to go below its content, and the Allowance-mode select's
   content was wider than a phone — the next wide control's will be too. */
.conventions .field {
  margin-bottom: 0;
  flex: 0 1 auto;
  min-width: 0;
}

/* A SENTENCE UNDER A CONTROL MAY NOT DECIDE THAT CONTROL'S WIDTH.  This is the
   rule the old `form-inline` was missing and the reason its wrapping looked
   random.  A field's flex basis is its max-content, and max-content takes the
   WIDEST child: Precision's hint is 103 characters on one unbroken line, so
   uncapped it is several times the select above it and the narrowest control in
   the block would be the one asking for the widest column — out of the room the
   Allowance-mode select needed while it stood here.
   Capped at a measure the hint's own type decides, `ch` being the width of a
   "0" at its 0.85rem, so the sentence wraps inside the column its control set
   instead of setting one.

   A COMPANION `.conventions .field-inline` CAP STOOD HERE FOR ONE DAY and is
   gone with the class that needed it.  Policy CA carried `field-inline`, which
   made a flex container of the field, and there `.field-inline .field-hint`'s
   `flex-basis: 100%` was the whole of what put the hint on its own line: flex
   breaks on an item's HYPOTHETICAL main size, so capping the hint alone
   stopped it forcing the break and landed it beside the box at 484px.  The cap
   on the field gave that `100%` something bounded to be 100% OF.
   Michael moved the label back above the box on 2026-08-30 — "this is to mach
   all the other filed on this page" — so the field is a plain block again,
   nothing resolves a percentage against it, and the pair has nothing to act
   on.  The hint cap below is NOT part of that pair and stays: max-content
   takes the widest child whatever the display mode is. */
.conventions .field-hint {
  max-width: 32ch;
}

/* A select is worth its width but not worth the field beside it.  Section 6
   leaves selects at `width: auto`, which is right on a wide card and is how the
   Allowance-mode box overflowed a narrow one before migration 0027 retired it;
   this caps every select in the block at its column and lets the browser
   truncate the option text, which is the failure that keeps the damage inside
   one field.  It stays with Precision as the only select left, because the
   rule is about the block and not about the control that taught it.
   Specificity (0,2,1) — two classes, one type — against section 6's (0,0,1). */
.conventions .field > select {
  max-width: 100%;
}

.report-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.6rem;
  margin-bottom: 1.25rem;
}

/* A field whose content is a couple of digits should not be as wide as its
   column. Inputs are block-level here and stretch, so a percentage box ended up
   the width of an Allowance-mode select.

   THE CLASS ON ITS OWN COULD NEVER HAVE WON, WHICH IS WHY THIS SURVIVED TWO
   FIXES.  Michael, 2026-08-29, who diagnosed it himself and was exactly right:

       Policy CA%, the input filed is under the label deu to been max width
       id=f-ca in css: width: 100%

   `input[type="text"]` in section 6 is (0,1,1): one attribute selector, one
   type.  A bare `.input-narrow` is (0,1,0).  The class count ties at one and
   the element count then decides, so the class lost regardless of which rule
   came last in the file, and `width: 100%` stood.

   WHAT THAT ACTUALLY RENDERED, measured in Chrome 2026-08-30 rather than
   reasoned from the cascade, because the two are not the same thing here.
   `.input-narrow` declared TWO properties and only one of them lost: the
   `width` lost the specificity contest, but its `max-width: 8rem` had no
   opponent at all and applied.  So the used width on this screen was
   min(386px, 128px) = 128px and the box sat beside its label the whole time —
   a CEILING doing the work of a size, 25px wider than the six characters it
   holds, and 12 rendered viewport widths from 360 to 1440 put it below its
   label in only two of them.  The screen that went full width and dropped
   under its label is the toolbar's box on reports/report.html, which carries
   no class and therefore had no ceiling either; that one is cured by the
   `.toolbar` rule in section 10, not by this.  Qualifying the class here is
   still right — 128px was never a chosen width and the `width` declaration
   was never doing anything — but it is a tidying of this screen and not the
   cure for the symptom Michael reported.

   Qualifying the class with the very selector it has to beat makes this
   (0,2,1) — one attribute, one class, one type — and that is a strict win
   rather than a tie settled by source order.  A tie would come apart the day
   somebody moves section 6 below section 14, and this bug has already had two
   attempts spent on it.

   Text inputs only, and deliberately: the class is on exactly one control in
   the whole application, `#f-ca` on reports/new.html.  A selector list covering
   types nobody uses it on is CSS that nobody can ever delete with confidence.
   If it is wanted on a `select`, add that compound here rather than assuming
   the class works — on a select the bare class changes nothing visible, since
   `select { width: auto }` in section 6 has already given it that width.

   AND THE CONTEST ABOVE IS NOW HISTORY, 2026-08-30: section 6's width default
   is `:where(…)` at (0,0,0), so a bare `.input-narrow` would win it today.
   The compound stays.  It is still the strict win the paragraph above asks
   for, `tests.test_layout` asserts the winning selector is more than (0,1,1),
   and nothing about the rendered page changes either way — so unqualifying it
   would be an edit made to shorten a selector, on the one declaration in this
   sheet that has already cost two attempts. */
input[type="text"].input-narrow {
  width: auto;
  max-width: 8rem;
}

/* The report head's gaps were correct from the day they shipped — what was
   wrong is that nobody could see them. nginx serves /static/ with
   `max-age=3600` and the stylesheet was linked unversioned, so a returning
   browser rendered new markup against an hour-old stylesheet.
   `_version_static_assets` in app/web/__init__.py now appends the file's own
   content hash, which is why this comment is here and not a third attempt at
   the spacing. */
.report-head + .toolbar {
  margin-top: 0;
}

/* A field whose control is a few characters wide puts its label beside it, not
   above.  `.form-label` is `display: block` for every other field, which is
   right when the control is a wide select and wasteful when it is a percentage
   box — it spends a whole row on a label and pushes the next control down.

   It was written for the Policy CA % box and Michael moved that label back
   above its box on 2026-08-30 ("this is to mach all the other filed on this
   page"), so what carries the class today is the Log's filter bar: four
   fields, on one line, each reading as a phrase.  `display: flex` is what puts
   a label and its box on one line at all; `align-items: baseline` is what sits
   them on the same one rather than at two heights; `flex-wrap: wrap` is the
   escape at a viewport too narrow for both. */
.field-inline {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.field-inline .form-label {
  display: inline;
  margin-bottom: 0;
  white-space: nowrap;
}

/* AND THE CONTROL SIZES ITSELF, which is the half the class never had.
 *
 * A control at `width: 100%` cannot sit beside anything: `.field-inline` is a
 * flex container and flex decides where a line breaks from each item's
 * HYPOTHETICAL main size, which for a definite `width` is that width.  100% of
 * the field leaves no room for the label on the same line, `flex-wrap: wrap`
 * puts the box on a line of its own, and the label above it reads as a heading
 * — the exact arrangement this class exists to prevent.  Michael, 2026-08-30,
 * on the Log:
 *
 *     the from and to labels, please be consistent and place them on the left
 *     side of the input fileds they link to not on top, all other once in line
 *     are on the left.
 *
 * WHY ONLY TWO OF THE FOUR FILTERS SHOWED IT, since a rule aimed at the two
 * would be the fourth instance patch in a row.  Actor is `.toolbar
 * input[type="text"]` (section 10) and Action is a `select`, and both of those
 * were already at `width: auto` for reasons that have nothing to do with this
 * class.  From and To are `input[type="date"]`, which no rule in the sheet had
 * ever named.  Nothing about `.field-inline` was working; two of its four
 * instances were being rescued by rules written for other screens.
 *
 * Every control an inline field can hold, therefore, and not the one type that
 * was reported.  `width: auto` on a text box is its `size` attribute, on a
 * select its widest option, on a date box the browser's own date editor —
 * which is what "sized by what it holds" has meant everywhere else in this
 * sheet since section 10.
 *
 * Specificity (0,1,1) — one class, one type — against section 6's `:where(…)`
 * default at (0,0,0).  A STRICT win, and it is strict only because section 6
 * was lowered in the same revision: against the old (0,1,1) this would have
 * been a tie settled by section 14 sitting below section 6 in the file, which
 * is the failure `input.input-narrow` two rules up was written to avoid and
 * which `tests.test_layout` reverses the whole stylesheet to check for.
 *
 * No `max-width` here.  The only control in an inline field that can grow
 * without bound is a `select` with a long option, and that is true of every
 * select in the application since section 6 — not something this rule
 * introduces, and not something to cure on four fields. */
.field-inline input,
.field-inline select,
.field-inline textarea {
  width: auto;
}

/* EXCEPT A DATE BOX, which states its width, since the second round of the
 * audit (2026-09-29) — for iPhone Safari, which cannot be run here and was
 * read from WebKit's source instead. On iOS a date box is drawn as a
 * drop-down button, and when its width is not a fixed length WebKit gives it
 * a minimum width and `box-sizing: content-box` AFTER the cascade, so section
 * 6's one-line height becomes the height of its content: 34.39px, plus the
 * 0.4rem of padding above and below and the border, 49px — the Log's From
 * and To boxes half as tall again as the Actor box and the Filter button
 * beside them, the "input fields been weird dimentions" Michael asked to be
 * rid of. With a fixed width WebKit leaves the box alone. 11rem is 176px, a
 * little wider than Chromium's own 162px date box, and the Log's bar still
 * takes one line at 1440 until a filter is applied — then Filter and Clear
 * take the second, together, as they did before by 14px less (Clear alone).
 * (0,2,1) against the rule above's (0,1,1). */
.field-inline input[type="date"] {
  width: 11rem;
}

.field-inline .field-hint {
  flex-basis: 100%;
  margin-top: 0.15rem;
}

/* The Log's before/after images. Scoped to .log-image rather than styling bare
   <details>, exactly as .manual-key is and for the same reason: the report and
   study-grid screens use unstyled <details> for their study pickers, and
   restyling those from here would be a change to two screens nobody asked to
   change.

   The max-width is load-bearing rather than cosmetic. An image is JSON in a
   <pre>, the <pre> is inside a table cell, and a cell has no width of its own:
   one row with a long study name in it stretched the whole table sideways and
   pushed When, Actor and Action off the side of a 1280px screen. The image
   scrolls inside its own box instead, and the columns somebody is scanning
   stay where they were. */
.log-image {
  max-width: 24rem;
}

/* "2 fields" is one line on a phone, where the table scrolls anyway (section
   9, ON A PHONE A TABLE SCROLLS, which holds it there with the usernames). It
   was one line at every width for a day, since the column squeezed it to "▶
   2" over "fields" at 1024 (2026-09-29) — and held whole it widened Before and
   After enough to push the Log past its pane at 1024 and past it by 32px at
   1440 on the production copy, where "▸ 2 fie" was cut at the pane's edge. A
   summary on two lines in a table that fits is the lesser fault. */
.log-image > summary {
  cursor: pointer;
  color: var(--accent);
  font-size: 0.85rem;
}

.log-image pre {
  margin: 0.4rem 0 0;
  font-size: 0.78rem;
  max-height: 18rem;
  overflow: auto;
}

/* ----------------------------------------------------------------- 15 */
/* THE PROJECT LIST'S YEAR GROUPS.  One `<details>` per year of creation, every
 * one of them shut on arrival, so the whole archive READS as a column of
 * summary lines and a table is laid out only where somebody opened one.
 *
 * WHAT IT COMPACTS IS WHAT IS SHOWN, NOT WHAT IS SENT.  `<details>` hides its
 * contents; it does not defer them.  Every row of every year is in the
 * document whichever groups are shut — measured at 394 projects across
 * sixteen years, 90.6% of the page is inside a closed group — and the grouped
 * page is about 4.7% LARGER than the flat one it replaces, because fifteen
 * extra `<thead>` blocks and fifteen extra group openers are the price of the
 * cut.  That is the right trade for a screen whose problem is sixteen years
 * of rows in one column, and it is worth writing down rather than implying a
 * saving that is not there: the useful consequence is that in-page find still
 * reaches a project inside a shut year in any browser that expands one to
 * show a match.  The lever for a list this control can no longer help is
 * pagination, not a second expander.
 *
 * Scoped to `.year-group` rather than styling bare `<details>`, exactly as
 * `.manual-key` (section 11) and `.log-image` are and for the same reason: the
 * report toolbar and the study-grid picker are unstyled `<details>`, and
 * restyling those from here would be a change to two screens nobody asked to
 * change.
 *
 * THE MEASUREMENT THAT SIZES THIS.  A summary line is one 15px/1.5 line box
 * (22.5px) plus 0.45rem of padding above and below (14.4px) = 37px.  The
 * archive import lands projects dated 2011 to 2026 — sixteen calendar years,
 * about fourteen of them with a project in — so the ceiling is sixteen lines
 * and fifteen 0.5rem seams: 16 x 37 + 15 x 8 = 712px, one screen with nothing
 * expanded.  That is the whole point of the control, and a taller line or a
 * wider seam is what spends it.
 *
 * SPECIFICITY.  `.year-group > summary` is (0,1,1) — one class, one type —
 * against nothing whatever: no rule in this sheet names a bare `summary`, and
 * the two that name one are scoped to their own class.  Nothing here is a tie
 * settled by source order.
 *
 * The summary is deliberately NOT `color: var(--accent)`, which is what
 * `.manual-key` and `.log-image` do with theirs.  Those are one incidental
 * disclosure apiece on a screen full of other things; these are a column of
 * consecutive lines carrying every route into the application, and a column of
 * accent-coloured lines reads as a column of links.  The affordance is
 * the disclosure marker and the pointer; 600 is the weight `thead th` already
 * uses to head a table. */
.year-group + .year-group {
  margin-top: 0.5rem;
}

.year-group > summary {
  cursor: pointer;
  padding: 0.45rem 0.25rem;
  font-weight: 600;
}

/* Open, the summary stops being a line in a list and becomes a heading over
 * its own table.  0.5rem is what `.manual-key[open] > summary` puts between the
 * two, for the same reason and to the same number. */
.year-group[open] > summary {
  margin-bottom: 0.5rem;
}
