/*
 * Medworx — app-only additions.
 *
 * style.css is the locked Phase 2C design system and the source of truth for
 * tokens, type, spacing and primitives. Anything here is a component the
 * wireframes did not cover (sign-in screen, flash messages, form fields).
 * Always reach for an existing token — never a raw colour.
 */

/* ────────────────────────────────────────────────
   `hidden` has to win
   ────────────────────────────────────────────────
   The browser's own [hidden] { display: none } is a user-agent rule, so ANY
   author rule that sets display beats it. `.field` is display:flex, so the
   failure-reason field would have stayed on screen while the attribute said it
   was hidden — the markup and the page disagreeing, silently.

   This is the third instance (see .combobox-list[hidden] below, and the
   section-toggle divs that only work because they carry no class at all), so
   it is fixed once, here, rather than per component. */
[hidden] { display: none !important; }

/* ────────────────────────────────────────────────
   Flash messages
   ──────────────────────────────────────────────── */
.flash-stack {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 16px;
}
.flash {
  font-size: 14px;
  padding: 9px 14px;
  border-radius: var(--radius-md);
  border: 0.5px solid var(--border-tertiary);
}
.flash-info {
  background: var(--bg-info);
  border-color: var(--border-info);
  color: var(--text-info-dark);
}
.flash-danger {
  background: var(--bg-danger);
  border-color: var(--border-danger);
  color: var(--text-danger);
}

/* ────────────────────────────────────────────────
   Form fields
   ──────────────────────────────────────────────── */
.field {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-bottom: 14px;
}
.field-label {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-secondary);
}
/* Form controls are styled here by CLASS, deliberately.
 *
 * style.css:279 styles them by enumerating input types — text, search, email,
 * number, plus textarea and select. Every type it does not name gets no
 * font-size, padding, border, radius, background or 36px height and falls back
 * to the browser default. Two were missed: `tel` (the client and site forms'
 * phone boxes) and `password` (the sign-in box, which therefore did not match
 * the email box directly above it). Joel found both by eye, unprompted, on the
 * 2026-08-11 click-through.
 *
 * The fix is the class rather than the two types. `.field-input` is already on
 * every one of these fields, so declaring here covers whatever helper a future
 * chunk renders — Chunk 10's booking `date_field` would have been the third
 * miss. Do not "fix" this by adding types to the list in style.css: that is the
 * locked token file (decision #69), and the next new type would be missed
 * again. Enforced by spec/requests/form_field_styling_spec.rb, which fails if a
 * form control is rendered without the class.
 *
 * The values are copied from style.css's rule so the two agree exactly. Where
 * both apply (a text input carries the class and matches the type selector),
 * the attribute selector wins on specificity and sets the identical value.
 */
.field-input {
  width: 100%;
  font-family: inherit;
  font-size: 14px;
  padding: 8px 12px;
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
  background: var(--bg-primary);
  color: var(--text-primary);
  height: 36px;
  outline: none;
  transition: border-color 80ms ease, box-shadow 80ms ease;
}

/* A textarea grows with its content. style.css's own `textarea { height: auto }`
 * is a bare element selector and loses to the class above, so without this a
 * notes box collapses to a single 36px line. */
textarea.field-input {
  height: auto;
  min-height: 64px;
}

/* ────────────────────────────────────────────────
   Buttons

   Styled by CLASS, never by element or input type — the same correction made
   for form fields above, and for the same reason.

   style.css:302 styles `button { … }`: the *element*. Rails' `form.submit`
   renders `<input type="submit">`, which that selector never matches, so every
   submit in the app was falling back to browser default for height, padding,
   border, radius and font while its class supplied only colour. Joel found two
   on 2026-08-12 — the sign-in button, and the site search inside a client,
   which had no class at all and so was entirely unstyled. There were seven.

   .btn-geometry below is the shared control shape; the variants add only what
   makes them different. A new button class inherits the shape by joining this
   selector list, and a submit with no button class at all fails
   spec/requests/form_field_styling_spec.rb.
   ──────────────────────────────────────────────── */
.btn-primary,
.btn-action,
.btn-danger,
.btn-compact {
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  line-height: 1;
  height: 36px;
  padding: 0 18px;
  border: 0.5px solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background 80ms ease;
}

/* An <a> is not a <button>: it does not centre its own text, so the label sat
   in the top-left of the box with the height rule holding the box open. Joel
   found this on the asset screen's "Performance verification" button
   (2026-08-18). Scoped to anchors deliberately — display:flex on an <input>
   is not something every browser agrees about, and a real button already
   centres. */
a.btn-primary,
a.btn-action,
a.btn-danger,
a.btn-compact {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-decoration: none;
}

/* The larger variant, for a screen whose whole purpose is the button you press
   next — the asset dashboard's "Start a new report" row, used on a tablet. */
.btn-lg {
  height: 44px;
  padding: 0 24px;
  font-size: 15px;
}

/* Full-width by default because its home is the sign-in card, which has one
   action. .btn-inline narrows it for a form with several side by side. */
.btn-primary {
  width: 100%;
  background: var(--brand-red);
  border-color: var(--brand-red);
  color: #fff;
  margin-top: 4px;
}
.btn-primary:hover { background: var(--brand-red-dark); }

/* A button that reads as a link — used for sign out, which must be a DELETE
   rather than a plain anchor. */
.btn-link {
  background: none;
  border: none;
  padding: 0;
  height: auto;
  font-size: 13px;
  color: var(--text-info);
  cursor: pointer;
}
.btn-link:hover { background: none; text-decoration: underline; }

/* button_to wraps its button in a form; keep it on the same line as the name. */
.app-header .user { display: flex; align-items: center; gap: 12px; }
.app-header .user form { margin: 0; }

/* Wordmark plus the always-available way back to the search screen. */
.app-header .brand-group {
  display: flex;
  align-items: center;
  gap: 14px;
}
.app-header .brand {
  color: var(--text-primary);
  text-decoration: none;
}
.app-header .brand:hover { color: var(--brand-red); }

/* ────────────────────────────────────────────────
   .btn-compact — the size modifier, and the replacement for `.btn-nav`.

   ⚠ `.btn-nav` IS DELETED, NOT RECOLOURED, AND THAT IS THE POINT. It was the
   quiet grey outline control — 26px, 12px text, --text-secondary on white — and
   it carried roughly thirty of this app's buttons: Search and Templates in the
   header, Edit client, Edit site, Edit asset, + Add site, every Edit/Archive in
   a table row, and every Cancel on a form. Joel, round 7:

     "lets make all the buttons blue with white writing, other than ones which
      confirm changes… a lot of buttons are small and greyed out, we need to
      make them consistent across the app and i like the blue with white"

   Recolouring `.btn-nav` would have left a class whose name says "navigation"
   wearing the colour #143 gives to any button to be pressed, and left the
   existing guard in message_and_button_styling_spec.rb — which fails on ANY
   `.btn-nav` on a report screen — asserting something meaningless. So the class
   is gone from the stylesheet and from every call site, the guard now walks the
   whole app, and a button reintroduced as grey fails the build.

   What remains is SIZE, not colour: this is the same blue `.btn-action`, one
   notch smaller, for a table row action or the header chrome, where a 36px
   control would set the row height. Colour comes from `.btn-action` beside it.
   ──────────────────────────────────────────────── */
.btn-compact {
  height: 30px;
  padding: 0 13px;
  font-size: 13px;
}

/* ────────────────────────────────────────────────
   Search screen (wireframe screen #20)

   Promoted verbatim from the inlined block in wireframes/search-results.html,
   per that file's "inline first, promote if reused" note. Keep the two in sync
   if the wireframe is revised.
   ──────────────────────────────────────────────── */
.search-row {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  padding: 16px 18px;
  margin-bottom: 14px;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr auto;
  gap: 10px;
  align-items: end;
}
.search-row label {
  display: block;
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 4px;
}
.search-row input[type="text"] {
  width: 100%;
  height: 36px;
  padding: 0 12px;
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  background: var(--bg-primary);
  font: 400 14px var(--font-sans);
  color: var(--text-primary);
}
.search-row input[type="text"]:focus {
  outline: none;
  border-color: var(--border-info);
  box-shadow: 0 0 0 3px var(--bg-info);
}
/* The Search button is .btn-action like every other blue action button. It used
   to be styled here by input type, which is why the identical button inside a
   client — the site search — was unstyled: it is the same control, and a
   type-based selector could not carry the style to it. */

/* Single-column on a phone — repair reporting (Chunk 20) is phone-targeted and
   techs will reach the search from the same device. */
@media (max-width: 640px) {
  .search-row { grid-template-columns: 1fr; }
}

.result-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  padding: 0 4px;
  margin-bottom: 10px;
  font-size: 13px;
  color: var(--text-secondary);
}
.result-meta .count { color: var(--text-primary); font-weight: 500; }
.result-meta .query {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: var(--bg-secondary);
  padding: 1px 6px;
  border-radius: 4px;
}
.result-meta a { color: var(--text-info); text-decoration: none; }
.result-meta a:hover { text-decoration: underline; }

.results {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.results-head,
.result-row-data {
  display: grid;
  grid-template-columns: 90px 130px 1fr 1fr 110px 90px;
  gap: 14px;
  padding: 14px 18px;
  align-items: center;
}
/* Joel, 2026-08-05: the header row was grey-on-grey and easy to miss. Darker,
   heavier and letter-spaced so the columns read as headings at a glance. */
.results-head {
  padding: 10px 18px;
  background: var(--bg-secondary);
  border-bottom: 0.5px solid var(--border-tertiary);
  font-size: 11px;
  color: var(--text-primary);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.result-row-data {
  border-bottom: 0.5px solid var(--border-tertiary);
  cursor: pointer;
  transition: background 80ms ease;
  font-size: 13px;
  text-decoration: none;
  color: inherit;
}
.result-row-data:last-child { border-bottom: none; }
.result-row-data:hover { background: var(--bg-secondary); }

/* ~12% dim for retired assets and archived clients (decisions #62, #82). */
.result-row-data.retired { opacity: 0.88; }

.result-row-data .asset-no {
  color: var(--text-primary);
  font-weight: 500;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
}
.result-row-data:hover .asset-no {
  text-decoration: underline;
  text-underline-offset: 3px;
}
.result-row-data .serial { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.result-row-data .serial .match,
.result-row-data .asset-no .match,
.result-row-data .org-site .match { font-weight: 700; }

.result-row-data .org-site .site {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: 2px;
}
.result-row-data .class-cell { font-size: 12px; color: var(--text-secondary); }

.results-footer {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px 4px 4px;
  font-size: 13px;
  color: var(--text-tertiary);
}

/* Neutral grey, not a status colour — archived is a client-level state, not an
   asset status, and must not read as one of the four status pills. */
.pill-archived {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--bg-muted);
  color: var(--text-tertiary);
  font-size: 11px;
  font-weight: 500;
  vertical-align: 1px;
}

/* #140 — a completed report whose certificate never reached R2, marked in the
   asset dashboard's report history so an admin can see it without opening every
   row. Same geometry as .pill-archived; amber rather than grey because this one
   asks for an action (open it and press Regenerate), where archived is simply a
   state. Deliberately NOT one of the four status pills — the report's status is
   genuinely "Complete", and this says something about its certificate. */
.pill-no-certificate {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--bg-warning);
  color: var(--text-warning);
  font-size: 11px;
  font-weight: 500;
  vertical-align: 1px;
}

/* "This report was filled as one job with another one from the same visit."
   Info blue rather than the warning amber above, because it is a plain fact
   about the record and asks for nothing — the pill beside it is a warning that
   wants an action, and the two must not read alike. Same geometry so the row
   stays level whichever of them is present. */
.pill-paired {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--bg-info);
  color: var(--text-info);
  font-size: 11px;
  font-weight: 500;
  vertical-align: 1px;
}

.search-empty {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  padding: 30px 24px;
  text-align: center;
  color: var(--text-secondary);
  font-size: 13px;
}
.search-empty-title {
  font-size: 15px;
  font-weight: 500;
  color: var(--text-primary);
  margin-bottom: 6px;
}
.search-empty p { margin: 0 auto; max-width: 46ch; }

/* ────────────────────────────────────────────────
   Register screens — shared bits
   (client overview, site overview, asset dashboard)
   ──────────────────────────────────────────────── */

/* Current page in a breadcrumb — a plain span, not a link to itself. */
.crumb .here { color: var(--text-primary); }

/* Screen-reader-only label for a column whose header is visually empty. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Archived client banner (decisions #47, #82, #85) — informational, not a
   warning. The client is still perfectly valid to look at. */
.archived-banner {
  background: var(--bg-muted);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  padding: 10px 14px;
  margin-bottom: 14px;
  font-size: 13px;
  color: var(--text-secondary);
}

/* Where a footer action would be, when there is a reason instead of a button
   (#105's site restore under an archived client). Sized to sit level with the
   buttons beside it rather than as body copy. */
.footer-note {
  margin: 0;
  align-self: center;
  font-size: 13px;
  color: var(--text-secondary);
}

/* #105's standing "restore the sites too" offer sits inside an archived-banner,
   so its button_to form must not break the sentence onto its own line. button_to
   renders a block-level <form>; without this the offer reads as two paragraphs
   with no connection between them. */
.archived-banner .inline-form {
  display: inline;
}

/* A disabled control is an affordance for something not yet built. Muted, not
   struck through — it says "not yet", not "forbidden". */
.footer-actions button[disabled],
.report-buttons button[disabled] {
  opacity: 0.5;
  cursor: not-allowed;
}
.status-edit.disabled,
.notes-block .edit.disabled {
  opacity: 0.5;
  cursor: not-allowed;
  text-decoration: none;
}

/* ────────────────────────────────────────────────
   Matching clients, above the asset rows (decision #87)
   ──────────────────────────────────────────────── */
.client-matches {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-bottom: 16px;
}
.client-matches-head {
  background: var(--bg-secondary);
  padding: 9px 14px;
  font-size: 11px;
  font-weight: 700;
  color: var(--text-primary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-bottom: 0.5px solid var(--border-tertiary);
}
.client-matches-head .more {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  color: var(--text-tertiary);
}
.client-match {
  display: grid;
  grid-template-columns: 1.6fr 1.4fr auto;
  column-gap: 16px;
  align-items: center;
  padding: 10px 14px;
  border-bottom: 0.5px solid var(--border-tertiary);
  font-size: 13px;
  text-decoration: none;
  transition: background 60ms ease;
}
.client-match:last-of-type { border-bottom: none; }
.client-match:hover { background: var(--bg-secondary); }
.client-match.retired { opacity: 0.88; }
.client-match .name { font-weight: 500; color: var(--text-primary); }
.client-match .name .match { font-weight: 700; }
.client-match .meta { color: var(--text-secondary); font-size: 12px; }
.client-match .open-link { color: var(--text-info); font-size: 12px; }

@media (max-width: 640px) {
  .client-match { grid-template-columns: 1fr; row-gap: 3px; }
  .client-match .open-link { display: none; }
}

/* ────────────────────────────────────────────────
   Typeahead dropdown (decisions #88, #89)
   ──────────────────────────────────────────────── */
.combobox { position: relative; }
.combobox-list {
  position: absolute;
  z-index: 20;
  top: 100%;
  left: 0;
  right: 0;
  margin: 4px 0 0;
  padding: 4px 0;
  list-style: none;
  background: var(--bg-primary);
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.10);
  max-height: 260px;
  overflow-y: auto;
  /* Wider than the field it hangs under, so the detail line has somewhere to
     go — a search field is a third of the row, and "serial · client — site"
     does not fit in that. Capped against the viewport so it cannot push off a
     tablet screen. */
  min-width: min(300px, 80vw);
}
.combobox-list[hidden] { display: none; }
.combobox-option {
  padding: 8px 12px;
  font-size: 13px;
  color: var(--text-primary);
  cursor: pointer;
}
.combobox-option:hover,
.combobox-option.active { background: var(--bg-secondary); }

/* Asset suggestions carry a second line (decision #100) — the other identifier
 * and where the machine is — because an asset number is not unique by design
 * (#65, #66) and two suggestions can otherwise read identically. Client and
 * site suggestions send no detail and stay a single line. */
.combobox-label { display: block; }

/* Retired / Archived client. Same grey pill the search results and client
   overview use, so the marker reads the same wherever it appears. On the top
   line beside the number, never on the end of the detail — the detail
   truncates to the field's width and would swallow it. */
.combobox-marker {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: var(--radius-pill);
  background: var(--bg-muted);
  color: var(--text-tertiary);
  font-size: 11px;
  font-weight: 500;
  vertical-align: 1px;
}
.combobox-detail {
  display: block;
  margin-top: 1px;
  font-size: 12px;
  color: var(--text-secondary);
  /* One line, because the dropdown sits over the results and a wrapping row
     would push the list past the fold on a tablet. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* style.css's .toolbar is a flex row that stretches `input` as a *direct*
   child. A real form has to wrap the field to submit it, which made the form
   the flex child instead and collapsed the search box to a stub. Make the form
   transparent to the layout so the field fills the row as the wireframe has it. */
.toolbar form {
  display: flex;
  flex: 1;
  gap: 8px;
  margin: 0;
}
.toolbar form .combobox { flex: 1; }
.toolbar form input[type="search"] { width: 100%; }

/* Notes are prose typed by a tech, never markup. They are rendered escaped —
   not through simple_format, which permits a subset of HTML — so line breaks
   have to be preserved here instead. */
.notes-block .body { white-space: pre-wrap; }

/* Asset notes reuse the shared yellow callout, with the edit affordance pulled
   to the right of the body (asset-dashboard wireframe). */
.notes-block.with-edit {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
}
.notes-block.with-edit .edit {
  font-size: 12px;
  color: var(--text-warning);
  text-decoration: underline;
  white-space: nowrap;
  align-self: center;
}

/* Table rows that navigate as a whole (decision #64). The pointer is the only
   visual cue beyond the existing row hover in style.css. */
.asset-table tbody tr.linked-row { cursor: pointer; }
.asset-table tbody tr.linked-row td a {
  color: var(--text-primary);
  text-decoration: none;
}
.asset-table tbody tr.linked-row:hover td a { text-decoration: underline; }

/* ────────────────────────────────────────────────
   Client overview (screens #5/#9)
   ──────────────────────────────────────────────── */
.contact-box {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  padding: 11px 14px;
  margin: 12px 0 14px;
  font-size: 13px;
}
.contact-box .line {
  color: var(--text-secondary);
  margin-bottom: 3px;
}
.contact-box .line:last-of-type { margin-bottom: 0; }
.contact-box .line.empty { color: var(--text-tertiary); font-style: italic; }
.contact-box .line a { color: var(--text-info); }
.contact-box .summary {
  color: var(--text-tertiary);
  font-size: 12px;
  margin-top: 2px;
}

.sites-tbl {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.sites-tbl .tbl-head,
.sites-tbl .row {
  display: grid;
  grid-template-columns: 1.5fr 1.3fr 0.85fr 0.7fr 0.4fr;
  column-gap: 16px;
  align-items: center;
}
.sites-tbl .tbl-head {
  background: var(--bg-secondary);
  padding: 9px 14px;
  font-size: 11px;
  font-weight: 700;
  color: var(--text-primary);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  border-bottom: 0.5px solid var(--border-tertiary);
}
.sites-tbl .tbl-head .num,
.sites-tbl .row .num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* The count carries its own unit, so a bare number in a narrow column can never
   be mistaken for something else. */
.sites-tbl .row .num .unit {
  color: var(--text-tertiary);
  font-size: 11px;
  margin-left: 2px;
}
.sites-tbl .row {
  padding: 10px 14px;
  border-bottom: 0.5px solid var(--border-tertiary);
  font-size: 13px;
  text-decoration: none;
  transition: background 60ms ease;
}
.sites-tbl .row:last-of-type { border-bottom: none; }
.sites-tbl .row:hover { background: var(--bg-secondary); }
.sites-tbl .site-name { font-weight: 500; color: var(--text-primary); }
.sites-tbl .addr { color: var(--text-secondary); font-size: 12px; }
.sites-tbl .last-visit {
  color: var(--text-secondary);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.sites-tbl .open-link {
  color: var(--text-info);
  text-align: right;
  font-size: 12px;
}

/* ────────────────────────────────────────────────
   Asset dashboard (screen #3)
   ──────────────────────────────────────────────── */
.identity {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 18px;
  gap: 24px;
}
.identity .title {
  font-size: 24px;
  font-weight: 500;
  margin: 0 0 4px;
  letter-spacing: -0.012em;
  line-height: 1.2;
}
.identity .title .label {
  color: var(--text-tertiary);
  font-weight: 400;
  margin-right: 2px;
}
.identity .title .num { font-variant-numeric: tabular-nums; }
.identity .title .sep {
  color: var(--text-tertiary);
  margin: 0 10px;
  font-weight: 400;
}
.identity .sub {
  font-size: 13px;
  color: var(--text-secondary);
  margin: 0;
}
/* Round 11: the asset class reads black and a size up from the grey line it used
   to share with the location — noticeable without being a button. The value
   carries the weight the edit screen's chosen class option carries. */
.identity .asset-class-line {
  font-size: 15px;
  color: var(--text-primary);
  margin: 2px 0 4px;
}
.identity .asset-class-line .value { font-weight: 500; }
.identity .status-cluster {
  display: flex;
  align-items: center;
  gap: 8px;
}
.identity .status-edit {
  font-size: 12px;
  color: var(--text-tertiary);
  text-decoration: underline;
}

.due-row {
  display: flex;
  gap: 8px;
  margin-bottom: 22px;
  flex-wrap: wrap;
}
.due-badge {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  padding: 8px 14px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 180px;
}
.due-badge .label {
  font-size: 11px;
  color: var(--text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-weight: 500;
}
.due-badge .value {
  font-size: 13px;
  color: var(--text-primary);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.due-badge .value .when {
  color: var(--text-secondary);
  font-weight: 400;
  margin-left: 4px;
}
.due-badge.overdue {
  background: var(--bg-danger);
  border-color: var(--border-danger);
}
.due-badge.overdue .label,
.due-badge.overdue .value { color: var(--text-danger); }
.due-badge.overdue .value .when {
  color: var(--text-danger);
  font-weight: 500;
}

/* Also used for the site overview's Site details tab. */
.details-grid {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  padding: 14px 18px;
  margin-bottom: 22px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px 22px;
}
/* ⚠ --text-secondary, not --text-tertiary: these are the field labels on the
   completed-report read-back (reports/show), which is the screen a tech
   confirms a certificate on and the reference 4b's generator gets compared
   against. #9999a1 on white is 2.83:1 and fails WCAG AA at any size — the same
   defect as the section headers, in the same role one screen over. Kept a step
   Also lifts sites/show and assets/show, which share this component.

   ⚠ --text-primary, not --text-secondary. The first fix moved it one step
   (tertiary → secondary) on the reasoning that the 14px values beside them
   should stay dominant. Joel, looking at it: "no, the section headers are fine
   but the field labels are that same grey colour as original" — two greys read
   as the same grey, and the change was invisible. Uppercase at 11px keeps them
   subordinate to the values without needing colour to do it. */
.details-grid .detail .label {
  font-size: 11px;
  color: var(--text-primary);
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin: 0 0 3px;
  font-weight: 500;
}
.details-grid .detail .value {
  font-size: 14px;
  color: var(--text-primary);
  margin: 0;
}
.details-grid .detail .value a { color: var(--text-info); }

.report-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  /* ⚠ `margin-top` IS NOT DECORATION — Joel, round 10: *"add section and save
     template still touch"*, measured at 0px. This row carries a form's submit,
     and it had `margin-bottom` only: the `gap` spaces the buttons from each
     other and the bottom margin spaces the row from what FOLLOWS, and between
     the row and whatever ends directly above it there was nothing at all.
     That is the whole shape of the defect, and it is why the round-9 spacing
     guard could not see it — "this container has spacing" was true. */
  margin-top: 22px;
  margin-bottom: 28px;
}

/* .btn-primary is width:100% for the sign-in card, which inside this flex row
   made the one anchor stretch while its sibling <button>s sized to content —
   three buttons of three different widths. They are one row of equal choices. */
/* ⚠ `max-width` IS LOAD-BEARING SINCE ROUND 8, and a render is what caught it.
   `flex: 1 1 200px` shares the row between however many buttons there are —
   which was fine when every asset offered at least a performance verification
   plus the disabled Log repair. Now a built-in is offered only to the class it
   describes, so on most machines Log repair is the ONLY child and it stretched
   the full width of the page: one enormous greyed-out button under a heading
   saying "Start a new report". The cap keeps a lone button button-shaped. */
.report-start .btn-primary,
.report-start .btn-action {
  width: auto;
  margin-top: 0;
  flex: 1 1 200px;
  max-width: 24rem;
}

/* The start-a-report screen's two buttons (round 3 finding 1, 2026-09-02).

   ⚠ EACH BUTTON IS ITS OWN <form>, because `button_to` renders one — so the
   flex children here are forms, not buttons, and sizing the button alone leaves
   two full-width forms stacked. Both levels are set.

   `flex: 1 1 240px` rather than `width: auto`: the two are equal choices (#126
   — offered, never defaulted), and a pairing button visibly narrower than the
   single-report one reads as the lesser option before a word is read. The
   240px basis is what makes them wrap to two full-width rows on a phone rather
   than squeezing "Start paired verification and maintenance" to three lines. */
.start-report-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}
.start-report-actions form {
  flex: 1 1 240px;
  margin: 0;
}
.start-report-actions .btn-action {
  width: 100%;
  margin-top: 0;
}

/* ⚠ CANCELS `.form-hint`'s NEGATIVE TOP MARGIN, which is what Joel saw on
   2026-09-03: "it currently cuts across a line of some sort". The line was the
   form card's own bottom border — `.form-hint` is `margin: -8px 0 14px`, an
   8px PULL UP that exists to tuck a hint under the field it describes, and this
   hint is not under a field. It is a sibling AFTER a bordered card, so the pull
   dragged its first line up through that border.

   Scoped to this one place rather than changed at the base: every other hint in
   the application does sit under a field and wants the pull. Same shape as
   `.field .form-hint`, which already overrides it for the opposite reason. */
.start-report-actions + .form-hint {
  margin-top: 16px;
}

/* Two actions in one cell (View / Edit on a completed report) need visible
   space between them, or they read as one long link. */
/* ⚠ FLEX WITH A GAP, NOT `a + a` (Joel, round 8: *"on report templates screen
   there is no gap between edit and archive"*). The old sibling selector matched
   an anchor after an anchor — but Archive and Restore are `button_to`, which
   wraps its button in a FORM, so `a + form` matched nothing and the two
   controls sat flush against each other. A gap that only applies between two
   controls of the same element type is not a gap. */
.row-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.row-actions form { margin: 0; display: inline-flex; }

/* ⚠ THE GAP ABOVE SPACES BUTTONS FROM EACH OTHER *WITHIN* A ROW. It does
   nothing between two rows in different parents, which is how the template
   editor ended up with "+ Add field" and "Remove section" at a measured 0px —
   Joel's round-9 finding, and the second time button spacing has come back.
   Declared here rather than on the section partial so any future row in a
   template card inherits it. */
.template-section .row-actions { margin-top: 12px; }

/* The move arrows on a field row. `flex: 0 0 auto` because the row is a flex
   box whose middle grows — without it the arrows are the first thing squeezed,
   and a 34px button that has been squeezed is one nobody can hit on a tablet. */
.template-move { flex: 0 0 auto; }

/* ⚠ THE FIELD ROW IS A TWO-COLUMN GRID, AND IT TOOK THREE GOES TO GET RIGHT.
   Joel, round 10: *"the move up and down works but now means that some of the
   remove buttons for rows are on a separate row. the UI is horrible."*

   The two wrong versions, both of which read correctly in the markup:
     1. the three buttons wrapped in a plain `.row-actions` — the flex row
        squeezed that nested box until the arrows sat ON TOP of a label;
     2. the three buttons as loose children of a wrapping flex row — each one
        then wrapped on its own, so a Reading row put the up arrow at the end of
        line one and the down arrow and Remove on line two, while a Tick box row
        fitted all three on one line. No two rows agreed with each other.

   A grid settles it: content left, actions right, and the actions are a column
   rather than a sibling that can wrap away or be shrunk. `minmax(0, 1fr)` is
   what lets the left column's own contents wrap instead of overflowing. */
.template-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.75rem;
}
.template-row-content {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
}
.template-row-actions {
  display: flex;
  align-items: center;
  gap: 8px;
}
.template-row-content > .field { min-width: 10rem; }

/* A pass/fail list's item boxes — #180, round 13. The block itself is laid out
   beside `.template-row .field`, below; these two only stack the boxes. */
.template-items-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.template-item-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 8px;
}
.clickable-row {
  cursor: pointer;
}

/* ────────────────────────────────────────────────
   Completed-report view (#115)
   Two panes since Chunk 4b: the generated certificate rendered by pdf.js onto
   canvases, and the recorded values beneath it in the certificate's own order.
   ──────────────────────────────────────────────── */
.preview-pane {
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  background: var(--bg-primary);
  padding: 20px 22px 24px;
  margin-bottom: 20px;
}
/* Room for the overlaid actions, so the last section is never underneath them
   at the end of a scroll. On the LAST pane only — there are two of them now,
   and 88px between the certificate and the values it was built from reads as a
   missing section rather than as breathing room. */
.preview-pane:last-of-type { margin-bottom: 88px; }
.preview-pane .section-header:first-of-type { margin-top: 0; }
.preview-pending {
  font-size: 13px;
  color: var(--text-secondary);
  background: var(--bg-secondary);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  margin: 0 0 18px;
}

/* ────────────────────────────────────────────────
   THE HOUSE RULE FOR MESSAGES — decision #143, Joel 2026-08-28.

   *"any important message should be red box white writing, and button to be
   clicked should be blue with white."*

   ⚠ This AMENDS #70, which said red does not go on status messages and that
   emphasis elsewhere should use info blue. Under #143 the two colours have one
   job each: red says READ THIS, blue says PRESS THIS. Do not put an important
   message in a neutral or amber box because it feels calmer — that is the
   change this rule exists to stop being re-litigated.

   The blue half is already `.btn-action`, which existed before the rule.

   Guarded by spec/requests/message_and_button_styling_spec.rb, which fails if
   this declaration loses its red ground or its white text, and if any of the
   named controls stops being `.btn-action`.
   ──────────────────────────────────────────────── */
.msg-important {
  font-size: 13px;
  font-weight: 500;
  color: #fff;
  background: var(--brand-red);
  border-radius: var(--radius-md);
  padding: 10px 12px;
  margin: 0 0 18px;
}
/* White on #CC1F26 measures 5.5:1, so the text clears AA at this size. Do not
   lighten the ground to a tint — a pale box with dark text is exactly the
   treatment #143 replaced. */
.msg-important a { color: #fff; text-decoration: underline; }

/* The status line, #124's Download and #112's Regenerate share a row at the top
   of the certificate pane. `margin-left: auto` rather than `justify-content:
   flex-end`, because the status is hidden whenever there is no message and the
   buttons must stay right-aligned on their own.

   ⚠ The auto margin is on the GROUP, not on each button. Two buttons each
   carrying `margin-left: auto` split the free space between them rather than
   sitting together, which left Download stranded mid-row. */
.certificate-actions {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 18px;
}
.certificate-actions .preview-pending,
.certificate-actions .msg-important { margin-bottom: 0; }
.certificate-buttons {
  display: flex;
  gap: 12px;
  margin-left: auto;
  flex-shrink: 0;
}
.certificate-actions .btn-action { flex-shrink: 0; }


/* ────────────────────────────────────────────────
   "Report saving, please wait" — Joel 2026-08-28, round 3.

   R1 made an in-app navigation a refusal rather than a question, and the
   refusal answered in the status line at the top of the certificate panel.
   He asked for it in the middle of the screen instead: a click on "Back to
   asset" is a click at the bottom of a long page, and an answer that appears
   above the fold is an answer nobody sees.

   `<dialog>` + showModal() centres it and gives a real backdrop for free, the
   same mechanism as `.ack-modal`. Red ground and white text per #143 — this is
   the most important thing on the screen at the moment it appears.

   ⚠ It has NO dismiss control, deliberately. It is not an acknowledgement; it
   is the refusal itself, and it closes when the certificate is stored. Giving
   it a button would put back the "leave anyway" escape R1 removed.
   ──────────────────────────────────────────────── */
.saving-modal {
  max-width: 420px;
  width: calc(100% - 32px);
  padding: 22px 26px;
  border: 0.5px solid var(--brand-red-dark);
  border-radius: var(--radius-lg);
  background: var(--brand-red);
  color: #fff;
  text-align: center;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
}
.saving-modal p {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
}
.saving-modal .saving-sub {
  margin-top: 6px;
  font-size: 13px;
  font-weight: 400;
  opacity: 0.9;
}
.saving-modal::backdrop { background: rgba(0, 0, 0, 0.45); }
/* ⚠ Regenerate is disabled while a pass is running, and a button that refuses
   a click while looking clickable is the defect Joel reported against the
   sign-in limiter (#101). Same treatment as `.photo-actions`. */
.certificate-actions button[disabled] {
  opacity: 0.35;
  cursor: not-allowed;
}

/* The stored filename (#141). These run long — client, site, asset number,
   make, serial, type, month, year — so it wraps mid-string rather than pushing
   the pane sideways, and the name itself is monospaced so an admin can match it
   character for character against what they are looking at in the bucket. */
.certificate-filename span {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  color: var(--text-primary);
  overflow-wrap: anywhere;
}

/* Each rendered page of the certificate. The canvas is drawn at device
   resolution and laid out at 100% width, so a graph in the appendix stays
   legible on a retina tablet — techs read these to check the upload order. */
.certificate-page {
  display: block;
  width: 100%;
  height: auto;
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-sm);
  background: var(--bg-primary);
}
.certificate-page + .certificate-page { margin-top: 16px; }
/* A wide certificate scrolls inside its own box rather than pushing the page
   sideways — the mobile gap (#106) is deferred, so nothing else here handles a
   narrow viewport. */
.certificate-preview { overflow-x: auto; }

/* Overlaid rather than in the flow: a certificate is long, and both answers
   must be reachable without scrolling to the end of it. */
.preview-actions {
  position: sticky;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  padding: 14px 0 18px;
  background: linear-gradient(to top, var(--bg-secondary) 70%, transparent);
}

/* Single column on a phone — the register screens are read on a tablet in a
   clinic at least as often as at a desk. */
@media (max-width: 640px) {
  .details-grid { grid-template-columns: 1fr; }
  .identity { flex-direction: column; gap: 10px; }
  .sites-tbl .tbl-head { display: none; }
  .sites-tbl .row { grid-template-columns: 1fr auto; row-gap: 4px; }
  .sites-tbl .row .addr,
  .sites-tbl .row .last-visit { grid-column: 1 / -1; }
  .sites-tbl .open-link { display: none; }
}

/* ────────────────────────────────────────────────
   Sign-in screen
   ──────────────────────────────────────────────── */
.auth-screen {
  display: flex;
  justify-content: center;
  padding-top: 9vh;
}
.auth-card {
  width: 100%;
  max-width: 380px;
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  padding: 28px 30px 30px;
}
.auth-brand {
  font-size: 13px;
  font-weight: 500;
  color: var(--brand-red);
  margin-bottom: 18px;
}
.auth-card .page-title { margin-top: 0; }
.auth-form { margin-top: 22px; }

/* The rate-limited state (Joel's finding #3, 2026-08-11).
 *
 * A working block used to be indistinguishable from a wrong password: same
 * screen, same fillable form, a flash that read like any other failure. So it
 * gets its own panel rather than a flash — danger colours, its own heading, and
 * a form greyed out underneath it, which is the part that actually reads as
 * "stopped" rather than "try again". */
.auth-blocked {
  margin-top: 20px;
  padding: 14px 16px;
  font-size: 14px;
  line-height: 1.45;
  background: var(--bg-danger);
  border: 0.5px solid var(--border-danger);
  border-radius: var(--radius-md);
  color: var(--text-danger);
}
.auth-blocked p { margin: 0 0 8px; }
.auth-blocked p:last-child { margin-bottom: 0; }
.auth-blocked-title {
  font-weight: 500;
}
.auth-blocked-note {
  font-size: 13px;
  color: var(--text-secondary);
}

/* Dimmed and not-allowed, so the pause is visible at a glance and the cursor
 * confirms it on the way to clicking. The enforcement is the controller's 429;
 * this is only what it looks like. */
.auth-form-blocked {
  opacity: 0.55;
}
.auth-form-blocked .field-input,
.auth-form-blocked .btn-primary {
  cursor: not-allowed;
}

/* ────────────────────────────────────────────────
   Register forms — client, site and asset create/edit
   (Chunk 3 pass 2)

   .field, .field-label and .field-input above are shared with the sign-in
   screen. Everything here is the multi-field page those two never needed.
   ──────────────────────────────────────────────── */
.form-card {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  padding: 22px 24px 24px;
  margin-top: 18px;
}

/* Three across on a desktop, one on a tablet held in a clinic. Fields that need
   the full width opt in with .span-2. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0 18px;
}
.form-grid .span-2 { grid-column: span 2; }

.form-card .section-header { margin-top: 18px; }
.form-card .section-header:first-child { margin-top: 0; }

/* ⚠ The colour is set HERE, in the base declaration, and NOT in a later
   override. Joel, 2026-08-21: "the note is now grey not red".
   `.elec-indicator-fail` (:1296) colours the auto-fail warning red and carries
   the same specificity as `.form-hint` — so an override appended further down
   the file beat it and silently turned a compliance warning grey. Any variant
   that colours a .form-hint must be defined AFTER this block, and this block
   must stay where it is. */
.form-hint {
  font-size: 12px;
  color: var(--text-secondary);
  margin: -8px 0 14px;
  line-height: 1.45;
}
.field .form-hint { margin: 2px 0 0; }

/* A value shown for context rather than edited — the client a site belongs to,
   which comes from the URL and is deliberately not a picker. */
.field-static {
  font-size: 14px;
  color: var(--text-secondary);
  margin: 0;
  padding: 8px 0;
}

/* The report form's testing standard and its "Change Class" button, side by
   side (Joel, round 7). The control has to sit BESIDE the value it changes —
   as a link inside the hint paragraph underneath, it read as prose and he
   missed it. `align-items: center` rather than baseline, because the two are
   different heights and a baseline here drops the button below the text. */
.standard-line {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.standard-line .field-static { font-weight: 500; color: var(--text-primary); }

.form-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 22px;
  padding-top: 18px;
  border-top: 0.5px solid var(--border-tertiary);
}

/* .btn-primary is full-width for the sign-in card; on a form with several
   actions side by side it has to size to its label. */
.btn-inline {
  width: auto;
  margin-top: 0;
  height: 36px;
  padding: 0 18px;
}

.form-errors {
  background: var(--bg-danger);
  border: 0.5px solid var(--border-danger);
  border-radius: var(--radius-md);
  padding: 11px 14px;
  margin-bottom: 18px;
  color: var(--text-danger);
  font-size: 13px;
}
.form-errors-title { font-weight: 500; margin: 0 0 4px; }
.form-errors ul { margin: 0; padding-left: 18px; }

/* #181 — Joel, round 13: the field a save error is about is outlined, with the
   message beneath it. `field_errors_controller` adds both and removes them the
   moment the tech edits the field.

   The note is #143's red box with white writing, small — it is an important
   message, and #143 settles what those look like. The outline is the same red,
   doubled with a ring so it reads on a tablet at arm's length, where a 0.5px
   border change does not. */
.field-invalid,
.field-input.field-invalid {
  border-color: var(--brand-red);
  box-shadow: 0 0 0 2px var(--brand-red);
}
.field-error {
  margin: 6px 0 0;
  padding: 5px 9px;
  border-radius: var(--radius-md);
  background: var(--brand-red);
  color: #fff;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
}

/* Asset class picker (decision #4) — radio buttons drawn as a button row, so it
   works with no JavaScript and the whole label is the tap target. */
.class-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 18px;
}
.class-option {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 0 14px;
  height: 36px;
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
  background: var(--bg-primary);
  font-size: 14px;
  cursor: pointer;
  transition: background 60ms ease, border-color 60ms ease;
}
.class-option:hover { background: var(--bg-secondary); }
/* :has keeps the checked state in CSS rather than needing a Stimulus controller
   to toggle a class. Browsers without it still show the radio itself, so the
   selection is never invisible. */
.class-option:has(input:checked) {
  border-color: var(--brand-red);
  background: var(--brand-red-tint);
  font-weight: 500;
}
.class-option:has(input:focus-visible) {
  box-shadow: 0 0 0 3px var(--bg-info-strong);
}

/* #178's electrical class modal (round 11) — the same option buttons as the
   asset class picker, stacked one per row because the labels are long
   ("Class 1 — AS/NZS 3760:2022") and read as a list to choose from. */
.elec-class-options {
  border: 0;
  padding: 0;
  margin: 14px 0 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.elec-class-options .class-option {
  display: flex;
  height: auto;
  min-height: 40px;
}
.elec-class-current {
  margin-left: auto;
  font-size: 12px;
  font-weight: 400;
  color: var(--text-secondary);
}

/* Duplicate asset number (decisions #65/#66). Info blue, not danger red: two
   records disagree, which is worth fixing — but nothing has failed and no
   certificate is wrong. Red here would read as "this asset is unsafe". */
.duplicate-warning {
  background: var(--bg-info);
  border: 0.5px solid var(--border-info);
  border-radius: var(--radius-md);
  padding: 11px 14px;
  margin-bottom: 20px;
}
.duplicate-warning .label {
  font-size: 12px;
  font-weight: 500;
  color: var(--text-info-dark);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin-bottom: 2px;
}
.duplicate-warning .body {
  font-size: 13px;
  color: var(--text-info-dark);
  margin: 0;
  line-height: 1.5;
}
.duplicate-warning .body a { color: var(--text-info-dark); text-decoration: underline; }

/* The "add a new client" line under the search results. Quiet on purpose —
   search is single-purpose (#61) and this must not compete with it. */
.search-aside {
  margin-top: 20px;
  font-size: 13px;
  color: var(--text-tertiary);
  text-align: center;
}

@media (max-width: 720px) {
  .form-grid { grid-template-columns: 1fr; }
  .form-grid .span-2 { grid-column: span 1; }
  .form-actions .btn-inline { flex: 1; }
}

/* ────────────────────────────────────────────────
   Action buttons — "+ Add new client", "+ Add new asset"
   (Joel, 2026-08-06)

   Info blue with white text, matching the search screen's Search button. Not
   Medworx red: #70 keeps red for the primary action *within* a form, and these
   are navigation to a form. Deliberately the same weight as Search, because
   adding a client is as routine as looking one up.
   ──────────────────────────────────────────────── */
/* The blue action button. Geometry comes from the shared rule above; this adds
   only the colour and the bits a link needs that an input does not. Used for
   "+ Add new client", "+ Add new asset", and both Search buttons. */
.btn-action {
  display: inline-flex;
  align-items: center;
  background: var(--text-info);
  border-color: var(--text-info);
  color: #fff;
  text-decoration: none;
}
.btn-action:hover {
  background: var(--text-info-dark);
  color: #fff;
  text-decoration: none;
}

/* Sits under the search row, right-aligned to line up with the Search button
   directly above it. */
.search-actions {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 16px;
}

/* The tab row doubles as an action row on the site overview. margin-left:auto
   pushes the button to the far right so it can never read as a fourth tab, and
   the negative margin lifts it clear of the row's bottom border. */
.tabs.with-action {
  display: flex;
  align-items: center;
}
.tabs .tabs-action {
  margin-left: auto;
  margin-bottom: 8px;
  height: 32px;
  padding: 0 14px;
  font-size: 13px;
}

/* ────────────────────────────────────────────────
   .btn-danger — a destructive action: Remove, Archive, Retire.

   ⚠ SOLID RED WITH WHITE WRITING SINCE ROUND 8 (2026-09-16), AND THAT REVERSES
   THE OUTLINE TREATMENT OF #45/#47/#52. Joel, having used it:

     "all the remove buttons, should be red with white writing like the other
      red with white writing buttons. including retire asset, archive client
      and archive site and any others"

   The outline was chosen because these actions are REVERSIBLE — a client can be
   un-archived from the same screen — and a solid fill read as heavier than the
   act deserved. He has now seen both and wants the weight. ⚠ **Do not restore
   the outline on a later reading of #47**: it is superseded on this point, and
   the reasoning above is why it looked right at the time.

   ⚠ IT IS THE SAME RED AS `.btn-primary`, WHICH MEANS SAVE AND DESTROY NOW
   SHARE A COLOUR. They never share a row — a form has Save and Cancel, a show
   screen has Edit and Archive — and every destructive control here keeps its
   confirm step, which is the actual guard. Named rather than left to be
   discovered.

   ⚠ THE CLASS WAS RENAMED FROM `.btn-danger-outline`. A name that says
   "outline" on a solid fill is the kind of false label this project keeps
   paying for. `.btn-danger` was also an INVENTED class on the signature screen
   until round 8 — it is real now, and the guard in
   message_and_button_styling_spec is what keeps it that way.
   ──────────────────────────────────────────────── */
.btn-danger {
  background: var(--brand-red);
  border-color: var(--brand-red);
  color: #fff;
}
.btn-danger:hover {
  background: var(--brand-red-dark);
  border-color: var(--brand-red-dark);
  color: #fff;
}

/* button_to wraps its button in a form, which would otherwise break the
   footer's flex row. */
.footer-actions form { margin: 0; display: inline-flex; }

/* ────────────────────────────────────────────────
   Acknowledgement modal (duplicate asset number)
   ──────────────────────────────────────────────── */
.ack-modal {
  max-width: 480px;
  width: calc(100% - 32px);
  padding: 22px 24px 20px;
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  background: var(--bg-primary);
  color: var(--text-primary);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.18);
}
.ack-modal::backdrop {
  background: rgba(22, 23, 28, 0.42);
}
.ack-title {
  font-size: 17px;
  font-weight: 500;
  margin: 0 0 10px;
  letter-spacing: -0.01em;
}
.ack-body {
  font-size: 14px;
  color: var(--text-secondary);
  line-height: 1.55;
  margin: 0 0 10px;
}
.ack-body strong { color: var(--text-primary); font-weight: 500; }
.ack-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  /* Wraps because the third button (Edit this asset number) took the row past
     the 480px modal on a narrow screen, and this is read on tablets in
     clinics. Right-aligned, so a wrapped row still ends under the primary. */
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 18px;
}
/* #105's restore question is the first modal whose primary answer is a POST, so
   the first with a button_to form in this row. Without this the form arrives as a
   flex item carrying the browser's default margin and the two answers sit at
   different heights. */
.ack-actions form {
  margin: 0;
  display: flex;
}

/* Google Places lookup above the address field (decision #96). Visually a
   subordinate control, not a second address — it writes into the field below
   it and is never itself submitted. */
.address-lookup {
  margin-bottom: 8px;
}
.address-lookup input:disabled {
  opacity: 0.6;
  cursor: progress;
}

/* The lookup could not start. It stays on the page saying so, rather than
   removing itself and leaving a misconfigured key looking identical to a
   feature nobody built. `not-allowed` rather than `progress`: nothing is
   loading, and it is not coming back on this page load. */
.address-lookup--unavailable input:disabled {
  cursor: not-allowed;
}
.form-hint-warning {
  color: var(--text-warning);
}

/* Google's suggestion dropdown. It attaches to <body>, so it cannot be styled
   as a descendant of the form — matched to the app's own combobox instead so it
   does not arrive looking like someone else's website.

   Every visual property here is set explicitly, `background` included. Google
   ships its own stylesheet for these classes by injecting a <style> element at
   runtime, and our CSP is `style-src 'self' 'nonce-…'` with no 'unsafe-inline';
   a nonce does not reliably reach a stylesheet injected by a library that the
   bootstrap loads, so that stylesheet is blocked and anything we leave to it is
   simply unset. That is how the dropdown came to render with no background at
   all, suggestions overlapping the page text beneath them. Widening the CSP
   would have fixed it too and is the wrong trade: a dropdown whose legibility
   depends on a third party's stylesheet surviving our own policy is a bad
   dependency even when it happens to work. Do not remove a declaration here on
   the grounds that Google already sets it. */
.pac-container {
  background: var(--bg-primary);
  color: var(--text-primary);
  border-radius: var(--radius-md);
  border: 0.5px solid var(--border-secondary);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.10);
  font-family: var(--font-sans);
  margin-top: 4px;
  overflow: hidden;
}
.pac-item {
  background: var(--bg-primary);
  padding: 7px 12px;
  font-size: 13px;
  line-height: 1.4;
  color: var(--text-secondary);
  border-top: none;
  cursor: pointer;
}
.pac-item:hover,
.pac-item-selected { background: var(--bg-secondary); }
.pac-item-query { font-size: 13px; color: var(--text-primary); }
/* Google's attribution row. It must stay visible — the Places terms require the
   attribution to be shown — so it gets the same ground as the items above it. */
.pac-logo:after { background-color: var(--bg-primary); }

/* ── Chunk 4: the report form ──────────────────────────────────────────────
   The live electrical-safety indicator. Two states only — the evaluation
   speaks in Pass/Fail, and the colors are the same tokens the status pills
   and the asset table's pass column already use. */
.elec-indicator-pass { color: var(--text-success); }
.elec-indicator-fail { color: var(--text-danger); }

/* F3 — the cycle-data archives listed on the report form (#128).
   Tokens only, per decision #69: style.css is the locked visual file and
   app-only additions live here. Every custom property below was checked
   against style.css's :root before use — the first draft of this block
   invented five that do not exist, which is the same trap as assuming a
   route helper name. */
.cycle-data-list {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  border-top: 1px solid var(--border-tertiary);
}

.cycle-data-list li {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem 0.75rem;
  padding: 0.75rem 0;
  border-bottom: 1px solid var(--border-tertiary);
}

.cycle-data-link {
  font-weight: 500;
}

.cycle-data-meta {
  color: var(--text-secondary);
  font-size: 0.8125rem;
  /* Pushes the admin's Remove button to the far edge, so a destructive control
     is never adjacent to the download link a tech is aiming for. */
  margin-right: auto;
}

.cycle-data-upload {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 0.75rem;
}

.cycle-data-upload .field { flex: 1 1 22rem; }

/* Certificate photos (#130). Tokens checked against style.css :root first. */
.photo-list {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.photo-item {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  padding: 0.75rem;
  border: 1px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  flex-wrap: wrap;
}

.photo-thumb {
  flex: 0 0 auto;
  width: 5rem;
  height: 5rem;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  background: var(--bg-secondary);
  border-radius: var(--radius-sm);
}

.photo-thumb img,
.photo-thumb canvas { width: 100%; height: 100%; object-fit: cover; }

/* F-B. The canvas renders at device pixel ratio and is scaled down by the rule
   above, so a cycle graph stays legible on a tablet — the monolith renders at a
   fixed scale 0.3 and squeezes it into 60px, which is soft on any retina
   screen. It is hidden until pdf.js has actually drawn something, so a failed
   render leaves the grey badge showing rather than an empty white box. */
.photo-thumb canvas { background: var(--bg-primary); }

.photo-badge {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--text-secondary);
}

.photo-caption-form { flex: 1 1 18rem; }
.photo-caption-row { display: flex; gap: 0.5rem; align-items: center; }
.photo-caption-row .field-input { flex: 1; }

/* Reorder + remove controls on a certificate photo (#130). */
.photo-actions { display: flex; align-items: center; gap: 0.4rem; }
.photo-move { display: inline; }
.photo-move .btn-inline { min-width: 2rem; }
.photo-actions button[disabled] { opacity: 0.35; cursor: not-allowed; }

/* Repeatable cycles on the PV report (#131, Joel 2026-08-21). One cycle is
   two blocks: a row of type + result inside Test results, and a
   microbiological block in the section below. Both need a visible edge once
   there is more than one, or two cycles read as one long list of fields and
   the tech cannot tell which "Cycle numbers" belongs to which. */
.cycle-row,
.cycle-block {
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
  padding: 0.75rem 1rem 1rem;
  margin-bottom: 0.75rem;
  background: var(--bg-primary);
}

.cycle-row-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  min-height: 1.5rem;
}

/* Both titles are hidden on a single-cycle report, where numbering would be
   noise — every report written before today is a single-cycle report.

   ⚠ Deliberately NOT uppercase, and deliberately subordinate to
   .section-header. As first written on 2026-08-21 these were 12.8px/600 in
   --text-secondary while their own parent section header was 11px/500 in the
   palest grey in the system — a sub-heading that outranked the section
   containing it.

   ⚠⚠ Then the fix for that put them BELOW the field labels: the labels went to
   --text-primary and these stayed --text-secondary, so a heading was lighter
   than the label underneath it. Joel caught it the same afternoon — "the cycle
   1 and cycle 2 headings are still dull and grey but the cycle type right
   below them is correct". Exactly the inversion the fix was for, reintroduced
   by the fix. **The rule to hold: a heading is never lighter than the thing it
   heads.** Check all three tiers together after touching any one of them.

   The tiers are now separated by case, size and weight rather than by colour,
   which is what makes them survive a colour change:
     .section-header    13px  700  UPPERCASE + red bar   --text-primary
     .cycle-*-title     13px  600  sentence case         --text-primary
     .field-label       12px  500  sentence case         --text-primary */
.cycle-row-title,
.cycle-block-title {
  margin: 0 0 0.4rem;
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
}

.cycle-row-title { margin-bottom: 0; }

.cycle-add-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 1rem;
}

.cycle-add-row .form-hint { margin: 0; }

/* The washer's four optional exclusion statements (Chunk 5). Each tick adds a
   paragraph to the certificate saying what this validation did NOT cover, so
   the tech has to read the whole sentence before ticking — which is why the
   text sits in the label rather than behind a "?" and why these are cards
   rather than a row of checkboxes. */
.statement-options {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.statement-option {
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  padding: 0.6rem 0.75rem;
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  cursor: pointer;
  font-size: 12px;
  color: var(--text-primary);
}

.statement-option input[type="checkbox"] { flex: none; margin-top: 0.15rem; }

/* The wording is the whole point of the control, so it wraps and is never
   truncated — and the hint inside a label needs its margin cleared or the two
   lines of the card drift apart. */
.statement-option .form-hint { margin: 0.15rem 0 0; }

/* The 29-item annual service checklist (Chunk 5). A list, not a form grid:
   every row is one tick and a long sentence, and .form-grid's two columns
   would break each label over three lines on a tablet.

   Rows alternate rather than carrying a border each — 29 hairlines read as a
   table nobody can scan, and the tech is looking for the next unticked box.
   The control column is fixed so the selects and inputs line up down the page;
   without it each one sizes to its own row and the column zig-zags. */
.checklist {
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
  overflow: hidden;
  margin-bottom: 1rem;
}

.checklist-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.5rem 0.75rem;
  background: var(--bg-primary);
}

.checklist-item:nth-child(even) { background: var(--bg-secondary); }

/* The whole label is the hit area — a 14px checkbox is a poor target on a
   tablet at a bench, which is where every one of these 29 gets ticked. */
/* 14px, matching .field-input — Joel, 2026-09-01: "can we make the writing on
   the maintenance checklist items larger? maybe the same size as the writing in
   the drop down boxes". These are read off a tablet at a bench and each one is
   a statement about work done on a client's machine. */
.checklist-label {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex: 1 1 auto;
  min-width: 0;
  cursor: pointer;
  font-size: 14px;
  color: var(--text-primary);
}

/* A Clean/Replace row: two tick boxes, each with its own full sentence, sharing
   the row rather than one label plus a control on the right. `align-items:
   flex-start` because these sentences wrap on a narrow tablet and a wrapped
   second line must not drag its tick box down with it. */
.checklist-item-choice {
  align-items: flex-start;
  gap: 1.25rem;
}

.checklist-choice {
  align-items: flex-start;
  flex: 1 1 0;
}

/* A CUSTOM template's choice row (Chunk 8, #167, amended round 4 2026-09-11).

   ⚠ THE SAME RULES AS THE BUILT-IN ABOVE, WITH NO LABEL OF ITS OWN. It used to
   print the author's label on the left with bare option words on the right, and
   Joel rejected that on sight: "this doesn't match the way autoclave
   maintenance does it... make it the same". Each box now carries the whole
   sentence, composed by ReportTemplateConfig#option_label.

   The wrapper survives the heading it was built beside, and it earns its keep:
   a template may offer more options than the built-in's two (Joel: "generally
   the options would be any of check, clean, replace"), so the row has to wrap
   rather than squeeze three sentences into one line on a tablet. `flex: 1 1 0`
   on the boxes matches the built-in, so two options split the row exactly as
   the fixed 29 do. */
.checklist-item-authored { gap: 1.25rem; }

.checklist-choice-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1.25rem;
  flex: 1 1 auto;
}

.checklist-choice-options .checklist-choice { flex: 1 1 12rem; }

.checklist-choice input[type="checkbox"] { margin-top: 0.15rem; }

.checklist-label input[type="checkbox"] { flex: none; }

/* Wraps rather than truncates: several of these labels are a full sentence
   ("Check hose connections for security and condition for wear or damage")
   and a tech ticking the wrong row is a false statement on a certificate. */
.checklist-label span { min-width: 0; }

.checklist-control {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex: none;
  width: 11rem;
  justify-content: flex-end;
}

/* .field-input is width:100%, which would push the unit out of the row. */
.checklist-control .field-input { width: auto; max-width: 7.5rem; }

.checklist-unit {
  font-size: 11px;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* On a phone the control drops under its label rather than squeezing a
   sentence into what is left beside an 11rem column. */
@media (max-width: 640px) {
  .checklist-item {
    flex-direction: column;
    align-items: stretch;
    gap: 0.4rem;
  }

  .checklist-control { width: auto; justify-content: flex-start; }
}

/* ────────────────────────────────────────────────
   Form typographic hierarchy — Joel, 2026-08-21
   ────────────────────────────────────────────────
   "all the field headings being dull grey, it makes it hard to orientate
   where you are in the report. should they not be bold black or something
   more noticeable?"

   He described the field labels; the fault was mostly one level up, and the
   real defect was an INVERTED hierarchy rather than a dull colour. Measured
   against --bg-primary (#ffffff), before this block:

     .page-title      24px  weight 500  #16171c   18.4:1
     .section-header  11px  weight 500  #9999a1    2.83:1   ← FAILS WCAG AA
     .field-label     12px  weight 500  #6c6d75    5.15:1
     .form-hint       12px  weight 500  #9999a1    2.83:1   ← FAILS WCAG AA

   So the eleven section headers that are the only landmarks on the report
   form were both SMALLER and LOWER-CONTRAST than the field labels beneath
   them, and the same grey as the optional hint prose. The element meant to
   tell a tech where they are was the quietest text on the page. 2.83:1 fails
   AA at every size, so this is a legibility defect and not a preference.

   ⚠ style.css is the locked token file (decision #69) and defines
   .section-header at :328. It is NOT edited — these are overrides here, which
   is the split that decision exists for. No new custom property is invented;
   every colour below is an existing token from style.css's :root.

   ⚠ There is no browser or visual-regression harness in this app, so none of
   this is verifiable except by eye on staging. */

/* The contrast fix is global, because 2.83:1 is a defect wherever it appears
   — the register and overview screens carry these headings too.

   Strengthened again the same day: 12px/600 near-black was still not enough to
   orientate by. Joel: "can we make the section headers (not the field ones)
   stand out even more then they do currently". */
.section-header {
  font-size: 13px;
  font-weight: 700;
  color: var(--text-primary);
}

/* What he actually asked for. Kept at 12px/500 so the values a tech types
   (14px) still dominate their own labels — a data-entry form should read as
   data first — while no longer being grey against a black heading. */
.field-label {
  color: var(--text-primary);
}

/* The orientation device, scoped to the long documents where scrolling is the
   problem: the report form and the certificate read-back. The register screens
   get the legibility fix above without a new visual rule, since nothing there
   is long enough to lose your place in.

   A rule UNDER each heading rather than above it, deliberately: `:first-child`
   does not match the first section header on the report form (the error
   partial renders ahead of it), so a top border would need an exception that
   silently stops working the day the markup moves. */
.form-card .section-header,
.preview-pane .section-header {
  padding-bottom: 6px;
  border-bottom: 0.5px solid var(--border-tertiary);

  /* The brand-red section marker. This is NOT an invention: style.css's own
     token comment (:54-56) lists "section dividers" among the sanctioned uses
     of --brand-red, alongside primary actions and the active tab underline —
     and explicitly rules it out for body links, row hover, status pills and
     general icons, none of which this is. The colour has been promised for
     this job since the design system was written and never used for it.

     The negative margin pulls the bar into the card's own padding so the
     heading TEXT stays aligned with the fields beneath it — an indented
     heading would trade one orientation problem for another. -14 + 3 + 11 = 0,
     and the card pads 24px (.form-card) / 22px (.preview-pane), so the bar
     sits inside the card rather than on its border. */
  border-left: 3px solid var(--brand-red);
  margin-left: -14px;
  padding-left: 11px;
}

/* ────────────────────────────────────────────────
   The two upload sections, inside the report form
   ────────────────────────────────────────────────
   Joel, 2026-08-21: "cycle data file chooser isn't right, it will show images
   and pdfs as options to select but not text files."

   The picker he opened was the PHOTOS one, and that is a fault in this layout
   rather than a misclick. Until F-A they were two separate `.form-card`s —
   two white cards with their own border and padding, unmistakably different
   places. Moving them inside the report form (so they sit between Electrical
   safety and Sign-off, where he asked for them) dropped that wrapper, leaving
   two near-identical runs of fields with their two file pickers adjacent and
   nothing between them.

   The cycle-data input carries no `accept` and the photos one is restricted to
   JPEG/PNG/PDF — verified in the running container, so the filtering he saw was
   the photos picker doing exactly its job in the wrong place. Giving each
   section its edge back is the fix; changing either `accept` would not be.

   ⚠ A CLASS, NOT THE TWO IDS IT USED TO NAME. Round 3 (2026-09-02) scoped every
   id in both sections to the report — `photos_section_41` — because a paired job
   renders the photo section twice, so `#photos_section` matches nothing any
   more. A selector that silently stops matching takes this whole treatment with
   it, and the thing it prevents is the misfiling Joel reported in the first
   place. */
.form-card .upload-section {
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-lg);
  background: var(--bg-secondary);
  padding: 16px 18px 4px;
  margin: 18px 0;
}

/* The heading is the section's own, so it loses the red bar and the rule that
   mark a heading of the REPORT — a nested block that shouted as loudly as its
   parent is the inversion this file has already had to correct twice today. */
.form-card .upload-section .section-header {
  border-left: 0;
  border-bottom: 0;
  margin-left: 0;
  padding-left: 0;
  padding-bottom: 0;
  font-size: 12px;
  color: var(--text-secondary);
}

/* The autosave indicator, made findable (Joel, 2026-08-21: "i cant even see
   where the autosave is now").

   ⚠ THIS WAS `position: sticky` AND THAT DID NOTHING. Sticky pins an element
   within its own parent's box, and this one lives inside `.report-buttons` — a
   single flex row at the very end of the form. Zero sticky range: it behaved
   exactly like a static element at the bottom of a very long page, which is
   where Joel could not find it. Shipped and written up as fixed without being
   looked at, which is the second time in one day.

   `fixed` instead, because it is the mechanism with the fewest silent failure
   modes: sticky needs a tall containing block AND no clipping ancestor, and
   both are easy to break from a distance. Fixed only breaks if an ancestor
   gains a transform, filter or will-change — checked, and the one transform in
   the stylesheets is a chevron on the register screens.

   `pointer-events: none` so it can never sit over a control and swallow a tap,
   which on a tablet at a bench would be a much worse bug than the one it is
   fixing. Bottom-RIGHT, because `.report-buttons` is a left-aligned flex row.

   Empty on a completed report, where nothing autosaves — and :not(:empty)
   means it then takes no space and paints nothing at all. */
.autosave-status:not(:empty) {
  position: fixed;
  right: 16px;
  bottom: 16px;
  z-index: 20;
  max-width: min(22rem, calc(100vw - 32px));
  pointer-events: none;
  background: var(--bg-primary);
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-pill);
  padding: 7px 15px;
  margin: 0;
  color: var(--text-primary);
  box-shadow: 0 3px 12px rgba(22, 23, 28, 0.16);
}

/* Carried violations on an amended certificate (Joel, 2026-08-21). Warning
   colours, not danger: nothing has failed and nothing is being refused — the
   report simply predates a rule. Styled as a sibling of .form-errors so the
   two read as the same kind of message at different weights. */
.form-warnings {
  background: var(--bg-warning);
  border: 0.5px solid var(--border-warning);
  border-radius: var(--radius-md);
  padding: 12px 16px;
  margin-bottom: 18px;
  color: var(--text-warning);
  font-size: 13px;
}
.form-warnings-title { font-weight: 600; margin: 0 0 6px; }
.form-warnings ul { margin: 0; padding-left: 20px; }
.form-warnings li { margin-bottom: 3px; }
.form-warnings-note { margin: 8px 0 0; font-size: 12px; opacity: 0.9; }

/* ── The second half of a paired report ───────────────────────────────────
   One form, two records (#116). Slot 2's own sections render inside slot 1's
   form, and this marks where one document's content ends and the other's
   begins — without it a tech scrolling a long form has no way to tell that the
   service checklist belongs to a different certificate from the bio grid above
   it.

   A left rule and a tinted ground rather than a box: the fields inside are
   ordinary form rows and must keep the geometry every other row on the page
   has. Tokens only — every value here is from style.css, which is the locked
   token file and is never edited (#69). */
.paired-slot {
  border-left: 3px solid var(--border-info);
  background: var(--bg-info);
  border-radius: var(--radius-sm);
  padding: 12px 16px 4px;
  margin: 24px 0;
}
.paired-slot-header { margin-top: 0; }

/* The upload sections nested inside a paired block need their own separation
   from the checklist above them, and they sit on the tinted ground rather than
   the card's white — so the border that separates them on a single report (see
   `.form-card .upload-section`, which this reuses by class) needs a ground of
   its own to stay legible. */
.paired-slot .upload-section {
  background: var(--bg-primary);
}

/* Which certificate an upload area's files go on — Joel, 2026-09-02 (round 3,
   finding 2): "make sure the photo uploads are clearly labeled for what report
   they go on - by clearly i mean make it stand out too."

   ⚠ THIS IS `.msg-important` (#143's red box, white writing) AND THAT IS A
   DELIBERATE READING OF THAT RULE, not a repaint. #143's own test is "a message
   the reader must act on or be harmed by missing": a service photo filed into
   the performance verification's appendix prints on a compliance certificate
   sent to a healthcare client, and nothing afterwards says it is in the wrong
   place. It renders on PAIRED forms only — on a single report there is one
   appendix, and a band that appears everywhere is one nobody reads.

   Only the geometry is overridden here; the ground and the text colour come
   from `.msg-important` itself, which `message_and_button_styling_spec` pins. */
.upload-owner {
  margin: 0 0 12px;
}

/* ── The template builder's rows ──────────────────────────────────────────────

   Joel, 2026-09-08, first use of the builder: "the text box for the checklist
   items and the statements can be much wider, take up all the space until the
   next thing on the line."

   ⚠ ITS OWN CLASS, NOT A WIDENING OF `.checklist-item`. That class is the REPORT
   FORM's checklist — 29 rows a tech ticks on a tablet at a bench, whose 11rem
   control column and 7.5rem input cap Joel signed off on 2026-09-01. The two
   screens look alike and want opposite things: the report row is a long label
   plus a small answer, the builder row is a short label being AUTHORED plus its
   settings. Widening the shared rule would have quietly reflowed a signed-off
   screen.

   The label/statement box takes the remaining width; the settings column sizes
   to its own content instead of a fixed 11rem. */
.template-row .field {
  flex: 1 1 auto;
  min-width: 0;
}

.template-row .field .field-input {
  width: 100%;
  max-width: none;
}

/* A validation template's report form — step 7d. A reading's unit sits beside
   its box, and a pass/fail item's two verdicts sit together at the row's end. */
.field-with-unit {
  display: flex;
  align-items: center;
  gap: 8px;
}
.validation-verdicts {
  display: flex;
  gap: 1rem;
}
.validation-section + .validation-section {
  margin-top: 1.5rem;
}

/* ⚠ A PASS/FAIL LIST TAKES A LINE OF ITS OWN — #180, found by rendering it.
   `.template-row .field` above gives every field `flex: 1 1 auto`, so the list
   sat squeezed between the type select and the Compulsory tick. Declared HERE,
   after that rule and one class more specific, rather than appended at the end
   of the file: an appended override of an equal-specificity rule is the 2026-08-21
   trap that greyed out a Fail indicator. */
.template-row .field.template-items {
  flex: 1 1 100%;
  gap: 8px;
}

.template-row .checklist-control {
  width: auto;
  flex: 0 0 auto;
}

/* The type select and the unit stay compact; only the choices box grows, since
   it is the one holding author-typed content. */
.template-row .checklist-control .field-input {
  width: auto;
  max-width: 9rem;
}

.template-row .checklist-control textarea.field-input {
  min-width: 12rem;
  max-width: none;
  flex: 1 1 auto;
}

@media (max-width: 700px) {
  .template-row .checklist-control { width: 100%; }
  .template-row .checklist-control textarea.field-input { min-width: 0; }
}

/* ── The test-table builder (step 7b) ────────────────────────────────────────

   ⚠ THE BUTTONS IN HERE ARE INVISIBLE TO `spec/button_spacing_spec.rb`, and the
   reason is structural rather than an oversight. That guard renders the real
   screens and walks the real DOM — but it renders them through a request spec,
   which runs no JavaScript, so the rows and columns this panel grows at the
   author's command are not in the document it measures. What it DOES see is the
   grid as the server renders it, which is why the panel is built in ERB and the
   controller only ever moves or clones those nodes: the markup the guard
   measures is the same markup the author ends up with.

   ⚠ SO THE SPACING BELOW IS LOAD-BEARING IN BOTH DIRECTIONS. Every button in
   the panel sits inside `.template-row-actions`, which already declares its own
   8px gap, and each block that holds one declares room above it. */
/* ⛔ THE FIELD'S OWN ACTIONS GO TO THE TOP WHEN IT IS A TABLE, and this was
   found by rendering the screen rather than by reading the markup. `.template-row`
   centres its action column, which is right for a one-line field — and on a
   table field the panel is hundreds of pixels tall, so the FIELD's Remove ended
   up level with the grid's own row Remove buttons. Three controls reading
   "Remove" at the same height, deleting a column, a row and the whole test grid.
   Aligned to the top it reads as belonging to the field, which is what it does. */
.template-row:has(> .template-row-content > .validation-table:not([hidden])),
.template-row:has(> .template-row-content > .template-items:not([hidden])) {
  align-items: start;
}

.validation-table {
  width: 100%;
  min-width: 0;
  margin-top: 1rem;
  padding: 0.75rem;
  border: 1px solid var(--border-color);
  border-radius: 6px;
  background: var(--bg-secondary);
}

.validation-table-heading {
  margin: 0.75rem 0 0.5rem;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-secondary);
}
.validation-table-heading:first-of-type { margin-top: 0; }

.validation-table-column {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}
.validation-table-column .field-input { flex: 1 1 8rem; min-width: 0; width: auto; }
.validation-table-column select.field-input { flex: 0 0 auto; }

/* A block of its own under whatever precedes it, so it needs a top margin of
   its own — the rule `button_spacing_spec` applies to every non-flex-child
   button container. */
.validation-table-add {
  display: flex;
  gap: 8px;
  margin-top: 0.75rem;
  margin-bottom: 0.75rem;
}

/* ⚠ THE GRID SCROLLS RATHER THAN SQUEEZING. Twenty columns on a tablet is the
   width this editor has to survive, and a table that shrinks its cells instead
   puts an author's target value behind an ellipsis. */
.validation-table-scroll {
  overflow-x: auto;
  margin-bottom: 0.25rem;
}

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

/* ⚠ THIS PADDING IS WHAT EXEMPTS `.template-row-actions` INSIDE A CELL from
   declaring a margin of its own, and `button_spacing_spec` asks the stylesheet
   whether it exists rather than taking it on trust. Deleting it turns the
   buttons in the last column back into a finding, which is the point. */
.validation-table-grid th,
.validation-table-grid td {
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--border-color);
  text-align: left;
  vertical-align: middle;
}

.validation-table-grid th {
  background: var(--bg-primary);
  font-weight: 600;
  white-space: nowrap;
}

.validation-table-grid .field-input {
  width: 100%;
  max-width: none;
  min-width: 6rem;
}

.validation-table-tolerance .field-input,
.validation-table-tolerance-head { min-width: 5rem; }

/* An editable column is the tech's to fill, so the builder shows what will be
   there rather than an input the author might type a target into by mistake. */
.validation-table-editable {
  display: inline-block;
  padding: 0.25rem 0;
  color: var(--text-secondary);
  font-style: italic;
  font-size: 0.75rem;
  white-space: nowrap;
}

.validation-table-options {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1.25rem;
  margin-top: 0.75rem;
}

.validation-table-auto {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1rem;
  margin-top: 0.75rem;
}
.validation-table-auto .checklist-control { width: auto; flex: 0 0 auto; }
.validation-table-auto .form-hint { flex: 1 1 14rem; margin: 0; }

.validation-table-auto-label {
  display: block;
  font-size: 0.75rem;
  color: var(--text-secondary);
  margin-bottom: 0.25rem;
}

@media (max-width: 700px) {
  .validation-table-column .field-input { flex: 1 1 100%; }
}
