/* ReSouris — Screen-specific styles (BPP-10 WP-10.2 onward)
 *
 * Patterns shared across the 8 primary screens: forms, cards, tables,
 * buttons, badges, empty-states. Built on the tokens.css vocabulary;
 * no hard-coded colours.
 *
 * a11y baselines:
 *   * Visible focus ring inherits from layout.css :focus-visible rule
 *   * Touch targets ≥ 44 × 44 px for interactive elements
 *   * Body text 16 px; line-height 1.5
 *   * Form labels are always present + associated via `for`
 */

/* ---- Buttons ---- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--space-2) var(--space-5);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: var(--line-tight);
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  text-decoration: none;
  transition: background-color var(--motion-fast) var(--ease-standard),
              border-color var(--motion-fast) var(--ease-standard),
              transform var(--motion-fast) var(--ease-standard),
              box-shadow var(--motion-fast) var(--ease-standard);
}

/* v1.4.2 idiom — subtle scale on interactive press. Reduced-motion
   respected via --motion-fast collapsing to 0ms. */
.btn:active:not(:disabled) {
  transform: translateY(1px) scale(0.995);
}

/* v1.4.2 idiom — aria-flavoured focus ring with soft glow. Uses
   :focus-visible so mouse clicks don't paint the ring but keyboard
   focus does. */
.btn:focus-visible {
  outline: none;
  box-shadow: var(--focus-ring-aria);
}

.btn--primary {
  background: var(--accent);
  color: var(--accent-on);
}
.btn--primary:hover { filter: brightness(0.92); }   /* darken-on-light, tint-on-dark */

.btn--secondary {
  background: transparent;
  color: var(--text-primary);
  border-color: var(--border-strong);
}
.btn--secondary:hover { background: var(--surface-raised); }

.btn--ghost {
  background: transparent;
  color: var(--text-secondary);
  border-color: transparent;
}
.btn--ghost:hover { background: var(--surface); color: var(--text-primary); }

.btn--disabled,
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* ---- Form fields ---- */

/* ⚠️ `.form-grid` HAD NO `display: grid` UNTIL WP-P51.A4, ANYWHERE.
 *
 * The only rule that ever mentioned columns was a phone media query further
 * down setting `grid-template-columns: 1fr` — which does nothing without a
 * grid. So `.form-field--wide` and `.form-actions { grid-column: 1 / -1 }`,
 * both written into markup and CSS across 41 templates, had never had any
 * effect, and every form in the product rendered as a single column that
 * `.entity-form` then capped at 520px.
 *
 * The result read fine on a phone, which is why it survived: it WAS a phone
 * layout, served to every browser. Measured on the Room Details tab at
 * 1440px — 520px of an 1130px card, 749px tall, six fields stacked; after,
 * 1082px and 412px.
 *
 * `auto-fit` + `minmax` rather than a breakpoint, so the column count follows
 * the space actually available: one column on a phone (unchanged), more when
 * there is room, and nothing to re-tune when the sidebar collapses or a card
 * is nested inside another.
 */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4) var(--space-5);
  align-items: start;
}

/* ⚠️ ANYTHING THAT IS NOT A FIELD SPANS THE ROW.
 *
 * These containers also hold headings, hints, flash messages and the odd
 * nested table. Under the old block layout those filled the width; as grid
 * items they would each be squeezed into one narrow column. Spanning is the
 * only safe default for a rule being switched on under 41 templates at once. */
.form-grid > *:not(.form-field) { grid-column: 1 / -1; }

/* The grid's `gap` owns the spacing now; the per-field margin would add to it
 * and reintroduce the vertical sprawl this change removes. */
.form-grid > .form-field { margin-bottom: 0; min-width: 0; }

/* Finally meaningful — this class has been in the markup for a year. */
.form-field--wide { grid-column: 1 / -1; }

/* Partial spans, for a field that needs more than one track but not the row —
 * a name beside two selects rather than alone above them. */
.form-field--span-2 { grid-column: span 2; }
.form-field--span-3 { grid-column: span 3; }
@media (max-width: 720px) {
  /* Below two comfortable tracks there is nothing to span; a `span 2` on a
   * one-column grid would otherwise overflow the container. */
  .form-field--span-2,
  .form-field--span-3 { grid-column: 1 / -1; }
}

/* ------------------------------------------------------- rows layout (v1.68)
 * Label and value SIDE BY SIDE, with the help text behind a disclosure.
 *
 * Marcus, 2026-10-01: *"Each field appears to be in a square tile. This may be
 * OK for a phone layout but does not look good for a wide screen... the field
 * and value should be side by side with some sort of icon that allows the help
 * text to be shown if asked for. This type of design saves a lot of screen
 * space."*
 *
 * ⚠️⚠️ **A MODIFIER, NOT A CHANGE TO `.form-grid`.** `.form-field` appears in
 * 74 templates and `.form-grid` in 55. Re-laying those out globally would
 * restyle every form in the product from one CSS edit, which is the exact
 * failure `tests/ui/test_widget_styling_is_not_page_scoped.py` exists to
 * catch. Opt in per form with `form-grid--rows`.
 *
 * ⚠️ Below 720px it REVERTS to the stacked layout, because side-by-side is the
 * thing that does not work on a phone — which is the half of Marcus's
 * observation that says the tiles were not wrong, just wrong on a wide screen.
 */
.form-grid--rows {
  display: block;
}

/* ⛔ **THIS CLASS HAD NO RULE WHEN IT WAS INTRODUCED.** Added with the markup
   and styled nowhere — the "a class name is not a style" defect, which this
   stylesheet has now met twice in one day. v1.91.
   One row, primary first, separated from the fields by the single rule the
   panel still has. */
.entity-form__actionbar {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin: 0 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border-subtle, var(--border));
}

/* ⚠️ Reclaims the gap between the tabs and the first control. Scoped with
   `:has()` so the other tabbed screens keep their 20px — `.entity-tabpanel`
   is shared, and changing its padding globally to tighten one panel is the
   mistake `test_widget_styling_is_not_page_scoped.py` watches for. */
.entity-tabpanel:has(.entity-form__actionbar) {
  padding-top: var(--space-2);
}
.form-grid--rows > .form-field {
  display: grid;
  grid-template-columns: minmax(10rem, 15rem) minmax(0, 1fr);
  align-items: baseline;
  column-gap: var(--space-5);
  row-gap: var(--space-1);
  padding: var(--space-1) 0;
  /* ⚠️ **NO ROW RULE.** v1.89 removed the 16px margin ON THE GROUNDS that the
     border separated the rows; Marcus, 2026-10-02: *"can we regain some
     height by removing the horizontal line under the field"*. So both are
     gone, and the structure now comes from the label column's left edge and
     the inputs' own borders rather than from a line per row. ⓘ If the rows
     ever read as a single block, the honest fix is to put ONE separator back
     between field GROUPS, not between every field. */
  /* ⚠️ **TWO SEPARATORS WERE DOING ONE JOB.** `.form-field` carries
     `margin-bottom: var(--space-4)` for the TILED layout this replaced, where
     each field was a free-standing block and needed to stand apart. v1.68 made
     them bordered rows — the rule already separates them — but the margin was
     never overridden, so every row read as line, gap, field. With the padding
     that was ~32px of chrome per row before any content, over eleven fields.
     v1.89, Marcus: *"can we reduce some of the vertical spacing on the field
     rows."*
     ⛔ Scoped to this layout. `.form-field` is used by 74 templates and
     changing its own margin to fix one panel is what
     `tests/ui/test_widget_styling_is_not_page_scoped.py` exists to catch. */
  margin-bottom: 0;
}

/* ⚠️⚠️ **THE ROW HEIGHT WAS AN ACCESSIBILITY LEVEL THIS PRODUCT DOES NOT
   CLAIM.** After v1.89/v1.90 a row is 52px = a 44px control + 4 + 4. The
   padding is spent; the height IS `min-height: 44px`.

   **44×44 is WCAG 2.5.5 — Target Size (Enhanced), level AAA.**
   `docs/accessibility-conformance.md` claims **WCAG 2.2 AA**, whose criterion
   is **2.5.8 — Target Size (Minimum), 24×24px**. The form was carrying 20px
   per row for a level the product neither claims nor tests.

   ✅ And the sheet already intended this. The touch block below says so:
   *"Desktop pointer:fine keeps the tight 28/32px sizes"* — but the
   unconditional `.form-field input` rule gives 44px on EVERY pointer, so
   desktop never got it and the coarse block was doing nothing for inputs.
   This does not weaken the design; it makes the sheet do what it says.

   Measured over 11 rows: 36px→88 saved, **32px→127**, 28px→148, 24px→154.
   ⚠️ 28 and 24 both yield a **39px row** — below ~32 the LABEL becomes the
   tallest thing and further shrinking spends accessibility for nothing.
   **32 is the knee, not a compromise.** v1.91, Marcus: *"I think 32 looks
   great."*

   ⓘ Checkboxes are included deliberately. A checkbox PAINTS at its intrinsic
   22×22 centred in whatever box it is given — measured — so this shrinks the
   box, not the control, and keeps every row the same height.
   ⛔ The global `.form-field` rule is untouched: 74 templates. Lowering that
   is v1.92, deliberately not done on the day of a demo. */
.form-grid--rows > .form-field input,
.form-grid--rows > .form-field select,
.form-grid--rows > .form-field textarea {
  min-height: 32px;
  padding-top: var(--space-1);
  padding-bottom: var(--space-1);
}
.form-grid--rows > .form-field--checkbox {
  min-height: 32px;
}

/* ⛔⛔ **DON'T SHOW A FIELD THAT CANNOT DO ANYTHING.** "Handover overlap" is
   inert unless "Allow split shifts" is ticked. Marcus, 2026-10-02: *"maybe it
   should even not be shown at all… don't show fields that are irrelevant."*

   ✅ Pure CSS, via the adjacent sibling — so it reveals on tick **with no JS
   at all**, which the previous opacity-based toggle could not do.
   ⚠️ Checked before hiding: **0 of 28 clinics** hold a non-zero
   `handover_minutes` with split shifts off, so this conceals no live value.
   The handler's `disabled` input is simply not submitted and defaults to 0.
   ⛔ `display: grid` is the correct restore — the 720px fallback changes only
   `grid-template-columns`, never `display`. */
.form-grid--rows > .form-field--dependent {
  display: none;
}
.form-grid--rows > .form-field:has(input[type="checkbox"]:checked) + .form-field--dependent {
  display: grid;
}

/* ⚠️ The label and its ⓘ must be ONE grid item. Left as siblings they took
   cells 1 and 2, which pushed the input onto the next row — measured
   label_right 504 against input_left 264, i.e. not side by side at all, which
   is the entire point of this layout. */
.form-grid--rows > .form-field > .form-field__labelrow {
  display: flex;
  align-items: center;
  padding-top: 0.35rem;
  min-width: 0;
}
.form-grid--rows > .form-field > .form-field__labelrow > label { font-weight: 500; }

/* ⚠️⚠️ **A CHECKBOX ROW MUST NOT USE THE TEXT-INPUT'S BASELINE OFFSET.**
   v1.90. Marcus, 2026-10-02: *"the labels for the checkboxes are too low with
   4+4."* He is right, and the cause is the `padding-top: 0.35rem` directly
   above: it exists to drop the label onto the text baseline of an `<input
   type="text">`, which sits low inside a 48px-tall control. A checkbox is
   ~16px and sits at the TOP of its track, so the same offset pushes the label
   below it — and tightening the row padding to 4+4 removed the slack that had
   been hiding the mismatch.

   ⛔ The fix is per-ROW, not a smaller global offset: shrinking 0.35rem would
   misalign all five text rows to fix four checkbox ones. `:has()` is already
   used 21 times in this stylesheet, and `.form-field__control` wraps a
   checkbox and nothing else (measured: 4 of 4, in one template). */
.form-grid--rows > .form-field:has(> .form-field__control) {
  align-items: center;
}
.form-grid--rows > .form-field:has(> .form-field__control) > .form-field__labelrow {
  padding-top: 0;
}
.form-grid--rows > .form-field > label { padding-top: 0.35rem; }

/* ⚠️⚠️ **A CHECKBOX IS A FIELD LIKE ANY OTHER: LABEL LEFT, CONTROL RIGHT.**
   v1.69. The first attempt moved the whole checkbox row into the value column,
   which left the label column EMPTY on those rows — Marcus: *"the checkboxes
   look wrong for a human. The labels should all be in the first column along
   with the help text icon."* He is right: a reader scans the label column
   downwards, and a field that contributes nothing to it breaks the scan.

   The template now lifts the `<input>` out of its `<label>` (the `for`/`id`
   pair already existed, so the label stays clickable) and puts it in
   `.form-field__control`, which lands in track two beside every other value. */
/* ⚠️ **ANY EXTRA CHILD BELONGS IN THE VALUE COLUMN, NOT UNDER THE LABEL.**
   v1.71. A `.form-field` can hold more than label + control: the colour field
   also carries `.sl-preview`, a worked example of the chosen colour. Grid
   auto-placement put the swatches in track two and then wrapped the preview
   onto the next row's track ONE — so the example sat under the words "Roster
   timeline colour" instead of under the colours it is an example of.

   Marcus: *"the example Roster timeline colour three panel should be shown
   UNDER the colours."*

   Pinning every non-label child to track two fixes this field and any future
   one that carries a preview, a unit suffix or a second control. The label
   row is excluded by name so it keeps track one. */
.form-grid--rows > .form-field > *:not(.form-field__labelrow):not(label) {
  grid-column: 2;
}

.form-grid--rows > .form-field > .form-field__control {
  display: flex;
  align-items: center;
  min-height: 2.1rem;
}
.form-grid--rows .checkbox-label { margin: 0; font-weight: 500; }

/* ⚠️ **A DISABLED CONTROL'S LABEL MUST LOOK DISABLED TOO.** Marcus, 2026-10-07:
   on the space's Estate Placement tab, *"'Offer to the availability pool' is
   black text when it is disabled"*. The input greys itself; the `<label>` is a
   separate element and kept full-contrast body colour, so a read-only view
   showed a crisp, confident label beside a control nobody can touch -- which
   reads as "you may change this" and is the one thing it means to deny.

   ⓘ `--text-disabled` is the token the placeholder colour already uses, so this
   adds no new value to keep in step. Both sibling orders are covered because the
   two markup shapes differ: the checkbox rows put the input FIRST, the labelrow
   pattern puts the label first.

   ⛔⛔ **AND THE THIRD SHAPE IS A CHILD OF A SIBLING, WHICH EVERY SELECTOR
   ABOVE MISSES.** v1.118 B shipped the five selectors above and verified them
   on `.form-field__row`, where the input really is the label's sibling. On the
   `__labelrow` + `__control` shape the input is `.form-field__control > input`
   -- a child of a sibling -- so `~ input:disabled` does not match it and the
   label stayed full-contrast. MEASURED 2026-10-07 on
   `/ui/admin/service-lines/<id>/edit?mode=view`: the disabled "Allow split
   shifts" label computed `rgb(15, 23, 42)` (`--text-primary`) while the space
   page's `__row` equivalent correctly computed `rgb(148, 163, 184)`.

   ⚠️ So the screen Marcus held up as the design clue had the very defect he
   raised, and B's own verification could not see it because it only looked at
   the screen that reported it. This rule is now what makes v1.118 D safe:
   converting a `__row` to `__control` without it RE-BREAKS the complaint.
   `tests/ui/test_field_contract.py` pins all three shapes.

   ⛔⛔ **AND THE RULE NAMED `input` ONLY, SO A DISABLED SELECT OR TEXTAREA KEPT
   ITS BLACK LABEL.** Marcus reported this on a checkbox, so `input` answered
   the instance and missed the defect. MEASURED 2026-10-07 in view mode, after
   the `__control` fix above: six labels still computed `rgb(15, 23, 42)` --
   Location, Building, Floor, Area and Assignment on the space's Estate
   Placement tab (all `<select>`), and "Description (optional)" on a competency
   (`<textarea>`). ⚠️ On Estate Placement that left FIVE black labels sitting
   directly above the one correctly greyed checkbox, which reads as the
   checkbox being the broken one.

   ⓘ `:is()` keeps the list to one selector per shape rather than tripling it,
   and takes its specificity from its most specific argument -- all three
   element selectors are (0,0,1), so the arithmetic is unchanged. */
.form-field :is(input, select, textarea):disabled ~ label,
.form-field :is(input, select, textarea):disabled + label,
.form-field label:has(~ :is(input, select, textarea):disabled),
.form-field__labelrow:has(~ :is(input, select, textarea):disabled) label,
.form-field__labelrow:has(~ .form-field__control :is(input, select, textarea):disabled) label,
label:has(> :is(input, select, textarea):disabled) {
  color: var(--text-disabled);
}

/* ⚠️ A DEPENDENT FIELD SHOULD LOOK DEPENDENT. "Handover overlap" is only
   live when "Allow split shifts" is ticked — the JS already fades it to 0.5
   and disables the input, but nothing says WHICH field it belongs to, so it
   reads as an unrelated greyed row. Indenting it under its parent, with a
   rule down the left, says "child of the thing above" without a word of
   copy. v1.91, Marcus: *"it should look like it is indented under the
   Parent."* */
.form-grid--rows > .form-field--dependent > .form-field__labelrow {
  padding-left: var(--space-4);
  border-left: 2px solid var(--border-subtle, var(--border));
  margin-left: 2px;
}

/* The hint is no longer a third line under every field: it is revealed by the
 * ⓘ control beside the label, and when shown it sits under the VALUE column
 * so the label column stays a clean vertical edge. */
.form-grid--rows .form-field__hint {
  grid-column: 2;
  margin-top: var(--space-1);
  /* ⚠️ **THE 62ch MEASURE IS LIFTED HERE, DELIBERATELY.** v1.70. Marcus:
     *"the help text should not just wrap within the space of the old tile but
     should wrap in the space under the field. This means it will typically not
     use too much vertical space."*

     Measured: the value column is ~836px and the hint was capped at 401px, so
     it wrapped to FOUR lines in a space that fits it in two. The 62ch cap
     exists to stop a hint running to 1000px+ in the stacked layout where it
     spans the whole card — a good rule there, and the wrong one here, because
     this column is already the constraint.

     ⛔ The global `.form-field__hint { max-width: 62ch }` is untouched; only
     this layout opts out. */
  max-width: none;
}
.form-field__help {
  /* ⚠️⚠️ **flex-shrink DEFAULTS TO 1, AND THAT MAKES A CIRCLE AN OVAL.**
     v1.72. `.form-field__labelrow` is a flex container. When its label wraps
     to a second line the row needs room, and the only shrinkable item is this
     button — so the browser took 3px off its WIDTH while `height` held, and a
     border-radius: 50% box became an ellipse.

     Measured on the Clinic General tab: eight icons at 18.4 x 18.4 and one at
     15.4 x 18.4, ratio 0.84 — the one beside "Trust plans patient numbers for
     this Clinic", which is the only label on the page that wraps.

     ⚠️ Marcus spotted this by eye. It is invisible in a grep and nearly
     invisible in a screenshot; the measurement is the aspect ratio. */
  flex: 0 0 auto;
  appearance: none;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-secondary);
  border-radius: 50%;
  width: 1.15rem;
  height: 1.15rem;
  line-height: 1;
  font-size: 0.72rem;
  font-weight: 700;
  cursor: pointer;
  margin-left: var(--space-2);
  padding: 0;
}
.form-field__help:hover { color: var(--text-primary); border-color: var(--text-secondary); }
.form-field__help:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.form-field__help[aria-expanded="true"] { background: var(--surface-raised); color: var(--text-primary); }
.form-grid--rows .form-field__hint[hidden] { display: none; }

@media (max-width: 720px) {
  /* ⚠️ Phones keep the stacked layout — see the note above. */
  .form-grid--rows > .form-field {
    grid-template-columns: 1fr;
    column-gap: 0;
  }
  .form-grid--rows .form-field__hint { grid-column: 1; }
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
}

.form-field label {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
}

/* Required-field indicator (UX baseline, BPP-29 follow-up).
 *
 * Any .form-field whose first interactive element carries the HTML
 * `required` attribute (or aria-required="true") auto-gets a red
 * asterisk after its label. No template changes needed — the rule
 * keys off `:has()` which is supported across all modern browsers
 * (Chrome 105+, Safari 15.4+, Firefox 121+).
 *
 * The asterisk is decorative; screen readers already announce
 * `required` / `aria-required` so we hide it from AT. The colour
 * mirrors `--danger` so it reads as "attention" not "error".
 * Authors who need to flag a non-input field (e.g. a fieldset, a
 * grouped radio set) can opt-in by adding `form-field--required`
 * on the wrapper. */
.form-field:has(> input[required]) > label::after,
.form-field:has(> select[required]) > label::after,
.form-field:has(> textarea[required]) > label::after,
.form-field:has(> input[aria-required="true"]) > label::after,
.form-field:has(> select[aria-required="true"]) > label::after,
.form-field:has(> textarea[aria-required="true"]) > label::after,
.form-field--required > label::after,
/* ⛔⛔ **THE ROWS LAYOUT BROKE EVERY ASTERISK AND NOTHING SAID SO.** The
   selectors above need the label to be a DIRECT child of `.form-field`.
   v1.68 wrapped it: `.form-field > .form-field__labelrow > label`. So from
   that day the rule matched nothing on this layout — measured on the Clinic
   General tab, `Name`, `State` and `Priority` are all `required` and all
   three had `::after { content: none }`.
   ⚠️ Meanwhile base.html kept auto-injecting "* indicates a required field",
   because that JS only counts `[required]` inputs. The page has been
   explaining a convention it does not use. Marcus, 2026-10-02: *"the *
   indicates a required field I don't think is needed… taking up space."* —
   the legend was the visible symptom of a missing marker. */
.form-field:has(> input[required]) > .form-field__labelrow > label::after,
.form-field:has(> select[required]) > .form-field__labelrow > label::after,
.form-field:has(> textarea[required]) > .form-field__labelrow > label::after,
.form-field:has(> input[aria-required="true"]) > .form-field__labelrow > label::after,
.form-field:has(> select[aria-required="true"]) > .form-field__labelrow > label::after,
.form-field:has(> textarea[aria-required="true"]) > .form-field__labelrow > label::after {
  content: " *";
  color: var(--danger, #dc2626);
  font-weight: 700;
  margin-left: 2px;
}

/* ⛔ **THE LEGEND BLOCK IS DELETED (2026-10-14).** `.form-required-legend`
   styled a paragraph `base.html` injected into every form — "* indicates a
   required field". Marcus asked for it gone: *"It takes up vertical space
   unnecessarily. It is obvious to a worker what this means. You can remove it
   everywhere."*

   ⓘ He had asked once before, on 2026-10-02 — *"the * indicates a required
   field I don't think is needed… taking up space"* — and the note directly
   above still records that request while the legend went on rendering. Asking
   twice is the cost of fixing a symptom and leaving the cause.

   ✅ The asterisk rule above STAYS: that is the actual marker, and with
   `required` / `aria-required` on the controls the required state is still
   conveyed programmatically. Only the sentence explaining it is gone. */

/* v1.92 — the height comes from the token, which a coarse pointer raises to
   44px. ⛔ Not a literal: see the warning on --control-min-h in tokens.css. */
.form-field input,
.form-field select,
.form-field textarea {
  min-height: var(--control-min-h);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-base);
  font-family: inherit;
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  transition: border-color var(--motion-fast) var(--ease-standard);
}
.form-field input:hover,
.form-field select:hover,
.form-field textarea:hover {
  border-color: var(--border-strong);
}
/* ⚠️ A DISABLED FIELD LOOKED EXACTLY LIKE AN EDITABLE ONE, everywhere in the
   product. Only `.btn:disabled` had a rule, so every `?mode=view` page --
   admin_room_edit, admin_equipment_edit, and now the estate -- presented a
   full set of boxes with the normal white background, the normal ink and the
   normal border, which silently refused typing. The only difference was the
   cursor, and only once you were already hovering over it.
   Measured 2026-09-01: view and edit computed identical bg, colour and border.

   A read-only page that looks like a form is worse than one that plainly is
   not, because it invites the attempt. Same reasoning admin_room_edit already
   records for its immutable facts -- "a value you cannot change is not a
   control" -- applied to the state rather than to individual fields. */
.form-field input:disabled,
.form-field select:disabled,
.form-field textarea:disabled {
  background: var(--surface-raised);
  color: var(--text-secondary);
  border-color: rgba(15, 23, 42, 0.06);
  cursor: not-allowed;
}

.form-field input::placeholder { color: var(--text-disabled); }

.form-field__hint {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  /* ⚠️ The reading measure belongs on the TEXT, not on the layout.
   *
   * These hints used to wrap at the input's edge because `.entity-form` caps
   * the whole form at 520px — a limit that exists to keep prose readable, but
   * applied to the controls as well. Now that a form can use the full card,
   * an unconstrained hint would run to 1000px+ and be worse. 62ch keeps the
   * line length a hint wants without constraining the fields around it. */
  max-width: 62ch;
}

/* v1.2 follow-up: checkbox fields. Checkbox + label sit on the SAME
   row, vertically centred; the optional hint wraps to the next line. */
.form-field--checkbox {
  /* Reserve the same vertical baseline as the date/select rows in the same
     form-grid so columns line up. ⚠️ v1.92: this MUST track the control
     token rather than carry its own number — v1.91 measured what happens
     when it does not, and the checkbox rows sat 12px taller than every
     other row. */
  min-height: var(--control-min-h);
  justify-content: center;
}
.form-field--checkbox .form-field__row {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}
.form-field--checkbox input[type="checkbox"] {
  appearance: auto;
  width: var(--control-target-min);
  height: var(--control-target-min);
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  cursor: pointer;
  flex: 0 0 auto;
}
.form-field--checkbox input[type="checkbox"]:hover {
  border-color: var(--accent);
}
.form-field--checkbox label {
  cursor: pointer;
  margin: 0;
  line-height: 1;
}

/* Global default for unscoped checkboxes. Scoped rules with higher
   specificity (.form-field--checkbox, .entity-table__check) still win where
   they apply — ⓘ and since v1.92 all three take the same token, so "which one
   wins" no longer changes the answer. */
input[type="checkbox"] {
  width: var(--control-target-min);
  height: var(--control-target-min);
  cursor: pointer;
}

.field-error {
  padding: var(--space-2) var(--space-3);
  background: rgba(248, 113, 113, 0.12);   /* var(--danger) at 12% */
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  margin-bottom: var(--space-4);
}

/* ---- Auth shell (sign-in centred card) ---- */
.auth-shell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 60vh;
}

.auth-card {
  width: 100%;
  max-width: 420px;
  padding: var(--space-8);
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

.auth-card__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-2xl);
  font-weight: 700;
  color: var(--text-primary);
}

.auth-card__subtitle {
  margin: 0 0 var(--space-6);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.auth-form__actions {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-6);
  flex-wrap: wrap;
}

/* ---- Page header (used by tenant overview + future screens) ---- */
.page-header {
  margin-bottom: var(--space-6);
}

.page-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-3xl);
  font-weight: 700;
  color: var(--text-primary);
  letter-spacing: -0.01em;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.page-title__icon {
  display: inline-flex;
  flex: 0 0 auto;
  width: 1.05em;
  height: 1.05em;
  color: var(--accent);
}
.page-title__icon svg { width: 100%; height: 100%; }

.page-subtitle {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.text-muted { color: var(--text-secondary); }

/* ---- Colleague claims on the attendance confirmation list ----
   What a colleague has already recorded for a shared session, shown BEFORE the
   worker types their own. ⚠️ Narrow and quiet on purpose: it is context for the
   two number fields beside it, not a column competing with them for attention.
   The cell can hold several names — up to 25 clinicians have shared one list on
   this estate — so the list must not stretch the row's height unnecessarily. */
.attendance-colleagues {
  max-width: 18rem;
  font-size: var(--text-sm);
}

.attendance-colleagues__list {
  margin: 0 0 var(--space-1);
  padding-left: var(--space-4);
}

.attendance-colleagues__list li { margin: 0; }

/* The "which activity?" question, asked only where a line offers several.
   Sits under the treatments field it qualifies rather than in a column of its
   own — it is part of that answer, not a separate one. */
.attendance-activity {
  margin-top: var(--space-2);
}

.attendance-activity select {
  max-width: 12rem;
}

/* ⚠️ `.attendance-overrun` WAS REMOVED WITH THE DIV IT STYLED, and both of its
   stated reasons were wrong.

   It gave `margin-top` to a wrapper stacking the overrun input under the
   treatments field, on the rationale that it *"qualifies that answer rather
   than being a separate question"*. A duration does not qualify a count. That
   stacking is what put three unrelated questions — a number, a choice and a
   duration — inside one cell under a header reading "My treatments", with no
   `<th>` on the page naming the overrun at all. It has its own column now, in
   the numeric run beside Booked, so there is nothing to indent.

   The comment also claimed the field was *"Labelled 'out of hours' to the
   worker"*. It never was: the label read "Ran over". The claim came from
   Marcus's note about how workers TALK about it, recorded as though it were
   about what the screen says.

   `.attendance-activity` above keeps its margin and its reasoning, which is
   sound for it: the activity genuinely does qualify the treatments figure
   ("six treatments — of which activity?") and stays in that cell. */

/* ---- Cards + card grid ---- */
.card {
  padding: var(--space-6);
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-6);
  transition: box-shadow var(--motion-normal) var(--ease-standard),
              transform var(--motion-normal) var(--ease-standard);
}

/* v1.4.2 idiom — hover shadow lift on interactive cards. Only
   applies to cards that carry the `card--interactive` modifier so
   read-only summary cards don't wiggle when the user's mouse crosses
   them. */
.card--interactive:hover {
  box-shadow: var(--shadow-hover);
  transform: translateY(-1px);
}

/* Card header row: title on left, action button on right. Layout-only;
   inside the card body keep the original padding. */
.card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}
.card__header h2,
.card__header h3 {
  margin: 0;
}

/* ---- Modal (<dialog>) ----
   Used by /ui/admin/staff?view=fixed-commitments "Add commitment".
   Native <dialog> centring; bordered card body inside a dimmed backdrop. */
.app-modal {
  width: min(720px, 92vw);
  max-height: 88vh;
  padding: 0;
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}

/* A modal that holds a table rather than a form. 720px is right for a column
   of fields and too narrow for a members list with its own actions column,
   where the default squeezes the email and the date against each other. */
.app-modal--wide { width: min(960px, 94vw); }

/* ⚠️ `form-inline` WAS USED BY TWO TEMPLATES AND DEFINED BY NOBODY. Both pages
   wrote it expecting a label, a control and a button on one line; with no rule
   behind it they stacked, so "Add a member" sat above a full-width select
   above its own Add button, taking three rows to say one thing.

   Found by opening the page, not by reading it: the markup looked correct and
   the class name looked like a house utility. It is now. */
.form-inline {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-2);
  margin: 0;
}
/* A field inside an inline form must not claim the full row width -- that is
   what pushed the button onto its own line. */
.form-inline .form-field { margin: 0; }
.app-modal::backdrop {
  background: rgba(15, 23, 42, 0.55);
  backdrop-filter: blur(2px);
}
.app-modal__body {
  display: flex;
  flex-direction: column;
  max-height: 88vh;
  margin: 0;
}
.app-modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-5) var(--space-6);
  border-bottom: 1px solid var(--border-subtle);
}
.app-modal__header h2 {
  margin: 0;
  font-size: var(--text-lg);
}
/* Close X in a modal header — larger hit target + readable glyph.
   Targets only the small ghost button in the header (the close X is
   always the second child after the <h2>). */
.app-modal__header > button.btn--small {
  font-size: 22px;
  line-height: 1;
  padding: 4px 10px;
  min-width: 36px;
  min-height: 36px;
}
/* ⛔⛔ **DO NOT INSERT A RULE BETWEEN THESE SELECTORS.** This is ONE selector
   list spanning several lines, and on 2026-10-15 a new `.details--spaced` rule
   was slotted in just above the final selector — which silently made
   `.form-hint` and `.form-grid` members of THAT rule instead. Every modal in
   the product lost its horizontal gutter: measured on the Add-holiday dialog,
   the header and footer were inset 24px and the form grid sat flush at 0,
   because `.form-grid`'s own `padding: 0` was all that remained.

   ⚠️ It passed every static check. `tests/arc/test_css_classes_resolve.py`
   asks whether a class RESOLVES, and `.form-grid` still resolved — to the
   wrong rule. Marcus found it by looking at the screen, which is the whole
   argument for auditing rendered output rather than source.

   ⓘ `.app-modal__gutter` is the opt-in for anything ELSE you put in a modal
   body: `.app-modal` is `padding: 0`, so a bare child — a paragraph holding a
   button, say — sits flush against the wall. House style §7 said so and left
   the next person to invent a class; two templates reached for an inline
   `style="padding:0 var(--space-6)"` instead. */
.app-modal__body > .form-hint,
.app-modal__body > .form-grid,
.app-modal__body > .app-modal__gutter {
  padding: 0 var(--space-6);
}

/* ⚠️ **AND THE BODY NEEDS VERTICAL ROOM, NOT ONLY HORIZONTAL.** Measured on
   the same dialog: the gap from the field grid to the footer was **0px**, so
   the last input sat directly on the button bar. The header supplies the space
   above the grid through its own padding; nothing supplied the space below. */
.app-modal__body > .form-grid:last-of-type {
  margin-bottom: var(--space-4);
}
.app-modal__body > .app-modal__gutter {
  margin-top: var(--space-3);
}

/* A disclosure tucked under a flash or summary line. ⚠️ Was an inline
   `style="margin-top:6px"` on the roster-area import summary — invisible to
   every static sweep, unthemeable, and a raw 6px where the scale has a token. */
.details--spaced {
  margin-top: var(--space-2);
}
/* ⚠️ A CLASS THE TEMPLATE ASKED FOR AND NOTHING DEFINED. `estate-note` was
   referenced on the "Held by the trust" paragraph and had no rule anywhere, so
   it inherited `.form-hint`'s 72ch cap and wrapped well short of the card --
   Marcus, 2026-08-29: "does not correctly use the full width of the panel and
   wraps too early".

   ⚠️ AND THEN THIS RULE SPENT TWO DAYS NOT APPLYING EITHER, WHICH IS WHY IT WAS
   REPORTED TWICE. It was written as `.app-modal__body > <comment> .estate-note`:
   comments are stripped at tokenisation, so the selector resolved to
   `.app-modal__body > .estate-note` and matched only inside a modal, which is
   the one place this paragraph never appears. Marcus, 2026-08-31, reporting the
   identical symptom: "text that does not wrap correctly. It needs to use the
   full width of the panel". A rule can be present, correct and dead, and only
   computed style can tell you which.

   ⚠️ AND IT IS `.estate-note.form-hint`, NOT `.estate-note`, FOR A SECOND
   REASON. The paragraph carries both classes; at equal specificity the later
   rule in the file wins, and `.form-hint`'s own `max-width: 72ch` is ~2300
   lines further down. Even with the selector repaired, the single-class version
   loses on order alone. Two classes beat one wherever this block is moved to.

   72ch is right for guidance sitting beside a form field, where a long line
   would drag the eye away from the input. This is a two-sentence note spanning
   a full-width card with nothing beside it, so the CARD is the correct bound
   and a second, tighter one just leaves the right half empty. */
/* The site's address, and the map of it (MIG-305). `<address>` is italic by
   UA default, which reads as an aside rather than as the fact the panel is
   for. */
.estate-address {
  font-style: normal;
  line-height: 1.5;
  margin: 0 0 12px;
  color: var(--text-primary);
}
/* ⚠️ A HEIGHT, NOT AN ASPECT RATIO. An iframe with no height collapses to the
   HTML default of 150px, which crops a map to a strip of road and looks broken
   rather than small. 320px shows a campus and its approaches at the bbox
   `_map_urls` builds. */
.estate-map {
  display: block;
  width: 100%;
  height: 320px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  /* ⚠️ `--surface-subtle` IS NOT DEFINED ANYWHERE — the one other use of it, on
     `.badge--draft`, carries a fallback and so survives. Without one the whole
     declaration computes to its initial value, which is a transparent
     background: the placeholder behind a map that has not loaded yet would show
     the page through it rather than reading as a pending tile. Caught by
     test_every_no_fallback_custom_property_is_defined, which is exactly the
     class of defect it exists for. */
  background: var(--surface-raised, var(--surface-raised));
}
.estate-map__links { margin-top: 8px; }
/* The trust-wide switch, set apart from the map it governs so it does not read
   as a control on this one site. */
/* ⚠️ NOT ALSO `.inline-form`, which is what this carried first. Both are
   single-class selectors, so they tie on specificity and the later one in the
   file wins — `.inline-form` sits at ~2338 and sets `display: inline-block`,
   which silently beat the `display: flex` here at ~500 and left the hint
   wrapping back to the left margin under the button. Equal specificity is
   decided by source order, and a rule that loses that way leaves no trace. */
.estate-map__toggle {
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--border-subtle);
  display: flex;
  align-items: flex-start;
  gap: 12px;
  flex-wrap: wrap;
}
/* ⚠️ A FLEX BASIS, or the sentence wraps back UNDER the button instead of
   sitting beside it — one flex item too wide for the row breaks to the next
   line and then spans the full width, which reads as an orphaned paragraph
   rather than as the button's explanation. */
.estate-map__toggle .form-field__hint {
  flex: 1 1 320px;
  margin: 0;
  align-self: center;
}

.estate-note.form-hint {
  max-width: none;
}

.form-hint { margin-top: var(--space-3); }
.app-modal__body > .form-grid { margin-top: var(--space-4); overflow-y: auto; }
.app-modal__footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-6);
  border-top: 1px solid var(--border-subtle);
  background: var(--surface-raised);
}

.card__title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--text-primary);
}

/* ⚠️ Used by 7 templates and defined by none, so every card subtitle rendered
   at body weight and colour — the visual distinction between a heading and its
   explanation simply was not there. Pulled up under the title, since
   .card__title already carries the gap below it. */
.card__subtitle {
  margin: calc(var(--space-3) * -1) 0 var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.45;
}

.card__metric {
  margin: 0;
  font-size: var(--text-3xl);
  font-weight: 700;
  color: var(--accent);
  letter-spacing: -0.02em;
}

.card__caption {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

/* ---- Availability preference cards (WP-P32.B5 UX polish) ----
   Each card narrates one preference as a titled definition list so the
   polarity + scope + slot + note read as a sentence rather than as
   parallel unlabelled pills. Left-border colour = polarity marker so
   the eye can scan available vs unavailable at a glance. */
.preference-cards {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  margin-top: var(--space-4);
}
.preference-cards__empty {
  padding: var(--space-4);
  border: 1px dashed var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--bg);
}
.preference-card {
  position: relative;
  padding: var(--space-4);
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-left: 4px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.preference-card--available {
  border-left-color: #16A34A;    /* emerald 600 */
  background: #F0FDF4;           /* emerald 50 tint */
}
.preference-card--unavailable {
  border-left-color: #DC2626;    /* rose 600 */
  background: #FEF2F2;           /* rose 50 tint */
}
.preference-card__header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}
.preference-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  color: #ffffff;
  flex: 0 0 auto;
}
.preference-card--available .preference-card__icon {
  background: #16A34A;
}
.preference-card--unavailable .preference-card__icon {
  background: #DC2626;
}
.preference-card__title {
  flex: 1 1 auto;
  margin: 0;
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
}
.preference-card__clear-form {
  margin: 0;
  flex: 0 0 auto;
}
.preference-card__clear {
  appearance: none;
  border: none;
  background: transparent;
  width: 28px;
  height: 28px;
  font-size: 20px;
  line-height: 1;
  color: var(--text-secondary);
  cursor: pointer;
  border-radius: 6px;
}
.preference-card__clear:hover {
  background: rgba(15, 23, 42, 0.06);
  color: var(--text-primary);
}
.preference-card__facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  column-gap: var(--space-4);
  row-gap: var(--space-2);
  margin: 0 0 var(--space-3) 0;
}
.preference-card__facts dt {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.02em;
  align-self: baseline;
}
.preference-card__facts dd {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-primary);
}
.preference-card__daterange {
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
.preference-card__note {
  font-style: italic;
  color: var(--text-secondary);
}
.preference-card__footer {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  padding-top: var(--space-3);
  border-top: 1px dashed var(--border-subtle);
}
.preference-card__footer a {
  color: inherit;
  text-decoration: underline;
}

.preference-history {
  margin-top: var(--space-5);
}
.preference-history > summary {
  cursor: pointer;
  font-weight: 500;
  color: var(--text-secondary);
  padding: var(--space-2) 0;
}
.preference-history__count {
  display: inline-block;
  min-width: 20px;
  padding: 0 6px;
  margin-left: var(--space-2);
  border-radius: 999px;
  background: var(--bg);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: 600;
  text-align: center;
  line-height: 20px;
}
.preference-history__hint {
  margin: var(--space-2) 0 var(--space-3);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* ---- Trust Admin lists (v2 post-demo polish) ----
   Kills the pale row-tint approach in favour of a leading LED cell +
   a status pill with matching LED + relative date chips. The visual
   language mirrors the roster rooms-grid LED so operators build a
   single mental model across surfaces. */
.trust-admin-summary {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-4);
  background: var(--bg);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  flex-wrap: wrap;
  align-items: center;
}
.trust-admin-summary__label {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-right: var(--space-2);
}
.trust-admin-summary__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 12px;
  border-radius: 999px;
  font-size: var(--text-sm);
  font-weight: 500;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
}
.trust-admin-summary__chip strong {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: var(--text-base);
}
.trust-admin-summary__led {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 1.5px solid rgba(0, 0, 0, 0.75);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.trust-admin-summary__led--complete           { background: #16A34A; }
.trust-admin-summary__led--started_incomplete { background: #EAB308; }
.trust-admin-summary__led--not_started        { background: #DC2626; }

.trust-admin-legend {
  padding: var(--space-3) var(--space-4);
  margin-top: var(--space-4);
  background: transparent;
  border: 1px dashed var(--border-subtle);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.trust-admin-legend strong {
  color: var(--text-primary);
}

.trust-admin-row td {
  vertical-align: middle;
  padding: var(--space-3) var(--space-3);
}
.trust-admin-row__led-cell {
  width: 32px;
  padding-right: 0 !important;
  padding-left: var(--space-4) !important;
}
.trust-admin-row__led {
  display: inline-block;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 1.5px solid rgba(0, 0, 0, 0.75);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35),
              0 1px 2px rgba(0, 0, 0, 0.15);
  background: #94A3B8;
}
.trust-admin-row__led--complete           { background: #16A34A; }
.trust-admin-row__led--started_incomplete { background: #EAB308; }
.trust-admin-row__led--not_started        { background: #DC2626; }

.trust-admin-row__date {
  min-width: 130px;
}
.trust-admin-row__date-chip {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin-bottom: 4px;
}
.trust-admin-row__date-chip--today  { background: #1D4ED8; color: #FFFFFF; }
.trust-admin-row__date-chip--soon   { background: #DBEAFE; color: #1E3A8A; }
.trust-admin-row__date-chip--past   { background: #E5E7EB; color: #374151; }
.trust-admin-row__date-chip--future { background: var(--surface-raised); color: var(--text-secondary); }
.trust-admin-row__date-full {
  font-size: var(--text-sm);
  line-height: 1.2;
}
.trust-admin-row__date-full small {
  font-size: 11px;
}

.trust-admin-row__sl strong {
  font-weight: 600;
  color: var(--text-primary);
}

.trust-admin-row__updated {
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

/* ---- Trust Admin lists — phone layout (<=640px) ----
   Below the tablet breakpoint the 5-column table would either
   horizontal-scroll (frustrating) or squash to unreadable widths.
   Instead we swap the table + row + cells to block/grid so each row
   renders as a stacked card that fits a portrait phone comfortably.
   Layout inside the card:
     Row 1: LED + date-chip + pretty date         (visual anchor)
     Row 2: Service line (bold) + department name (identity)
     Row 3: Status pill full-width                 (touch-friendly action)
     Row 4: "updated N ago" right-aligned          (secondary)
   The <thead> hides — the LED colour + status pill carry meaning
   without column headers on such narrow layouts. */
@media (max-width: 900px) {
  /* Explicit .trust-admin-table class rather than :has() selector so
     the layout works on older mobile browsers (pre-2022 Android Chrome
     / iOS Safari 15.3-). Also bumped breakpoint 640 -> 900 so it hits
     tablet-portrait viewports too (iPad portrait is 810 CSS px). */
  table.trust-admin-table,
  table.trust-admin-table thead,
  table.trust-admin-table tbody {
    display: block !important;
    width: 100% !important;
  }
  table.trust-admin-table thead { display: none !important; }

  table.trust-admin-table tbody tr.trust-admin-row {
    display: grid !important;
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "date"
      "sl"
      "status"
      "updated";
    row-gap: 8px;
    padding: 14px;
    margin: 0 0 10px;
    border: 1px solid var(--border-subtle);
    border-left: 4px solid var(--border-strong);
    border-radius: 10px;
    background: var(--surface);
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
  }
  .trust-admin-row--complete           { border-left-color: #16A34A; }
  .trust-admin-row--started_incomplete { border-left-color: #EAB308; }
  .trust-admin-row--not_started        { border-left-color: #DC2626; }

  /* Override .entity-table's td/tr rules that would otherwise leak in:
     - .entity-table td { padding + border-bottom + vertical-align }
     - .entity-table tbody tr:nth-child(even) td:not(.rg-cell) { background }
     - .entity-table tbody tr:hover td { background }
     All defined later in the file with higher specificity; win by
     using !important on the cell-level resets. */
  table.trust-admin-table tbody tr.trust-admin-row,
  table.trust-admin-table tbody tr.trust-admin-row:hover,
  table.trust-admin-table tbody tr.trust-admin-row:nth-child(even) {
    background: var(--surface) !important;
  }
  table.trust-admin-table tbody tr.trust-admin-row td,
  table.trust-admin-table tbody tr.trust-admin-row:nth-child(even) td,
  table.trust-admin-table tbody tr.trust-admin-row:hover td {
    background: transparent !important;
    border: none !important;
    padding: 0 !important;
    vertical-align: baseline !important;
    text-align: left !important;  /* Beat any inherited table alignment */
    min-width: 0;
    overflow: hidden;              /* Guard against pill/select overflow */
  }

  .trust-admin-row td {
    display: block;
    width: 100%;
  }
  /* LED column removed on phone — redundant because the status pill
     carries the same colour + the row's coloured left-border stripe
     already anchors the state visually. Frees horizontal space for
     the date chip + service line to left-align cleanly. */
  .trust-admin-row__led-cell { display: none !important; }
  .trust-admin-row__date     { grid-area: date;     min-width: 0; }
  .trust-admin-row__sl       { grid-area: sl;       min-width: 0; }
  .trust-admin-row__status   { grid-area: status;   min-width: 0; }
  .trust-admin-row__updated  { grid-area: updated;  text-align: right; font-size: 11px; }

  /* Compact date column: keep the TODAY / TOMORROW chip, keep the
     pretty date, drop the ISO fallback (redundant + noisy on phone). */
  .trust-admin-row__date-full { display: inline-block; }
  .trust-admin-row__date-full small { display: none; }
  .trust-admin-row__date-chip { margin-right: 6px; margin-bottom: 0; vertical-align: middle; }
  .trust-admin-row__date-full strong { vertical-align: middle; }

  /* Service line stays bold; dept collapses to a small subtitle. */
  .trust-admin-row__sl small { display: block; margin-top: 2px; }

  /* Status pill spans full row width for a comfortable tap target
     (44px+ hit area). Force box-sizing + max-width so its own
     padding/border can't push it past the row's inner width at
     phone dimensions (~320px). Dropdown grows to fill inside. */
  .trust-admin-row__status .trust-admin-pill {
    display: flex !important;
    width: 100% !important;
    max-width: 100%;
    box-sizing: border-box;
    padding: 10px 12px;
    border-radius: 10px;
    font-size: 15px;
    min-width: 0;
  }
  .trust-admin-row__status .trust-admin-pill__select {
    flex: 1 1 auto;
    min-width: 0;         /* Beats the desktop 9em floor */
    max-width: 100%;
    font-size: 15px;
    padding: 4px 24px 4px 4px;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }

  /* Date row: chip + date wrap gracefully on very narrow widths.
     Both stay left-aligned; if the date needs to wrap onto its own
     line under the chip, that reads naturally. */
  .trust-admin-row__date { text-align: left !important; }
  .trust-admin-row__date-full {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: middle;
  }

  /* Service line + department name — left-aligned, no ellipsis
     truncation of the SL name (it's the most important label). */
  .trust-admin-row__sl { text-align: left !important; }
  .trust-admin-row__sl strong { display: block; }

  /* Filter bar: stack fields vertically. Search takes full width;
     status pills wrap; dropdowns stretch. */
  .ta-filter__form {
    padding: var(--space-3);
    gap: var(--space-3);
  }
  .ta-filter__field,
  .ta-filter__field--search {
    flex: 1 1 100%;
    min-width: 0;
  }
  .ta-filter__field select,
  .ta-filter__field input[type="date"] {
    width: 100%;
    min-width: 0;
  }
  .ta-filter__search-wrap input[type="search"] { font-size: 16px; }
  .ta-filter__toggles { justify-content: flex-start; }
  .ta-filter__pill {
    padding: 8px 12px;
    font-size: 14px;
  }

  /* Summary strip: chips wrap comfortably; label goes above. */
  .trust-admin-summary {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
    padding: var(--space-3);
  }
  .trust-admin-summary__label { font-size: 13px; }
  .trust-admin-summary__chip {
    justify-content: space-between;
    padding: 8px 12px;
  }

  /* Active-filter chip strip: wrap onto multiple lines cleanly. */
  .ta-filter__active {
    padding: var(--space-2) var(--space-3);
  }
  .ta-filter__active-label { flex: 1 1 100%; margin: 0 0 4px; }
}

.trust-admin-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px 4px 10px;
  border-radius: 999px;
  background: var(--surface);
  border: 1.5px solid rgba(0, 0, 0, 0.15);
}
.trust-admin-pill--complete           { background: #DCFCE7; border-color: #16A34A; }
.trust-admin-pill--started_incomplete { background: #FEF3C7; border-color: #EAB308; }
.trust-admin-pill--not_started        { background: #FEE2E2; border-color: #DC2626; }
.trust-admin-pill__led {
  display: inline-block;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 1.5px solid rgba(0, 0, 0, 0.75);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
  flex: 0 0 auto;
}
.trust-admin-pill--complete           .trust-admin-pill__led { background: #16A34A; }
.trust-admin-pill--started_incomplete .trust-admin-pill__led { background: #EAB308; }
.trust-admin-pill--not_started        .trust-admin-pill__led { background: #DC2626; }
.trust-admin-pill--readonly {
  padding-right: 12px;
  font-weight: 500;
  color: var(--text-primary);
  font-size: var(--text-sm);
}
.trust-admin-pill__select {
  appearance: none;
  border: none;
  background: transparent;
  padding: 2px 24px 2px 4px;
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-primary);
  cursor: pointer;
  min-width: 9em;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M0 0l5 6 5-6z' fill='%230F172A'/></svg>");
  background-repeat: no-repeat;
  background-position: right 6px center;
  background-size: 8px 5px;
}
.trust-admin-pill__select:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}
.trust-admin-inline-form {
  margin: 0;
}

/* ---- Trust Admin filter panel (WP-P32 elevated polish) ----
   Shared between /upcoming-clinics + /all-clinics. Card carries a
   title strip + inline "Clear all" link + a single-row responsive
   grid of filter fields + an active-filter chip strip. Selects and
   checkboxes auto-submit on change (see the inline <script> in the
   templates); the Apply button is a fallback for the search Enter
   flow and screen-reader users. */
.ta-filter {
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: 0 1px 3px rgba(15, 23, 42, 0.05);
  margin: 0 0 var(--space-4);
  overflow: hidden;
}
.ta-filter__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-2) var(--space-4);
  background: linear-gradient(180deg, var(--bg) 0%, var(--surface-raised) 100%);
  border-bottom: 1px solid var(--border-subtle);
}
.ta-filter__title {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.ta-filter__title svg { width: 16px; height: 16px; }
.ta-filter__reset {
  font-size: var(--text-sm);
  color: var(--accent, #1D4ED8);
  text-decoration: none;
}
.ta-filter__reset:hover { text-decoration: underline; }

.ta-filter__form {
  display: flex;
  gap: var(--space-4);
  align-items: flex-end;
  flex-wrap: wrap;
  padding: var(--space-3) var(--space-4);
  margin: 0;
}
.ta-filter__field {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 0;
  padding: 0;
  border: none;
  flex: 0 0 auto;
  min-width: 0;
}
.ta-filter__field--search { flex: 1 1 240px; min-width: 200px; }
.ta-filter__field--status { flex: 0 0 auto; }
.ta-filter__label {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 0;
}
.ta-filter__field select {
  height: 36px;
  padding: 4px 28px 4px 10px;
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  font-size: var(--text-sm);
  min-width: 13em;
  background: var(--surface) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M0 0l5 6 5-6z' fill='%230F172A'/></svg>") no-repeat right 10px center / 8px 5px;
  appearance: none;
}
.ta-filter__field select:focus,
.ta-filter__field input[type="search"]:focus {
  outline: 2px solid var(--accent, #1D4ED8);
  outline-offset: 1px;
  border-color: var(--accent, #1D4ED8);
}

.ta-filter__search-wrap {
  position: relative;
  display: block;
}
.ta-filter__search-icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--text-secondary);
  pointer-events: none;
  display: inline-flex;
}
.ta-filter__search-wrap input[type="search"] {
  width: 100%;
  height: 36px;
  padding: 4px 10px 4px 34px;
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  font-size: var(--text-sm);
  background: var(--surface);
}

/* Native browser search-clear button — keep it, subtle */
.ta-filter__search-wrap input[type="search"]::-webkit-search-cancel-button {
  cursor: pointer;
  opacity: 0.6;
}
.ta-filter__search-wrap input[type="search"]::-webkit-search-cancel-button:hover {
  opacity: 1;
}

.ta-filter__toggles {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
}
.ta-filter__pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  cursor: pointer;
  font-size: var(--text-sm);
  transition: background 120ms ease, border-color 120ms ease;
  user-select: none;
}
/* Hide the checkbox but keep it interactive so label clicks toggle it
   and screen readers announce the checked state. `sr-only` pattern:
   1px non-collapsing box, clipped, absolute so it doesn't disturb
   layout. */
.ta-filter__pill input[type="checkbox"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
.ta-filter__pill:hover { background: var(--bg); }
/* Instant visual feedback on click via :has() -- no wait for server
   round-trip. Modern browsers only (Chrome 105+ / FF 121+ / Safari
   15.4+); older browsers fall back to the server-rendered .is-active
   class after page reload. */
.ta-filter__pill:has(input:checked) {
  background: #EFF6FF;
  border-color: #1D4ED8;
  color: #1E3A8A;
  font-weight: 600;
}
.ta-filter__pill--complete:has(input:checked)           { background: #DCFCE7; border-color: #16A34A; color: #14532D; }
.ta-filter__pill--started_incomplete:has(input:checked) { background: #FEF3C7; border-color: #CA8A04; color: #713F12; }
.ta-filter__pill--not_started:has(input:checked)        { background: #FEE2E2; border-color: #DC2626; color: #7F1D1D; }
.ta-filter__pill-led {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 1.5px solid rgba(0, 0, 0, 0.75);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35);
  flex: 0 0 auto;
}
.ta-filter__pill--complete           .ta-filter__pill-led { background: #16A34A; }
.ta-filter__pill--started_incomplete .ta-filter__pill-led { background: #EAB308; }
.ta-filter__pill--not_started        .ta-filter__pill-led { background: #DC2626; }
.ta-filter__pill.is-active {
  background: #EFF6FF;
  border-color: #1D4ED8;
  color: #1E3A8A;
  font-weight: 600;
}
.ta-filter__pill.is-active.ta-filter__pill--complete           { background: #DCFCE7; border-color: #16A34A; color: #14532D; }
.ta-filter__pill.is-active.ta-filter__pill--started_incomplete { background: #FEF3C7; border-color: #CA8A04; color: #713F12; }
.ta-filter__pill.is-active.ta-filter__pill--not_started        { background: #FEE2E2; border-color: #DC2626; color: #7F1D1D; }
.ta-filter__pill:focus-within {
  outline: 2px solid var(--accent, #1D4ED8);
  outline-offset: 2px;
}

.ta-filter__hint {
  display: flex;
  align-items: flex-end;
  padding-bottom: 2px;
  margin-left: auto;
}
.ta-filter__apply {
  appearance: none;
  padding: 6px 14px;
  height: 36px;
  border-radius: 6px;
  border: 1px solid var(--border-subtle);
  background: var(--surface);
  color: var(--text-secondary);
  font-size: var(--text-sm);
  cursor: pointer;
}
.ta-filter__apply:hover { background: var(--bg); color: var(--text-primary); }

.ta-filter__active {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  padding: var(--space-2) var(--space-4);
  background: #EFF6FF;
  border-top: 1px solid #DBEAFE;
}
.ta-filter__active-label {
  font-size: 11px;
  font-weight: 600;
  color: #1E3A8A;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-right: 4px;
}
.ta-filter__active-chip {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px 4px 10px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid #93C5FD;
  color: #1E3A8A;
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.ta-filter__active-chip:hover {
  background: #DBEAFE;
  border-color: #1D4ED8;
}
.ta-filter__active-chip strong { font-weight: 600; }
.ta-filter__active-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px; height: 18px;
  border-radius: 50%;
  background: rgba(30, 58, 138, 0.08);
  color: #1E3A8A;
  font-size: 14px;
  line-height: 1;
}
.ta-filter__active-chip:hover .ta-filter__active-x {
  background: rgba(30, 58, 138, 0.18);
}
.ta-filter__active-chip--complete           { background: #F0FDF4; border-color: #86EFAC; color: #14532D; }
.ta-filter__active-chip--started_incomplete { background: #FEFCE8; border-color: #FDE68A; color: #713F12; }
.ta-filter__active-chip--not_started        { background: #FEF2F2; border-color: #FCA5A5; color: #7F1D1D; }

/* ---- Roster viewer summary panel (WP-P32 post-demo polish) ----
   Above-the-fold count-chip strip so the flat-table viewer isn't a
   dumb page-of-rows -- operators can see roster shape at a glance
   before drilling into any week. Mirrors .trust-admin-summary. */
.roster-summary {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  margin: 0 0 var(--space-4);
  background: var(--bg);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  flex-wrap: wrap;
  align-items: center;
}
.roster-summary__chip {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 6px 14px;
  border-radius: 8px;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  font-size: var(--text-sm);
}
.roster-summary__chip strong {
  font-size: var(--text-lg);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}
.roster-summary__chip--accent {
  background: #EFF6FF;
  border-color: #93C5FD;
}
.roster-summary__chip--accent strong {
  color: #1D4ED8;
}
.roster-summary__label {
  color: var(--text-secondary);
}
/* ⚠️ THE SAME COLOURED BAR THE REST OF THE PRODUCT USES. These chips were flat
   grey boxes while the `.list-kpi` tiles on forty-five other pages carried a 4px
   accent bar, so the roster read as a different product to everything around it.
   Marcus, 2026-09-01: "style them with a coloured left border so it carries the
   same type of display theme as all other pages."

   ⚠️ `::before` RATHER THAN `border-left`, for the reason recorded on `.list-kpi`:
   a border participates in the box model, so a chip with a bar would shift its
   content 4px and stop aligning with one without. Absolutely positioned, it
   paints over the corner radius and costs no layout. `currentColor` means one
   rule serves every variant -- a chip sets `color` and the bar follows.

   The existing 14px left padding already clears a 4px bar, so nothing moves. */
.roster-summary__chip {
  position: relative;
  overflow: hidden;
  color: var(--accent, #2563eb);
}
.roster-summary__chip::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 4px;
  background: currentColor;
  opacity: 0.85;
}
/* Colours borrowed from the `.list-kpi--*` vocabulary so the two components
   cannot drift into different palettes. Provenance is the point of the row, so
   the three figures that reconcile are the ones that carry meaning:
   solver-placed reads as a plain count, hand-edited as the human intervention
   worth noticing, and the bulk-placed SPA rows as context rather than a claim. */
.roster-summary__chip--solver { color: #2563eb; }  /* blue 600  -- a count */
.roster-summary__chip--hand   { color: var(--warning); }  /* amber 700 -- a person acted */
.roster-summary__chip--bulk   { color: var(--text-secondary); }
.roster-summary__chip--people { color: #7c3aed; }  /* violet 600 */
.roster-summary__chip--time   { color: #0e7490; }  /* cyan 700 */
.roster-summary__chip--muted  { color: var(--text-secondary); }

/* ⚠️ THE DENOMINATOR IS DELIBERATELY QUIETER THAN THE FIGURE, not equal to it.
   "2,666 of 2,682" is one fact -- how much of the work the solver was asked to
   do it actually did -- and setting both numbers at chip-strong size reads as
   two competing figures. It sits tight to the numerator (small negative margin
   against the flex gap) so the pair scans as a single token. */
.roster-summary__of {
  margin-left: -2px;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}
.roster-summary__link {
  margin-left: auto;
  padding: 6px 14px;
  border-radius: 8px;
  color: var(--accent, #1D4ED8);
  font-size: var(--text-sm);
  font-weight: 500;
  text-decoration: none;
}
.roster-summary__link:hover {
  text-decoration: underline;
}

/* ---- Bulk-action toolbar ----
   Renders the submit button(s) for a form[data-bulk-form]. Dual
   placement: one bar above the table, one below, so the operator
   never has to scroll to find the action. Buttons disable themselves
   until at least one row checkbox is checked and rewrite their label
   to '<base> (<n>)' — wired by host/ui/static/bulk-actions.js. */
.bulk-action-bar {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  align-items: center;
}
.bulk-action-bar--top {
  margin-bottom: var(--space-2);
}
.bulk-action-bar--bottom {
  margin-top: var(--space-2);
}

/* ---- Data tables ---- */
.data-table,
.entity-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--text-sm);
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  /* No `overflow: hidden` here — it clips the absolutely-positioned
     row-menu popover when the table only has one or two rows.
     Per-corner rounding on the outer cells preserves the visual without
     establishing a clipping box. */
}

.data-table thead tr:first-child th:first-child,
.entity-table thead tr:first-child th:first-child { border-top-left-radius: 6px; }
.data-table thead tr:first-child th:last-child,
.entity-table thead tr:first-child th:last-child { border-top-right-radius: 6px; }
.data-table tbody tr:last-child td:first-child,
.entity-table tbody tr:last-child td:first-child { border-bottom-left-radius: 6px; }
.data-table tbody tr:last-child td:last-child,
.entity-table tbody tr:last-child td:last-child { border-bottom-right-radius: 6px; }

.data-table thead,
.entity-table thead {
  background: var(--surface-raised);
  text-align: left;
  border-bottom: 1px solid var(--border-strong);
}

.data-table thead tr,
.entity-table thead tr {
  border-bottom: 1px solid var(--border-strong);
}

.data-table th,
.entity-table th {
  padding: var(--space-3) var(--space-4);
  font-weight: 600;
  color: var(--text-primary);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  text-align: left;
  background: var(--surface-raised);
  border-bottom: 2px solid var(--border-strong);
  white-space: nowrap;
}

.data-table td,
.entity-table td {
  padding: var(--space-3) var(--space-4);
  color: var(--text-primary);
  border-bottom: 1px solid var(--border-subtle);
  vertical-align: middle;
}

/* Pills + badges are dual-use: page-level banners (with bottom
   margin so they breathe before the next section) AND inline row
   chips (no margin needed — the cell padding handles spacing).
   Zero the margin when nested inside a table cell so rows stay
   compact. */
/* ⚠️ `.pill` and `.badge` are STANDALONE alert components — chunky padding and
   a bottom margin sized to separate them from the block below. Inline in a
   table row they need neither, and without `nowrap` a date in a narrow column
   breaks across two lines inside a red capsule (seen on Equipment → Next
   service). The wrap is the symptom; the cause is that the column never claims
   the width its content needs, which `nowrap` fixes at source. */
.entity-table td .pill,
.entity-table td .badge,
.data-table td .pill,
.data-table td .badge,
.admin-table td .pill,
.admin-table td .badge {
  margin: 0;
  padding: 2px var(--space-2);
  font-size: var(--text-xs);
  white-space: nowrap;
  vertical-align: baseline;
}

.data-table tbody tr:nth-child(even) td:not(.rg-cell),
.entity-table tbody tr:nth-child(even) td:not(.rg-cell) {
  background: var(--surface-raised);
}

.data-table tbody tr:last-child td,
.entity-table tbody tr:last-child td { border-bottom: none; }

.data-table tbody tr:hover td,
.entity-table tbody tr:hover td {
  background: var(--accent-soft);
}

.entity-table__actions {
  text-align: right;
  white-space: nowrap;
}

.entity-table__check {
  width: 2.5rem;
  padding-right: 0;
  text-align: center;
}
.entity-table__check input[type="checkbox"] {
  width: var(--control-target-min);
  height: var(--control-target-min);
  cursor: pointer;
}

/* Flash banner — the success/error notification rendered after a POST
   redirect (e.g. "Skill added"). Pre-this rule there was no .flash CSS
   anywhere in the app, so banners rendered as plain unstyled <div>
   text — visible only to readers who knew where to look. Now: solid
   coloured left border for visual anchor, light tinted background,
   bold leading icon, optional close button + inline action link. */
.flash {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  margin: 12px 0;
  border-radius: 6px;
  border-left: 4px solid;
  font-size: 14px;
  line-height: 1.4;
  animation: flash-in 200ms ease-out;
}
.flash__icon {
  flex-shrink: 0;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
}
.flash__body { flex: 1; }
.flash__action {
  color: inherit;
  font-weight: 600;
  text-decoration: underline;
  white-space: nowrap;
}
.flash__close {
  background: transparent;
  border: 0;
  cursor: pointer;
  font-size: 22px;
  line-height: 1;
  color: currentColor;
  opacity: 0.55;
  padding: 0 4px;
}
.flash__close:hover { opacity: 1; }
.flash--success { background: #ecfdf5; border-color: #10b981; color: var(--success); }
.flash--info    { background: #eff6ff; border-color: #3b82f6; color: #1e40af; }
.flash--warning { background: #fffbeb; border-color: #f59e0b; color: #92400e; }
.flash--error   { background: #fef2f2; border-color: #ef4444; color: #991b1b; }
/* ⚠️ `danger` was MISSING while 13 templates used it, so every one of those
   warnings rendered with the base .flash rule alone — grey, borderless on the
   left, and indistinguishable from an informational note. Aliased to the same
   values as --error rather than invented, because the two are the same
   severity and two reds that almost match is worse than one. */
.flash--danger  { background: #fef2f2; border-color: #ef4444; color: #991b1b; }
@keyframes flash-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: translateY(0); }
}

/* Filter / search row — the small inline form that sits above a list
   page or under a section header (e.g. "Filter rooms", "Search staff",
   "Add required skill"). Distinct from `.bulk-toolbar` (which is the
   raised-background bar for bulk-mark-then-delete flows). One source
   of truth so every screen renders the same height + button weights. */
.filter-form {
  display: flex;
  gap: 8px;
  align-items: stretch;
  flex-wrap: wrap;
  margin-bottom: var(--space-2, 8px);
}
.filter-form > label:not(.visually-hidden) {
  align-self: center;
  font-weight: 600;
  font-size: 13px;
  color: var(--text-primary, #1e293b);
}
.filter-form input[type="search"],
.filter-form input[type="text"],
.filter-form select {
  min-height: 40px;
  padding: 8px 12px;
  font-size: 14px;
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  background: var(--surface);
}
.filter-form input[type="search"] {
  flex: 1;
  max-width: 480px;
}
.filter-form .btn {
  white-space: nowrap;
  min-height: 40px;
}
/* "Showing 5 of 22" and the Clear beside it — how many rows the filters
   excluded, sitting with the controls that caused it rather than above the
   table. `margin-left: auto` pushes the PAIR to the end of the row; on a narrow
   viewport the flex-wrap above drops them to their own line together instead of
   squeezing the selects. The wrapper is what keeps them together: as bare
   siblings the number stayed on the first line and the button dropped to the
   next, so the two halves of one statement read as unrelated controls. */
.filter-form__result {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}
.filter-form__count {
  font-size: 13px;
  color: var(--text-secondary, var(--text-secondary));
  white-space: nowrap;
}
.filter-form__count strong {
  color: var(--text-primary, #1e293b);
}

/* Bulk-action toolbar that sits above a checkboxed entity-table. */
.bulk-toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}
.bulk-toolbar__label {
  flex: 0 0 auto;
  font-size: var(--text-sm);
  color: var(--text-primary);
}
.bulk-toolbar select {
  min-height: 36px;
  padding: 4px var(--space-2);
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-sm);
}

.entity-table caption {
  caption-side: top;
  text-align: left;
  padding: var(--space-3) 0;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* ---- Empty-state ---- */
.empty-state {
  padding: var(--space-6);
  text-align: center;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* ---- Badges ---- */
.badge {
  display: inline-block;
  padding: 2px var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  border-radius: var(--radius-sm);
  /* ⚠️ WAS text-transform: lowercase. It was there because most badges render a
     state machine's own token (`active`, `superseded`), and forcing every badge
     down to lowercase made those look deliberate rather than leaked.

     It cost more than it hid. Ten templates DO write a human label -- "Published",
     "In use", "Resolved", "Expired" -- and this rule threw the capital away in
     each one; a badge on the ledger page reads "PPY", an acronym, and this
     rendered it "ppy". The fix for a leaked token is to translate it in the
     template, which is now done where the token was reaching a page. Templates
     still emitting a raw lowercase value look exactly as they did before,
     because their source text was already lowercase. */
  letter-spacing: 0.01em;
}
.badge--active     { background: rgba(52, 211, 153, 0.15); color: var(--success); }
.badge--suspended  { background: rgba(251, 191, 36, 0.15); color: var(--warning); }
.badge--deactivated{ background: rgba(248, 113, 113, 0.15); color: var(--danger); }
/* ⚠️ USED BY admin_consistency_panels.html SINCE IT WAS WRITTEN, DEFINED
   NOWHERE. "Closed 2026-07-02" rendered with the bare base badge, so a closed
   panel was typographically identical to an unstyled span. Neutral rather than
   red: a panel that has finished its work is not an error. */
.badge--closed { background: rgba(100, 116, 139, 0.14); color: var(--text-secondary); }
/* Training-plan lifecycle. Without these every state but `active` fell through
   to the bare base style, so an achieved plan and a withdrawn one were the same
   grey. Progression reads warm-to-cool: draft and paused are holding states,
   approved and achieved are settled, withdrawn is closed. */
.badge--draft      { background: var(--surface-raised); color: var(--text-secondary); }
.badge--proposed   { background: rgba(59, 130, 246, 0.13); color: var(--info, #1d4ed8); }
.badge--approved   { background: rgba(52, 211, 153, 0.13); color: var(--success); }
.badge--paused     { background: rgba(251, 191, 36, 0.15); color: var(--warning); }
.badge--achieved   { background: rgba(52, 211, 153, 0.22); color: var(--success); }
.badge--withdrawn  { background: rgba(100, 116, 139, 0.12); color: var(--text-secondary); }

/* ---- Pills (status / inline alerts) ----
   Pill is a softer alert container than .flash; used for inline validation
   reasons where the message should sit in line with form controls. */
.pill {
  display: inline-block;
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-4);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 500;
  line-height: 1.3;
}
.pill--danger {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
  border: 1px solid color-mix(in srgb, var(--danger) 35%, transparent);
}
.pill--warning {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  color: var(--warning);
  border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent);
}
.pill--success {
  background: color-mix(in srgb, var(--success) 14%, transparent);
  color: var(--success);
  border: 1px solid color-mix(in srgb, var(--success) 35%, transparent);
}
/* ⚠️ Was the only one of these four pills hardcoding its colours — `#2563eb`
   and `#1d4ed8` — while danger, warning and success above all used tokens. In
   dark mode that put blue-700 on a dark tint at 1.93–2.40:1 across **250
   rendered sites**. The three rules above were the specification; `--info` is
   the token the palette was missing. v1.35 §10.13.1. */
.pill--info {
  background: color-mix(in srgb, var(--info) 14%, transparent);
  color: var(--info);
  border: 1px solid color-mix(in srgb, var(--info) 35%, transparent);
}
.pill--muted {
  background: var(--surface-raised);
  color: var(--text-secondary);
  border: 1px solid #cbd5e1;
  font-variant-numeric: tabular-nums;
}

/* Competency-level pills (MIG-066). Green = independent (Proficient),
   T1 direct and T2 indirect had shared the orange palette; they are now blue
   and amber respectively. See the note on the rules themselves. */
.level-pill {
  display: inline-block;
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.4;
  border: 1px solid transparent;
  white-space: nowrap;
}
.level-pill--proficient {
  background: color-mix(in srgb, var(--success) 14%, transparent);
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 35%, transparent);
}
/* ⚠️ THREE TIERS, THREE COLOURS -- a change of mind, recorded as one. T1 and T2
   shared the amber palette on the reasoning noted above: the at-a-glance read
   was binary, "green = work alone, amber = needs supervision", with the label
   left to separate direct from indirect. Marcus, 2026-09-02: "the T1: direct
   supervision pill should be light blue to distinguish it from the T2: indirect
   supervision".

   It reads better as a ladder than as a binary. Blue is the conventional "in
   training" signal and carries no alarm, so the ramp runs blue (learning) ->
   amber (nearly there) -> green (independent), and the two supervised tiers
   stop being one indistinguishable block on a competencies tab that lists
   dozens of them. What is lost is that colour alone no longer answers "can
   this person work unsupervised?" -- that is now green vs not-green rather
   than green vs amber, which is the same question with one more colour in it. */
.level-pill--t1_direct {
  background: color-mix(in srgb, #0284c7 12%, transparent);
  color: var(--accent);
  border-color: color-mix(in srgb, #0284c7 35%, transparent);
}
.level-pill--t2_indirect {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
}

/* Production-readiness traffic-light pills. Solid-coloured chips so
   green/amber/red read at a glance without the operator squinting at
   text. Mirrors .pill--{success,warning,danger} but keyed to the
   readiness colour names. */
.readiness-pill {
  display: inline-block;
  padding: 3px 12px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.4px;
  text-transform: uppercase;
  line-height: 1.3;
  border: 1px solid transparent;
}
.readiness-pill--green {
  background: color-mix(in srgb, var(--success) 14%, transparent);
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 35%, transparent);
}
.readiness-pill--amber {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
}
.readiness-pill--red {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}

/* Big solid banner for the production-readiness summary card.
   Replaces the bare 'Overall: RED' text with a high-signal colour
   block — the page's primary visual answer to 'are we ready?'. */
.readiness-banner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-5);
  border-radius: 8px;
  border: 1px solid transparent;
  margin-bottom: var(--space-3);
}
.readiness-banner__label {
  font-size: 22px;
  font-weight: 700;
  flex: 1;
}
.readiness-banner--green {
  background: color-mix(in srgb, var(--success) 18%, transparent);
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 45%, transparent);
}
.readiness-banner--amber {
  background: color-mix(in srgb, var(--warning) 18%, transparent);
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 45%, transparent);
}
.readiness-banner--red {
  background: color-mix(in srgb, var(--danger) 18%, transparent);
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
}

/* ---- Metadata list (key/value pairs in cards) ---- */
.metadata-list {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-2) var(--space-4);
  margin: 0;
  font-size: var(--text-sm);
}
.metadata-list dt {
  font-weight: 500;
  color: var(--text-secondary);
}
.metadata-list dd {
  margin: 0;
  color: var(--text-primary);
}
.metadata-list code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  background: var(--bg);
  padding: 2px var(--space-1);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-subtle);
}

/* ---- Nav extensions for WP-10.2 (disabled items + sign-out) ---- */
.app-nav__link--disabled {
  cursor: not-allowed;
  color: var(--text-disabled);
}
.app-nav__sign-out {
  margin-left: auto;
}

/* ---- Catalogue list/edit additions (WP-10.3) ---- */
.card__actions {
  margin-bottom: var(--space-4);
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.btn--small {
  min-height: 32px;
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-xs);
}

.btn--danger-ghost,
.btn--danger-ghost:hover,
.btn--danger-ghost:focus {
  color: var(--danger, #dc2626);
}
.btn--danger-ghost:hover {
  background: color-mix(in srgb, var(--danger, #dc2626) 10%, transparent);
}

/* Icon button: square, language-independent, ≥32px touch target.
   The aria-label / title is the accessible name; the icon is purely
   visual. Use alongside .btn .btn--ghost for the ghost style. */
.btn--icon {
  min-height: 32px;
  min-width: 32px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.btn--icon .icon {
  width: 18px;
  height: 18px;
}
.btn--icon.btn--small {
  min-height: 28px;
  min-width: 28px;
}
.btn--icon.btn--small .icon {
  width: 16px;
  height: 16px;
}

/* Action group: cluster icon-buttons with a thin divider between
   logical sub-groups (e.g. reorder | edit | lifecycle). */
.action-group {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.action-group .inline-form {
  display: inline-flex;
}
/* Inline <details> nestled into an action group: the open form
   should drop below the row without forcing the buttons to wrap. */
.inline-details {
  position: relative;
}
.inline-details > summary {
  list-style: none;
  cursor: pointer;
}
.inline-details > summary::-webkit-details-marker {
  display: none;
}
.inline-details[open] > .entity-form {
  position: absolute;
  z-index: 5;
  top: calc(100% + 4px);
  /* Anchor on the right of the summary button so the form opens
     leftward -- works whether the action cell is at the table's
     right edge (most common) or buried in a wider action group. */
  right: 0;
  left: auto;
  min-width: 22em;
  max-width: 32em;
  background: var(--surface-raised, var(--surface));
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  /* Leave room for the absolute ✕ close button in the top-right
     corner so labels never tuck under it. */
  padding-top: calc(var(--space-4) + 28px);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
  /* The action cell uses text-align:right to right-align its icon
     buttons; the form inside is its own context and must read
     left-to-right (or RTL, per the user's locale). */
  text-align: left;
}
.inline-details[open] > .entity-form label,
.inline-details[open] > .entity-form .form-field {
  text-align: left;
}

/* Close button inside the floating .inline-details edit form.
   Absolute top-right; the global click handler (base.html) closes
   the enclosing <details>. Templates opt in by adding one button
   with `data-inline-details-close` inside the .entity-form. */
.inline-details__close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  width: 28px;
  height: 28px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  color: var(--text-secondary, #555);
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
}
.inline-details__close:hover {
  background: var(--surface, #f0f0f0);
  color: var(--text-primary, #111);
}
.inline-details__close:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Leading icon inside a text-button (e.g. "+ Add room") — keeps the
   icon vertically centred + nudged slightly left of the label. */
.btn__icon-leading {
  display: inline-flex;
  align-items: center;
  margin-right: var(--space-2);
}
.btn__icon-leading .icon {
  width: 16px;
  height: 16px;
}

/* Table-cell typography helpers. Used by admin lists where we want
   visual hierarchy without changing the table's column widths.
   `white-space: nowrap` on identifier-style columns keeps short
   labels on one line; long values still get a horizontal scroll on
   the table rather than wrapping mid-cell. */
.cell--primary {
  font-weight: 600;
  white-space: nowrap;
}
.cell--muted {
  color: var(--text-secondary, #555);
  white-space: nowrap;
}

/* Prose columns -- descriptions, notes, anything sentence-length.
 *
 * ⚠️ Use this, NOT `.cell--muted`, for prose. They look identical (same muted
 * colour) but `.cell--muted` carries `white-space: nowrap`, which is right for
 * the identifier-style values it was written for and wrong for a sentence: the
 * column grows to the full width of the longest description, shoves every other
 * column off the viewport, and the operator gets a horizontal scrollbar to read
 * a table that would otherwise fit. That is exactly what the Skills catalogue
 * did once its descriptions became real sentences.
 *
 * The measure is capped rather than left free so one long row cannot dictate
 * the whole table's layout; `overflow-wrap` covers unbroken tokens (URLs, long
 * codes) that would otherwise overflow the cap. */
.cell--prose {
  color: var(--text-secondary, #555);
  white-space: normal;
  max-width: 52ch;
  overflow-wrap: break-word;
}
.cell--numeric {
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* Role holders -- the people who hold a role, in the cell that used to carry
 * a bare count.
 *
 * ⚠️ Capped measure and wrapping, for the same reason `.cell--prose` is: a
 * trust role can be held by four hundred people, and an uncapped cell would
 * hand the operator a horizontal scrollbar to read a table that otherwise
 * fits. The template shows five and puts the rest behind a disclosure, so the
 * cap only has to hold five names.
 *
 * Each name is a link to that user's role matrix, so "who holds this?" and
 * "change what they hold" are one click apart rather than a hunt through the
 * user list. */
.role-holders {
  white-space: normal;
  max-width: 34ch;
}
.role-holders__who {
  display: inline-flex;
  flex-direction: column;
  padding: 2px 8px;
  margin: 0 4px 4px 0;
  border: 1px solid #cbd5e1;
  border-radius: var(--radius-sm, 4px);
  background: var(--bg);
  color: var(--text-primary, var(--text-primary));
  font-size: var(--text-sm);
  line-height: 1.3;
  text-decoration: none;
}
.role-holders__who:hover,
.role-holders__who:focus-visible {
  border-color: #94a3b8;
  background: var(--surface-raised);
}
/* The department a grant is scoped to. `user_roles` models scoped and
 * trust-wide grants as separate rows (two partial unique indexes), and the
 * difference decides how far the role reaches -- so it is shown, not folded
 * away into the name. */
.role-holders__scope {
  display: block;
  color: var(--text-secondary, #555);
  font-size: var(--text-xs, 11px);
}
.role-holders__more > summary {
  cursor: pointer;
  color: var(--text-secondary, #555);
  font-size: var(--text-sm);
}

/* A date sitting mid-table. `cell--numeric` was the only tabular-figures class
   available and it right-aligns, which is right for a quantity and wrong for a
   date in the fifth of seven columns -- it pulls the value away from its own
   header and towards the next column's. This gives the figure alignment
   without the edge alignment, so a column of ISO dates lines up digit for
   digit under a left-aligned heading. */
.cell--date {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.cell-disclosure summary {
  cursor: pointer;
  list-style: none;
  display: inline-block;
}
.cell-disclosure summary::-webkit-details-marker { display: none; }
.cell-disclosure summary::after {
  content: " \25BE";
  color: var(--text-secondary, #6b7a8a);
  font-size: 0.85em;
}
.cell-disclosure[open] summary::after { content: " \25B4"; }
.cell-disclosure__list {
  margin: 0.4em 0 0;
  padding-left: 1.1em;
  text-align: left;
  font-weight: normal;
  font-size: 0.9em;
  white-space: normal;
}
.cell-disclosure__list li { margin-bottom: 2px; }
.cell-disclosure__meta {
  color: var(--text-secondary, #6b7a8a);
  margin-left: 0.4em;
  font-size: 0.85em;
}
.plan-target--derived {
  color: var(--text-secondary, #6b7a8a);
  font-style: italic;
}
.plan-target--set {
  color: inherit;
  font-weight: 500;
}
.cell--id code {
  color: var(--text-secondary, #555);
  font-size: 0.9em;
  background: var(--surface, #f4f4f4);
  padding: 2px 6px;
  border-radius: var(--radius-sm, 4px);
}

/* Position cell with inline reorder controls: [↑] #N [↓]. The up/down
   forms hug the badge so the reorder affordance reads as 'move this
   position' rather than getting lost in a generic Actions column.
   Archived rows render just the badge (no arrows) — moving an archived
   row is meaningless since the solver skips it. */
.position-cell__inner {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}
.position-cell__inner .inline-form { margin: 0; }
.position-cell__inner .position-badge { margin: 0 2px; }

/* Position badge — small monospaced #N pill. */
.position-badge {
  display: inline-block;
  min-width: 2.5em;
  text-align: center;
  padding: 2px 6px;
  border-radius: 999px;
  background: var(--surface, #f0f0f0);
  color: var(--text-secondary, #555);
  font-variant-numeric: tabular-nums;
  font-size: 0.85em;
}

/* Dim archived rows so the visual focus stays on what's actively in
   use. Status pill + colour cue together = no language-tied label. */
.room-row--archived {
  opacity: 0.55;
}
.room-row--archived:hover {
  opacity: 1;
}

/* =================================================================
   Mobile breakpoints
   ================================================================= */

/* ---- Touch targets ----
   pointer: coarse = primary input is a finger; bumps every icon
   button to the 44×44 WCAG 2.5.5 size. Desktop pointer:fine keeps
   the tight 28/32px sizes. */
@media (pointer: coarse) {
  /* ✅✅ v1.92 — THE WHOLE TOUCH-TARGET RESTORE, IN ONE DECLARATION.
     A finger gets 44×44 (WCAG 2.5.5) everywhere a control takes its height
     from the token.

     ⛔ **This replaces a cascade problem that cannot be solved with more
     rules.** The restores further down this block work by matching
     specificity and sitting later in the file — which is why they only ever
     covered selectors ABOVE this point. `.recon-scope` (~4970) and the bare
     global (~5080) are below it and were unreachable. A custom property
     INHERITS, so it reaches every rule in the file regardless of position,
     and the next control rule someone adds at the bottom is covered the day
     it is written rather than silently exempt.

     ⚠️ Keep this INSIDE this block, not in a new one: the guard in
     `tests/ui/test_form_rows_keep_touch_targets.py` resolves the FIRST
     `@media (pointer: coarse)` in the file and asserts the `.form-grid--rows`
     restore lives there. An earlier second block would make it inspect the
     wrong one while still passing. */
  :root {
    --control-min-h: 44px;
  }

  .btn--icon,
  .btn--icon.btn--small {
    min-width: 44px;
    min-height: 44px;
  }
  .btn--icon .icon,
  .btn--icon.btn--small .icon {
    width: 22px;
    height: 22px;
  }
  /* Form controls bumped to comfortable touch size. */
  input[type="text"],
  input[type="email"],
  input[type="password"],
  input[type="number"],
  input[type="date"],
  input[type="search"],
  input[type="url"],
  select,
  textarea {
    min-height: 44px;
    font-size: 16px; /* iOS doesn't auto-zoom at >= 16px */
  }

  /* ⛔⛔ **WITHOUT THIS, A TABLET SILENTLY GETS A 32px TOUCH TARGET.**
     v1.91 lowers `.form-grid--rows` controls to 32px for a mouse. That
     selector is specificity **(0,2,1)**; the `input[type="text"]` rule above
     is **(0,1,1)**, and **a media query adds no specificity** — so the
     desktop rule BEATS the touch rule even on a finger. Verified in the
     browser with both installed: computed `min-height` came back `32px`.

     > The desktop looks right, every test passes, and the touch target is
     > gone. This restore exists only because that was measured.

     ✅ Equal specificity, later in the file, so it wins on source order.
     ⛔ Keep it in this block. Moving the v1.91 rules below this point
     re-breaks it with no visible symptom on any desktop. Pinned by
     `tests/ui/test_form_rows_keep_touch_targets.py`. */
  .form-grid--rows > .form-field input,
  .form-grid--rows > .form-field select,
  .form-grid--rows > .form-field textarea {
    min-height: 44px;
    padding-top: var(--space-2);
    padding-bottom: var(--space-2);
  }
  .form-grid--rows > .form-field--checkbox {
    min-height: 44px;
  }
}

/* ---- Card layout below 768px ----
   The 8-column entity-table is unreadable on a phone; transform
   each row into a self-contained card. <td>s become labelled rows
   inside the card; the action-group sits at the bottom of the card.
   Requires data-label="..." on each <td> in the template. */
@media (max-width: 768px) {
  .entity-table {
    display: block;
    border: 0;
  }
  .entity-table thead {
    /* Visually hidden but accessible to screen readers. */
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }
  .entity-table tbody,
  .entity-table tr {
    display: block;
    width: 100%;
  }
  .entity-table tr {
    margin-bottom: var(--space-4);
    padding: var(--space-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-raised, var(--surface));
  }
  .entity-table td {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: var(--space-2) 0;
    border: 0;
    gap: var(--space-3);
    text-align: left;
  }
  .entity-table td::before {
    content: attr(data-label);
    font-size: 0.85em;
    color: var(--text-secondary, #555);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    flex: 0 0 auto;
    margin-right: var(--space-2);
  }
  /* Action cell takes full width with its action-group wrapped. */
  .entity-table td.entity-table__actions,
  .entity-table td:last-child {
    flex-direction: column;
    align-items: stretch;
  }
  .entity-table td.entity-table__actions::before,
  .entity-table td:last-child::before {
    margin-bottom: var(--space-2);
  }
  .entity-table .action-group {
    width: 100%;
    justify-content: flex-start;
  }

  /* Floating edit-form drops out of "floating" mode + uses card width. */
  .inline-details[open] > .entity-form {
    position: static;
    width: 100%;
    min-width: 0;
    max-width: none;
    margin-top: var(--space-3);
    box-shadow: none;
  }

  /* Modal forms go full-screen — easier targets, no clipped chrome. */
  .app-modal {
    width: 100vw;
    height: 100vh;
    max-width: none;
    max-height: none;
    border-radius: 0;
    margin: 0;
  }
  .app-modal__body {
    height: 100%;
    overflow-y: auto;
  }
  /* form-grid collapses to single column on phones so labels + inputs
     stack vertically and never overlap. */
  .form-grid {
    grid-template-columns: 1fr;
  }

  /* Sticky header inside cards: keeps `+ Add room` + Export +
     Bulk-import within reach when scrolling a long list. */
  .card .card__header {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--surface-raised, var(--surface));
    margin: calc(-1 * var(--space-4)) calc(-1 * var(--space-4)) var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border-subtle);
  }
  /* Bulk-toolbar (filter rows) also sticky just below the card header. */
  .card .bulk-toolbar {
    position: sticky;
    top: 64px;
    z-index: 2;
    background: var(--surface-raised, var(--surface));
    padding: var(--space-2) 0;
    margin-bottom: var(--space-3);
  }
}

.inline-form {
  display: inline-block;
  margin: 0;
}

/* ⚠️ THE OTHER HALF OF THE 749px-TALL FORM (WP-P51.A4).
 *
 * Giving `.form-grid` a `display: grid` only helps if the form has room to put
 * columns in. This cap was 520px, so a gridded form still resolved to two
 * narrow tracks inside an 1130px card — better than one, still a phone layout.
 *
 * Raised rather than removed. A cap on a FORM is not the same mistake as a cap
 * on prose: it stops a single-field form from stretching a text input across an
 * ultrawide monitor. 1100px is four `minmax(220px, 1fr)` tracks, which is as
 * many as any form here has fields to fill; the reading measure for the words
 * lives separately on `.form-field__hint` and `.form-hint`.
 */
.entity-form {
  max-width: 1100px;
}
/* When an .entity-form IS the modal body it must fill the dialog
   so header/grid/footer span the full modal width — otherwise the
   X-close looks mid-modal and the footer collapses inward. */
.entity-form.app-modal__body {
  max-width: none;
  width: 100%;
}

.form-actions {
  display: flex;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

/* ---- Week navigation (WP-10.4 roster viewer; redesign 2026-05) ----
   Filled secondary-style prev/next buttons with chevron decoration,
   centred week label, keyboard ← / → shortcuts. Used by
   /ui/my-roster, /ui/rosters/{id}/view, /ui/rosters/{id}/rooms. */
.week-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.week-nav__label {
  font-weight: 700;
  font-size: 16px;
  color: var(--text-primary);
  flex: 1 1 220px;
  text-align: center;
  min-width: 220px;
}

.week-nav__btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid #cbd5e1;
  border-radius: 6px;
  font-weight: 600;
  font-size: 14px;
  text-decoration: none;
  transition: background 0.12s, border-color 0.12s, box-shadow 0.12s;
  min-height: 40px;
  cursor: pointer;
}

.week-nav__btn:hover,
.week-nav__btn:focus-visible {
  background: var(--bg);
  border-color: #94a3b8;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

.week-nav__btn:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

.week-nav__btn--prev::before,
.week-nav__btn--next::after {
  /* Render the arrow inside its own inline-flex box with line-height 1
     so the parent button's `align-items: center` aligns the arrow's
     glyph box exactly to the optical middle of the text — without
     this, the larger 22px arrow gets baseline-aligned with the 14px
     button text and visually sits low. translateY(-1px) compensates
     for the arrow glyph's typical 1-pixel descent-bias in system
     fonts. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  font-size: 22px;
  font-weight: 400;
  color: var(--text-secondary);
  transform: translateY(-1px);
}

.week-nav__btn--prev::before {
  content: "\2190";       /* ← LEFTWARDS ARROW */
}

.week-nav__btn--next::after {
  content: "\2192";       /* → RIGHTWARDS ARROW */
}

@media (max-width: 900px) {
  .week-nav__btn { padding: 8px 14px; }
}

/* ---- Week jump form ----
   Sits centred below .week-nav as a secondary action — small font,
   inline label, ghost Go button. Shared by /ui/my-roster and
   /ui/rosters/{id}/rooms so the two calendar surfaces look the same. */
.week-jump {
  display: flex;
  gap: 6px;
  align-items: center;
  justify-content: center;
  margin-top: var(--space-3);
  flex-wrap: wrap;
}

.week-jump > label,
.week-jump .form-label {
  font-size: 12px;
  color: var(--text-secondary);
  margin: 0;
  font-weight: 500;
}

.week-jump input[type="text"],
.week-jump input[type="date"] {
  font-size: 12px;
  padding: 4px 8px;
  border: 1px solid #cbd5e1;
  border-radius: 4px;
  min-height: 32px;
}

/* ---- Filter row (WP-10.4 audit viewer) ---- */
.filter-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--space-4);
  align-items: end;
}

.filter-row .form-field {
  margin-bottom: 0;
}

.filter-row .form-actions {
  margin-top: 0;
  align-self: end;
  grid-column: 1 / -1;
}

/* ---- Status badges for roster + revision + job states ---- */
.badge--draft       { background: rgba(148, 163, 184, 0.15); color: var(--text-secondary); }
.badge--generating  { background: rgba(56, 189, 248, 0.15);  color: var(--accent); }
.badge--ready       { background: rgba(56, 189, 248, 0.15);  color: var(--accent); }
.badge--published   { background: rgba(52, 211, 153, 0.15);  color: var(--success); }
.badge--archived    { background: rgba(148, 163, 184, 0.15); color: var(--text-secondary); }
.badge--queued      { background: rgba(148, 163, 184, 0.15); color: var(--text-secondary); }
.badge--running     { background: rgba(56, 189, 248, 0.15);  color: var(--accent); }
.badge--succeeded   { background: rgba(52, 211, 153, 0.15);  color: var(--success); }
.badge--failed      { background: rgba(248, 113, 113, 0.15); color: var(--danger); }
.badge--cancelled   { background: rgba(251, 191, 36, 0.15);  color: var(--warning); }
.badge--warning     { background: rgba(251, 191, 36, 0.15);  color: var(--warning); }
.badge--revoked     { background: rgba(248, 113, 113, 0.15); color: var(--danger); }
/* Same gap, same fix: `badge--danger` is used by the staff-edit and training
   calendar templates and had no rule at all. */
.badge--danger      { background: rgba(248, 113, 113, 0.15); color: var(--danger); }
.badge--superseded  { background: rgba(148, 163, 184, 0.15); color: var(--text-secondary); }
.badge--expired     { background: rgba(148, 163, 184, 0.15); color: var(--text-secondary); }
.badge--success     { background: rgba(52, 211, 153, 0.15);  color: var(--success); }
.badge--failure     { background: rgba(248, 113, 113, 0.15); color: var(--danger); }

/* ---- Evaluation banner (WP-10.5; REQ-040) ---- */
.eval-banner {
  margin-bottom: var(--space-6);
  padding: var(--space-3) var(--space-4);
  background: rgba(251, 191, 36, 0.12);  /* var(--warning) at 12% */
  color: var(--warning);
  border: 1px solid var(--warning);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}
.eval-banner strong { color: var(--text-primary); }

/* ---- Visually hidden (for screen-reader-only content) ---- */
.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;
}

/* ============================================================
 * Path B picker — shared by SL Positions tab + Staff Skills tab.
 * Markup hooks live in `host/ui/static/path_b_picker.js`.
 * Page-specific styles (sl-pos-card, sl-pos-badge, etc.) stay
 * inside the page templates because they don't cross pages.
 * ============================================================ */

.sl-tag {
  display: inline-flex; align-items: center;
  padding: 1px 8px; border-radius: 10px;
  font-size: 11px; font-weight: 500; letter-spacing: 0.2px;
  text-transform: uppercase;
}
.sl-tag--neutral {
  background: rgba(0,0,0,0.05); color: #555;
  border: 1px solid rgba(0,0,0,0.06);
}

/* Status badge — green dot when ≥1 row, red dot when 0. */
.sl-status {
  display: inline-flex; align-items: center; gap: 6px;
  flex: 0 0 auto;
  padding: 4px 10px;
  font-size: 12px; font-weight: 500;
  border-radius: 12px;
}
.sl-status__dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.sl-status--ok {
  background: color-mix(in srgb, var(--success, #16a34a) 10%, var(--surface));
  color: color-mix(in srgb, var(--success, #16a34a) 85%, #000);
  border: 1px solid color-mix(in srgb, var(--success, #16a34a) 30%, transparent);
}
.sl-status--ok .sl-status__dot { background: var(--success, #16a34a); }
.sl-status--warn {
  background: color-mix(in srgb, var(--danger, #dc2626) 10%, var(--surface));
  color: color-mix(in srgb, var(--danger, #dc2626) 85%, #000);
  border: 1px solid color-mix(in srgb, var(--danger, #dc2626) 30%, transparent);
}
.sl-status--warn .sl-status__dot { background: var(--danger, #dc2626); }

/* Chip list + rows. Per-page CSS may override the row pseudo-element
 * to suppress the "OR" ribbon (e.g. Staff Skills tab — it's a set,
 * not a boolean expression). */
.sl-pos-form { margin: 0; }
.sl-req-list { display: flex; flex-direction: column; gap: 0; margin: 0 0 14px; }
.sl-req-row {
  display: flex; gap: 12px; align-items: center;
  padding: 10px 14px;
  background: var(--surface);
  border: 1px solid var(--border-subtle, #e5e5e5);
  border-radius: 8px;
  transition: border-color 0.12s, box-shadow 0.12s;
}
.sl-req-row:hover { border-color: color-mix(in srgb, var(--accent, #2563eb) 25%, var(--border-subtle, #e5e5e5)); }
.sl-req-row + .sl-req-row {
  position: relative;
  margin-top: 18px;
}
.sl-req-row + .sl-req-row::before {
  content: 'AND';
  position: absolute;
  top: -11px; left: 18px;
  padding: 1px 9px;
  font-size: 10px; font-weight: 700; letter-spacing: 0.6px;
  background: var(--surface);
  color: var(--accent, #2563eb);
  border: 1px solid color-mix(in srgb, var(--accent, #2563eb) 35%, transparent);
  border-radius: 999px;
}
.sl-req-row__skill {
  flex: 1; min-width: 0;
  font-weight: 600; font-size: 14px; color: #111;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sl-req-row .level-pill { flex: 0 0 auto; }
/* The editable twin of that pill: Worker > Competencies swaps the read-only
   level pill for a select in the same place, so it takes the same width rule
   rather than stretching and pushing the remove button off the row. */
.sl-req-row__level { flex: 0 0 auto; }
.sl-req-row__remove {
  flex: 0 0 auto;
  width: 28px; height: 28px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: transparent;
  color: #999; font-size: 18px; line-height: 1;
  border-radius: 50%; cursor: pointer;
  transition: background 0.12s, color 0.12s;
}
.sl-req-row__remove:hover {
  background: color-mix(in srgb, var(--danger, #dc2626) 10%, transparent);
  color: var(--danger, #dc2626);
}
.sl-req-row__remove:focus-visible {
  outline: 2px solid var(--accent, #2563eb); outline-offset: 1px;
}

/* Empty-state. The danger variant (default) is for SL Positions where
 * 0 reqs = unfillable. Pages that want a neutral empty-state apply
 * `.sl-req-empty--neutral` to soften the dashed-red box. */
.sl-req-empty {
  display: flex; gap: 12px; align-items: center;
  padding: 14px 16px;
  background: color-mix(in srgb, var(--danger, #dc2626) 5%, var(--surface));
  border: 1px dashed color-mix(in srgb, var(--danger, #dc2626) 35%, transparent);
  border-radius: 8px;
  color: #444; font-size: 13px;
}
.sl-req-empty__icon {
  flex: 0 0 auto;
  width: 28px; height: 28px;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--danger, #dc2626) 15%, var(--surface));
  color: var(--danger, #dc2626);
  border-radius: 50%; font-weight: 700;
}
.sl-req-empty strong { color: var(--danger, #dc2626); }
.sl-req-empty--neutral {
  background: rgba(0,0,0,0.02);
  border-color: rgba(0,0,0,0.12);
  color: #555;
}
.sl-req-empty--neutral strong { color: #222; }
.sl-req-empty--neutral .sl-req-empty__icon {
  background: rgba(0,0,0,0.06); color: #555;
}

/* Add-row picker — three columns (combobox, level select, CTA). */
.sl-req-add {
  display: grid;
  grid-template-columns: minmax(280px, 1fr) 220px auto;
  gap: 14px; align-items: end;
  padding: 14px;
  background: color-mix(in srgb, var(--accent, #2563eb) 3%, #f8f8f8);
  border: 1px solid var(--border-subtle, #e5e5e5);
  border-radius: 8px;
}
.sl-req-add__field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.sl-req-add__field > label {
  font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
  text-transform: uppercase; color: #555;
}
.sl-req-add__field .form-field__hint { font-size: 11px; color: #777; }
.sl-req-add__level {
  width: 100%; padding: 7px 10px;
  border: 1px solid var(--border-subtle); border-radius: 4px;
  background: var(--surface); font-size: 14px;
}
.sl-req-add__level:focus {
  outline: none; border-color: var(--accent, #2563eb);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #2563eb) 22%, transparent);
}
.sl-req-add__action { align-self: end; }
.sl-req-add__action .btn[disabled] { opacity: 0.45; cursor: not-allowed; }
@media (max-width: 720px) {
  .sl-req-add { grid-template-columns: 1fr; }
}

/* Form footer + dirty pulse pill. */
.sl-pos-form__foot {
  display: flex; gap: 12px; align-items: center; justify-content: flex-end;
  margin-top: 14px;
}
.sl-pos-dirty {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 10px;
  font-size: 12px; font-weight: 500;
  color: color-mix(in srgb, var(--warning, #d97706) 85%, #000);
  background: color-mix(in srgb, var(--warning, #d97706) 10%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--warning, #d97706) 30%, transparent);
  border-radius: 12px;
}
.sl-pos-dirty__dot {
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--warning, #d97706);
  animation: sl-pulse 1.4s ease-in-out infinite;
}
@keyframes sl-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.55; transform: scale(0.78); }
}

/* Searchable combobox. Pattern: WAI-ARIA combobox (input + listbox). */
.sl-combo-wrap { position: relative; }
.sl-combo { position: relative; }
.sl-combo__icon {
  position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
  pointer-events: none; color: #888; font-size: 14px;
}
.sl-combo__input {
  width: 100%; padding: 7px 30px 7px 28px;
  border: 1px solid var(--border-subtle); border-radius: 4px;
  background: var(--surface); font-size: 14px;
}
.sl-combo__input:focus {
  outline: none; border-color: var(--accent, #2563eb);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #2563eb) 22%, transparent);
}
.sl-combo__input--picked { background: color-mix(in srgb, var(--accent, #2563eb) 6%, var(--surface)); font-weight: 500; }
/* ⚠️ v1.92 found this at 22×22 — an interactive clear button below the 24px
   2.5.8 floor, the same defect as the three checkboxes and found only because
   the sweep enumerated every 22px box rather than the ones the task named.
   ⓘ 24px still fits: the input reserves `padding-right: 30px` and this sits
   at `right: 6px`, so 24 + 6 = 30 exactly. */
.sl-combo__clear {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  width: var(--control-target-min); height: var(--control-target-min);
  border: none; background: transparent;
  color: #777; font-size: 16px; line-height: 1; cursor: pointer; border-radius: 3px;
}
.sl-combo__clear:hover { background: rgba(0,0,0,0.06); color: #333; }
.sl-combo__list {
  position: absolute; left: 0; right: 0; top: calc(100% + 4px);
  z-index: 20; max-height: 320px; overflow-y: auto;
  margin: 0; padding: 4px 0; list-style: none;
  background: var(--surface); border: 1px solid var(--border-subtle);
  border-radius: 6px; box-shadow: 0 8px 28px rgba(0,0,0,0.14);
}
.sl-combo__opt {
  display: flex; gap: 10px; align-items: baseline;
  padding: 7px 14px; cursor: pointer; font-size: 13px;
  border-left: 3px solid transparent;
}
.sl-combo__opt:hover { background: rgba(0,0,0,0.04); }
.sl-combo__opt--active {
  background: color-mix(in srgb, var(--accent, #2563eb) 8%, var(--surface));
  border-left-color: var(--accent, #2563eb);
}
.sl-combo__name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sl-combo__cat {
  flex: 0 0 auto; font-size: 11px; color: #666;
  background: rgba(0,0,0,0.05); padding: 1px 6px; border-radius: 3px;
}
.sl-combo__opt mark {
  background: color-mix(in srgb, var(--accent, #2563eb) 22%, transparent);
  color: inherit; padding: 0 1px; border-radius: 2px;
}
.sl-combo__empty, .sl-combo__more {
  padding: 10px 14px; font-size: 12px; color: #777;
  font-style: italic; text-align: center;
}
.sl-combo__more { border-top: 1px solid rgba(0,0,0,0.06); background: rgba(0,0,0,0.02); }

/* --- P43 UX polish: cycles list + shared list-page patterns --- */

/* Page header right-side CTA slot */
.page-header { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--space-4); flex-wrap: wrap; }
.page-header > div:first-child { flex: 1 1 auto; min-width: 0; }
/* ⚠️ **`margin-left: auto` KEEPS IT RIGHT EVEN WHEN IT WRAPS.** `.page-header`
   is `space-between`, which right-aligns the actions only while they SHARE a
   line with the titles. A long subtitle makes the titles block take the width,
   the actions drop to a second flex line, and `space-between` on a line with
   one item puts it at the START — so the buttons appeared bottom-LEFT.
   Measured on the Equipment screen 2026-10-16: 937px of gap to the header's
   right edge, where the Competency screen (short subtitle, no wrap) had 0.
   Marcus: *"I thought house style was to have those buttons right aligned."*
   `margin-left: auto` pushes the item to the end of its own line, so it holds
   whether it wraps or not. */
.page-header__actions {
  flex: 0 0 auto;
  display: flex;
  gap: var(--space-2);
  align-items: center;
  margin-left: auto;
}

/* ⚠️ **THE TITLE AND ITS SUBTITLE ARE ONE FLEX ITEM, NOT TWO.** `.page-header`
   is `space-between`; a template that passes `<h1>`, `<p class="page-subtitle">`
   and an actions block as three BARE children gets three columns, and the
   subtitle is stranded in the middle of the header with no visible relationship
   to anything. Marcus, 2026-10-02: *"the 'Roster area… State…' text appears to
   float."*

   ✅ `flex-wrap` carries both of his requirements with no media query:
   beside the title, on its baseline, when the header is wide — *"it should
   look like it is on the same line as the title"* — and wrapped underneath
   when it is not, which is *"when the screen is narrowed that text should be
   UNDER the title not in its own column."*

   ⓘ The wrapper is a `<div>`, so the pre-existing
   `.page-header > div:first-child { flex: 1 1 auto; min-width: 0 }` already
   applies and this adds no sizing of its own.
   ⛔ Three templates grew their own page-scoped copies of this idea —
   `ars-`, `awc-` and `awd-page-header__main`. v1.94 does NOT migrate them;
   that is the push-everywhere phase, not a hot fix on the day of a demo. */
.page-header__titles {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: var(--space-4);
  row-gap: var(--space-1);
}
/* ⛔ `.page-title` carries `margin: 0 0 var(--space-2)` for the stacked case.
   Inside the flex group the row-gap does that job, and the margin would push
   the baseline alignment out by 8px. */
.page-header__titles > .page-title { margin-bottom: 0; }

/* Collapsible primer card */
.card--primer > summary { cursor: pointer; padding: 0; list-style: none; }
.card--primer > summary::-webkit-details-marker { display: none; }
.card--primer > summary::before {
  content: "▶ "; display: inline-block; transition: transform 0.15s;
  color: var(--text-secondary); font-size: 0.8em; margin-right: 0.4em;
}
.card--primer[open] > summary::before { transform: rotate(90deg); }

/* ⛔⛔ **CLASSES SHIPPED WITH NO RULE — BY THE PHASE THAT COMPLAINED ABOUT
   EXACTLY THAT.** v1.91 turned the "Closing this clinic" block from a
   `<section class="card">` into a `<details class="card sl-closing">` with
   `sl-closing__summary` and `sl-closing__hint`, wrote a commit message about
   "a class name is not a style", and then defined none of the three.
   ⓘ Caught by `tests/arc/test_css_classes_resolve.py`'s ratchet (203 → 205),
   not by eye — the block still *looked* fine because `.card` carried it and
   the browser's default `<summary>` is clickable.
   ⚠️ The red border the old section had is deliberately NOT restored: the
   point of the disclosure is that a destructive block stops shouting at
   everyone editing a name. */
.sl-closing {
  margin-top: var(--space-6);
  border-color: var(--danger-border, #fecaca);
}
.sl-closing > .sl-closing__summary {
  cursor: pointer;
  list-style: none;
  font-weight: 600;
  color: var(--text-primary);
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
}
.sl-closing > .sl-closing__summary::-webkit-details-marker { display: none; }
.sl-closing > .sl-closing__summary::before {
  content: "▶";
  display: inline-block;
  transition: transform 0.15s;
  color: var(--text-secondary);
  font-size: 0.8em;
}
.sl-closing[open] > .sl-closing__summary::before { transform: rotate(90deg); }
/* ⓘ The "retire or delete" hint says what is behind the disclosure without
   opening it — the reason a closed block is acceptable here at all. */
.sl-closing__hint {
  font-weight: 400;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}
.card--primer > summary strong { font-size: var(--text-base); color: var(--text-primary); }

/* Cycle-state description list — grid of badge + description rows */
.cycle-states { display: grid; grid-template-columns: minmax(120px, max-content) 1fr; column-gap: var(--space-4); row-gap: var(--space-2); margin: var(--space-3) 0; }
.cycle-states > div { display: contents; }
.cycle-states dt { margin: 0; }
.cycle-states dd { margin: 0; color: var(--text-secondary); font-size: var(--text-sm); }

/* Clickable-row admin table: hover state + link styling */
.admin-table--rowlinked tbody tr { transition: background 0.1s ease; }
.admin-table--rowlinked tbody tr:hover { background: color-mix(in srgb, var(--accent, #2563eb) 6%, transparent); }
.admin-table__actions { text-align: right; white-space: nowrap; }
.link--primary { color: var(--accent); text-decoration: none; }
.link--primary:hover { text-decoration: underline; }

/* Empty state with call-to-action */
.empty-state--cta { padding: var(--space-6) var(--space-4); text-align: center; border: 1px dashed var(--border-subtle); border-radius: 8px; }
.empty-state--cta > .btn { margin-top: var(--space-3); }

/* badge--info: info/accent variant (missing from the palette; used for negotiating cycle) */
.badge--info { background: rgba(56, 189, 248, 0.15); color: var(--accent, #38bdf8); }

/* Screen-reader-only utility */
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

/* ================================================================
   Shift pattern form (/ui/shifts/{id}/edit, /ui/shifts/new)
   ================================================================ */

/* Start and end sit side by side because they are one thought — "08:00 to
   18:00" — and stacking them invited reading the second as unrelated. */
.shift-times {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-4);
}

.form-hint {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin: 0 0 var(--space-4) 0;
  line-height: 1.5;
  /* Same reasoning as `.form-field__hint` (WP-P51.A4): now that a form can use
   * the full card, a paragraph of guidance would otherwise run past 1000px.
   * A touch wider than the field hint because this one is prose at --text-sm,
   * not a caption. */
  max-width: 72ch;
}

/* Opt out of the 72ch measure where the hint is a full-width statement rather
   than a paragraph of prose. Marcus, 2026-10-14, on the room clinics list:
   *"whatever text there is needs to take up the whole width of the page not
   wrap after about 1/3 of the space."*

   ⚠️ A MODIFIER, NOT A CHANGE TO `.form-hint`. The 72ch cap is deliberate
   (WP-P51.A4: a form can now use the full card, so unbounded guidance would run
   past 1000px). Widening the base class would re-break every other hint in the
   product to fix one of them. */
.form-hint--full { max-width: none; }

/* A hint that sits INSIDE a card header beside the title, where the base
   class's own margin pushes it off the header's baseline. ⚠️ Was an inline
   `style="margin:0"`, which no static sweep can see and no theme can touch. */
.form-hint--flush { margin: 0; }

/* The lede under a tab's heading. ⚠️ Was an inline `style="margin-top:…"` on
   four paragraphs across two space templates. */
.rf-note { margin-top: var(--space-3); }

/* ── Space > Allocation, read mode ──────────────────────────────────────────
   The grid cell shows WHICH clinic runs, in place of the picker. A reader of
   a week's allocation must not be one stray click from rewriting it. */
/* ⛔ **IT WAS ONLY IN `owner.css`, WHICH THE TENANT UI DOES NOT LOAD.** Two
   tenant templates already used `.table-scroll` — the Equipment items table
   and the Allocation grid — and got no rule at all, so a wide table pushed the
   page sideways instead of scrolling inside its own box.
   ⓘ `test_css_classes_resolve.py` reads both stylesheets, so the name resolved
   while the rule never reached the page that asked for it. The same shape as
   the `.rf-rule` case earlier today. */
.table-scroll { overflow-x: auto; max-width: 100%; }

/* ⛔ **A READ CELL WEARS THE FIELD BOX, LIKE EVERY OTHER READ VALUE.**
   It was bare text at `4px 2px`, so the week read as a jumble of words while
   the EDIT grid — the same data in `<select>`s — read as a grid. House style
   §1a: read mode is the editor minus the control, not minus the structure.
   Same tokens as `.form-field__readonly`, so a cell and a field match. */
.rc-cell-static {
  display: block;
  padding: 4px 10px;
  min-height: 32px;
  border: 1px solid var(--border-subtle);
  border-radius: 8px;
  background: var(--surface-raised);
  color: var(--text-primary);
  font-size: var(--text-sm);
  /* A clinic name longer than its column wraps rather than widening the
     table past its scroller. */
  overflow-wrap: break-word;
}

/* ⓘ "Runs, but no clinic authored" — a real answer, so it keeps the box and
   loses only the fill. An empty cell that looked identical to a missing one
   was how "— none —" read as a rendering failure. */
.rc-cell-static--empty {
  color: var(--text-secondary);
  background: transparent;
  border-style: dashed;
}

/* ⚠️ `fixed` is what makes the columns EQUAL. Without it the browser sizes
   each column to its content, so a day holding "ECG" ends up half the width
   of one holding "Echo in-Patient (Bubble)".

   ⓘ **BOTH MODES, NOT JUST READ.** Marcus reported the read grid, and the
   first fix applied there only — but the measurement showed EDIT collapsing
   its two weekend columns to 26px and 30px for exactly the same reason, which
   would have left the tab inconsistent with itself the moment you switched
   mode. Fixing the one screen that raised the complaint is the divergence
   house style §5a describes. */
.rc-grid { width: 100%; table-layout: fixed; }
.rc-grid thead th:first-child { width: 15%; }

/* A select in a fixed column takes the column, rather than forcing it wider. */
.rc-grid select { width: 100%; max-width: 100%; }
.rc-actions { margin-top: var(--space-4); display: flex; gap: var(--space-2); align-items: center; flex-wrap: wrap; }

/* ── The shared day grid, read mode ─────────────────────────────────────────
   ⚠️ A day is ON or OFF, rendered as a fact rather than a greyed tickbox
   (house style §1). `--on` has to carry more than colour, so it also takes
   the border and the weight: a reader who cannot distinguish the two hues
   still sees which days run. */
.wps-day--static { cursor: default; }
.wps-day--static.wps-day--on {
  background: var(--accent-soft);
  border-color: var(--accent);
  font-weight: 600;
  color: var(--text-primary);
}

/* ⚠️ **SHARED, NOT PAGE-LOCAL.** `.rf-rule` and `.rf-table--spaced` were
   defined inside `admin_room_edit.html`'s own `<style>` block. The Equipment
   screen then used the same names and got NOTHING — a template style block is
   scoped to the page that carries it, and `test_css_classes_resolve.py` reads
   both, so the name resolved and the rule did not reach it. Two pages means
   the stylesheet. */
.rf-rule { margin: var(--space-6) 0; }
.rf-table--spaced { margin-top: var(--space-2); }

/* Equipment > Competencies: column widths that were inline, and the
   level-plus-button pair that shared a row. */
.eq-col-level { width: 200px; }
.eq-col-add-level { width: 280px; }
.eq-inline-form { display: flex; gap: var(--space-2); }

/* ---- Breaks table ----
   Was three stacked rows of unlabelled inputs in a form-grid, so the amount,
   the label and the paid box had no visible relationship and no column
   headings — you had to guess which box wanted minutes. A table gives the
   three columns names once instead of three times. */
.breaks {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-4);
  margin-bottom: var(--space-5);
}

.breaks > legend {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-primary);
  padding: 0 var(--space-2);
}

.breaks__table {
  width: 100%;
  border-collapse: collapse;
}

.breaks__table th {
  text-align: left;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  padding: 0 var(--space-2) var(--space-2);
  border-bottom: 1px solid var(--border-subtle);
}

.breaks__table td {
  padding: var(--space-2) var(--space-2);
  vertical-align: middle;
}

/* Inputs inside the table drop the form-field margin — the cell owns spacing. */
.breaks__table input {
  width: 100%;
  min-height: 40px;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-base);
  font-family: inherit;
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.breaks__col-num { width: 8.5rem; }
.breaks__table td.breaks__col-num input {
  /* Tabular figures so 7.5 and 37.5 line up on the decimal point down the
     column, which is how you spot the one you fat-fingered. */
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.breaks__col-paid { width: 4rem; text-align: center; }
.breaks__table td.breaks__col-paid { text-align: center; }
.breaks__table td.breaks__col-paid input {
  /* ⚠️ v1.92 found this at 20×20 — the SMALLEST interactive control in the
     product and four px below the 24px WCAG 2.2 AA floor (2.5.8). ⓘ It hid
     from the checkbox sweep because the selector carries no
     `[type="checkbox"]`; only the derived height scan caught it. */
  width: var(--control-target-min);
  height: var(--control-target-min);
  min-height: 0;
  accent-color: var(--accent);
}

.th-optional {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  font-style: italic;
  color: var(--text-muted);
}

.breaks__note { margin: var(--space-3) 0 0 0; }

/* ---- Live paid-hours readout ----
   Shows the sum being done, not just its answer, so a surprising number can be
   traced to the input that caused it without saving first. */
.calc {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-5);
  background: var(--surface-raised, var(--surface-raised));
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

.calc__item { display: flex; flex-direction: column; gap: 2px; }

.calc__label {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
}

.calc__value {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
}

.calc__sep {
  font-size: var(--text-lg);
  color: var(--text-secondary);
  align-self: center;
  padding-top: var(--space-3);
}

.calc__item--total .calc__value {
  font-size: var(--text-lg);
  color: var(--success, #047857);
}

.calc__warning {
  flex-basis: 100%;
  font-size: var(--text-sm);
  color: var(--danger, #dc2626);
}

.calc--warn {
  border-color: color-mix(in srgb, var(--danger) 40%, transparent);
  background: color-mix(in srgb, var(--danger) 6%, var(--surface-raised, var(--surface-raised)));
}

.calc--warn .calc__item--total .calc__value { color: var(--danger, #dc2626); }

/* ---- Read mode ----
   Read mode used to be the edit form with `disabled` on every control. A
   disabled text box is still box-shaped, so the page read as "a form that is
   broken" and people kept clicking into fields that refused them. These rules
   style a plain definition list instead: no borders, no boxes, nothing that
   looks aimable. The only control on the page is "Edit this shift". */
.record { margin: 0; }

.record__row {
  display: grid;
  grid-template-columns: minmax(9rem, 12rem) 1fr;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--border-subtle);
}

.record__row:last-child { border-bottom: 0; }

.record__row > dt {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
}

.record__row > dd {
  margin: 0;
  font-size: var(--text-base);
  color: var(--text-primary);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
}

.record__strong { font-weight: 600; font-variant-numeric: tabular-nums; }
.record__muted { font-size: var(--text-sm); color: var(--text-secondary); }

.record__list { list-style: none; margin: 0; padding: 0; width: 100%; }
.record__list > li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-1) 0;
}

/* .pill carries a block margin for its standalone use; inline in a record row
   that margin drags the baseline down. */
.record .pill {
  margin-bottom: 0;
  padding: 2px var(--space-2);
  font-size: var(--text-xs);
}

.record__row--total { border-bottom: 0; padding-top: var(--space-4); }
.record__row--total > dd .record__strong {
  font-size: var(--text-lg);
  color: var(--success, #047857);
}

@media (max-width: 640px) {
  .record__row { grid-template-columns: 1fr; gap: var(--space-1); }
  .breaks__col-num { width: 6.5rem; }
}

/* ================================================================
   KPI tiles — the shared list-page summary strip
   ================================================================

   ⚠️ THIS IS A GRADUATION, NOT A NEW DESIGN.

   The pattern was written for the throughput dashboard and left inline
   in `admin_throughput.html` under a comment saying it would "graduate
   into screens.css during v1.7.7.1 hotfix bundling". That never
   happened, so one page had coloured tiles and the twenty-two pages
   including `_list_kpis.html` had flat grey boxes — the polish was not
   lost, it was never shared. Values below are the throughput ones, so
   that page is unchanged and the rest catch up to it.

   The bar is a `::before` rather than `border-left` because a border
   participates in the box model: with `border-left: 4px` the tile's
   content shifts 4px right and a tile with a bar no longer aligns with
   one without. Absolute-positioned, it paints over the corner radius
   and costs no layout. `currentColor` means one rule serves every
   variant — a tile sets `color` and the bar follows. */

.list-kpi-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px;
  margin-bottom: 12px;
}

.list-kpi {
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: 8px;
  padding: 12px 14px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  position: relative;
  overflow: hidden;
  /* A tile is a SURFACE on the page background, not a box inside a card.
     `--surface` is white and `--bg` is slate-50, so the contrast comes from
     the page. Nesting these in a `.card` made it white-on-white and the
     panels vanished — the row read as a bare list of numbers. */
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
  /* Default axis colour; a --<tier> modifier overrides it. */
  color: var(--accent);
}

.list-kpi::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 4px;
  background: currentColor;
  opacity: 0.85;
}

/* ⚠️ LABEL AND NOTE WERE BOTH 11px, AND 11px IS A SCREEN SIZE RATHER THAN A ROOM
   SIZE. These tiles are the first thing shown on 45 pages and the first thing an
   audience reads on a projector; uppercase at 11px with 0.04em tracking is
   legible at a desk and a grey smear from the back of a room. 13px is the
   smallest step that fixes it.

   The VALUE deliberately stays 24px. The hierarchy is the point of the tile --
   the number has to dominate -- and 24:13 still reads as a headline over a
   caption where 24:11 read as a headline over nothing. */
.list-kpi__label {
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
}

.list-kpi__value {
  font-size: 24px;
  font-weight: 700;
  color: var(--text-primary);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

/* ⚠️ WAS 13px, MATCHED TO THE LABEL ABOVE -- AND THE LABEL'S OWN 11px->13px
   FIX (see above) TURNED OUT NOT TO BE "THE SMALLEST STEP THAT FIXES IT" FOR
   THIS ONE. Marcus, presenting this same tile row: "the bottom text like
   'not staffed in this horizon' is too small... make it a standard text
   size and do the same for every tile that has text under the main number."
   "Standard" taken literally: --text-base (16px), the size of ordinary body
   text, not another notch of caption. This is the shared partial 45 pages'
   worth of KPI tiles render through, so the fix reaches every one of them
   in this one place -- not a per-page patch. */
.list-kpi__note { font-size: var(--text-base); color: var(--text-secondary); line-height: 1.35; }

/* Tier sets the tile's `color`, which the bar inherits. The VALUE keeps
   its own colour for good/warn/bad because a red number is the point of
   flagging one; on a neutral tile the number stays ink and only the bar
   carries the accent. */
/* ⚠️ WAS #047857/#b45309/#b91c1c -- hex, not tokens, in the one file whose own
   header rule is "TOKENS, NOT HEX". Harmless on a plain white tile (5.0-5.5:1,
   AA-passing), but stale: tokens.css corrected --success (4.49:1 -> 7.34:1)
   and --warning (4.24:1 -> 5.98:1) on 2026-09-03, both failing on the tinted
   pill backgrounds they were actually measured on, and this shared partial --
   used by 22+ admin list pages -- kept quoting the pre-correction values
   instead of picking the fix up automatically. */
.list-kpi--good { color: var(--success); }
.list-kpi--warn { color: var(--warning); }
.list-kpi--bad  { color: var(--danger); }
.list-kpi--good .list-kpi__value { color: var(--success); }
.list-kpi--warn .list-kpi__value { color: var(--warning); }
.list-kpi--bad  .list-kpi__value { color: var(--danger); }

/* Axis colours, for tiles that mean a category rather than a health
   state. Same palette the throughput strip uses. */
.list-kpi--count     { color: #2563eb; }
.list-kpi--time      { color: #0e7490; }
.list-kpi--people    { color: #7c3aed; }
.list-kpi--muted     { color: var(--text-secondary); }

/* Dates and other single-token values that must stay on one line. Applied to
   the header too, so the column reserves its width even when every row is
   empty. */
.cell--date,
th.cell--date {
  white-space: nowrap;
}

/* ---- Working-pattern template: shifts per day ----
   Replaces the "A·  AP  ·P" tick grid. Fourteen ticks could say
   roughly-morning but never 07:00-15:00, and sat beside an `hours` number
   that was a second independent fact — every template in the dev tenant
   disagreed with itself on conversion.

   One column per ISO day so the week keeps its shape at a glance; the days
   somebody does not work stay visible as gaps rather than collapsing, because
   "Tue and Thu off" is the thing an operator is scanning for. */
.wp-pattern__days {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 3px;
  min-width: 260px;
}

.wp-pattern__day {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 3px 2px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

.wp-pattern__day--off { background: transparent; }

.wp-pattern__dow {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--text-secondary);
  letter-spacing: 0.04em;
}

.wp-pattern__shift {
  font-size: 10px;
  line-height: 1.25;
  text-align: center;
  color: var(--text-primary);
  /* Shift names are authored, so they can be long. Wrap rather than
     truncate: a clipped name is indistinguishable from a different shift. */
  overflow-wrap: anywhere;
}

.wp-pattern__none { font-size: 11px; color: var(--text-muted); }

.wp-pattern__more {
  display: block;
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-secondary);
}

.wp-pattern .pill { margin-bottom: 0; }

/* ================================================================
   Working-pattern template: shifts on days
   ================================================================

   Replaces one `<select multiple size="3">` per day listing the whole shift
   catalogue. That control needed ctrl-click to pick a second option and to
   DESELECT, made you scroll the same long list once per day, and rendered a
   demanding widget on days off — when "not working" is the resting state and
   should cost no interaction.

   Shift-major instead: one row per shift, carrying the days it runs. */

.wps-shift {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--surface);
}

.wps-shift__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}

.wps-shift__name { font-weight: 600; color: var(--text-primary); }

.wps-days {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
}

.wps-days {
  padding: 4px 0;
}

/* Weeks after the first get a rule above them: in a 4-week rotation the strips
   run together otherwise, and "which week am I ticking" is the question the
   whole screen has to keep answering. */
.wps-days + .wps-days {
  border-top: 1px dashed var(--border-subtle);
  margin-top: 4px;
}

.wps-days__week {
  font-size: var(--text-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  min-width: 3rem;
}

/* A single-week cycle has no "week 1" to distinguish, so the label is noise. */
.wps--single-week .wps-days__week { display: none; }

/* A day is a hit target, not a bare checkbox: the label and the box are one
   control, so the whole chip is clickable. */
.wps-day {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  cursor: pointer;
  user-select: none;
  background: var(--surface);
}

.wps-day:hover { border-color: var(--accent); }

/* Ticked days carry the accent, so the week's SHAPE is readable at a glance
   rather than requiring you to look at seven small boxes. */
.wps-day:has(input:checked) {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-color: var(--accent);
  font-weight: 600;
}

.wps-day:has(input:focus-visible) { outline: 2px solid var(--focus-ring); outline-offset: 2px; }

/* Weekends muted, NOT hidden — a weekend shift is unusual, not impossible, and
   hiding it would make on-call patterns unrepresentable. */
.wps-day--weekend { color: var(--text-secondary); border-style: dashed; }

.wps-empty {
  padding: var(--space-4);
  border: 1px dashed var(--border-subtle);
  border-radius: var(--radius-md);
  color: var(--text-secondary);
  text-align: center;
}

.wps-add { margin-top: var(--space-4); }
.wps-add__label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  margin-bottom: var(--space-1);
}

.wps-results { list-style: none; margin: var(--space-2) 0 0; padding: 0; }

.wps-results__item {
  display: block;
  width: 100%;
  text-align: left;
  padding: 7px 12px;
  border: 0;
  border-left: 3px solid transparent;
  background: none;
  font: inherit;
  cursor: pointer;
  border-radius: var(--radius-sm);
}

.wps-results__item:hover,
.wps-results__item:focus-visible {
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  border-left-color: var(--accent);
}

.wps-results__none {
  padding: 7px 12px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  font-style: italic;
}

/* ---- The add-a-shift result set ----
   It was a bare list of text with a hover colour, which reads as prose, not as
   results. A search result needs three things a paragraph does not have: a
   container that floats above the page, a subject with supporting detail
   beneath it, and the query visible inside each hit. */
.wps-add { position: relative; }

.wps-results {
  list-style: none;
  margin: 4px 0 0;
  padding: 4px;
  /* Floating, not inline: the panel belongs to the input, and pushing the page
     down on every keystroke makes the rows move while you aim at them. */
  position: absolute;
  z-index: 20;
  left: 0;
  right: 0;
  max-height: 320px;
  overflow-y: auto;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
}

.wps-results[hidden] { display: none; }

/* The count. Turns a list into a RESULT SET — the single clearest signal that
   what follows was searched for rather than merely listed. */
.wps-results__head {
  padding: 6px 10px 8px;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
  border-bottom: 1px solid var(--border-subtle);
  margin-bottom: 4px;
}

.wps-result {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  cursor: pointer;
}

/* The + says "this adds something" before you read a word of the row. */
.wps-result__add {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  line-height: 1;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

.wps-result__body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.wps-result__name { font-weight: 600; color: var(--text-primary); }

/* Supporting detail under the subject: the times and what it pays. A result
   with no detail is indistinguishable from a menu item. */
.wps-result__meta {
  font-size: var(--text-xs);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}

.wps-result__dot { margin: 0 6px; }

.wps-result:hover,
.wps-result--active {
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

.wps-result--active { outline: 2px solid var(--accent); outline-offset: -2px; }
.wps-result:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: -2px; }

/* The matched substring, inside each hit. Nothing else says "search" as
   directly as seeing your own query highlighted in the answer. */
.wps-result mark {
  background: color-mix(in srgb, var(--warning) 35%, transparent);
  color: inherit;
  border-radius: 2px;
  padding: 0 1px;
}

/* Read-only shift rows on the staff working-pattern tab. Same markup as the
   template editor, so the two surfaces read identically — but nothing here is
   editable yet, and a tickable-looking checkbox that ignores you is worse than
   a plain one. */
.wps--readonly .wps-shift__remove { display: none; }
.wps--readonly .wps-day { cursor: default; }
.wps--readonly .wps-day:hover { border-color: var(--border-subtle); }
.wps--readonly .wps-day input { pointer-events: none; }

/* Sub-tabs inside a tab panel. Quieter than the page's own tab bar — a nested
   navigation that shouts as loudly as its parent makes the hierarchy
   unreadable. */
.entity-tabs__list--sub {
  display: flex;
  gap: var(--space-1);
  list-style: none;
  margin: 0 0 var(--space-3);
  padding: 0 0 var(--space-1);
  border-bottom: 1px solid var(--border-subtle);
}

.entity-tabs__list--sub .entity-tabs__link {
  font-size: var(--text-sm);
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  border-bottom: 2px solid transparent;
}

.entity-tabs__list--sub .entity-tabs__link--active {
  border-bottom-color: var(--accent);
  font-weight: 600;
}

/* Effective-from + cycle length sit side by side above the shift rows: they
   qualify the same save, and stacking them made the form read as three
   unrelated steps. */
.wp-eff {
  display: flex;
  gap: var(--space-4);
  flex-wrap: wrap;
  align-items: flex-start;
  margin-bottom: var(--space-3);
}
.wp-eff .form-field { margin-bottom: 0; }

/* "Create X as a new shift" — the picker's escape hatch when the catalogue has
   nothing. Styled as an action rather than a result: it does something
   different from the rows above it, and looking identical to them would invite
   the click that adds a shift rather than the one that creates it. */
.wps-results__create {
  display: block;
  padding: 8px 10px;
  margin-top: 2px;
  border-top: 1px solid var(--border-subtle);
  color: var(--accent);
  text-decoration: none;
  font-size: var(--text-sm);
}
.wps-results__create:hover { text-decoration: underline; }

/* =====================================================================
   WP-P49 — My Roster 24-hour timeline
   ---------------------------------------------------------------------
   One shared 24-column track per day row, so gaps and overlaps align
   vertically down the week. Blocks are positioned from LOCAL minutes
   computed in the handler; nothing here does clock arithmetic.

   Colour is the SERVICE LINE (.sl-1..10 from tokens.css). Time is
   position. Trust Admin state is the LED. Sign-off owed is a labelled
   pill. Four facts, four channels, no hue doing two jobs.
   ===================================================================== */
.rtl {
  --rtl-blk: 66px;   /* three lines at --text-sm; do not shrink for lanes */
  --rtl-gap: 4px;
  --rtl-pad: 6px;
  --rtl-daycol: 92px;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  padding: 14px 16px 8px;
  overflow-x: auto;
}
.rtl__ruler,
.rtl__row { display: grid; grid-template-columns: var(--rtl-daycol) 1fr; gap: 0 10px; }
.rtl__ruler { align-items: end; padding-bottom: 6px; min-width: 640px; }
.rtl__row { border-top: 1px solid var(--border-subtle); min-width: 640px; }
.rtl__ticks { position: relative; height: 20px; }
.rtl__tick {
  position: absolute; top: 0; transform: translateX(-50%);
  font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
}
/* Same overhang, same fix, on the worker timeline — see the note on
   `.rgt__ticks`. The two rulers are separate components with the identical
   defect; leaving one is how they drift. */
.rtl__ticks .rtl__tick:first-child { transform: none; }
.rtl__ticks .rtl__tick:last-child { transform: translateX(-100%); }
.rtl__daycol { padding: 8px 0; font-size: var(--text-sm); color: var(--text-secondary); }
.rtl__daycol b { display: block; color: var(--text-primary); font-weight: 640; }
.rtl__date { font-family: var(--font-mono); font-size: var(--text-xs);
             font-variant-numeric: tabular-nums; }

/* The row grows with its lanes rather than halving its blocks — halving
   would undo the vertical stacking that keeps all three facts on screen. */
.rtl__track {
  position: relative;
  min-height: calc(var(--lanes, 1) * var(--rtl-blk)
                   + (var(--lanes, 1) - 1) * var(--rtl-gap) + var(--rtl-pad) * 2);
}
.rtl__grid { position: absolute; inset: 0; pointer-events: none; }
.rtl__gl { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
.rtl__gl--major { background: var(--border-strong); }
.rtl__empty {
  position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  margin: 0; font-size: var(--text-xs); color: var(--text-muted);
}

.rtl__blk {
  position: absolute; height: var(--rtl-blk);
  display: flex; flex-direction: column; justify-content: center; gap: 1px;
  padding: 4px 7px; overflow: hidden; border-radius: 6px; cursor: pointer;
  color: var(--sl-ink); border: 1px solid rgba(0, 0, 0, 0.08);
}
.rtl__blk:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rtl__t {
  font-family: var(--font-mono); font-size: var(--text-sm); font-weight: 650;
  font-variant-numeric: tabular-nums; line-height: 1.25;
  display: flex; gap: 5px; align-items: center;
}
.rtl__clock, .rtl__loc, .rtl__sl { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rtl__pts { flex-shrink: 0; opacity: 0.95; margin-left: auto; }
.rtl__pts::after { content: "p"; opacity: 0.6; margin-left: 1px; }
.rtl__loc { font-size: var(--text-sm); font-weight: 620; line-height: 1.25; }
.rtl__sl { font-size: var(--text-sm); opacity: 0.9; line-height: 1.25; display: flex; align-items: center; }
/* Below ~36 minutes no character renders honestly. The block keeps its true
   width so the day's shape stays accurate; detail lives in the tooltip. */
/* ⚠️ AN INVITATION, NOT A BOOKING (MIG-300). A briefing -- Safety Brief, CPD,
   Trust Governance -- reaches a worker's calendar without an assignment: it
   consumes no programmed activity, constrains no solve, and can be missed.
   Marcus, 2026-08-29: "These are not service_lines so should be visibly
   different".

   Deliberately NEUTRAL rather than another colour in the service-line palette,
   because every one of those means "this is the line you are on". Grey with a
   dashed edge reads as pinned-up rather than allocated, and the dashes carry
   the meaning for anyone who cannot separate the greys. */
/* The same distinction in the week and month grids. ⚠️ ONE RULE, BOTH GRIDS --
   a briefing that looked like an invitation on the timeline and like a clinic
   on the month view would be the same "two answers to one question" this
   codebase has produced twice already. */
.slot--invitation {
  background: var(--surface-raised) !important;
  color: var(--text-secondary) !important;
  border: 1px dashed rgba(74, 85, 99, 0.55) !important;
}

/* The legend swatch for a briefing — same treatment as the block it explains,
   so the key and the thing it keys cannot drift apart. */
.rtl__legend-invitation {
  display: inline-block; width: 14px; height: 10px; border-radius: 3px;
  background: var(--surface-raised);
  border: 1px dashed rgba(74, 85, 99, 0.55);
  vertical-align: -1px; margin-right: 5px;
}

.rtl__blk--invitation {
  background: var(--surface-raised);
  color: var(--text-secondary);
  border: 1px dashed rgba(74, 85, 99, 0.55);
  box-shadow: none;
}
.rtl__blk--invitation .rtl__pts::after { content: ""; }
.rtl__blk--chip { padding: 0; }
.rtl__blk--cut-r { border-top-right-radius: 0; border-bottom-right-radius: 0;
                   border-right: 2px dashed rgba(255, 255, 255, 0.55); }
.rtl__blk--cut-l { border-top-left-radius: 0; border-bottom-left-radius: 0;
                   border-left: 2px dashed rgba(255, 255, 255, 0.55); }
/* The rail lives in RESERVED padding, so it cannot occlude — the space is
   taken out of the text box rather than laid over it. */
.rtl__blk--needs { padding-left: 27px; }
.rtl__blk--needs::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 21px;
  background: rgba(0, 0, 0, 0.32);
  border-right: 1px solid rgba(255, 255, 255, 0.38);
  border-radius: 5px 0 0 5px;
}
.rtl__blk--needs::after {
  content: ""; position: absolute; left: 5px; top: 50%; width: 11px; height: 11px;
  margin-top: -6px; border: 2px solid #fff; border-radius: 3px;
}
/* Too narrow for the rail AND the checkbox: keep the rail, drop the glyph.
   A stripe still says "this one" where a 2px checkbox would say nothing. */
.rtl__blk--chip.rtl__blk--needs { padding-left: 0; }
.rtl__blk--chip.rtl__blk--needs::before { width: 100%; border-right: 0; border-radius: 5px; }
.rtl__blk--chip.rtl__blk--needs::after { display: none; }

/* ⚠️ THE ANSWERED STATE, WHICH THE TIMELINE NEVER HAD. `--needs` drew a rail
   with an EMPTY white box -- an unticked checkbox -- and there was no ticked
   counterpart, so the default view's only response to a sign-off was that the
   prompt stopped appearing. A worker could not tell "I did this" from "this
   never asked me".

   Deliberately the SAME rail geometry with the box replaced by a tick: the two
   states are one control, and a reader who has learnt the amber rail does not
   have to learn a second vocabulary. Green carries "done" here without
   colliding with the LED below, which is Trust Admin state and never green. */
.rtl__blk--signed { padding-left: 27px; }
.rtl__blk--signed::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 21px;
  background: rgba(22, 163, 74, 0.45);
  border-right: 1px solid rgba(255, 255, 255, 0.38);
  border-radius: 5px 0 0 5px;
}
.rtl__blk--signed::after {
  content: ""; position: absolute; left: 8px; top: 50%; width: 5px; height: 10px;
  margin-top: -7px; border: solid #fff; border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}
/* A chip is too narrow for a rail beside text, so it becomes the whole block --
   mirroring how `--needs` already degrades. */
.rtl__blk--chip.rtl__blk--signed { padding-left: 0; }
.rtl__blk--chip.rtl__blk--signed::before {
  width: 100%; border-right: 0; border-radius: 5px;
}
.rtl__blk--chip.rtl__blk--signed::after { display: none; }

.rtl__legend-signed {
  position: relative; display: inline-block; width: 21px; height: 15px;
  background: rgba(22, 163, 74, 0.45); border: 1px solid var(--border-strong);
  border-radius: 4px; vertical-align: middle;
}
.rtl__legend-signed::after {
  content: ""; position: absolute; left: 7px; top: 2px; width: 4px; height: 8px;
  border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
}

/* The legend shows the rail itself rather than describing it. */
.rtl__legend-needs {
  position: relative; display: inline-block; width: 21px; height: 15px;
  background: rgba(0, 0, 0, 0.32); border: 1px solid var(--border-strong);
  border-radius: 4px; vertical-align: middle;
}
.rtl__legend-needs::after {
  content: ""; position: absolute; left: 4px; top: 2px; width: 9px; height: 9px;
  border: 2px solid #fff; border-radius: 2px;
}
/* ⚠️ IN THE FLOW, NOT OVER THE TEXT. These replace an absolutely-positioned
   "⚠ CONFIRM" pill that sat at top-right — precisely where the patient count
   lives — so the number a worker checks before a clinic was hidden behind a
   word. Overlay was the defect, not the offsets: anything positioned over a
   block this dense lands on something. Inline, they cannot occlude at all. */
.rtl__ind {
  flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
  width: 13px; height: 13px; border-radius: 50%; font-style: normal;
  font-size: 10px; font-weight: 800; line-height: 1;
}
/* ⚠️ NOT A COLOURED DOT, and not for want of trying one. Two objections
   killed it:
     1. A 13px dot is missed at a glance across a week — the thing it marks is
        an action the worker OWES, so it has to survive peripheral vision.
     2. Amber already means Trust Admin state on the LED a few pixels away.
        A warning-orange dot puts one hue on two jobs, which is the exact
        confusion the four-channel rule exists to prevent.
   So this drops hue as the carrier and uses SHAPE and CONTRAST instead: a
   full-height rail, darkening whatever fill sits behind it, holding an empty
   checkbox. It reads identically on all ten palette fills because it does not
   depend on any of them, and an empty checkbox affords its own action without
   a caption. Down a week, unconfirmed sessions line up as a column. */
.rtl__ind--override,
.rtl__ind--anomaly { background: rgba(255, 255, 255, 0.9); color: #7c2d12; width: auto;
                     min-width: 13px; padding: 0 3px; border-radius: 4px; }

/* Trust Admin traffic light — the same three hues as .rg-ta-led on the rooms
   grid. A WHITE ring, because these sit on saturated fills where the rooms
   grid's dark ring disappears. */
.rtl__led {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
  margin-right: 5px; border: 1.5px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.25); background: #94A3B8;
}
.rtl__led--not_started { background: #DC2626; }
.rtl__led--started_incomplete { background: #EAB308; }
.rtl__led--complete { background: #16A34A; }

.rtl__legend {
  display: flex; flex-wrap: wrap; gap: 16px; align-items: center;
  margin-top: 10px; font-size: var(--text-xs); color: var(--text-secondary);
}
.rtl__legend span { display: inline-flex; align-items: center; }
.rtl__legend-hint { margin-left: auto; }

/* ⚠️ DECLARED BEFORE `.sl-1..10`, AND THAT ORDER IS THE WHOLE POINT.
   `.sl-N` and a chip's own class have the SAME specificity, so whichever is
   written later wins. The chips first carried `background` in their templates'
   inline <style>, which the browser applies after this stylesheet — so every
   chip came out uniformly grey and the identity fill never landed. The fill
   must be the last word, and a fallback for a chip without a slot must come
   before it. */
.my-roster-grid__slot,
.month-grid__slot,
.my-team-slot { background: var(--surface-raised); }

.sl-1 { background: var(--sl-1); } .sl-2 { background: var(--sl-2); }
.sl-3 { background: var(--sl-3); } .sl-4 { background: var(--sl-4); }
.sl-5 { background: var(--sl-5); } .sl-6 { background: var(--sl-6); }
.sl-7 { background: var(--sl-7); } .sl-8 { background: var(--sl-8); }
.sl-9 { background: var(--sl-9); } .sl-10 { background: var(--sl-10); }

/* Timeline detail popover (WP-P49). */
.rtl-pop {
  position: fixed; z-index: 60; min-width: 230px; max-width: 320px;
  background: var(--surface); color: var(--text-primary);
  border: 1px solid var(--border-strong); border-radius: 9px;
  box-shadow: 0 12px 32px rgba(15, 23, 42, 0.22); padding: 14px 16px;
}
.rtl-pop[hidden] { display: none; }
.rtl-pop h4 { margin: 0 0 8px; font-size: var(--text-sm); font-weight: 660; }
.rtl-pop dl { margin: 0 0 12px; display: grid; grid-template-columns: auto 1fr;
              gap: 4px 12px; font-size: var(--text-xs); }
.rtl-pop dt { color: var(--text-secondary); }
.rtl-pop dd { margin: 0; font-variant-numeric: tabular-nums; }

/* ---- WP-P49: the contracted shift, drawn ABOVE the blocks ----
   ⚠️ The z-order is load-bearing, not cosmetic. Behind the blocks the rule's
   start edge is covered by any assignment beginning at the same time — the
   common case, and precisely when you need to read it. A rule with end caps
   rather than a filled bar so it reads as a DIMENSION about the row instead
   of another datum competing with the work. `--text-primary` rather than
   literal black so it stays the strongest mark in dark theme too. */
.rtl__shiftrule { position: absolute; top: 1px; height: 9px; z-index: 5; pointer-events: none; }
.rtl__shiftrule::before {
  content: ""; position: absolute; left: 0; right: 0; top: 0; height: 2px;
  background: var(--text-primary); opacity: 0.72; border-radius: 1px;
}
.rtl__cap { position: absolute; top: 0; width: 2px; height: 9px;
            background: var(--text-primary); opacity: 0.72; border-radius: 1px; }
.rtl__cap--s { left: 0; } .rtl__cap--e { right: 0; }
.rtl__legend-rule { display: inline-block; width: 22px; height: 2px;
                    background: var(--text-primary); opacity: 0.72;
                    margin-right: 6px; vertical-align: 3px; }
.rtl__shifthrs, .rtl__nopattern {
  display: block; font-size: 9.5px; opacity: 0.72; font-variant-numeric: tabular-nums;
}
.rtl__shifthrs { font-family: var(--font-mono); }
.rtl__nopattern { font-style: italic; }

/* A public holiday, named in the day column. NOT `.pill`: that carries
   `--text-sm` plus `--space-2/3` padding and a `--space-4` bottom margin, all
   sized for a table cell, and "Summer Bank Holiday" is 19 characters going into
   `--rtl-daycol` = 92px. The grid view can afford the real pill; this column
   cannot, so the badge wraps instead of overflowing and keeps the row height it
   already had. Muted deliberately — the day is a fact, not a warning, and amber
   here would compete with `.rtl__ind--anomaly` on the track beside it. */
.rtl__hol {
  display: block;
  margin-top: 3px;
  padding: 1px 4px;
  border: 1px solid #cbd5e1;
  border-radius: 4px;
  background: var(--surface-raised);
  color: var(--text-secondary);
  font-size: 9.5px;
  line-height: 1.25;
  font-weight: 560;
  text-wrap: balance;
}

/* An override is EXPLAINED, not wrong — marked, never alarmed. Amber is
   reserved for the case nobody authorised. `--unknown` and `--in` are
   deliberately unstyled: a worker with no recorded pattern must not be
   decorated as an exception. */
.rtl__blk--override { outline: 2px dotted var(--text-primary); outline-offset: -3px; }
.rtl__blk--anomaly { box-shadow: 0 0 0 2px var(--warning); }


/* ---- WP-P49 step 3: the service-line colour picker ----
   Swatches, never a hex field — MIG-240 stores a palette SLOT, so the control
   can only offer what the palette vetted. */
.sl-swatches { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 6px 0 12px; }
.sl-swatch { display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
             border: 1px solid var(--border-strong); border-radius: 7px; padding: 5px 8px; }
.sl-swatch input { margin: 0; }
.sl-swatch:has(input:checked) { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.sl-swatch:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.sl-chip { display: inline-block; width: 18px; height: 18px; border-radius: 4px;
           border: 1px solid rgba(0, 0, 0, 0.12); }
.sl-chip.sl-1 { background: var(--sl-1); } .sl-chip.sl-2 { background: var(--sl-2); }
.sl-chip.sl-3 { background: var(--sl-3); } .sl-chip.sl-4 { background: var(--sl-4); }
.sl-chip.sl-5 { background: var(--sl-5); } .sl-chip.sl-6 { background: var(--sl-6); }
.sl-chip.sl-7 { background: var(--sl-7); } .sl-chip.sl-8 { background: var(--sl-8); }
.sl-chip.sl-9 { background: var(--sl-9); } .sl-chip.sl-10 { background: var(--sl-10); }
/* The preview is a REAL block beside its real neighbours — a swatch on its own
   cannot answer "does this clash?", which is the only reason to open the picker. */
.sl-preview { display: flex; gap: 10px; flex-wrap: wrap; padding: 14px; border-radius: 9px;
              background: var(--bg); border: 1px dashed var(--border-strong); }
.sl-preview .rtl__blk { position: static; width: 168px; height: auto; min-height: 50px; }

/* WP-P49 step 4: the day-start control. On the screen it affects rather than
   in a settings page — the effect is the explanation. */
.rtl__dayctl { display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
               margin-bottom: 10px; padding: 10px 14px; border-radius: 8px;
               background: var(--surface-raised); border: 1px solid var(--border-subtle);
               font-size: var(--text-xs); }
.rtl__dayctl label { color: var(--text-secondary); font-weight: 560; }
.rtl__dayctl select { font: inherit; padding: 4px 8px; border-radius: 6px;
                      border: 1px solid var(--border-strong);
                      background: var(--surface); color: var(--text-primary); }
.rtl__dayctl-hint { color: var(--text-secondary); }
.rtl__rowfrom { display: block; font-family: var(--font-mono); font-size: var(--text-xs);
                opacity: 0.7; font-variant-numeric: tabular-nums; }

/* WP-P49 step 5: the window control, and the phone case it exists for. */
.rtl__dayctl-set { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.rtl__widened { color: var(--warning); font-weight: 600; }

/* Under ~640px the day column and the track compete for a width neither can
   spare, so the label moves above the track and the track takes the row. This
   is what the narrowed window is FOR — a 10-hour window on a full-width track
   is a readable block; the same work on a 24-hour scale is a sliver. */
@media (max-width: 640px) {
  .rtl__row { grid-template-columns: 1fr; }
  .rtl__ruler .rtl__daycol { display: none; }
  .rtl__daycol { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline;
                 padding-bottom: 4px; }
  .rtl__rowfrom { display: inline; }
  .rtl__dayctl { font-size: 11px; }
}


/* Layout switcher (Timeline / Week / Month).

   ⚠️ MOVED OUT OF `_roster_schedule.html`. It lived in that template's inline
   <style>, so the timeline — which does not include that partial — rendered
   the switcher as three unstyled links. A shared partial whose CSS is not
   shared is only half extracted. */
.view-toggle {
  display: inline-flex; gap: 0; margin-bottom: var(--space-2, 8px);
  border: 1px solid var(--border-strong, #cbd5e1); border-radius: 4px; overflow: hidden;
}
.view-toggle__btn {
  padding: 6px 14px; font-size: var(--text-sm); text-decoration: none;
  color: var(--text-primary); background: var(--surface);
  border-right: 1px solid var(--border-strong, #cbd5e1);
}
.view-toggle__btn:last-child { border-right: 0; }
.view-toggle__btn:hover { background: var(--surface-raised); }
.view-toggle__btn--active,
.view-toggle__btn--active:hover { background: var(--accent); color: #fff; }
.view-toggle__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ============================================================
   Rooms timeline (WP-P49) — /ui/rosters/{id}/rooms?layout=timeline

   Deliberately the same visual language as the My Roster timeline: one
   proportional 24-hour track, identity fill by service line, the Trust Admin
   LED for state. Rooms are the rows here rather than days, because the
   question this page answers is "which room is free at 14:00".

   ⚠️ Blocks set NO background of their own — `.sl-1..10` supplies it, and a
   background here would beat it on source order. That is exactly how the week
   and month grids came out uniformly grey.
   ============================================================ */
.rgt {
  --rgt-roomcol: 150px;
  --rgt-blk: 34px;
  /* Vertical space between stacked lanes in one room-day track. */
  --rgt-lane-gap: 3px;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: 10px;
  padding: 10px 14px 6px;
  overflow-x: auto;
}
.rgt__ruler,
.rgt__row { display: grid; grid-template-columns: var(--rgt-roomcol) 1fr; gap: 0 10px;
            min-width: 720px; }
.rgt__ruler { align-items: end; padding-bottom: 4px; }
.rgt__row { border-top: 1px solid var(--border-subtle); align-items: center; }
.rgt__ticks { position: relative; height: 18px; }
.rgt__tick {
  position: absolute; top: 0; transform: translateX(-50%);
  font-family: var(--font-mono); font-size: var(--text-xs);
  color: var(--text-secondary); font-variant-numeric: tabular-nums;
}
/* ⚠️ BOTH ENDS, not just the first.
 *
 * `:first-child` was already pulled back to `transform: none` so 00:00 does not
 * hang off the left edge. The LAST tick had no such rule: it sits at
 * `left: 100%` still centred, so half of "00:00" overhangs the right edge of a
 * container that is `overflow-x: auto` — and a horizontal scrollbar appeared on
 * a track that otherwise fitted exactly. Right-aligning it puts its right edge
 * on the final gridline, which is where the eye reads it from anyway. */
.rgt__ticks .rgt__tick:first-child { transform: none; }
.rgt__ticks .rgt__tick:last-child { transform: translateX(-100%); }
.rgt__roomcol { padding: 6px 0; font-size: var(--text-sm); color: var(--text-primary);
                overflow: hidden; text-overflow: ellipsis; }
.rgt__roomcode { display: block; font-family: var(--font-mono); font-size: var(--text-xs);
                 color: var(--text-secondary); }
/* ⚠️ The track grows with its deepest lane. `--rgt-lanes` is set inline per
   room-day by the handler; the fallback of 1 keeps the old height for any
   caller that does not set it. Without this the second lane is drawn outside
   the box and lands on top of the next room's row. */
.rgt__track { position: relative;
              min-height: calc(var(--rgt-lanes, 1) * (var(--rgt-blk) + var(--rgt-lane-gap)) + 7px); }
.rgt__grid { position: absolute; inset: 0; pointer-events: none; }
.rgt__gl { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--border-subtle); }
.rgt__gl--major { background: var(--border-strong); }
.rgt__blk {
  /* ⚠️ `top` is a FUNCTION OF THE LANE, and used to be a literal 5px. With one
     shared top, two people in one room at one time drew at the same pixel and
     one of them simply was not there — 48 of 160 blocks on a single week. */
  position: absolute;
  top: calc(5px + var(--rgt-lane, 0) * (var(--rgt-blk) + var(--rgt-lane-gap)));
  height: var(--rgt-blk);
  display: flex; flex-direction: column; justify-content: center; gap: 1px;
  padding: 3px 6px; overflow: hidden; border-radius: 5px;
  color: var(--sl-ink); border: 1px solid rgba(0, 0, 0, 0.08);
}
.rgt__blk--cut-r { border-top-right-radius: 0; border-bottom-right-radius: 0;
                   border-right: 2px dashed rgba(255, 255, 255, 0.55); }
.rgt__blk-t {
  font-family: var(--font-mono); font-size: var(--text-xs); font-weight: 650;
  font-variant-numeric: tabular-nums; line-height: 1.15;
  display: flex; align-items: center; gap: 4px;
}
.rgt__blk-s { font-size: var(--text-xs); line-height: 1.15; opacity: 0.92;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
              display: flex; align-items: baseline; gap: 4px; min-width: 0; }

/* ⚠️ The time range must not be the thing that gets squeezed. `.rgt__blk-t` is
   a flex row holding the LED, the range and the patient count; without this the
   range and the staff name compete and the end time is what disappears. The
   range keeps its intrinsic width, the count is pinned right, and the STAFF
   NAME is the only elastic part — a truncated name is recoverable from the
   tooltip, a truncated clock time is a misreading. */
.rgt__blk-time { flex: 0 0 auto; }
.rgt__blk-end { opacity: 0.75; }
.rgt__blk-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

/* The service-line chip: the same `<code>` treatment the AM/PM grid cell gives
   it, sized down to sit inside a 34px block. */
.rgt__blk-sl {
  flex: 0 0 auto; font-family: var(--font-mono); font-size: 10px;
  padding: 0 3px; border-radius: 3px;
  background: rgba(255, 255, 255, 0.32); color: inherit;
}

/* Pinned to the right edge, mirroring `.rtl__pts` on the worker timeline —
   the count lives in the same place on both timelines so the eye learns it
   once. The "p" suffix is the worker timeline's convention; kept identical. */
.rgt__pts { flex: 0 0 auto; margin-left: auto; opacity: 0.95; }
.rgt__pts::after { content: "p"; opacity: 0.6; margin-left: 1px; }
/* The layout guidance above the switcher. Two descriptions side by side once
   there is room for them, stacked on a narrow screen — width spent on
   structure, not on longer lines of prose. */
.rg-viewintro { margin: 0 0 var(--space-4, 16px); }
.rg-viewintro__lead {
  font-size: var(--text-sm); color: var(--text-secondary);
  line-height: 1.5; margin: 0 0 10px; max-width: 80ch;
}
.rg-viewintro__opts {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 10px 18px; list-style: none; margin: 0; padding: 0;
}
.rg-viewintro__opt {
  font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.45;
  padding: 8px 12px; border-radius: 8px;
  border: 1px solid var(--border-subtle); background: var(--surface);
  /* Neutral by default so the ACTIVE one is the only thing that stands out. */
  border-left: 3px solid var(--border-subtle);
}
.rg-viewintro__opt b { display: block; color: var(--text-primary); margin-bottom: 2px; }
/* ⚠️ Marks which layout you are ON, so the paragraph you are reading is
   anchored to the thing in front of you — the whole failure this replaces was
   text describing a view the reader had not chosen. */
.rg-viewintro__opt--active {
  border-left-color: var(--accent, #2563eb);
  background: color-mix(in srgb, var(--accent, #2563eb) 4%, var(--surface));
}

.rgt-day { margin-top: var(--space-4, 16px); }
.rgt-day__title { font-size: var(--text-base); margin: 0 0 6px; display: flex;
                  align-items: center; gap: 8px; flex-wrap: wrap; }
.rgt-day__idle { font-size: var(--text-xs); color: var(--text-secondary); font-weight: 400; }
.rgt-day__count { font-size: var(--text-xs); color: var(--text-secondary); font-weight: 400; }

/* Day collapse. The toggle is a button styled as the heading it replaces —
   it must look like the day name, because that is what people reach for. */
.rgt-dayctl { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
              margin: var(--space-3, 12px) 0 0; }
.rgt-dayctl__hint { font-size: var(--text-xs); color: var(--text-secondary); }
.rgt-day__toggle {
  display: inline-flex; align-items: center; gap: 8px;
  background: none; border: 0; padding: 2px 4px 2px 0; margin: 0;
  font: inherit; color: inherit; cursor: pointer; border-radius: 6px;
}
.rgt-day__toggle:hover .rgt-day__chev { color: var(--accent, #2563eb); }
.rgt-day__toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* A CSS triangle rather than a glyph, so it cannot land on a font that lacks
   the character and cannot be read out as "black down-pointing triangle". */
.rgt-day__chev {
  width: 0; height: 0; flex: 0 0 auto;
  border-left: 5px solid currentColor;
  border-top: 4.5px solid transparent;
  border-bottom: 4.5px solid transparent;
  color: var(--text-secondary);
  transform: rotate(90deg);
  transition: transform var(--motion-fast, .12s) var(--ease-standard, ease);
}
/* ⚠️ Driven by `aria-expanded`, NOT by a separate class. One source of truth:
   if the attribute and a class could disagree, the sighted state and the
   announced state would eventually disagree too. */
.rgt-day__toggle[aria-expanded="false"] .rgt-day__chev { transform: rotate(0deg); }
.rgt-day__toggle[aria-expanded="false"] ~ .rgt-day__count { font-weight: 600; color: var(--text-primary); }
.rgt-day[data-rgt-collapsed] > .rgt { display: none; }
@media (prefers-reduced-motion: reduce) {
  .rgt-day__chev { transition: none; }
}

/* The room chips in the AM/PM grid take the identity fill too, so switching
   layouts changes the arrangement and never the colour of a clinic. */
.rg-assignment { color: var(--sl-ink); }
/* The AM/PM chip is now a coloured block, so it needs the shape of one. */
.rg-assignment { padding: 3px 5px; border-radius: 4px; }
.rg-assignment .rg-staff { opacity: 0.95; }

/* ⚠️ WHITE INK IS CORRECT ON A FILL AND INVISIBLE WITHOUT ONE.
   `--sl-ink` is a ROOT CONSTANT (#FFFFFF in tokens.css), not a per-line colour:
   it is the ink for text sitting ON a service-line fill, and all ten --sl-N
   fills are dark, so white is right for them. `.rg-assignment` takes it because
   in the AM/PM grid the chip IS a coloured block.

   The Working from home table is a plain table and applies no fill -- computed
   background is rgba(0,0,0,0) -- so the same rule painted white text on a white
   cell and every manually-added service line was simply not there. Reported by
   Marcus 2026-08-30: "white cannot be read at all."

   Fixed at the chip that has no fill rather than by weakening --sl-ink, which
   the filled chips depend on. */
.rg-assignment[data-work-mode="wfh"] { color: var(--text-primary); }
/* A cancelled session reads the same in both layouts. */
.rgt__blk--cancelled { opacity: 0.55; text-decoration: line-through; }
.rgt__blk { cursor: context-menu; }

/* Room sessions on the rooms timeline (Cohort F made the data; this shows it).

   The band is what the room is OPEN for; the blocks are what is BOOKED into
   it. A booking then visibly sits inside or outside the room's hours.

   ⚠️ NEUTRAL, NOT A HUE. Black is the shift rule product-wide, --sl-1..10 are
   service-line identity, and red/amber/green are the Trust Admin LED. A fourth
   hue here would put two meanings on one channel.

   ⚠️ The band sets a background but is a SIBLING of the blocks, never a parent
   and never a background on .rgt__track — a background up there would beat
   .sl-N on the blocks and grey the whole row, which is exactly how the week
   and month grids broke. */
.rgt__band {
  position: absolute; top: 0; bottom: 0; z-index: 0;
  background: var(--surface-raised);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
  border-radius: 3px;
}
/* A guess, drawn like one: no room has said these are its hours. */
.rgt__band--fallback {
  background: transparent;
  box-shadow: none;
  border: 1px dashed var(--border-subtle);
  opacity: 0.75;
}

/* ⚠️ ABOVE THE BLOCKS (z-index 5, matching the shift rule), because a session
   whose first booking starts on time would otherwise hide its own start edge —
   the ordinary case, not an edge case. */
.rgt__band-start {
  position: absolute; top: -2px; bottom: -2px; width: 2px; z-index: 5;
  background: var(--text-secondary); opacity: 0.85; border-radius: 1px;
  pointer-events: none;
}
.rgt__band-start::after {
  content: ""; position: absolute; top: -3px; left: -2px;
  border-left: 3px solid transparent; border-right: 3px solid transparent;
  border-top: 4px solid var(--text-secondary); opacity: 0.9;
}
.rgt__band-start--fallback { opacity: 0.4; }

.rgt__bandstate {
  display: block; font-size: var(--text-xs); color: var(--text-secondary);
  font-style: italic;
}
.rgt__bandstate--closed { color: var(--text-muted); }

/* A booking outside the room's authored sessions. Only ever set where a
   schedule EXISTS to be outside of — see the handler. */
.rgt__blk--outside { box-shadow: 0 0 0 2px var(--warning); }

/* ⚠️ A 23-row table on a dashboard of 16 panels takes more vertical space
   than everything around it. The detail is worth keeping — it is the record —
   but it does not get to be the page. Summarised above, scrolled within. */
.widget-summary { margin: 0 0 var(--space-2); font-size: var(--text-sm); color: var(--text-secondary); }
/* ⚠️ IT WAS CLIPPING A ROW IN HALF. A bare max-height cuts wherever it lands,
   and a half-drawn row at the bottom of a panel reads as a rendering fault,
   not as "there is more below". Only visible on a screenshot -- the markup is
   identical either way, which is why it survived several passes.

   The inset shadow at the bottom edge is the affordance: it says the content
   continues without needing a scrollbar to be present. */
.widget-scroll {
  max-height: 18rem;
  overflow-y: auto;
  overflow-x: auto;
  border-bottom: 1px solid var(--border-subtle, rgba(15, 23, 42, 0.10));
  background:
    linear-gradient(var(--surface, var(--surface)) 30%, rgba(255, 255, 255, 0)) top / 100% 1.5rem no-repeat,
    linear-gradient(rgba(255, 255, 255, 0), var(--surface, #fff) 70%) bottom / 100% 1.5rem no-repeat,
    linear-gradient(rgba(15, 23, 42, 0.08), rgba(255, 255, 255, 0)) top / 100% 0.5rem no-repeat,
    linear-gradient(rgba(255, 255, 255, 0), rgba(15, 23, 42, 0.08)) bottom / 100% 0.5rem no-repeat;
  background-attachment: local, local, scroll, scroll;
}
.widget-scroll .widget-table { width: 100%; }
.widget-scroll thead th { position: sticky; top: 0; background: var(--surface, var(--surface)); z-index: 1; }

/* ── Widget base styling ──────────────────────────────────────────────────
   ⚠️ EVERY WIDGET RULE USED TO BE SCOPED UNDER `.dr-widget`, AND ONLY TWO
   PAGES APPLY THAT CLASS.

   The widget adapter packs emit `widget__title`,
   `widget__metric`, `widget__caption`, `widget-no-data` and friends as their
   PUBLIC OUTPUT — that markup is the widget contract. The styling for it
   lived in page-scoped <style> blocks inside dashboard_render.html and
   admin_widget_dashboards_preview.html, under a `.dr-widget` ancestor those
   two pages add.

   /ui/admin/job-plan-monitoring renders the same sixteen widgets inside a
   plain `<div data-widget-code>` in a `.card-grid`. No `.dr-widget`
   ancestor, so not one rule applied: bare <h3>s, headline numbers at body
   size, no card, no separation. Marcus: "looks a right mess", then "looks
   awful". He was describing unstyled HTML, not a layout choice.

   Same shape as `row-menu__trigger`: markup written against CSS that is not
   loaded where the markup renders. These are the BASE rules, unscoped, so a
   widget looks like a widget anywhere. The `.dr-widget`-scoped rules stay —
   they are dashboard-specific overrides (alignment, accent, title hiding)
   and are more specific, so they still win where they apply.

   ⚠️ `widget__caption` AND `widget__sub` HAD NO CSS ANYWHERE AT ALL, on any
   page, scoped or not. Every caption written by every adapter has been
   rendering as an unstyled paragraph since the packs shipped. */
/* ⚠️ THE CARD IS THE WRAPPER, NOT THE <article>. The template renders the
   "No data source connected yet" note as a SIBLING of the article, so
   bordering the article would leave that note stranded outside the card it
   belongs to -- on precisely the two panels whose whole message is that
   note. */
[data-widget-code] {
  background: var(--surface, var(--surface));
  /* ⚠️ #cbd5e1, NOT #e5e7eb. The hairline was there and too faint to do the
     job: on a white panel a #e5e7eb border is nearly invisible, so a tile's
     caption sat directly beside the next tile's caption and the two read as
     one block of text. "the help text under the number is right next to text
     from another tile and needs to be visually separated." A border that
     exists but cannot be seen is the same as no border. */
  border: 1px solid var(--border-strong, #cbd5e1);
  border-radius: 8px;
  padding: var(--space-3, 1rem);
  display: flex;
  flex-direction: column;
}
[data-widget-code] > article { flex: 1 1 auto; }

/* ⚠️ CENTRED, BUT ONLY WHERE THERE IS A SINGLE FIGURE TO CENTRE.
   A metric tile is a headline number with a label above and a caption below;
   centring those reads as one object and pulls the eye to the figure.
   Centring a TABLE's prose would re-justify rows of text that need a common
   left edge to be scannable, so the rule is scoped to tiles that contain no
   table. */
[data-widget-code]:not(:has(table)) > article { text-align: center; }
[data-widget-code]:not(:has(table)) .widget__caption { text-align: center; }

/* The caption is separated from the figure it explains, so it cannot be read
   as running on from the tile beside it. */
[data-widget-code] .widget__caption { margin-top: auto; }

/* ⚠️ A TABLE WIDGET NO LONGER SITS IN THE GRID AT ALL.
   An earlier pass gave it `grid-column: span 2` inside `.card-grid`, which
   was a workaround for putting a table in a `minmax(220px, 1fr)` cell in the
   first place. The monitoring page now renders metric widgets in the grid and
   tabular ones beneath it, full width -- so the span rule is gone rather than
   left behind matching nothing.

   The `:has(table)` selector stays available for any OTHER page that still
   mixes the two in one grid. */
.card-grid > [data-widget-code]:has(table) { grid-column: 1 / -1; }

/* A tabular widget below the tile row: the section's full width, and spaced
   from the row above it. */
/* Air between tiles, so each border reads as a boundary rather than a line
   drawn between two touching blocks of text. */
.card-grid { gap: var(--space-3, 1rem); }
.monitoring-full { margin-top: var(--space-3, 1rem); }
.monitoring-full > [data-widget-code] { width: 100%; }
[data-widget-code] .widget__title,
[data-widget-code] > article > h3 {
  margin: 0 0 0.5rem 0;
  font-size: 0.85em;
  font-weight: 600;
  color: var(--text-secondary, #6b7280);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
[data-widget-code] .widget__metric {
  margin: 0;
  font-size: 1.9em;
  font-weight: 700;
  color: var(--accent, #2563eb);
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}
[data-widget-code] p { margin: 0.2rem 0; }
/* The number's supporting line: "Due within 30 days: 0". */
[data-widget-code] .widget__sub {
  margin: 0.25rem 0 0;
  font-size: var(--text-sm, 0.875rem);
  color: var(--text-secondary, #6b7280);
  font-variant-numeric: tabular-nums;
}
/* What the number means. Sits last in the card, so it is separated rather
   than crowding the figure it explains. */
[data-widget-code] .widget__caption {
  margin: 0.5rem 0 0;
  padding-top: 0.5rem;
  border-top: 1px solid var(--border-subtle);
  font-size: var(--text-sm, 0.875rem);
  color: var(--text-secondary, #6b7280);
}
[data-widget-code] .widget-no-data p,
[data-widget-code] .widget__pending {
  color: var(--text-secondary);
  font-size: 0.85em;
  font-style: italic;
  margin: 0;
}
[data-widget-code] .widget-load-error,
[data-widget-code] .widget-render-error { color: var(--danger); }
[data-widget-code] .widget-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm, 0.875rem); }
[data-widget-code] .widget-table th,
[data-widget-code] .widget-table td { padding: 6px 8px; text-align: left; border-bottom: 1px solid var(--border-subtle); }

/* ⚠️ ALIGN BY COLUMN POSITION, NOT BY SIBLING TYPE.
   This was `td + td, th + th { text-align: right }`, and it put a header and
   its own data in different alignments. The coverage table's body rows begin
   with `<th scope="row">Emergency…</th>` and then `<td>`s, so the FIRST td
   follows a th -- it never matched `td + td` and stayed left -- while the
   header "Coverage" did match `th + th` and went right. Header right, values
   left, in one column.

   Marcus, 2026-08-26: "the 'Coverage' title is centred. It does not appear
   correctly above the numbers." Reading it as centring was the natural
   conclusion: a right-aligned header over left-aligned values looks adrift
   from both edges.

   `:not(:first-child)` asks the question that actually matters -- is this the
   first column? -- so a row-header cell no longer changes what follows it. */
[data-widget-code] .widget-table th:not(:first-child),
[data-widget-code] .widget-table td:not(:first-child) {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* ⚠️ THE HEADER ROW LOOKED LIKE DATA. Same size, same weight, same colour as
   the values beneath it, separated only by the row border every other row
   also has. "the titles of the columns are hard to distinguish between them
   and the data under them." */
[data-widget-code] .widget-table thead th {
  background: var(--surface-raised, var(--surface-raised));
  font-size: 0.75rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary, #6b7280);
  border-bottom: 2px solid var(--border-subtle);
}
[data-widget-code] .widget-table tbody th { font-weight: 500; }
[data-widget-code] .widget-table tbody tr:last-child th,
[data-widget-code] .widget-table tbody tr:last-child td { border-bottom: 0; }

/* ── Traffic-light pills ──────────────────────────────────────────────────
   ⚠️ SIX TEMPLATES USED THESE AND NONE OF THEM EVER RENDERED A COLOUR.
   `pill--green`, `pill--amber` and `pill--red` had no rule anywhere in the
   codebase; only `pill--success`, `pill--warning` and `pill--danger` existed.
   So every traffic-light on the quarterly reconciliation dashboard -- the
   feature that page is built around, "colour-coded traffic-light per plan by
   absolute variance percent" in its own module docstring -- rendered as plain
   unstyled text, along with the legend explaining the colours.

   admin_critical_rules, admin_diagnostics,
   admin_production_readiness_gates, admin_reconciliation_dashboard,
   admin_workload_plan_detail and admin_workload_plan_reconciliation.

   Found by SCREENSHOTTING the page. It is invisible to every other check:
   the markup is right, the class is applied, the template is correct, and
   the CSS simply has nothing to say about it. Same family as
   `row-menu__trigger` and `widget-kv`.

   Named for the traffic-light vocabulary the data uses (`traffic_light` is
   'green' | 'amber' | 'red' in job_plan_quarterly_reconciliation), rather
   than remapped to success/warning/danger at each call site -- the mapping
   would be a second place to get it wrong, and amber has no equivalent. */
.pill--green {
  background: color-mix(in srgb, #16A34A 14%, transparent);
  color: var(--success);
}
.pill--amber {
  background: color-mix(in srgb, #D97706 16%, transparent);
  color: var(--warning);
}
.pill--red {
  background: color-mix(in srgb, #DC2626 14%, transparent);
  color: var(--danger);
}
/* Inside a table cell a pill must not carry the block-level bottom margin the
   base class uses for standalone banners. */
.entity-table .pill,
.admin-table .pill,
.recon-legend .pill {
  margin-bottom: 0;
  padding: 2px 8px;
}

/* ── Filter-bar controls ──────────────────────────────────────────────────
   ⚠️ THE HOUSE CONTROL RULE IS SCOPED TO `.form-field`, AND A FILTER BAR IS
   NOT A FORM FIELD. So every bare `<select>` in a toolbar got browser
   defaults: measured on the reconciliation page, 19px tall against the
   house's 44px minimum, no padding, 13px type — sitting beside a 32px button
   that it could not line up with. "the drop down looks a bit short
   vertically."

   The 44px is not decoration: it is the touch-target minimum the form rule
   was written to hold. A filter is as clickable as a field.

   Same family as `row-menu__trigger`, `widget-kv` and `pill--green`: markup
   written outside the scope its styling was defined in. */
.recon-scope select,
.recon-scope input,
.recon-toolbar select,
.recon-toolbar input,
.cycle-filter select,
.cycle-filter input {
  /* ⛔⛔ v1.92: THIS RULE SITS BELOW THE COARSE BLOCK, so no restore rule in
     that block can ever reach it — at equal specificity, source order wins.
     Before the token, lowering this height would have handed every filter bar
     in the product a 32px touch target on a tablet, invisibly. The token
     inherits, so position in the file stops mattering. */
  min-height: var(--control-min-h);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-base);
  font-family: inherit;
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}
.recon-scope select:hover,
.recon-toolbar select:hover,
.cycle-filter select:hover { border-color: var(--border-strong); }
.recon-scope label,
.recon-toolbar label { font-size: var(--text-sm); font-weight: 600; color: var(--text-secondary); }

/* ── Variance pills: direction by hue, magnitude by strength ──────────────
   ⚠️ TWO FACTS IN ONE MARK, AND BOTH ARE NEEDED.
   Colour used to be the traffic light alone, so a column where every plan
   landed within 5% rendered as an undifferentiated block of green and the
   over/under words did all the work. Marcus: "should change the colour
   depending on whether it is over or under" and "I do like the colour to
   show the amount out too".

   Direction alone would have thrown away the governance signal -- 5% and 15%
   are the thresholds the page exists to police. So:

       hue       shortfall runs WARM, surplus runs COOL
       strength  paler within 5%, stronger past 15%

   Warm-for-shortfall is the way round that matches the domain: delivering
   less than planned is the condition a reconciliation is looking for. A
   surplus is worth seeing and is not an alarm.

   ⚠️ EQUAL WIDTH IS PART OF THE ENCODING. Pills that size to their text make
   "0.1% under" and "12.7% under" different-sized objects, and the eye reads
   size as significance before it reads the number. A fixed min-width means
   the only thing varying down the column is colour. */
.pill--var {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 7.5rem;
  margin-bottom: 0;
  padding: 2px 8px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Shortfall — delivered less than planned. */
.pill--var-under-green { background: #FEF3C7; color: #92400E; }
.pill--var-under-amber { background: #FDE68A; color: #78350F; }
.pill--var-under-red   { background: #FEE2E2; color: #991B1B; font-weight: 700; }

/* Surplus — delivered more than planned. */
.pill--var-over-green  { background: #DBEAFE; color: #1E40AF; }
.pill--var-over-amber  { background: #BFDBFE; color: #1E3A8A; }
.pill--var-over-red    { background: #E0E7FF; color: #3730A3; font-weight: 700; }

/* Exactly on plan — neither direction, so neither hue. */
.pill--var-on-green,
.pill--var-on-amber,
.pill--var-on-red { background: var(--surface-raised, var(--surface-raised)); color: var(--text-secondary, var(--text-secondary)); }

.recon-legend__sep { color: var(--text-muted); }
.recon-legend__note { color: var(--text-secondary, #6b7280); font-size: var(--text-sm); }

/* ── Form controls, everywhere ────────────────────────────────────────────
   ⚠️ THIS IS THE THIRD PAGE WITH THE SAME SYMPTOM, SO IT GETS A ROOT FIX.
   The house control rule is scoped to `.form-field`. Anything outside one --
   a filter bar, a table-row control, a date picker in a panel -- fell back to
   browser defaults: measured at 19px against the 44px minimum, no padding,
   13px type. Reported three times as pages looking "short vertically" or
   "poorly presented", each time from a different screen.

   Patching per container was whack-a-mole; the model is simply that a control
   looks like a control. Declarations match `.form-field`'s exactly, so where
   both apply nothing changes.

   ⚠️ SPECIFICITY IS DELIBERATELY LOW (a bare type selector), so `.form-field`
   and any page rule still win. Buttons, checkboxes, radios and hidden inputs
   are excluded -- they have their own sizing and are not text fields.

   ⚠️ AND FOR A YEAR IT WAS NOT LOW, BECAUSE THE COMMENT ABOVE DESCRIBED AN
   INTENT THE SELECTOR DID NOT IMPLEMENT. Every `:not([type="..."])` carries
   the weight of the attribute selector inside it, so six of them made this
   (0,6,1) -- higher than `.form-field input` (0,1,1) and higher than any
   component rule anyone was likely to write. It won everything, silently.

   Found from the Trust Admin filter: `.ta-filter__search-wrap
   input[type="search"]` (0,2,1) sets `padding-left: 34px` to clear an
   absolutely-positioned magnifying glass at `left: 10px`. It lost, the padding
   computed to this rule's 12px, and the icon sat ON TOP of the "S" of
   "Service line or department name" -- which reads as a strange character
   followed by "ervice line", exactly as reported.

   `:where()` contributes ZERO specificity, so the selector is now (0,0,1) --
   a bare type selector, which is what the paragraph above always claimed.
   Matching is identical; only the weight changes.

   ⚠️ Fixed together with the `var(--border-subtle)` typo below and in fifteen other
   places, and the order matters: `--border` is defined NOWHERE, so
   `border: 1px solid var(--border-subtle)` is invalid at computed-value time and
   `border` falls back to its initial `border-style: none`. While this rule
   out-ranked them the invalid declarations never applied and the borders came
   from here. Lowering the specificity without fixing the token would have
   handed those components a rule that computes to NO BORDER AT ALL. */
input:where(
    :not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="range"])
  ),
select,
textarea {
  /* ⛔ v1.92: **THIS IS THE TRUE GLOBAL, and it is easy to miss.**
     `:where()` contributes ZERO specificity, so this rule is (0,0,1) — a bare
     element selector reaching every control in the product. The
     `.form-field` rule above merely overrides it. Lowering only
     `.form-field` would have left everything outside a form-field at 44px. */
  min-height: var(--control-min-h);
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-base);
  font-family: inherit;
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}
/* ⓘ Unaffected by the token in practice: 5rem = 80px wins on the same
   element, so a textarea keeps its multi-line height. */
textarea { min-height: 5rem; }

/* A numeric override sitting in a table cell: full control treatment, but
   sized to the three characters it holds rather than stretching the column. */
.spa-override__input { width: 5.5rem; text-align: right; font-variant-numeric: tabular-nums; }

/* ⚠️ A BAND NOBODY IS IN IS STILL EDITABLE, JUST NOT COMPETING. Seven of the
   twelve bands on this estate cover no one, and they carried exactly the same
   weight as band 7, which covers 28 people. */
.row--vacant td,
.row--vacant th { opacity: 0.62; }
.row--vacant:hover td,
.row--vacant:hover th { opacity: 1; }

/* =========================================================================
   COLLAPSIBLE TREE — shared by /ui/admin/reporting-structure and
   /ui/admin/estate.  Behaviour lives in static/tree.js.

   ⚠️ HOISTED OUT OF admin_reporting_structure.html ON 2026-08-28, where it
   had been inline since v1.7.3.2. The estate browser needed the same tree
   and the choice was a second copy or one shared home. Class names keep
   their `ars-` prefix: six test files pin them in markup, and a rename is
   churn a reader gains nothing from.

   ⚠️ ONLY THE PRIMITIVE IS HERE. The fan disclosure, re-root arrow, edit
   button and --pending highlight are reporting-structure features and stay
   in that template. Hoisting those would be the same mistake inverted.
   ========================================================================= */

/* --- tree layout --- */
.ars-tree {
  list-style: none;
  padding: 0;
  margin: 0;
}
.ars-tree__node {
  padding: 0.4rem 0;
  position: relative;
}
.ars-tree__children {
  list-style: none;
  padding: 0;
  margin: 0.4rem 0 0 0;
  border-left: 2px solid var(--border-subtle);
  padding-left: 1rem;
}
/* Estate search: one line, above the tree it filters. */
.estate-search {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2);
  margin-bottom: var(--space-4);
}
.estate-search label { font-size: var(--text-xs); color: var(--text-secondary); }
.estate-search input[type=search] { min-width: 22rem; flex: 0 1 28rem; }
.estate-results { margin-bottom: var(--space-6); }

.ars-tree__label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  flex-wrap: wrap;
  transition: border-color 0.1s ease;
}
.ars-tree__label:hover {
  border-color: var(--accent);
}

/* Estate hierarchy prefix icons — Location › Building › Floor › Area › Space.
   The five levels are otherwise told apart only by indentation, which is the
   first thing lost when a label wraps (.ars-tree__label is flex-wrap: wrap) or
   when somebody is reading a projected tree from the back of a room.

   ⚠️ THE SIZE HAS TO BE DECLARED HERE. `.icon` carries no dimensions of its own
   — the macro deliberately emits no width/height so the parent can size it, and
   the only existing rules are `.btn--icon .icon`. Without this block the SVG
   falls back to the CSS-initial 300×150 and blows the row apart, which is not a
   thing a source grep for "icon" would ever show you.

   Muted rather than accent: the icon is a signpost to the NAME beside it, and a
   column of coloured glyphs would out-shout the estate itself. Decorative only
   — the macro emits aria-hidden and the tree's structure carries the meaning. */
.estate-icon {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  color: var(--text-secondary);
}
/* ⚠️ THE SVG IS SIZED, NOT THE WRAPPER. Putting the dimensions on .estate-icon
   alone leaves the <svg> at its CSS-initial 300×150 inside a shrink-wrapped
   span, so the row still explodes -- the wrapper looks styled and the glyph
   is not. */
.estate-icon .icon {
  width: 1.05rem;
  height: 1.05rem;
}

/* The Space glyph sits in a table cell rather than a flex row, so it needs its
   own gap and baseline nudge; the flex parents above get theirs from `gap`. */
.estate-icon--lead {
  margin-right: 0.35rem;
  vertical-align: -0.19em;
}

/* --- chevron toggle (collapse/expand) --- */
.ars-tree__chev {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 6px;
  background: transparent;
  border: 1px solid transparent;
  cursor: pointer;
  color: var(--text-secondary);
  padding: 0;
  flex-shrink: 0;
}
.ars-tree__chev:hover {
  background: var(--surface-raised);
  border-color: var(--border-subtle);
  color: var(--text-primary);
}
.ars-tree__chev:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.ars-tree__chev--leaf {
  width: 2.75rem;
  height: 2.75rem;
  cursor: default;
  visibility: hidden;   /* keeps the label alignment stable */
}
.ars-tree__chev-icon {
  display: inline-block;
  font-size: 1.75em;
  line-height: 1;
  transition: transform 0.14s ease;
  transform: rotate(90deg);   /* expanded state: chevron points down */
}
.ars-tree__node--collapsed > .ars-tree__label > .ars-tree__chev .ars-tree__chev-icon {
  transform: rotate(0deg);    /* collapsed state: chevron points right */
}
.ars-tree__node--collapsed > .ars-tree__children {
  display: none;
}
/* A bulk control with nothing to act on says so rather than looking live. */
.ars-tree-controls .btn[disabled] {
  opacity: 0.55;
  cursor: not-allowed;
}
.ars-tree__node--collapsed > .ars-tree__label {
  background: rgba(37, 99, 235, 0.04);
  border-style: dashed;
}
.ars-tree__hidden-hint {
  display: none;   /* only visible when the node is collapsed */
  font-size: 0.8em;
  color: var(--accent);
  font-weight: 500;
}
.ars-tree__node--collapsed > .ars-tree__label > .ars-tree__hidden-hint {
  display: inline;
}
/* ⚠️ AND THE BADGE GOES WHEN THE HINT ARRIVES, or the row says the number
   twice: "Community Diagnostic Centre  RXX02  1  · 1 building hidden".
   Marcus, 2026-09-01: "the double 1 is bad." The two carry the same count and
   only one of them says what it counts, so the silent one yields. Expanded, the
   hint is gone and the badge is the only thing left holding the number. */
.ars-tree__node--collapsed > .ars-tree__label > .ars-tree__count {
  display: none;
}

/* --- expand/collapse controls (top of chart) --- */
.ars-tree-controls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.55rem 0.8rem;
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  margin: 0 0 0.8rem;
  flex-wrap: wrap;
}
.ars-tree-controls__label {
  font-size: 0.85em;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.ars-tree-controls__btn {
  cursor: pointer;
  padding: 0.25rem 0.7rem;
  background: var(--surface);
  border: 1px solid var(--border-subtle);
  border-radius: 4px;
  font-size: 0.85em;
  font-weight: 500;
  color: var(--text-primary);
  font-family: inherit;
}
.ars-tree-controls__btn:hover {
  border-color: var(--accent);
  background: rgba(37, 99, 235, 0.04);
}
.ars-tree-controls__hint {
  margin-left: auto;
  font-size: 0.8em;
  /* ⚠️ --color-text-muted IS NOT DEFINED ANYWHERE, so this always fell back
     to #9ca3af: 2.43:1 on this surface, the worst ratio on the Estate screen
     and barely over half of what 1.4.3 asks. The real token is
     --text-secondary. See the header of tokens.css -- screens.css references
     47 `--color-*` variables that no stylesheet declares, and every one of
     them silently renders its fallback instead. */
  color: var(--text-secondary);
  font-style: italic;
}
.ars-tree-controls__chev {
  color: var(--accent);
  font-style: normal;
}

/* --- direct-report count badge --- */
.ars-tree__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5em;
  padding: 0.1rem 0.4rem;
  background: var(--surface-raised);
  color: var(--text-secondary);
  border-radius: 999px;
  font-size: 0.8em;
  font-variant-numeric: tabular-nums;
}

/* Per-row actions, pushed to the far end so names stay in one readable column.
   `margin-left:auto` rather than `justify-content`, because a row's other
   children vary in number -- a site may or may not carry an ODS code, an
   off-site pill, a count badge. */
.ars-tree__row-actions {
  margin-left: auto;
  display: flex;
  gap: 0.35rem;
  flex-shrink: 0;
}

/* The explanatory line under a tree — "numeric badge shows ...".
 *
 * ⚠️ THIS CLASS HAD NO RULE ANYWHERE. /ui/admin/reporting-structure has marked
 * its caption `.ars-chart-caption` since v1.7.3.2 and nothing ever defined it,
 * so the sentence rendered as an ordinary body paragraph competing with the
 * chart above it. Found while checking that every class the estate page uses
 * resolves to something — a class name is not a style, and a source grep for
 * the name finds the usage either way.
 */
.ars-chart-caption {
  margin: 0.9rem 0 0;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.5;
}

/* ─────────────────────────────────────────────────────────────────────────
   Room availability  (/ui/admin/room-availability)

   The estate half of the Availability pair. Deliberately built from the same
   parts as the staff view — the shared `_list_kpis.html` strip, the same
   week/month/quarter stride, the same card surfaces — because the two pages
   answer halves of one question and a reader should not have to relearn the
   layout to cross between them.

   ⚠️ FREE IS THE COLOUR THAT CARRIES MEANING HERE, WHICH INVERTS THE USUAL
   RULE. On most screens in this product green means "done" and grey means
   "nothing". On this one an empty room is the FINDING — it is the thing you
   came to look for — so green is the loud state and occupied sessions recede
   to a neutral surface. Colouring "in use" green, as the staff grid does for
   available people, would bury the answer.
   ───────────────────────────────────────────────────────────────────────── */
/* ---- Reconciliation: delivered-against-plan sparkline ----
   ⚠️ BARS DIVERGE FROM THE PLAN LINE RATHER THAN RISING FROM THE FLOOR. Under
   the old bottom-anchored encoding a quarter 1.1% over plan drew 0.14px above
   half height and vanished inside the 1px reference stroke -- so "a bar above
   the line is over-delivery", which is what Act 6 says out loud, was never once
   true on screen. Height now encodes |pct - 100| / 2 and the SIDE encodes the
   sign, which makes direction readable at any magnitude.

   Over-delivery is a real and common answer here, not an error state. */
.recon-trend-cell { white-space: nowrap; }
.recon-spark { display: block; overflow: visible; }

/* ⚠️ THE PLAN RULE IS THE ONLY REFERENCE IN THE CELL, so it is drawn at full
   ink rather than as a hairline. Its predecessor was `--border-subtle` -- slate
   at 10% -- against slate-100 bars, i.e. invisible, on the one mark Act 6 points
   at by name. Dashed distinguishes a target from a measurement; the line itself
   is solid. */
.recon-spark__plan {
  stroke: var(--text-primary);
  stroke-width: 1;
  stroke-dasharray: 3 3;
  opacity: 0.7;
}
.recon-spark__line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.75;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;  /* preserveAspectRatio="none" would otherwise stretch it */
}
/* ⚠️ THE DOTS CARRY THE SIDE, THE LINE CARRIES THE SHAPE. Colouring the whole
   line by its final quarter would repaint three quarters of history to describe
   the last one; per-point fills say which side of plan EACH quarter landed,
   which is the question the column exists to answer.

   The surface-coloured stroke is a knockout ring, so a dot sitting on the plan
   rule stays a dot instead of merging into it. */
.recon-spark__dot {
  fill: var(--accent);
  stroke: var(--surface);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.recon-spark__dot--over { fill: var(--warning); }
.recon-spark__dot--onplan { fill: var(--text-primary); }
/* The latest quarter is the one the rest of the row is reporting, so it is the
   one the eye should land on. */
.recon-spark__dot--last { stroke-width: 1.25; }

/* ⚠️ STACKED UNDER THE CHART, NOT BESIDE IT. The cell is 123x51 and the old
   three-bar sparkline used 23x26 of it; putting the delta below buys the chart
   the full width instead of splitting it, which is what made a trajectory
   legible at all. */
.recon-spark__delta {
  display: block;
  margin-top: 1px;
  font-size: var(--text-xs); font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.recon-spark__delta--up { color: var(--success); }
.recon-spark__delta--down { color: var(--danger); }
.recon-spark__delta--flat { color: var(--text-secondary); }

/* ---- Lending pool: filter bar, day groups, held count ----
   The filters are a tool for reaching a row, not a feature to admire, so they
   sit compact above the list rather than in their own card. Server-side by
   design: the list is capped, and filtering the loaded rows in the browser
   would search a truncated set and report "nothing matches" when the match is
   on a page nobody was sent. */
.ra-pool__held { margin: 0; font-size: var(--text-sm); color: var(--text-secondary); }
.ra-pool__subtitle { margin: var(--space-4) 0 var(--space-2); font-size: var(--text-base); }
.ra-pool__filters {
  display: flex; flex-wrap: wrap; align-items: flex-end; gap: var(--space-3);
  margin: var(--space-3) 0;
}
.ra-pool__filter { display: flex; flex-direction: column; gap: 0.35rem; min-width: 10rem; }
.ra-pool__filter label {
  font-size: 0.8125rem; font-weight: 600; color: var(--text-secondary);
}
.ra-pool__day { border-top: 1px solid var(--border-subtle); }
.ra-pool__day[open] { padding-bottom: var(--space-3); }
.ra-pool__daysum {
  display: flex; align-items: baseline; gap: var(--space-3);
  padding: var(--space-2) 0; cursor: pointer;
}
.ra-pool__dayname { font-weight: 600; }
.ra-pool__foot { margin-top: var(--space-3); }

.ra-filter__form {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: flex-end;
}
.ra-filter__field { display: flex; flex-direction: column; gap: 0.35rem; min-width: 12rem; }
.ra-filter__field label { font-size: 0.8125rem; font-weight: 600; color: var(--text-secondary); }
.ra-filter__field select,
.ra-filter__field input {
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--border-subtle);
  border-radius: 6px;
  background: var(--surface);
  font: inherit;
  color: var(--text-primary);
}
.ra-filter__actions { margin-left: auto; }

.ra-grid-head {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: flex-start;
  justify-content: space-between;
}
.ra-grid-lede {
  margin: 0.25rem 0 0;
  font-size: 0.8125rem;
  color: var(--text-secondary);
  line-height: 1.5;
}
.ra-pager { display: flex; gap: 0.4rem; flex-shrink: 0; }

.ra-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  margin: 0.9rem 0 0.4rem;
  font-size: 0.75rem;
  color: var(--text-secondary);
}
.ra-legend__item { display: inline-flex; align-items: center; gap: 0.4rem; }
.ra-chip--swatch { width: 18px; height: 14px; padding: 0; display: inline-block; }

/* ⚠️ The table scrolls INSIDE this box, not the page. A quarter view is 91
   columns wide and letting the document scroll horizontally drags the nav and
   the KPI strip off screen with it. */
.ra-scroll { overflow-x: auto; margin-top: 0.75rem; }

.ra-grid { border-collapse: separate; border-spacing: 0; width: 100%; font-size: 0.75rem; }
.ra-grid__roomhead, .ra-grid__room {
  /* Sticky room column: at 91 columns the row label is the only thing telling
     you which room you are looking at. */
  position: sticky;
  left: 0;
  z-index: 2;
  background: var(--surface);
  text-align: left;
  min-width: 190px;
  border-right: 1px solid var(--border-subtle);
  padding: 0.5rem 0.7rem 0.5rem 0;
  vertical-align: top;
}
.ra-grid__dayhead {
  padding: 0.4rem 0.3rem;
  font-weight: 600;
  color: var(--text-secondary);
  border-bottom: 1px solid var(--border-subtle);
  white-space: nowrap;
  min-width: 96px;
}
.ra-grid__dayhead--weekend { background: rgba(148, 163, 184, 0.10); }
.ra-grid__dow { display: block; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.04em; }
.ra-grid__dom { display: block; font-weight: 500; color: var(--text-primary); }
.ra-grid__roomname { display: block; font-weight: 600; color: var(--text-primary); font-size: 0.8125rem; }
.ra-grid__roommeta { display: block; color: var(--text-secondary); margin-top: 0.1rem; }
.ra-grid__cell {
  padding: 0.25rem;
  border-bottom: 1px solid var(--border-subtle);
  vertical-align: top;
}
.ra-grid__cell--weekend { background: rgba(148, 163, 184, 0.06); }
.ra-grid__none { color: #cbd5e1; display: block; text-align: center; padding: 0.4rem 0; }

.ra-chip {
  display: block;
  border-radius: 4px;
  padding: 0.2rem 0.35rem;
  margin-bottom: 0.2rem;
  border-left: 3px solid transparent;
  line-height: 1.25;
  overflow: hidden;
}
.ra-chip__time { display: block; font-variant-numeric: tabular-nums; font-size: 0.6875rem; opacity: 0.75; }
.ra-chip__body { display: block; font-weight: 600; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
.ra-chip--spare      { background: #dcfce7; border-left-color: #16a34a; color: #14532d; }
.ra-chip--taken      { background: var(--surface-raised); border-left-color: #cbd5e1; color: var(--text-secondary); }
/* Briefings borrow the invitation palette from the roster timeline so the same
   concept looks the same wherever it appears. */
.ra-chip--attendance { background: #fef3c7; border-left-color: #d97706; color: #78350F; }
/* Lent to another roster area. Distinct from `taken` on purpose: taken is
   a question for the rota, lent is a question for a phone call. */
.ra-chip--lent       { background: #ede9fe; border-left-color: #7c3aed; color: #4c1d95; }

.ra-util {
  display: block;
  height: 4px;
  margin-top: 0.35rem;
  background: #e2e8f0;
  border-radius: 999px;
  overflow: hidden;
  max-width: 170px;
}
.ra-util--wide { max-width: none; min-width: 140px; height: 8px; margin: 0; }
.ra-util__fill { display: block; height: 100%; background: #0ea5e9; border-radius: 999px; }
.ra-util__fill--cold { background: #f97316; }

.ra-trend { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.ra-trend th, .ra-trend td {
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border-subtle);
  text-align: left;
}
.ra-trend thead th {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-secondary);
}
.ra-trend__barcol { width: 30%; }
.ra-num { text-align: right; font-variant-numeric: tabular-nums; }
.ra-trend__row--cold { background: rgba(249, 115, 22, 0.06); }

.ra-tag {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 600;
  vertical-align: middle;
}
.ra-tag--cold { background: #ffedd5; color: #9a3412; }
.ra-tag--hot  { background: #e0f2fe; color: #075985; }

.ra-notes__list { margin: 0.5rem 0 0; padding-left: 1.1rem; line-height: 1.6; font-size: 0.8125rem; color: var(--text-secondary); }
.ra-notes__list li { margin-bottom: 0.35rem; }
.ra-notes__list strong { color: var(--text-primary); }
.ra-error { border-left: 3px solid #dc2626; }

@media (max-width: 720px) {
  .ra-filter__actions { margin-left: 0; }
  .ra-grid__roomhead, .ra-grid__room { min-width: 140px; }
}
/* "Rarely opened" — a room that opens on a cycle, distinct from one that is
   open daily and empty. Amber would read as a second alarm; this is a note. */
.ra-tag--quiet { background: #ede9fe; color: #5b21b6; }

/* Pool state tags. `in pool` is the affirmative state and reads calm rather
   than loud: being offered is normal, not an alert. */
.ra-tag--pool  { background: #dcfce7; color: #14532d; }
.ra-tag--off   { background: var(--surface-raised); color: var(--text-tertiary); }
.ra-trend__nobar { font-size: 0.75rem; color: var(--text-secondary); font-style: italic; }
.ra-empty-pool { border-left: 3px solid #0ea5e9; }
.ra-empty-pool p { margin: 0.5rem 0 0; line-height: 1.6; font-size: 0.875rem; }

/* Counterfactual batch telemetry (MIG-302). Placed above every derived figure
   because the age of a measurement changes what the figures mean. */
.cf-batch { border: 1px solid var(--border-subtle); border-left: 3px solid #94a3b8;
            border-radius: 8px; padding: 0.9rem 1.1rem; margin: 0 0 1.2rem; background: var(--surface); }
.cf-batch--running { border-left-color: #0ea5e9; }
.cf-batch--stuck   { border-left-color: #f59e0b; }
.cf-batch--stale   { border-left-color: #dc2626; }
.cf-batch__title { font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em;
                   color: var(--text-secondary); margin: 0 0 0.4rem; }
.cf-batch__line { margin: 0 0 0.3rem; font-size: 0.875rem; line-height: 1.5; }
.cf-batch__meta { margin: 0.3rem 0 0; font-size: 0.8125rem; color: var(--text-secondary); line-height: 1.5; }
.cf-batch__msg  { color: var(--text-secondary); }
.cf-batch__bar  { display: block; height: 6px; background: #e2e8f0; border-radius: 999px; overflow: hidden; margin: 0.5rem 0; }
.cf-batch__fill { display: block; height: 100%; background: #0ea5e9; border-radius: 999px; }
.cf-batch__stop { margin-top: 0.6rem; }
/* MIG-303. Queued is its own colour because it is its own state: nothing is
   running yet, so it must not borrow the live-run blue. */
.cf-batch--queued  { border-left-color: #8b5cf6; }
.cf-batch__run  { margin-top: 0.6rem; display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
/* The cost sits beside the control, not behind a tooltip: an operator deciding
   whether to press it needs the 84 minutes in the same glance as the button. */
.cf-batch__cost { font-size: 0.8125rem; color: var(--text-secondary); line-height: 1.5; }

/* ===========================================================================
   Classes the templates use that nothing defined
   ===========================================================================

   ⚠️ MEASURED, NOT GUESSED, AND THE FIRST TWO MEASUREMENTS WERE WRONG.
   A grep for `\.alert\b` across the stylesheets says "absent", which is true
   but not sufficient — several families are defined in per-page `<style>`
   blocks instead, so a stylesheet-only search over-reports. Counting both, 211
   distinct classes used in `class="..."` are defined nowhere at all.

   ⚠️ AND A GREP FOR `\.btn--\w+` REPORTED `.btn--danger` AS DEFINED WHEN IT IS
   NOT. `\w` excludes `-`, so the match stopped inside `.btn--danger-ghost` and
   printed a truncation. The pattern answered a different question than the one
   asked. Confirmed the real state in a browser instead: an element with
   `class="alert alert--danger"` differs from a bare <div> on ZERO computed
   properties.

   The families below are the ones with unambiguous intent and real damage —
   87 uses of `.alert*` rendering error banners as plain text, 18 destructive
   buttons that do not look destructive, 12 `.pill--warn` (the stylesheet spells
   it `--warning`). The remaining long tail is recorded in
   tests/arc/test_css_classes_resolve.py rather than styled speculatively:
   inventing a look for a class whose intent is unclear is how a design system
   acquires two of everything.
   =========================================================================== */

/* Block-level sibling of .pill. Same colour vocabulary, full width, because an
   alert is the page telling you something rather than a label on a row. */
.alert {
  display: block;
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  border-radius: var(--radius-md, 8px);
  border: 1px solid var(--border-subtle);
  font-size: var(--text-sm);
  line-height: 1.4;
}
.alert--danger,
/* `--error` and `--danger` are used interchangeably across templates. Rather
   than rename 8 call sites and leave the next author guessing which is right,
   both resolve to the same thing. */
.alert--error {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
}
.alert--success {
  background: color-mix(in srgb, var(--success) 14%, transparent);
  color: var(--success);
  border-color: color-mix(in srgb, var(--success) 35%, transparent);
}
.alert--warning {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  color: var(--warning);
  border-color: color-mix(in srgb, var(--warning) 35%, transparent);
}

/* ⚠️ A TYPO CLASS, KEPT AS AN ALIAS RATHER THAN CORRECTED IN 12 TEMPLATES.
   The stylesheet spells it `--warning`; the templates say `--warn`. Aliasing
   fixes every existing page at once and cannot regress; renaming 12 call sites
   fixes the same pages and leaves a 13th to be written wrong tomorrow. */
.pill--warn {
  background: color-mix(in srgb, var(--warning) 14%, transparent);
  color: var(--warning);
  border: 1px solid color-mix(in srgb, var(--warning) 35%, transparent);
}

/* Cards carrying a state. The border does the work: a card is already a
   surface, so tinting the whole background would fight the content inside it. */
.card--danger,
.card--error {
  border-color: color-mix(in srgb, var(--danger) 45%, transparent);
  border-left: 3px solid var(--danger);
}
.card--success {
  border-color: color-mix(in srgb, var(--success) 45%, transparent);
  border-left: 3px solid var(--success);
}

/* ⚠️ DESTRUCTIVE BUTTONS DID NOT LOOK DESTRUCTIVE. `.btn--danger-ghost` exists;
   plain `.btn--danger` did not, so 18 Delete buttons rendered with default
   button styling. On the same day the CSP was found to have disabled their
   confirm dialogs, that is two independent layers of "this is dangerous"
   missing from the same control. */
.btn--danger {
  background: var(--danger);
  color: #fff;
  border: 1px solid var(--danger);
}
.btn--danger:hover {
  background: color-mix(in srgb, var(--danger) 85%, #000);
}

/* ── Estate labels (/ui/admin/estate-labels) ─────────────────────────────
 *
 * ⚠️ THE PANELS ARE HIDDEN ONLY ONCE THE TABS WORK. `.el-panel--hidden` is
 * inert until `behaviours.js` marks the form `data-tabs-ready`, so a browser
 * that never runs the script shows all three panels stacked and the tablist
 * not at all — the pre-tabs layout, which was usable. Hiding first and
 * enhancing after would leave a no-JS operator with two thirds of the form
 * unreachable and no way to know.
 *
 * This product has already shipped a CSP that blocked its own scripts for two
 * days (see behaviours.js). "The JavaScript did not run" is a state worth
 * designing for.
 */
.el:not([data-tabs-ready]) .entity-tabs { display: none; }
.el[data-tabs-ready] .el-panel--hidden { display: none; }

.el-panel { padding: var(--space-4) 0 0 0; }

.el-lede {
  color: var(--text-secondary);
  margin: 0 0 var(--space-4) 0;
  max-width: 68ch;
}

.el-note {
  color: var(--text-secondary);
  font-size: var(--text-sm);
  margin: var(--space-3) 0 0 0;
  max-width: 68ch;
}

.el-table { width: 100%; border-collapse: collapse; }

.el-table__head {
  text-align: left;
  padding: var(--space-2) var(--space-3);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

.el-row__caption {
  text-align: left;
  padding: var(--space-3);
  font-weight: 600;
  white-space: nowrap;
  width: 1%;                       /* shrink to the word; give the inputs the rest */
}

.el-row__cell { padding: var(--space-2) var(--space-3); }

.el-input {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  font: inherit;
  color: var(--text-primary);
  background: var(--surface);
}
.el-input:focus { outline: none; box-shadow: var(--focus-ring); border-color: var(--accent); }
/* A placeholder here is the DEFAULT the box falls back to, not a hint to
   ignore, so it reads a shade darker than a normal placeholder. */
.el-input::placeholder { color: var(--text-secondary); opacity: 1; }

/* `.entity-tabs__link` was written for an <a>; these are <button>s inside a
   form, which need the browser's button chrome removed to match. */
button.entity-tabs__link {
  font: inherit;
  font-weight: 500;
  cursor: pointer;
  appearance: none;
}
button.entity-tabs__link--active { font-weight: 700; }

.el-badge {
  display: inline-block;
  min-width: 1.4em;
  padding: 0 0.4em;
  margin-left: var(--space-2);
  border-radius: var(--radius-pill, 999px);
  background: var(--accent);
  color: #fff;
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
}

.el-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-5);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border-subtle);
}

/* Tabs hide two thirds of the form, so the button has to say that it saves
   more than what is on screen. */
.el-actions__note { color: var(--text-secondary); font-size: var(--text-sm); }

@media (max-width: 768px) {
  .el-row__caption { white-space: normal; }
  .el-actions { flex-wrap: wrap; }
}

/* ---------------------------------------------------------------------------
 * Rule-pack enforcement state
 *
 * ⚠️ WRITTEN BECAUSE THE CLASS WOULD OTHERWISE BE A NAME WITH NO STYLE. A
 * template can reference `.rule-enforcement--not_enforced` forever and every
 * source grep, mypy run and template test will pass while the operator sees
 * undifferentiated grey text. That has happened in this codebase before.
 *
 * ⚠️ AND IT USES THE PALETTE'S OWN TOKENS RATHER THAN NEW HEX. `--warning` and
 * `--danger` are defined for both the light and dark themes; a literal colour
 * here would look correct in whichever theme it was written in and fail in the
 * other, silently.
 * ------------------------------------------------------------------------ */

.rule-enforcement {
  /* Inherits .form-field__hint's size and measure; adds the signal. */
  border-left: 3px solid var(--text-secondary);
  padding-left: 8px;
  margin-top: 4px;
}

/* Built, but nothing can exercise it yet — a real constraint with no reachable
 * input. Amber: the setting is not wrong, it is not yet load-bearing. */
.rule-enforcement--unreachable {
  border-left-color: var(--warning);
  color: var(--warning);
}

/* Nothing reads it at all. ⚠️ Red because this is the one that misleads: the
 * field accepts a number, saves it, shows it back, and changes no roster. */
.rule-enforcement--not_enforced {
  border-left-color: var(--danger);
  color: var(--danger);
}

/* Nobody has classified it. Treated as "assume no effect" — see
 * `host/rules/enforcement.py::is_effective`, which defaults the same way. */
.rule-enforcement--unclassified {
  border-left-color: var(--warning);
  color: var(--warning);
}

/* ── Cross-cover grant windows (MIG-340) ─────────────────────────────────────
 *
 * ⚠️ A CLASS NAME IS NOT A STYLE. The template referenced these four classes
 * before this block existed; every source grep, mypy run and template test
 * passes while the operator sees undifferentiated grey text. That has happened
 * in this codebase before, which is why `tests/ui` reads the STYLESHEET.
 *
 * ⚠️ AND THE PALETTE'S TOKENS, NOT LITERAL HEX — `--success`, `--muted` and
 * `--text-secondary` are defined for both themes; a hard-coded colour looks
 * right in whichever theme it was authored in and fails silently in the other.
 */
.cover-window {
  font-weight: 600;
  white-space: nowrap;
}

/* Granting something right now. */
.cover-window--live {
  color: var(--success);
}

/* ⚠️ Deliberately MUTED rather than red. An expired rotation is the system
 * working correctly — the grant stopped on its own, which is the entire point
 * of MIG-340. Alarming the operator about it would train them to ignore it.
 *
 * ⚠️ `--text-secondary`, NOT `--muted`. The first version used `--muted`, which
 * is USED in this stylesheet and DEFINED NOWHERE — so the whole declaration
 * silently computes to its initial value and the text renders in the inherited
 * colour. Caught by `tests/ui/test_css_custom_properties_resolve.py`; my own
 * check had grepped for token USAGES and read them as definitions. */
.cover-window--dormant {
  color: var(--text-secondary);
}

.cover-window__dates {
  display: block;
  font-size: 11px;
  color: var(--text-secondary);
  white-space: nowrap;
}

/* ── Breadcrumbs ────────────────────────────────────────────────────────────
 *
 * ⚠️ **MOVED HERE FROM `sidebar.css`, WHERE THEY DID NOT BELONG.** A breadcrumb
 * trail is a component, not sidebar furniture — and filing it there meant any
 * surface that loads the component layer without the sidebar got the markup and
 * none of the styling. The Owner console did exactly that: `.breadcrumbs__trail`
 * is `display: flex`, so without it the crumbs stacked one per row and the links
 * fell back to browser default colours. Reported as both symptoms at once,
 * which is what a single missing stylesheet looks like.
 *
 * ⚠️ **THE HARD-CODED TEAL WENT WITH THE MOVE.** `var(--accent, #0f766e)` and
 * `rgba(15, 118, 110, 0.06)` are literals from a palette this product no longer
 * uses; the fallback only shows when the token is missing, so it would have sat
 * there looking correct and rendering last year's accent on any surface that
 * loaded this file without `tokens.css`.
 */
/* Polished v1.7.7.1: chevron separator, home glyph on the first crumb,
   subtle hover state, tighter vertical rhythm. Links stay muted;
   current-page crumb is the only bold token so the eye reads the
   trail as "path" and the last item as "you are here". */

.breadcrumbs {
  margin: 0 0 var(--space-4) 0;
  font-size: var(--text-sm);
  line-height: 1.4;
}

.breadcrumbs__trail {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: 2px;
}

.breadcrumbs__crumb {
  display: inline-flex;
  align-items: center;
}

.breadcrumbs__link {
  color: var(--text-secondary);
  text-decoration: none;
  padding: 2px 6px;
  border-radius: 3px;
  transition: background 0.12s, color 0.12s;
}

.breadcrumbs__link:hover {
  color: var(--accent);
  background: var(--accent-soft);
  text-decoration: none;
}

.breadcrumbs__link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.breadcrumbs__current {
  color: var(--text-primary);
  font-weight: 600;
  padding: 2px 6px;
}

/* ⚠️ **TWO CRUMBS WERE SAYING "YOU ARE HERE".** `.breadcrumbs__current` is
   applied to EVERY crumb without an href — which includes the group label
   ("Organisation"), a crumb that has no page and never will. So the group
   rendered at weight 600 in the same near-black as the actual current page,
   and only the invisible `aria-current` told them apart.
   ⓘ Surfaced by v1.93: giving detail pages a fourth crumb put two bold
   near-black items in one trail, which reads as two destinations.
   ✅ Keyed on `aria-current`, which the partial already sets on the last
   crumb only — no markup change, and it corrects every trail in the
   product, not just this page. */
.breadcrumbs__crumb:not([aria-current]) > .breadcrumbs__current {
  font-weight: 400;
  color: var(--text-secondary);
}

.breadcrumbs__sep {
  display: inline-block;
  margin: 0 2px;
  /* ⚠️ NOT --text-disabled — a separator is not a disabled control. 2 of the 18
     light-mode failures on the audited screen. v1.35 §9.2. */
  color: var(--text-muted);
  font-weight: 400;
  user-select: none;
}

/* Home glyph on the very first crumb — replaces the "Home" text-only
   affordance with a tiny house icon that scans as "root of the trail".
   Falls back to just the label if font glyph is unavailable. */
.breadcrumbs__crumb:first-child .breadcrumbs__link::before,
.breadcrumbs__crumb:first-child .breadcrumbs__current::before {
  content: "\1F3E0"; /* U+1F3E0 HOUSE */
  margin-right: 4px;
  font-size: 0.95em;
  opacity: 0.75;
}

/* ── Entity tabs ────────────────────────────────────────────────────────────
 *
 * ⚠️ **MOVED HERE FROM `sidebar.css` FOR THE SAME REASON AS THE BREADCRUMBS.**
 * A tab strip is a component, not sidebar furniture. Filed there, any surface
 * loading the component layer without the sidebar got the markup and none of
 * the styling — the Owner console rendered "Provider" and "Group mappings" as a
 * bare bulleted list, because `.entity-tabs__list` is the flex container that
 * makes them a row.
 *
 * ⚠️ **AND `--color-surface-2` IS DEFINED NOWHERE IN THIS PRODUCT.** The rest
 * state read `background: var(--surface-raised)`, so every unselected
 * tab fell back to a hard-coded near-white — invisible-by-luck on the light
 * theme and a white pill on a dark page. Replaced with `--surface-raised`,
 * which exists in both themes. A fallback that is never exercised in the theme
 * you develop in is a bug waiting for the theme you do not.
 */
/* ============================================== Entity tabs ========== */
/* Secondary tab strip used inside an entity edit page (admin staff edit,
 * etc.) to break a long record into focused panels. Navigation is via
 * URL query (`?tab=...`) so it works without JS; active tab styled via
 * Jinja conditional class. */

.entity-tabs {
  border-bottom: 1px solid var(--border-subtle);
  margin: 0 0 var(--space-5) 0;
}

.entity-tabs__list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

/* v1.7.3.6 PR Y — unified rest-state visual clue: unselected tabs
   carry a subtle 1px top/side border + muted background pill so they
   read as clickable at rest, not plain text. Hover strengthens the
   border + brightens text. Active tab wins with accent bottom border
   and bold weight. */
.entity-tabs__link {
  display: inline-block;
  padding: var(--space-3) var(--space-4);
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--text-secondary);
  text-decoration: none;
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-bottom: 2px solid transparent;
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  margin-bottom: -1px;     /* overlap the section border below */
  transition: color var(--motion-fast) var(--ease-standard),
              background var(--motion-fast) var(--ease-standard),
              border-color var(--motion-fast) var(--ease-standard);
}

.entity-tabs__link:hover {
  color: var(--text-primary);
  background: var(--surface);
  border-color: var(--border-subtle);
  border-bottom-color: var(--border-subtle);
}

.entity-tabs__link:focus {
  outline: none;
  box-shadow: var(--focus-ring);
}

.entity-tabs__link--active {
  color: var(--accent);
  background: var(--surface);
  border-color: var(--border-subtle);
  border-bottom-color: var(--accent);
  font-weight: 700;
}
.entity-tabs__link--active:hover {
  border-bottom-color: var(--accent);
}

.entity-tabpanel {
  padding: var(--space-5) 0;
}

.entity-tabpanel--hidden {
  display: none;
}

@media (max-width: 768px) {
  .entity-tabs__list {
    overflow-x: auto;
    flex-wrap: nowrap;
    scrollbar-width: thin;
  }
  .entity-tabs__link {
    white-space: nowrap;
  }
}


/* ⚠️ Both of these were referenced by templates and defined nowhere — the `<li>`
 * wrappers inside a tab strip and the console nav. They are laid out by their
 * flex parents, so the absence was harmless; declared explicitly so an audit of
 * "class used but never defined" comes back clean and the next real omission is
 * not lost in the noise. */
.entity-tabs__item,
.owner-nav__item {
  display: block;
  margin: 0;
}

/* MIG-351 requirement sets. Entries inside a block are ALL required; blocks
   are alternatives. The connector between rows says AND for that reason -- it
   said OR until 2026-09-19, when it became the wrong word rather than merely
   a decorative one. The disjunction now lives on the block lede, in words. */
.sl-req-set {
  display: flex; flex-direction: column; gap: 0;
  padding: 12px 12px 8px;
  border: 1px dashed color-mix(in srgb, var(--accent, #2563eb) 28%, var(--border-subtle, #e5e5e5));
  border-radius: 10px;
  background: color-mix(in srgb, var(--accent, #2563eb) 3%, var(--surface));
}
.sl-req-set + .sl-req-set { margin-top: 16px; }
.sl-req-set__head { margin: 0 0 10px; }
.sl-req-set__lede {
  font-size: 12px; line-height: 1.4;
  color: var(--text-muted, #555);
}
.sl-req-set__lede strong { color: #111; }
.sl-req-set__lede--alt strong { color: var(--accent, #2563eb); }
.sl-req-set__add {
  align-self: flex-start;
  margin-top: 10px; padding: 4px 8px;
  font-size: 12px; line-height: 1.3;
  background: transparent; border: 1px dashed transparent;
  color: var(--accent, #2563eb);
  border-radius: 6px; cursor: pointer;
}
.sl-req-set__add:hover {
  border-color: color-mix(in srgb, var(--accent, #2563eb) 40%, transparent);
  background: color-mix(in srgb, var(--accent, #2563eb) 6%, transparent);
}
.sl-req-set__add:focus-visible {
  outline: 2px solid var(--accent, #2563eb); outline-offset: 2px;
}

/* ---- TOTP enrolment: the setup key and its QR ----
   ⚠️ IN screens.css BECAUSE THAT IS WHAT base.html LOADS. These rules were
   first written into widgets.css, which base.html does NOT link -- only
   `_widget_iframe.html` does. They would have been a second undefined class
   sitting one file away from the page, which is the same defect in a new
   place. The four stylesheets base.html links are tokens, layout, sidebar and
   screens; a rule for a full-page screen goes in the last of those.
   ⚠️ `.code-block` WAS REFERENCED BY BOTH ENROLMENT TEMPLATES AND DEFINED
   NOWHERE. The Base32 setup key rendered in the body font, unspaced, as one
   run of 32 characters — which is the single string a user is most likely to
   have to transcribe by hand onto a phone, and the one case where `l`/`1` and
   `O`/`0` decide whether the factor works. No source grep finds this: the
   class name is present, the template renders, every test passes.

   Base32 has no `0`, `1` or `8`, so the remaining confusions are `I`/`l` and
   `S`/`5` — a monospace face separates both. */
.code-block {
  display: inline-block;
  padding: var(--space-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  /* ⚠️ Wide tracking is deliberate, not decorative: it is what makes a 32
     character run readable in groups when someone is copying it across. */
  letter-spacing: 0.08em;
  word-break: break-all;
  user-select: all;
}

/* ⚠️ A WHITE QUIET ZONE, ALWAYS — NOT `--surface-*`. segno draws black modules
   on transparent, so on a dark theme the code would be black-on-dark and
   unscannable. Scanners also need the light margin around the symbol, which is
   why the padding is here rather than on the paragraph. */
.totp-qr {
  display: block;
  padding: var(--space-2);
  background: var(--surface);
  border-radius: var(--radius-md);
  max-width: 100%;
  height: auto;
}

/* ---- Overrun time (v1.18) ---- */

/* ⚠️ THESE TWO CLASSES EXIST BECAUSE THE TEMPLATE USES THEM, AND THIS CODEBASE
   TESTS THAT. A class name is not a style: an undefined one renders as ordinary
   text and nothing fails, which is why `.text-muted` has a guard of its own. */

/* Numeric table cells. Right-aligned and tabular so a column of minutes lines
   up on the digit rather than the glyph width — a manager scanning for the
   biggest number should not have to read them. */
.table .num,
th.num,
td.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* The trend chart is rendered server-side as SVG, so it needs a box that lets
   it scale rather than a fixed size. Capped in height because a full-width
   chart on a wide monitor otherwise pushes the worker list below the fold —
   and the list is the thing the page is for. */
.overrun-chart svg {
  width: 100%;
  height: auto;
  max-height: 360px;
  display: block;
}

/* ======================================================================
   Requirement tree editor (MIG-383)

   Marcus, 2026-10-14: *"The current UI is awful. I want to be able to add
   boolean requirements for competency and level in a similar way to
   PowerAutomate UI."* The reference he gave is a nested-group builder: an
   operator per group, conditions indented beneath it with a connector, and an
   "+ Add" offering a row or a nested group.

   ⚠️ **THE CONNECTORS ARE BORDERS ON A WRAPPER, NOT BACKGROUND IMAGES.** A
   tree drawn with images cannot follow a row that grows to two lines, which is
   exactly what a long competency name does at this width.
   ====================================================================== */
.rt-group {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  padding: var(--space-3);
}
/* The root carries no chrome: it is the whole expression, not a group inside
   one, and a box around everything reads as an extra level that is not there. */
.rt-group--root { border: 0; background: transparent; padding: 0; }

.rt-group__head { display: flex; align-items: center; gap: var(--space-2); }
.rt-group__op { min-width: 5.5rem; }
.rt-group__lede { color: var(--text-secondary); font-size: var(--text-sm); }

.rt-group__children { margin: var(--space-2) 0 0 var(--space-3); }

/* One branch = one child plus its connector. `::before` draws the elbow. */
.rt-branch { position: relative; padding-left: var(--space-4); }
.rt-branch + .rt-branch { margin-top: var(--space-2); }
.rt-branch::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--border-subtle);
}
/* The last branch's spine stops at its elbow, so the line does not dangle
   past the final condition. */
.rt-branch:last-child::before { bottom: auto; height: 1.15rem; }
.rt-branch::after {
  content: "";
  position: absolute;
  left: 0;
  top: 1.15rem;
  width: var(--space-3);
  height: 1px;
  background: var(--border-subtle);
}

.rt-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.rt-row__lede { color: var(--text-secondary); font-size: var(--text-sm); }
.rt-row__lede--muted { color: var(--text-tertiary); }
/* The competency name is the long one and gets the room; the level is a fixed
   vocabulary and does not need to grow. */
.rt-row__skill { flex: 1 1 18rem; min-width: 12rem; }
.rt-row__level { flex: 0 0 auto; }

.rt-empty {
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-style: italic;
  padding: var(--space-2) 0;
}

/* ---- the ⋯ and + Add menus ---- */
.rt-menu { position: relative; margin-left: auto; }
.rt-menu__btn {
  border: 1px solid transparent;
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
  line-height: 1.6;
  font-size: var(--text-base);
}
/* ⚠️ `--surface-sunken` DOES NOT EXIST AND NEVER DID. Both hovers here named
   it, so the whole declaration computed to its initial value and neither the
   ⋯ trigger nor its menu items highlighted at all. `--accent-soft` is the
   house hover wash (`.data-table tbody tr:hover`, `.breadcrumbs__link:hover`)
   and is TRANSLUCENT, which matters for the item rule below: the menu panel is
   already `--surface-raised`, so a raised-on-raised hover would have been
   invisible in a second, quieter way. */
.rt-menu__btn:hover { background: var(--accent-soft); color: var(--text); }
.rt-menu__list {
  position: absolute;
  right: 0;
  top: 100%;
  z-index: 20;
  min-width: 13rem;
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / 12%));
  padding: var(--space-1) 0;
}
.rt-menu__item {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  background: transparent;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  color: var(--text);
  cursor: pointer;
}
.rt-menu__item:hover:not(:disabled) { background: var(--accent-soft); }
/* ⚠️ Inapplicable moves are DISABLED, not hidden: a menu whose items change
   position between openings has to be re-read every time. */
.rt-menu__item:disabled { color: var(--text-tertiary); cursor: default; }

.rt-add { margin: var(--space-3) 0 0 var(--space-3); display: inline-block; }
.rt-add__btn {
  border: 1px dashed var(--border-subtle);
  background: transparent;
  color: var(--text-secondary);
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-3);
  font: inherit;
  cursor: pointer;
}
.rt-add__btn:hover { border-style: solid; color: var(--text); }

/* Read mode. ⛔ Not disabled controls — a greyed `<select>` still looks like a
   control and still invites a click. Marcus, 2026-10-14: *"we need a READ
   version - it looks like you can edit when in read mode."* */
.rt-row__static { font-weight: 600; }
.rt-group__op--static {
  display: inline-block;
  min-width: 2.5rem;
  font-weight: 600;
  color: var(--text-secondary);
  text-transform: uppercase;
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
}

/* ===================================================== v1.140 cohort B ===
   "Are you still there?" — the cover for an unattended screen.

   ⛔ **z-index ABOVE EVERYTHING, AND 10000 IS ALREADY TAKEN.** The roster grid
   and the reschedule-proposals page each paint a tooltip at `z-index: 10000`,
   and the roster grid has sticky headers besides. A cover that a tooltip can
   sit on top of is not a cover: it would leave a staff name legible over an
   otherwise blanked screen, which is the one thing this exists to prevent.

   ⚠️ **OPAQUE, NOT TRANSLUCENT.** `--surface-scrim` is 0.85 alpha and the text
   beneath it stays readable — v1.35 §10.3 measured exactly that composite
   problem. This paints solid.

   ⚠️ `position: fixed` with all four insets pinned, so it cannot be scrolled
   past: an absolutely-positioned cover moves with the document and a long
   roster scrolls straight out from under it. */
.idle-cover {
  position: fixed;
  inset: 0;
  z-index: 100000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--surface);
  padding: var(--space-4);
}

/* `hidden` must win over `display: flex` — an attribute the script toggles is
   worthless if a class beats it. */
.idle-cover[hidden] {
  display: none;
}

.idle-cover__panel {
  max-width: 28rem;
  text-align: center;
}

.idle-cover__title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-xl);
  color: var(--text-primary);
}

.idle-cover__countdown {
  margin: 0 0 var(--space-3);
  color: var(--text-secondary);
  /* ⚠️ A fixed min-height so the panel does not jolt each second as the digit
     count changes — the countdown is already an anxious moment. */
  min-height: 1.5em;
}

.idle-cover__why {
  margin: var(--space-3) 0 0;
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

/* Nothing behind the cover should scroll while it is up. */
body.idle-cover-open {
  overflow: hidden;
}

/* ===================================== v1.141 — the service-line read view ===
   `.form-field__readonly` is the house primitive for "a value, not a control",
   and it had NO RULE AT ALL until now — it was one of the names the
   unresolved-class ratchet counts. v1.138 step 3's ruling depends on it:
   *"a greyed `<select>` still looks like a control and still invites a click"*,
   so a read view renders values. A value that inherits an input's box defeats
   that on sight.

   ⚠️ It is used on BOTH an `<input readonly>` (Worker > General's calculated
   WTE) and a `<p>` (the Training capacity read view), so the rule has to work
   for either without assuming a form control's box. */
/* ⚠️ **IT HAS THE FIELD BOX AGAIN — RULED 2026-10-15.** Marcus, comparing the
   Roster-area read screen with the Clinic one: *"The font is not the same and
   the fields do not have borders like in Clinics. Fix that."*

   ⓘ **WHAT CHANGED AND WHAT DID NOT.** §1's ruling was about a greyed
   `<select>` — *"still looks like a control and still invites a click"* — and
   that still holds: a read view renders VALUES, never disabled controls. But
   the Clinic General tab reaches its look with real `disabled` inputs
   (measured: 22 of them, `1px` border, `--surface-raised` fill, `8px` radius,
   `32px` tall), and a bare `<p>` beside that reads as unfinished rather than
   as deliberate.

   So the BOX comes back and the CONTROL does not: no caret, no dropdown arrow,
   nothing to click. The tokens are the ones the disabled input already
   resolves to, so the two surfaces match in both themes rather than by
   coincidence in one.

   ⚠️ Used on BOTH a `<p>` (every read view) and an `<input readonly>`
   (Worker > General's calculated WTE), so the rule must not assume a form
   control's box — hence the explicit `width` and `font`. */
.form-field__readonly {
  margin: 0;
  padding: 4px 12px;
  border: 1px solid var(--border-subtle);
  border-radius: 8px;
  background: var(--surface-raised);
  color: var(--text-primary);
  font: inherit;
  /* An input still needs its width removed, or it keeps the field box's. */
  width: auto;
  /* ⛔ **NOT `display: flex`, AND THIS COST A RENDER TO FIND.** It was, to
     centre the value vertically against the 32px box. But in a flex container
     every child becomes a flex ITEM and the whitespace between items is not
     rendered — so a value containing inline markup lost its spaces:
     "5 recorded, 4 available.Manage them on the Items tab" ran together on the
     Equipment screen, measured 2026-10-16.

     ⓘ No centring is needed anyway. 16px text at the inherited line-height is
     ~24px, plus 4px padding top and bottom, which IS the 32px box — the height
     falls out of the padding rather than needing a layout mode. */
  min-height: 32px;
}

/* ⓘ A DESCRIPTION IS THE ONE READ VALUE THAT CAN RUN TO SEVERAL LINES.
   Everything else on these screens is a name, a number or a state, so the base
   rule assumes one line. A competency's description came from a `<textarea>`
   and keeping its breaks is the difference between a paragraph and a run-on
   sentence. */
.form-field__readonly--multiline {
  white-space: pre-wrap;
  min-height: 4.5rem;
}

/* ⓘ A URL is one long unbroken token, so it neither wraps nor shrinks: an
   issuer like `https://login.microsoftonline.com/<guid>/v2.0` would push the
   field box past the card. Breaking anywhere is right for a URL and wrong for
   prose, which is why this is a modifier and not the base rule. */
.form-field__readonly--url {
  overflow-wrap: anywhere;
  word-break: break-all;
}

/* ── Rule pack ──────────────────────────────────────────────────────────────
   ⓘ Four classes replacing six inline styles (house style §9). The table is
   the EDIT rendering only; read mode answers with `.rp-coverage`. */
/* ⓘ The old inline rule used `--color-border` (#E5E7EB light / #34383F dark).
   That token is real and themed — but it is not the one the field boxes use.
   They take `--border-subtle`, an alpha grey that sits on whatever surface is
   behind it, so the separator was a slightly different, flatter grey from
   every border around it. Switched to match. */
.sl-rule {
  margin: var(--space-5) 0;
  border: 0;
  border-top: 1px solid var(--border-subtle);
}

/* ── Equity adjustments: the revoke disclosure ──────────────────────────────
   ⓘ Replaces `style="margin-top:…; display:flex; gap:4px"` plus a
   `style="flex:1"` on its input (house style §9). */
.eq-revoke {
  margin-top: var(--space-1);
  display: flex;
  gap: var(--space-1);
  align-items: center;
}

.eq-revoke input[type="text"] { flex: 1; min-width: 12ch; }

.rp-version { font-weight: normal; }

.rp-coverage {
  margin: 0;
  padding-left: var(--space-4);
  line-height: 1.6;
}

.rp-coverage-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: var(--space-2);
}

.rp-coverage-table td { padding: 2px 4px; }
.rp-coverage-table__qty { width: 120px; }
.rp-coverage-table input { width: 100%; }

.sl-tc-card {
  margin-bottom: var(--space-3);
}

.sl-tc-card__title {
  margin: 0;
}

.sl-tc-card__empty {
  margin-top: 0;
}

/* ⚠️ A SIBLING OF THE UPDATE FORM, NOT A CHILD. It used to be nested inside it,
   which HTML forbids — the parser dropped the inner tag and the Remove button
   posted to the update endpoint instead. Spaced away from the primary action
   rather than beside it: this one ends a record. */
.sl-tc-card__remove {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--border-subtle);
}

/* A tab panel's first heading. ⚠️ Replaces `style="margin-top:0"` repeated
   inline on the Rooms and Equipment tabs — an inline style is invisible to
   every static sweep this repo runs. */
.sl-tab-heading {
  margin-top: 0;
}

/* The narrow trailing Actions column in an entity table, and the inline form
   inside it. Replaces `style="width:80px; text-align:center"` plus
   `style="display:inline"`. */
.cell--actions {
  width: 80px;
  text-align: center;
}

.cell--actions__form {
  display: inline;
}

/* The "Qty per session" column on the service-line Equipment grid: a narrow
   number beside its Save button. Replaces `style="width:200px"`,
   `style="display:flex; gap:6px"` and `style="width:80px"`. */
.cell--qty {
  width: 200px;
}

.cell--qty__form {
  display: flex;
  gap: var(--space-2);
}

.cell--qty__input {
  width: 80px;
}

/* The service-line Activities tab. Replaces `style="margin-top:12px"`,
   `style="margin-top:8px"` and two `style="margin-top:16px"` — inline spacing
   that no static sweep can see and no theme can reach. */
.sl-activities__form { margin-top: var(--space-3); }
.sl-activities__actions { margin-top: var(--space-2); }
.sl-activities__table { margin-top: var(--space-4); }
.sl-activities__empty { margin-top: var(--space-4); }

/* ==================================== The Add-space / Add-equipment pickers ==
   Marcus, 2026-10-15: *"we need an ADD space button above the list and an Add
   space modal with search capability. The current screen would not work for
   say 250 spaces."*

   The tab's heading row: title on the left, the Add button on the right. The
   button is gated on edit mode, so the heading has to sit correctly with and
   without it. */
.sl-tab-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
}

/* ⚠️ **THE LIST SCROLLS, THE DIALOG DOES NOT.** With 500 candidates the modal
   would otherwise grow past the viewport and take its own search box off
   screen — so the one control you need to narrow the list would be the first
   thing to disappear. Capped against the viewport rather than a fixed height,
   because a short list should not leave a tall empty box. */
.picker-list {
  max-height: min(60vh, 32rem);
  overflow-y: auto;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
}

/* A row hidden by the filter must not leave its border behind. */
.picker-list [data-picker-row][hidden] {
  display: none;
}

/* ⚠️ **THE PICKER'S BODY CHILDREN NEED THE MODAL'S OWN GUTTER.** `.app-modal`
   is `padding: 0`; the header and footer bring their own, and the only body
   children the house pads are `.form-hint` and `.form-grid`
   (`padding: 0 var(--space-6)`). A picker's children are a `.form-field` and a
   `.picker-list`, so without this they sit flush against the dialog walls and
   the last row rests on its bottom edge — Marcus, 2026-10-15: *"The Add space
   and Add equipment dialogs have bad spacing."*

   ⛔ Scoped to `[data-picker]` rather than added to the shared selector above:
   `.app-modal__body > .form-field` would re-space every other modal in the
   product to fix two. */
[data-picker] > .form-field {
  padding: var(--space-5) var(--space-6) 0;
}

/* ⚠️ MARGIN, NOT PADDING. The list carries a border, and padding sits INSIDE
   it — so padding would widen the bordered box to the dialog edge rather than
   inset it. */
[data-picker] > .picker-list {
  margin: var(--space-3) var(--space-6) var(--space-6);
}

[data-picker] > .empty-state {
  padding: var(--space-4) var(--space-6) var(--space-6);
}

/* The sticky header keeps the column names visible while scrolling 500 rows —
   without it, the only labels are gone after the first scroll. */
.picker-list thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface);
}

/* A small count in a table — a capacity, a pool size. Tabular figures so the
   digits line up, but LEFT aligned under its heading.

   ⚠️ **`cell--numeric` IS FOR A COLUMN YOU READ DOWN**: money, totals, things
   compared or summed, where right alignment puts the units under each other.
   A capacity of 0 or 4 is read ACROSS, beside the space it belongs to, and
   right-aligning it pushes a single digit to the far side of a wide column,
   away from the row it describes. Marcus, 2026-10-15: *"The spaces grid
   CAPACITY column is right aligned and looks bad."* */
.cell--count {
  font-variant-numeric: tabular-nums;
  text-align: left;
  white-space: nowrap;
}

/* The picker's live "Showing N of M".

   ⚠️ **DELIBERATELY NOT `.form-field__hint`.** The field contract
   (`tests/ui/test_field_contract.py`) says a hint is HELP TEXT and must sit
   behind an ⓘ — and it caught this when it was one. A running count of the
   list below is a status line, not help about the field, so it gets its own
   class rather than an exemption. ⓘ `role="status"` so a screen reader hears
   the count change as the filter narrows; otherwise the list silently shrinks. */
.picker-count {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  min-height: 1.25em;
}

/* A card header's trailing row of actions. Replaces
   `style="display:flex; gap:8px; align-items:center"` on the roster-area list —
   an inline style no static sweep can see and no theme can reach. */
.card__header-actions {
  display: flex;
  gap: var(--space-2);
  align-items: center;
  flex-wrap: wrap;
}
