/*
 * fra.css — the FRA module's own page layer.
 *
 * The `ipi-*` primitives (page header, stats strip, list area) come from
 * fra-shell.css, which layout/app.php swaps in whenever AppMode resolves to FRA.
 * This file owns ONLY what is specific to FRA pages: `.fra-*`.
 *
 * ⛔ EVERY CUSTOM PROPERTY USED HERE IS AN `--ipi-*` TOKEN, and every one of them is
 * declared on `.fra-page` by fra-shell.css. A bare `var()` with no fallback against a
 * property that does not exist resolves to nothing and the rule silently disappears —
 * so page CSS that reaches for a `:root` token the shell does not set is how a fork
 * ends up unstyled in one mode only.
 *
 * ⛔ Nothing FRA-related belongs in desktop.css: it is linked
 * `media="(min-width: 1367px)"` and is inert at 1024, 900, 810, 390 and 360.
 *
 * The 900px breakpoint is the tenant app's standard mobile/desktop split.
 */

/* 🔴 STAGE 1, ITEM 1.1 — NO FRA FORM RENDERS A CARD.
 *
 * `style.css:3836` styles the bare `form` ELEMENT as a card: `background:
 * var(--card)`, 16px padding, 12px radius, `display: grid`. Every FRA form sits
 * inside an `.ipi-panel` that already supplies that surface, so the card is always a
 * SECOND one — the matrix editor was a white card containing five white panels, and
 * setup was a card inside a card. Eight FRA forms had learned to neutralise it one
 * at a time (`.fra-premises-form`, `.fra-inline-form`, `.ipi-filter-bar`, ...) and
 * eight more had not, which is how the same defect kept coming back on the next form
 * anybody added.
 *
 * ⛔ THIS IS THE MODULE'S ONE RESET AND IT IS DELIBERATELY UNCONDITIONAL. A form that
 * wants a layout declares its own `display` after this rule — `.fra-inline-form` is
 * inline-flex, `.ipi-filter-bar` is grid, `.fra-capture__form` is a flex column.
 * Adding a ninth per-form reset instead of relying on this one is the pattern being
 * retired.
 *
 * ⚠ `:where()` IS LOAD-BEARING AND IS NOT DECORATION. It contributes ZERO
 * specificity, so this rule scores 0,0,1 — exactly the global `form` rule it is
 * cancelling, and it wins only because `fra.css` loads later. Written as a plain
 * `.fra-page form` it would score 0,1,1 and beat EVERY class on an FRA form:
 * `.fra-rp-form`'s dashed box would lose its padding and border, and any future
 * `display` here would flatten `.ipi-filter-bar`'s grid and `.fra-inline-form`'s
 * inline-flex. A class must be able to override this; `:where()` is what lets it.
 *
 * ⚠ `display` AND `margin` ARE DELIBERATELY NOT RESET. The global rule's
 * `display: grid; gap: 12px` is a reasonable default for the unclassed forms and
 * every classed one already declares its own; `margin` is what `.fra-rp__actions`
 * uses to sit clear of the row above it.
 */
:where(.fra-page) form {
  background: none;
  padding: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.fra-note {
  padding: 12px 16px;
  border: 1px solid var(--ipi-accent-border);
  border-radius: 10px;
  background: var(--ipi-accent-bg);
  color: var(--ipi-ink-2);
  font-size: 13px;
  line-height: 1.5;
}

.fra-note strong {
  display: block;
  color: var(--ipi-ink);
  font-weight: 600;
  margin-bottom: 2px;
}

/* 🔴 STAGE 1, ITEM 1.2 — `.fra-panel` AND `.fra-panel__title` ARE GONE.
   They were a second panel system layered on `.ipi-panel` (radius 10 vs 12,
   padding 18-20 vs 16-20) and a second panel heading (14px display sentence case
   vs the 11px uppercase letter-spaced label), sitting on the same elements across
   ~40 panels on 10 pages. `fra.css` loads AFTER `fra-shell.css`, so at equal
   specificity the fork won every one of them — which is the single largest reason
   the module read as a different product. The markup now uses
   `.ipi-panel > .ipi-panel__head > h2.ipi-panel__title`, exactly as Analytics and
   Fire Stopping do. ⛔ Do not reintroduce either class.

   What survives is the ROW LIST those panels held, which has no `.ipi-*`
   equivalent, renamed off the retired block so nothing reads as a panel part. */

.fra-rowlist {
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 0;
  border-bottom: 1px solid var(--ipi-rule);
  font-size: 13px;
  color: var(--ipi-ink-2);
}

.fra-row:last-child {
  border-bottom: none;
}

.fra-row__main {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fra-row__meta {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--ipi-ink-4);
  font-variant-numeric: tabular-nums;
}

/* 🔴 STAGE 1, ITEM 1.7 — AN IN-CARD LINK INHERITS INK.
   The global `a { color: var(--brand) }` (style.css:1098) paints every unstyled
   anchor gold. Gold is #c49510, which measures ~2.7:1 on a white card and fails
   WCAG AA for body text — and the review-overdue rows, the matrix picker and the
   evaluation prose were all unstyled anchors inside a panel. Both other dashboards
   set `color: inherit` plus a focus ring for precisely this reason
   (dashboard.css:825-831), so this does the same for the whole module.

   ⚠ SCOPED TO UNCLASSED ANCHORS AND THE TWO ROW LINKS. A `.btn`, an
   `.ipi-action-btn` or an `.ipi-filter-railbtn` inside a panel carries its own
   colour pair and must keep it; so does `.fra-control__mirror-link`, which is blue
   at 6.1:1 on purpose. */
.fra-page .ipi-panel a:not([class]),
.fra-page a.fra-row__main {
  color: inherit;
}

/* ⚠ A ROW LINK AND A PROSE LINK NEED DIFFERENT AFFORDANCES, and giving both
   `text-decoration: none` was wrong. A row link IS the row — `.fra-row__main` fills
   its line, and the row is the click target, which is exactly why
   `.dbp-td-prop-link` (dashboard.css:825-831) drops the underline. An UNCLASSED
   anchor is a link inside a sentence — `_evaluation.php`'s "Configure the risk
   matrix" sits mid-paragraph — and with the colour now inherited, removing the
   underline as well would leave it indistinguishable from the body text around it.
   Caught in review 2026-09-07; it is the cost of item 1.7 and not a separate bug. */
.fra-page .ipi-panel a:not([class]) {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.fra-page a.fra-row__main {
  text-decoration: none;
}

.fra-page a.fra-row__main:hover {
  text-decoration: underline;
}

.fra-page .ipi-panel a:not([class]):focus-visible,
.fra-page a.fra-row__main:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* 🔴 STAGE 1, ITEM 1.5 — COUNTRY IS NOT A STATUS.
   The premises directory used to paint England and Wales with the class the
   dashboard uses to mean *issued* and Scotland and Northern Ireland with the one
   that means *superseded*, so a reader was taught green = issued on one page and
   green = assessable on the next. Country now has its own pair, and it shares no
   class with any status: assessable is the neutral inset chip, outside the Order
   is an outlined one. ⛔ Never give either of these a status colour.
   ⚠ The assessable tone is deliberately the same NEUTRAL as `.ipi-chip--draft`, and
   that is not an oversight: neutral is the one tone in the palette that asserts
   nothing about state, which is exactly what a country needs to say. They share no
   class, and the two never appear in the same column. */
/* 🔴 STAGE 2, ITEM 2.7 — THE ASSESSOR'S DECLARED REGISTRATION, UNDER THEIR NAME.
   The Assessors card is the Fire Stopping team card, and that card states three things
   about a person: name, role, presence. FRA has a fourth — the registration body and
   number the assessor declared — and it has nowhere in the shared `.dbp-team-*` row to
   put it.

   ⛔ IT DOES NOT GO ON `.dbp-team-role`. That class is `white-space: nowrap` with no
   shrink (fra-dashboard.css:1114-1118), sized for the single word Fire Stopping puts
   in it; "Owner · IFE UI-REVIEW-0001" cannot shrink and pushed the 320px workspace
   card past its own edge at 1440. That was MEASURED by the render lane
   (`fra-page-content-does-not-overflow-its-own-column`), not reasoned about.

   ⚠ SCOPED TO `.fra-page`, and this is the only place FRA's page layer touches a
   `.dbp-*` class. The alternative was a divergent copy of `.dbp-team-name` inside
   fra-dashboard.css, which would break the one-stylesheet-three-names rule item 2.1
   exists to protect. Two declarations, on the module's own root, is the smaller debt.

   ⚠ `--ipi-ink-4` rather than `--dbp-ink-4`: fra.css's whole contract is that every
   custom property in it is an `--ipi-*` token declared on `.fra-page` by fra-shell.css.
   The two tokens carry the identical value (#9a9188), so this reads the same and stays
   inside the contract. */
/* 🔴 THE ROW WRAPS; THE REGISTRATION TAKES THE SECOND LINE WHOLE.
   `.dbp-team-member` is a nowrap flex row of four items, three of them rigid — the
   28px avatar, `.dbp-team-role` (`white-space: nowrap`) and `.dbp-team-status`
   (`flex: 0 0 auto`, 110px for "Last seen today"). `.dbp-team-name` was the only
   flexible one, so inside a 320px workspace card it settled at 70px and the
   registration wrapped mid-string.
   Allowing the row to wrap and giving the registration `flex: 0 0 100%` moves it to a
   line of its own, where it has the full 288px. The name goes back to one line and
   nothing above it has to shrink.
   ⛔ `align-items: center` on the base rule would centre the two lines against the
   avatar once the row wraps, so the top row is pinned with `align-content` instead.
   ⚠ `margin-left` matches avatar (28px) + row gap (10px) so the second line hangs
   under the NAME rather than under the avatar. If either changes, this changes. */
/* 🔴 THE COLUMNS FILL THE CARD; THE DIVIDERS RUN ITS FULL HEIGHT.
   `.dbp-card--field-work` is `display: flex; flex-direction: column` in all three
   dashboards, and the card is stretched to its grid row by the taller Quick access
   rail beside it. Fire Doors happens to fill that height with content (grid 313px in
   a 397px card); FRA's three columns are 184px in a 404px card, so 136px of undivided
   white was left below them and the card read as having run out. `flex: 1` hands the
   remainder to the columns, so the rules between them run the whole way down.
   ⚠ Scoped to `.fra-page`: the twins do not need it and must not inherit it.
   ⛔ Do not "fix" this by dropping the card out of the stretch instead — that moves
   the same gap outside the card, between it and Recent assessments. */
.fra-page .dbp-card--field-work .dbp-field-cols {
  flex: 1;
}

.fra-page .dbp-team-member {
  flex-wrap: wrap;
  align-content: flex-start;
  row-gap: 2px;
}

.fra-page .dbp-team-name {
  min-width: 0;
}

.fra-assessor-registration {
  flex: 0 0 100%;
  margin-left: 38px;
  font-size: 11px;
  font-weight: 400;
  line-height: 1.35;
  color: var(--ipi-ink-4);
  overflow-wrap: anywhere;
}

/* ⚠ A `.dbp-card` has NO padding of its own — its header and body each carry their
   own `16px 20px`. A bare `<p>` appended as a card child therefore renders flush to
   the card edge, 20px out of alignment with everything above it. Measured on the
   workspace card: content at x=1193, this footnote at x=1173. */
.fra-page .fra-card-foot {
  padding: 0 20px 16px;
}

/* 🔴 THE TITLE AND ITS LEDE ARE ONE FLEX ITEM, WHICH IS THE ONLY WAY THE LEDE
   REACHES ITS OWN LINE.
   `.ipi-panel__lede` was declared in all three shells on 2026-08-29 and, until this
   page, used by NO view — so its behaviour inside `.ipi-panel__head` had never once
   been rendered. The head is `display: flex; justify-content: space-between;
   flex-wrap: wrap`, so a bare lede becomes the SECOND flex item and lands hard right
   of the title on the same baseline (measured: title at x=268, lede at x=937).
   ⛔ `flex: 0 0 100%` DOES NOT FIX IT, and that is the non-obvious part — the
   primitive also sets `max-width: 68ch`, which clamps the item's hypothetical main
   size to 536px, and 124px of title plus 536px still fits in 1205px, so the line
   never breaks. That was tried in the browser and rejected there.
   Grouping the two into one block-flow item is what `.dbp-card-title-group` already
   does for the dashboard cards; the lede keeps its 68ch measure and its 4px offset.
   ⚠ Fixed in FRA's page layer, NOT in fra-shell.css — the three shells are compared
   declaration-block by declaration-block by FraModeWiringTest and editing one would
   fail parity. If a second module uses a lede, move this into all three at once. */
/* ⛔ NAMED `.fra-headgroup`, NOT `.fra-panel-headgroup`, AND IT MATTERS.
   FraStage1ForkRetirementTest guards the retired `.fra-panel` family with a plain
   SUBSTRING match — deliberately, because `_` is a word character and a `` guard
   let `.fra-panel__title` and `.fra-panels` slip back through it (caught in review
   2026-09-07). Any class PREFIXED `fra-panel` therefore trips that guard, however
   legitimate it is. This helper did, in six views, on 2026-09-11.
   ⚠ The fix is the name, not the guard. Loosening the tripwire to allow a prefix
   would reopen exactly the family it exists to keep retired — and a helper called
   `fra-panel-*` was confusing anyway, sitting inside `.ipi-panel__head` on a page
   whose own comment says `.fra-panel` is gone. */
.fra-headgroup {
  min-width: 0;
}

/* ⚠ `flex-basis: 320px` WAS WIDER THAN THE ROW IT SITS IN. Once the panel became a
   space-between row, the form only gets what the title leaves it — measured 268px — so
   a 320px basis consumed all of it and pushed "Open" onto a second line. A fixed,
   smaller width keeps the pair on one row at every panel width. */

/* ⛔ R5-01 (2026-09-23): IT MAY NEVER WRAP OR SHRINK. Inside `.ipi-switchrow` between
   901px and 1365px the flex row gave the switch 37px and set the caption one letter
   per line, 149px tall. The label is the flex item that shrank, so it carries the fix. */
.ipi-switchrow > .toggle-switch {
  flex-shrink: 0;
  white-space: nowrap;
}

/* A field that takes the whole two-up row rather than leaving a dead cell beside it. */
.ipi-form-grid > .fra-field--span {
  grid-column: 1 / -1;
}

.fra-country-chip {
  background: var(--ipi-bg-inset);
  color: var(--ipi-ink-2);
  border: 1px solid var(--ipi-rule);
}

.fra-country-chip--outside {
  background: transparent;
  color: var(--ipi-ink-2);
  border: 1px dashed var(--ipi-rule-h);
}

/* ==========================================================================
   Stage 4 — the premises pages and the assessor declaration.

   ⛔ EVERY custom property below is an `--ipi-*` token declared on `.fra-page` by
   fra-shell.css. A bare var() against a property that does not exist resolves to
   nothing and the whole declaration disappears silently — which is precisely how a
   forked module ends up unstyled in one mode only.

   ⛔ Nothing here goes in desktop.css: it is linked media="(min-width: 1367px)" and
   is inert at 1180, 1024, 900, 810, 390 and 360.

   The 900px breakpoint is the tenant app's standard mobile/desktop split.
   ========================================================================== */

/* 🔴 STAGE 3, ITEM 3.6 — THE PANEL MARGIN IS GONE, AND THE FORMS DECLARE THE GAP.
   `.ipi-list-area` is already `display:flex; flex-direction:column; gap:16px`
   (fra-shell.css:292-297). This file used to add `margin-bottom:16px` to the same flex
   items, so FRA panels sat 32px apart where every other page in the app uses 16px —
   the module read as loose next to Analytics without anything looking obviously wrong
   on either page alone.

   ⚠ A FORM IS THE FLEX CHILD, NOT ITS PANELS, which is why deleting the margin is not
   the whole fix. `style.css:3836` styles the bare `form` tag `display:grid; gap:12px`
   and the module's `:where()` reset deliberately leaves `display` alone, so the panels
   inside the two multi-panel forms were 12px + 16px = 28px apart and would have
   dropped to 12px. Both forms now declare the list area's own 16px, so every FRA panel
   on every FRA page sits at exactly one rhythm.

   ⚠ THE DASHBOARD IS NOT ONE OF THESE: Stage 2 rebuilt `?page=fra` on
   `.dashboard-proto` / `.dbp-content-grid`, which owns its own spacing. */
.fra-premises-form {
  gap: 16px;
}

.fra-note--refusal {
  border-color: var(--ipi-red-border);
  background: var(--ipi-red-bg);
  color: var(--ipi-ink-2);
}

.fra-note--refusal strong {
  color: var(--ipi-red);
}

/* A stat whose value is a phrase rather than a number. `.ipi-stat-num` is sized for
   digits and set in tabular numerals; a methodology name at that size overflows the
   tile at 360px. */
.fra-stat-num--text {
  font-size: 14px;
  line-height: 1.3;
  font-variant-numeric: normal;
  white-space: normal;
}

.fra-inline-form {
  display: inline-flex;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
}

/* ---------------------------------------------------------------- directory */

/* 🔴 STAGE 1, ITEM 1.6 — `.fra-filter-count` IS GONE.
   It was `.ipi-tab-count`'s exact 18x18 geometry with `background: var(--ipi-accent);
   color: #fff` bolted on. White on #c49510 measures ~2.7:1, and css-standards.md
   forbids `--accent` as a fill for anything but rings and borders; it was also this
   file's only real hardcoded hex. The rail button's count badge is a SHELL
   primitive — `.ipi-filter-railbtn .ipi-tab-count`, declared identically in all
   three shells beside the fire-door `[data-insp-filters-count]` rule it generalises
   — so the markup carries `.ipi-tab-count` and this file styles nothing.
   ⛔ Do not reintroduce a module-local count badge. */

/* 🔴 THE ASSESSMENTS BAR IS FLEX, NOT THE SHELL'S SIX-COLUMN GRID, AND THAT IS
   DELIBERATE — IT IS THE ONE FILTER BAR IN THIS APP WHOSE ITEM COUNT VARIES.
   `.ipi-filter-bar` is `grid-template-columns: minmax(220px,1fr) repeat(4,auto) auto`
   — exactly six cells, identical in fra-shell.css:391 and inspections_shell.css:427.
   Premises fits because it emits exactly six. Assessments emits SEVEN (search, status,
   premises, assessor, date range, sort, actions) and only SIX when the workspace has no
   assessor options to offer, because that select is conditional. A fixed template can
   serve one of those two and leaves a dead cell on the other; the old markup emitted
   eight and dropped the last two onto a second row with four empty cells beside them.
   Flex takes the count it is given.
   ⚠ THE SEARCH KEEPS THE GRID'S PROPORTIONS: `flex: 1 1 220px` reproduces
   `minmax(220px, 1fr)`, so this bar still measures like Premises' at the same width.
   ⛔ Do not generalise this to `.ipi-filter-bar` — fra.css:33 records why that class
   must stay grid, and Premises depends on it. */
:is(.fra-assessments-filter-bar, .fra-actions-filter-bar) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

/* ⚠ 160px, NOT 220px (2026-09-23, R1-19). At 220 the seven controls needed ~1,300px and
   the actions dropped onto a second row at 1440 in a 1,149px bar. The search still grows
   into whatever the row leaves over; it only starts narrower. */
:is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-search {
  flex: 1 1 140px;
  min-width: 0;
}

/* The drawer captions (R1-18). Above 900px the wrapper does not exist as a box and the
   caption is not drawn, so every control is still a direct flex item of the bar. */
:is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-filter-field,
:is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-daterange__part {
  display: contents;
}

:is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-filter-field__caption {
  display: none;
}

/* ⚠ `width: auto` IS LOAD-BEARING. style.css:4167 sets `input, textarea, select
   { width: 100% }` globally. Under the shell's GRID that is harmless — 100% of an
   `auto` track is the track's max-content width. Under flex it resolves against the
   BAR, so every select claimed a full row and the strip stacked vertically. Verified
   in the browser, not reasoned about: the first flex build rendered six stacked rows. */
:is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-select {
  flex: 0 1 auto;
  width: auto;
  max-width: 150px;
}

:is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-filter-actions {
  margin-left: auto;
}

/* ⚠ ONE HEIGHT FOR THE ROW (R1-19). The rail buttons are 38px and every control beside
   them 34px, so Clear/Apply sat 4px proud. Desktop only — below 900px the drawer rules
   further down (and the module-wide 44px floor) own the size. */
@media (min-width: 901px) {
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-filter-actions .ipi-filter-railbtn {
    min-height: 34px;
    height: 34px;
    padding-top: 0;
    padding-bottom: 0;
  }
}

/* ⚠ The two date inputs read as ONE control, so they share one bordered box rather
   than carrying a border each — two adjacent bordered date fields read as two
   unrelated filters, which is what "Assessed from" / "Assessed to" already had to
   explain in words. The inner inputs are stripped back to text; the group carries the
   chrome, matching `.ipi-filter-select`'s height so the row keeps one baseline. */
.fra-daterange {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 8px;
  height: 34px;
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
  background: var(--ipi-bg-card);
}

.fra-daterange input[type='date'] {
  width: auto;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  font-size: 12px;
  color: var(--ipi-ink-2);
}

.fra-daterange input[type='date']:focus {
  outline: none;
}

.fra-daterange:focus-within {
  border-color: var(--ipi-accent);
}

.fra-daterange__sep {
  /* ink-3: the faint ink-4 is under 4.5:1 on white and the inset ground at 12px. */
  color: var(--ipi-ink-3);
  font-size: 12px;
}

.fra-filter-actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}


/* R1-32: small text uses `--ipi-ink-3` (the faint `-4` is 3.10:1 on white, under AA below
   14px). */
.fra-table__sub {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  color: var(--ipi-ink-3);
}

.fra-table__foot {
  margin: 10px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

/* ------------------------------------------ assessments list, 2026-09-23 */

/* The FRA-NNNN reference that leads each row's sub-line (R1-05). Tabular so the column
   of references reads as one. */
.fra-assessments-page .fra-table__ref {
  font-variant-numeric: tabular-nums;
  color: var(--ipi-ink-3);
}

/* 🔴 THE PHONE DRAWER (R1-18). The desktop bar used to be reused as-is inside the sheet:
   a ragged flex wrap of unlabelled controls at three different heights. Here it is one
   column of captioned, full-width fields and a two-up Clear / Apply, the shape the
   inspections drawer and `tnt-filter-drawer.css`'s action grid use. Sizes are only what
   this bar needs beyond the module-wide ≤900 floor in fra-shell.css. */
@media (max-width: 900px) {
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) {
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    gap: 14px;
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-filter-field,
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-daterange__part {
    display: grid;
    gap: 6px;
    min-width: 0;
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-filter-field__caption {
    display: block;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ipi-ink-3);
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-search,
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-select {
    flex: none;
    width: 100%;
    max-width: none;
    min-height: 44px;
    box-sizing: border-box;
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-select,
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-search input {
    font-size: 16px;
  }

  /* One field ground in the sheet: the search and the dates were the primitive's inset
     beige beside four white selects. */
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-search {
    background: var(--ipi-bg-card);
  }

  /* ⚠ `background-color`, not the shorthand: below 768px mobile.css paints the select's
     chevron as a background image. Between 768 and 900 the primitive's inset ground
     showed through instead of white. */
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-select {
    background-color: var(--ipi-bg-card);
  }

  /* 44, not the 46 the 16px text and the primitive's 8px padding add up to, so every
     field in the sheet is one height. */
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .ipi-filter-select {
    height: 44px;
    padding-top: 0;
    padding-bottom: 0;
  }

  /* The range becomes two captioned fields side by side; the dash has nothing to join. */
  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-daterange {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    height: auto;
    padding: 0;
    border: 0;
    background: none;
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-daterange__sep {
    display: none;
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-daterange input[type='date'] {
    width: 100%;
    min-height: 44px;
    padding: 0 10px;
    box-sizing: border-box;
    border: 1px solid var(--ipi-rule-h);
    border-radius: 7px;
    background: var(--ipi-bg-card);
    font-size: 16px;
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-daterange input[type='date']:focus {
    border-color: var(--ipi-accent-border);
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-filter-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
    margin-left: 0;
  }

  :is(.fra-assessments-filter-bar, .fra-actions-filter-bar) .fra-filter-actions .ipi-filter-railbtn {
    justify-content: center;
    min-height: 44px;
  }

  /* R1-26: the rail's filter summary gets the full width above its buttons instead of a
     ~120px column squeezed beside them. */
  .fra-assessments-filter-shell .ipi-filter-rail {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }

  .fra-assessments-filter-shell .ipi-filter-rail__actions {
    justify-content: flex-start;
  }
}

/* DESKTOP TABLE (R1-20, R1-23). Above 900px only — below it the table is a card
   (table-cards.css) and none of this applies. */
@media (min-width: 901px) {
  /* Dates and the review date never break "23 Sep / 2026" across two lines. */
  .fra-assessments-page [data-fra-assessments-table] td[data-label='Assessed'],
  .fra-assessments-page [data-fra-assessments-table] td[data-label='Review due'] {
    white-space: nowrap;
  }

  /* One line per row at every desktop width. At 1024px an issued row's sub-line (with
     the client's name) and "Jordan Price" both wrapped, taking the row from 67px to 85.
     `max-width: 0` + a percentage width is what lets an auto-layout table cell truncate:
     the column takes its share instead of growing to its text. Both carry a `title`. */
  .fra-assessments-page [data-fra-assessments-table] td[data-rc='title'] {
    width: 32%;
    max-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .fra-assessments-page [data-fra-assessments-table] td[data-rc='title'] > .fra-table__sub {
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .fra-assessments-page [data-fra-assessments-table] td[data-label='Assessor'] {
    white-space: nowrap;
    max-width: 16ch;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The Actions header sits over the buttons, and the buttons start at the same x on
     every row: left-aligned, so "Open" does not shift by the width difference between
     "Capture" and "Review" in the row beside it. */
  .fra-assessments-page [data-fra-assessments-table] th.ipi-actions {
    text-align: left;
  }

  .fra-assessments-page [data-fra-assessments-table] td.ipi-actions .ipi-row-actions {
    justify-content: flex-start;
  }
}

/* ------------------------------------------------- the Clause 12 record */

.fra-factual__intro {
  margin-top: -4px;
  margin-bottom: 12px;
}

.fra-factual__list {
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: none;
}

.fra-factual__item {
  padding: 14px 0;
  border-top: 1px solid var(--ipi-rule);
  min-width: 0;
}

.fra-factual__item:first-child {
  border-top: none;
  padding-top: 0;
}

.fra-factual__clause {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
  min-width: 0;
}

.fra-factual__letter {
  flex: 0 0 auto;
  font-weight: 700;
  font-size: 12px;
  color: var(--ipi-accent);
  font-variant-numeric: tabular-nums;
}

.fra-factual__text {
  min-width: 0;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.45;
  color: var(--ipi-ink);
}

/* ⚠ The value column wraps rather than truncating. `.ipi-keyvals__value` is
   `white-space: nowrap` because it was written for figures; a Clause 12 value is
   frequently a sentence, and truncating a factual record is how a reader is shown
   half a fact and cannot tell. */
.fra-factual__values .ipi-keyvals__value {
  white-space: normal;
  text-align: right;
  max-width: 60%;
}

.fra-factual__elsewhere {
  margin: 0;
  padding: 8px 10px;
  border-left: 2px solid var(--ipi-accent-border);
  background: var(--ipi-bg-inset);
  border-radius: 0 6px 6px 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

/* ------------------------------------------------- responsible persons */

/* 🔴 R4-05 — REDESIGNED 2026-09-23 TO SPEAK THE RECORD'S OWN LANGUAGE. It was a stack of
   grey inset cards (1,124px against its neighbours' 195–370), a 352px label track and a
   right-aligned address box, with two always-open forms under it. Now: a flat list
   separated by the same `--ipi-rule` the record rows use, the name and its chip on one
   line with Edit and ⋯ at the right, and `.fra-rec--stacked` beneath — exactly the
   label-above-value rows of "The record" and "Further factual record".
   ⛔ ONE BLOCK PER INSTANCE, NEVER A FLAT TABLE ROW. A UK service address is multi-line
   free text and a wrapped narrative in a table cell is this project's own shipped-defect
   mechanism. */
.fra-rp__help {
  margin: -6px 0 12px;
}

.fra-rp-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-rp {
  position: relative;
  min-width: 0;
  padding: 14px 0 12px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-rp:first-child {
  border-top: 0;
  padding-top: 2px;
}

.fra-rp:last-child {
  padding-bottom: 0;
}

/* (10b) The fact rows now carry their own rule, so the person divider needs no second one
   directly beneath the last row's. */
.fra-rp + .fra-rp {
  border-top: 0;
}

.fra-rp__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  min-width: 0;
}

.fra-rp__who {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  min-width: 0;
  padding-top: 5px;
}

/* ⚠ THE NAME WRAPS; IT IS NEVER ELLIPSISED. A responsible person's name is the statutory
   fact this section exists to record, and half of one is not a record. */
.fra-rp__name {
  min-width: 0;
  font-size: 13.5px;
  font-weight: 650;
  line-height: 1.4;
  color: var(--ipi-ink);
  overflow-wrap: anywhere;
}

.fra-rp__rel {
  flex-shrink: 0;
}

.fra-rp__tools {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

.fra-rp__more > summary.fra-rp__more-btn {
  justify-content: center;
  min-width: 36px;
  padding-inline: 8px;
}

/* ⚠ THE PERSON'S ⋯ PANEL OPENS UNDER ITS OWN BUTTON AT EVERY WIDTH. The shared ≤620px rule
   below re-anchors `.show-more-actions` to the header's action row; a person's menu is not
   in that row, so it keeps its own anchor (right-aligned, 220px). */
.fra-premises-page .fra-rp__more.show-more-actions {
  position: relative;
}

.fra-premises-page .fra-rp__more .show-more-actions__panel {
  left: auto;
  right: 0;
  min-width: 220px;
}

/* ⚠ `.ipi-panel summary::after` (fra-shell) draws a disclosure chevron on every summary
   in a panel; a ⋯ menu button is not a disclosure row and must not carry one. */
.ipi-panel .fra-rp__more > summary.fra-rp__more-btn::after {
  content: none;
  display: none;
}

/* Three facts side by side on a wide document, one column below the split.
   R4 wave 3 (10b) — THE ROWS KEEP THE NEIGHBOURS' RHYTHM: `.fra-rec--stacked`'s own 9px
   padding and rule under each fact, as "The record" and Clause 12 draw them. The override
   that dropped both made a person read as a different kind of record. */
.fra-rp__rec.fra-rec--stacked {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin-top: 4px;
}

/* The add button follows the list (10a) — the section head carries the title alone. */
.fra-rp__add {
  margin-top: 14px;
}

.fra-rp__address {
  white-space: normal;
}

/* The one field set the add and edit dialogs share. */
.fra-rp-form .fra-field--span {
  grid-column: 1 / -1;
}

/* ------------------------------------------------------------ the edit form */

.fra-premises-form {
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
}

.fra-form-clause {
  margin: 0 0 14px;
  padding: 12px 14px;
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
  min-width: 0;
}

.fra-form-clause > legend {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 0 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ipi-ink-2);
}

.fra-field {
  min-width: 0;
}

.fra-field__unit {
  font-weight: 400;
  color: var(--ipi-ink-4);
}

.fra-field__checks {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.fra-field__checks label {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  font-weight: 400;
  color: var(--ipi-ink-2);
}

@media (max-width: 900px) {
  .fra-factual__values .ipi-keyvals__value {
    max-width: 55%;
  }

  .fra-rp__rec.fra-rec--stacked {
    grid-template-columns: minmax(0, 1fr);
  }

  /* ⚠ 44px TARGETS on the person's own controls — A7's module floor covers the shell
     primitives; this is the ⋯ square, which it cannot size. */
  .fra-rp__tools .ipi-row-action-btn {
    min-height: 44px;
    min-width: 44px;
    justify-content: center;
  }

  .fra-rp__who {
    padding-top: 11px;
  }

  /* One control height in the dialog: `.tenant-modal` sizes selects 40px while the
     sheet's anti-zoom rule lifts text inputs to 44, so the two rows disagreed.
     R4 wave 3 (1) — `!important` AT HIGHER SPECIFICITY, because style.css's canonical
     `.modal.modal--sheet select { min-height: 0 !important }` beat the plain rule and the
     selects stayed 40px. Scoped to the FRA pages this agent owns (the building record's
     person and delete dialogs, the template dialogs, the matrix); the canonical rule is untouched. */
  :is(.fra-premises-page, .fra-template-page, .fra-matrix-page) .modal.modal--sheet :is(select, input:not([type='hidden']):not([type='checkbox']):not([type='radio'])) {
    min-height: 44px !important;
  }

  .fra-factual__item {
    padding: 12px 0;
  }

  .fra-form-clause {
    padding: 10px 12px;
  }

  /* At 390 and 360 a two-column form grid gives every control a ~150px box, which is
     narrower than a date input's own intrinsic width and pushes the page into
     horizontal scroll. One column below the split. */
  .fra-premises-form .ipi-form-grid,
  .fra-rp-form .ipi-form-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* 🔴 STAGE 3, ITEM 3.7 — THE PAGE-LEVEL CARD-IFICATION IS GONE, and stays gone.
   The premises table once carried a 60-line mobile fork here that fought the shell's
   `.ipi-table--stack` card cell by cell. Since table-cards plan 8.6 the premises and
   assessments lists are phone/tablet record cards laid out by table-cards.css
   (`data-rc="card"`, which also opts them out of the shell's 780px floor), so neither
   `--stack` nor any page rule shapes them. ⛔ Do not add a page-level card layout back. */

/* ⚠ THE RESPONSIBLE-PERSON EDIT FORM IS NO LONGER A `<details>` UNDER EACH PERSON — it
   and the add form are one field set in a `.tenant-modal` dialog (R4-05), so the
   `.fra-rp__edit` / `.fra-rp__actions` / dashed `.fra-rp-form` rules that lived here are
   gone with the markup. */

/* ==========================================================================
   Stage 5 — the assessment page: the catalogue rendered.

   ⛔ EVERY custom property below is an `--ipi-*` token declared on `.fra-page` by
   fra-shell.css. A bare var() against a property that does not exist resolves to
   nothing and the whole declaration disappears silently.

   ⛔ Nothing here goes in desktop.css: it is linked media="(min-width: 1367px)"
   and is inert at 1180, 1024, 900, 810, 390 and 360 — every viewport the render
   lane checks except one.

   ⚠ NO TABLES ON THIS PAGE, DELIBERATELY. A judgement's narrative, a limitation
   and a notional basis are all paragraphs, and a wrapped paragraph inside a table
   cell is this project's own shipped-defect mechanism — PdfRowBudget packs whole
   blocks and cannot split one, so an oversized cell runs off the bottom of its
   sheet. One block per thing, at every width.
   ========================================================================== */

/* 🔴 ITEM 3.6 — deleted here too. `.ipi-list-area`'s 16px gap already separates these
   panels; the margin made it 32px. */

/* The limitations appendix. It is an .ipi-panel like every other block on the page;
   this rule exists so the list has its own bottom rule rather than inheriting the
   panel's padding as its last separator. */
.fra-limitations .fra-limitation:first-of-type {
  padding-top: 0;
}

/* ---------------------------------------------------------------- methodology */

.fra-methodology__statement {
  margin: 0 0 10px;
  font-size: 15px;
  font-weight: 600;
  line-height: 1.45;
  color: var(--ipi-ink);
}

/* One block per note. They are separate statements about separate questions and
   must never read as one paragraph — merging them is what produced the original
   short-term-let error. */
.fra-methodology__note {
  margin: 0 0 8px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
}

.fra-methodology__warning {
  margin: 0 0 8px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-accent-bg);
  border: 1px solid var(--ipi-accent-border);
  border-radius: 8px;
}

/* ------------------------------------------------------------------ fra_type */

.fra-fra-type__numbering {
  margin: 0 0 12px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-accent-bg);
  border: 1px solid var(--ipi-accent-border);
  border-radius: 8px;
}

.fra-fra-type__options {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The scope text is the control, not a tooltip on it. An assessor choosing a type
   is agreeing to that list, and a list they have to click to see is a list they
   will not read. */
.fra-fra-type__option {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 12px;
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
}

.fra-fra-type__option.is-selected {
  border-color: var(--ipi-accent);
  box-shadow: inset 0 0 0 1px var(--ipi-accent);
}

.fra-fra-type__label {
  font-size: 14px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-fra-type__scope,
.fra-fra-type__qualification {
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

.fra-fra-type__qualification {
  padding-top: 6px;
  border-top: 1px dashed var(--ipi-rule);
}

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

.fra-section__number {
  color: var(--ipi-ink-3);
  font-variant-numeric: tabular-nums;
}

.fra-section__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* 🔴 STAGE 1, ITEM 1.3 — THE CAPTURE CARD IS AN `.ipi-field` HOST.
   The control inside it is a DIRECT child of this element, so carrying
   `.ipi-field` in the markup is what makes `.ipi-field > input/select/textarea`
   (fra-shell.css) skin it — 8/11 padding, 13px, radius 7 and the `:focus-visible`
   accent ring — instead of the `.fra-control__input` fork that had none of them.

   ⚠ THE PRIMITIVE'S OWN TYPE IS RESET HERE, DELIBERATELY. `.ipi-field` is shaped
   for ONE label above ONE control and sets 12px/600 plus a 5px flex gap on itself.
   A capture card is not one label: it holds a label, guard help, help text, a
   control, six finding radios and a definitions list, each of which already
   declares its own type and spacing in margins. Resetting the inherited type and
   zeroing the gap leaves the card's interior byte-identical to what it was, and
   changes only the control skin — which is the whole point of the item.
   ⛔ This is not a fork of `.ipi-field`: nothing here restates the input skin. */
.fra-field {
  padding: 12px;
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
}

/* ⚠ SCOPED TO `.fra-control`, NOT TO `.fra-field`. `premises/form.php` builds
   `.ipi-field.fra-field` — the primitive's own one-label-one-control shape — and it
   WANTS the 5px gap and the 12px/600 label type this rule cancels. Only the capture
   card, which is `.fra-field.fra-control`, needs them off. */
.fra-field.fra-control {
  gap: 0;
  font-size: 13px;
  font-weight: 400;
  color: var(--ipi-ink-2);
}

.fra-field__label {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 6px;
}

.fra-field__label-text {
  font-size: 13.5px;
  font-weight: 650;
  color: var(--ipi-ink);
}

/* The P-guard badge is a label, not a warning colour: the branch is a normal
   property of a field and colouring it red would read as an error state on a
   correctly-filled form. */
.fra-field__guard-badge {
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: 0.02em;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 999px;
}

.fra-field__guard-help,
.fra-field__help {
  margin: 0 0 8px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

/* ⚠ STILL USED, AND ONLY BY THE READ-ONLY PAGE. Item 5.10 turned the CAPTURE
   form's guard prose into a disclosure; `assessment/_field.php` — which renders
   `?page=fra-assessment-show`, where there is no form to be buried under —
   still emits this paragraph. Deleting the rule with the capture markup would
   have left an unstyled paragraph on 65 fields of the read page, which is the
   T-2 failure this file already carries a warning about. Derive before
   removing: `grep -rn 'fra-field__guard-help' app/Views/`. */
.fra-field__guard-help {
  padding: 8px 10px;
  background: var(--ipi-bg-inset);
  border-left: 3px solid var(--ipi-rule);
  border-radius: 0 8px 8px 0;
}

/* ───────────────────────────── item 5.10 — the guard prose is a disclosure ──
 *
 * 🔴 65 OF THE CATALOGUE'S 426 FIELDS CARRY A P-GUARD PARAGRAPH averaging 305
 * characters, and it rendered always-open above the control. Section 2 is 13
 * fields carrying 2,995 characters of it; section 10 is 44 fields carrying
 * 6,496. On a phone that was more help text than form, on every visit.
 *
 * ⛔ `<details>`, NOT A BUTTON AND A PANEL. This form has to work on a device
 * where the module graph did not load — the reason the capture module is a
 * dynamic import — and a JavaScript disclosure would put the prose out of
 * reach on exactly that device. The BODY reuses `.ipi-help__panel`, which all
 * three shells already declare and no view had used; only the summary is
 * FRA's, because the shell's `.ipi-help__toggle` is a 26px `?` circle built
 * to sit beside a heading, not a per-field row that names its branch.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 */
.fra-field__guard {
  margin: 0 0 8px;
}

.fra-field__guard-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 32px;
  padding: 4px 0;
  font-size: 12px;
  font-weight: 650;
  color: var(--ipi-ink-3);
  cursor: pointer;
  list-style: none;
}

/* Safari renders `summary` with its own disclosure widget and ignores
   `list-style`; this is the documented way to remove it. */
.fra-field__guard-toggle::-webkit-details-marker {
  display: none;
}

/* ✅ THE SECOND MARKER, FOUND 2026-09-23. It was not Chrome's: `fra-shell.css` gives
   EVERY `.ipi-panel summary` an `::after` chevron (and 22px of right padding for it), and
   this toggle sits inside a panel — so it showed that chevron beside its own `::before`
   triangle. The toggle keeps its triangle; the panel's chevron is switched off here. */
.ipi-panel .fra-field__guard-toggle {
  padding-right: 0;
}

.ipi-panel .fra-field__guard-toggle::after {
  content: none;
}

.fra-field__guard-toggle::before {
  content: "▸";
  font-size: 10px;
  color: var(--ipi-ink-4);
}

.fra-field__guard[open] > .fra-field__guard-toggle::before {
  content: "▾";
}

.fra-field__guard-toggle:hover {
  color: var(--ipi-ink);
}

.fra-field__guard-toggle:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ⚠ THE PANEL'S 16px BOTTOM MARGIN IS THE SHELL'S AND IS WRONG INSIDE A CARD,
   where the control is 8px below it. Nothing else about `.ipi-help__panel` is
   restated — this is not a fork of it. */
.fra-field__guard-panel {
  margin-bottom: 8px;
}

.fra-field__value {
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  overflow-wrap: anywhere;
}

/* ──────────────────── item 5.2 — which finding is selected, at arm's length ──
 *
 * 🔴 `is-selected` WAS NEVER EMITTED ON THIS FORM. The rule below existed; the
 * class was applied only by the READ-ONLY page's partial, and the capture
 * module never touched it. So on a section with 44 judgements the only thing
 * saying which had been answered was a 13px native radio dot — at 390px, in a
 * stairwell, through gloves. This is much of what "capture step looks terrible"
 * was about, and the fix was mostly already written.
 *
 * ⛔ THE PADDING MOVED FROM THE `li` TO THE `label`. The chip was the list item
 * and the label was inside it, so the chip's own 4/10 padding was NOT part of
 * the hit area — a 44px-looking pill with a ~20px target. The label is the
 * whole chip now.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 */
.fra-field__findings {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-field__finding {
  padding: 0;
  font-size: 12.5px;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 999px;
}

.fra-field__finding > label {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 44px;
  padding: 4px 14px;
  cursor: pointer;
}

/* ⚠ THE SELECTED STATE DIFFERS IN FILL, INK, WEIGHT **AND** BORDER — four
   channels, not one. A ring alone was the previous rule and it is what a
   reader with the phone at arm's length in bad light cannot see. */
.fra-field__finding.is-selected {
  color: var(--ipi-ink);
  font-weight: 700;
  background: var(--ipi-accent-bg);
  border-color: var(--ipi-accent);
  box-shadow: inset 0 0 0 1px var(--ipi-accent);
}

/* ⚠ AND `:has()` IS THE BELT TO THE MODULE'S BRACES. The class is applied by
   the server on load and by `paintFindings()` on change; this makes the state
   correct even on a build where the capture module failed to resolve, which
   is the failure the whole module is written to survive. Browsers without
   `:has()` ignore the whole rule and fall back to the class. */
.fra-field__finding:has(input:checked) {
  color: var(--ipi-ink);
  font-weight: 700;
  background: var(--ipi-accent-bg);
  border-color: var(--ipi-accent);
  box-shadow: inset 0 0 0 1px var(--ipi-accent);
}

/* What the state you just chose will demand before this assessment can be
   issued — the bounds FraJudgement enforces on save, said before the save. */
.fra-field__requires {
  margin: 6px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

/* A notional acceptance is neither a pass nor a fail and must not be coloured as
   either — it is a recorded assumption and reads as one. */
.fra-field__notional {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 8px 0 0;
  padding: 8px 10px;
  font-size: 12.5px;
  line-height: 1.5;
  background: var(--ipi-bg-inset);
  border: 1px dashed var(--ipi-rule);
  border-radius: 8px;
}

.fra-field__notional-claim {
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-field__notional-basis {
  color: var(--ipi-ink-2);
}

.fra-field__limitations {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 8px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

.fra-field__limitation.is-missing {
  color: var(--ipi-red);
}

/* --------------------------------------------------------------- limitations */

.fra-limitation {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr);
  gap: 4px 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-limitation:last-child {
  border-bottom: 0;
}

.fra-limitation__number {
  grid-row: 1 / span 2;
  font-size: 14px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ipi-ink-3);
}

.fra-limitation__text {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ipi-ink);
  overflow-wrap: anywhere;
}

.fra-limitation__cited-by {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

.fra-limitation__citer {
  font-variant-numeric: tabular-nums;
}

.fra-limitation__citer + .fra-limitation__citer::before {
  content: ", ";
}

@media (max-width: 900px) {
  .fra-fra-type__options {
    grid-template-columns: minmax(0, 1fr);
  }

  .fra-limitation {
    grid-template-columns: 26px minmax(0, 1fr);
  }
}

/* ==========================================================================
   STAGE 6 - THE CAPTURE FLOW
   ==========================================================================

   ⛔ EVERY VALUE BELOW IS AN --ipi-* TOKEN OR A LITERAL THIS FILE ALREADY USES.
   A bare var() with no fallback naming a token that does not exist fails
   silently: the declaration is dropped and the element renders unstyled, which
   on a form reads as "the control is broken" rather than "the CSS is wrong".

   ⚠ THE 900px BREAKPOINT IS THE TENANT APP'S, and it is the one this file
   already uses. desktop.css is media-gated at (min-width: 1367px) and is inert
   below it, so nothing here may go there.
   ========================================================================== */

/* -------------------------------------------------------------- the setup page */

.fra-lede {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--ipi-ink-2);
}

/* ⚠ THE SETUP PAGE OPENS WITH ONE NOTE (R3-25). The separate `.fra-setup__state` box
   that sat under it is gone — its sentence is the note's own now — so the spacing it
   carried moves onto the note. */
.fra-setup-page [data-fra-setup-lede] {
  margin: 0 0 16px;
}

.fra-setup__state-line {
  display: block;
  margin-top: 6px;
}

.fra-setup__form {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* 🔴 TWO COLUMNS, NOT `auto-fit`.
   `repeat(auto-fit, minmax(240px, 1fr))` gave FOUR columns at 1205px, so each type
   card was 294px — narrower than a phone — and Type 1's scope (437 characters, twice
   the other three) ran to forty lines. Grid rows stretch to their tallest item, so it
   set a 472px height for every card beside it: measured 775/241/256/245 characters
   producing four identical 472px cards, three of them mostly white.
   Two columns doubles the card to ~590px, which is where that scope reads as five
   lines rather than forty. */
/* ⚠ `stretch`, NOT `start` (R3-26). With the Type 1 caveat out of the card the four
   scopes are close in length, and cards of one row at different heights read as a
   ragged grid rather than four equal choices. */
.fra-setup__types {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: stretch;
  gap: 10px;
  margin: 0;
  padding: 0;
  border: 0;
}

/* ⚠ A REFUSED TYPE PAINTS THE CARDS (R3-12). The fieldset carried `.ipi-field--error`
   and nothing styled it — `.ipi-field--error` rules target an `.ipi-field`'s own control,
   and this fieldset has four cards instead. Each card takes the error border, and the
   message renders above them (setup.php). */
.fra-setup__types.ipi-field--error > .fra-setup__type {
  border-color: var(--ipi-red);
  box-shadow: inset 0 0 0 1px var(--ipi-red);
}

.fra-setup-page [data-fra-setup-error="fra_type"] {
  margin: 0 0 10px;
}

.fra-setup__typehelp {
  margin: 0 0 12px;
}

.fra-setup__typehelp .ipi-help__panel p + p {
  margin-top: 8px;
}

/* ⛔ BOTH SPAN THE FULL ROW. A `<fieldset>` that IS the grid makes every child a grid
   item, and the numbering note was silently taking a card slot — see the note in
   setup.php for what that did. The legend needs the same treatment for the same
   reason. */
.fra-setup__types > legend,
.fra-setup__types > .fra-fra-type__numbering {
  grid-column: 1 / -1;
}

.fra-setup__types > .fra-fra-type__numbering {
  margin: 0 0 2px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ipi-ink-3);
}

/* 🔴 THE CARD IS A RADIO, A TITLE AND A PARAGRAPH — THREE THINGS, NOT ONE RUN-ON LINE.
   `.fra-setup__type` is `display: block` and the title and scope are both `<span>`, so
   they rendered INLINE: "Type 1 — common parts, non-intrusive Common parts only, with
   no opening up. Includes a sample of…" as a single paragraph, with the second line
   wrapping back under the radio and leaving a ragged left edge. The `.fra-fra-type__*`
   text classes carry no `display` of their own because the OTHER user of them
   (`.fra-fra-type__option`, on assessment/show.php) is a flex column and stacks them
   for free — this card had nothing doing that job.
   ⚠ Scoped to `.fra-setup__type-choice` so show.php's version is untouched.
   ⚠ The radio spans both text rows so the paragraph keeps one left edge instead of
   wrapping under the control. */
.fra-setup__type {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 3px 10px;
  align-items: start;
  /* ⚠ The card is stretched to its row (R3-26); its two text rows stay at the top
     rather than sharing out the extra height. */
  align-content: start;
}

.fra-setup__type > input[type="radio"] {
  grid-row: 1 / span 2;
  width: auto;
  margin: 3px 0 0;
}

.fra-setup__type > .fra-fra-type__label,
.fra-setup__type > .fra-fra-type__scope {
  grid-column: 2;
  display: block;
}

.fra-setup__type > .fra-fra-type__label {
  line-height: 1.35;
}

/* ⚠ THE SCOPE IS BODY TEXT, NOT A SECOND TITLE (R3-26). It inherited the label's
   600 weight, so Type 1's scope read as eleven lines of bold under a bold title. */
.fra-setup__type > .fra-fra-type__scope {
  font-weight: 400;
  color: var(--ipi-ink-2);
}

/* ⚠ `.fra-setup__caveat` LIVED HERE. The Type 1 caveat now sits in the disclosure above
   the cards with the A–D note (R3-25), which the module's help panel styles. */

.fra-setup__types legend {
  padding: 0;
  margin-bottom: 8px;
  font-size: 13.5px;
  font-weight: 650;
  color: var(--ipi-ink);
}

/* ⚠ `.fra-setup__required` LIVED HERE AND WAS RENAMED `.fra-required` BY ITEM 7.4.
   It was the only visible required marker in the module and only the setup page
   could use it, which is why the premises form had none. The rule moved verbatim
   to the Stage 7 block at the foot of this file. */

.fra-setup__type {
  padding: 12px;
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
  cursor: pointer;
}

/* 🔴 THE HIGHLIGHT KEYS OFF THE RADIO, NOT OFF THE SERVER ATTRIBUTE.
   `data-fra-type-selected="1"` is written by the server from the STORED answer, and
   this page loads no JavaScript at all — so with the attribute styled, choosing a
   different type lit the new card while the old one stayed lit too: TWO highlighted
   options on a single-choice picker, until reload. Measured; it is what belt-and-braces
   costs on a control the user can change without a round trip.
   ⚠ `:checked` IS SERVER-RENDERED TOO — the radio carries `checked` from the same
   stored value — so `:has()` alone is correct on load AND live on change. The
   attribute stays in the markup for tests and for anything keying off it; it just
   does not paint.
   ⛔ Do not restore a `[data-fra-type-selected]` background rule beside this one. */
.fra-setup__type:has(input[type="radio"]:checked) {
  border-color: var(--ipi-accent-border);
  background: var(--ipi-accent-bg);
}

/* 🔴 STAGE 1, ITEM 1.3 — THE SETUP ROWS ARE `.ipi-field`s.
   `.fra-setup__row` was a third skin of the same primitive (9/10 at 13.5px radius
   8, no focus ring) wrapped round a hand-styled label and help span. All three are
   gone: the row is an `.ipi-field`, its label is the primitive's own label type and
   its help is the primitive's `> small`.

   ⚠ WHAT SURVIVES IS THE MEASURE, NOT THE SKIN. `.ipi-field > input` is `width:
   100%`, and a setup panel is the full width of the list area — a date input
   stretched to 1200px is not a control anyone can read as a date. The 480px cap the
   fork carried is layout, so it stays; it just no longer drags a whole input skin
   with it. */
/* ⚠ The measure applies to a field standing on its own. Inside `.ipi-form-grid` the
   COLUMN is the constraint, and a 480px cap inside a ~600px column just leaves a
   ragged right edge. */
.fra-setup__form > .ipi-field {
  max-width: 480px;
}

/* ⚠ The form is a flex COLUMN and the three cards are its items, so the gap between
   them is the form's, not a margin on each. */
.fra-setup__form {
  gap: 16px;
}

/* ⚠ The save bar is a card like the other three, so it sits on the same ground rather
   than floating on the page with a bare rule above it. `.ipi-panel__foot--actions`
   normally CLOSES a panel that has content above it, so its own top margin, padding
   and rule separate it from nothing here. Same as `.fra-assessor-savebar`. */
.fra-setup__savebar .ipi-panel__foot--actions {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

/* ⚠ THE GAP UNDER THE PAGE HEADER, which this page was missing exactly as the assessor
   page was. `.ipi-page-header` ends with 20px of padding and every FRA page that opens
   with `.ipi-stats-strip` gets 20px more from the strip — 41px measured on Premises.
   This page has no strip, so it had 21px. Same fix, same reason, scoped the same way.
   ⛔ Not on `.ipi-list-area` generally: on a page that HAS a strip it would stack. */
.fra-setup-page .ipi-list-area {
  padding-top: 20px;
}

/* A row of the agreed answers lines its controls up: the help moved under the control
   (setup.php), and the fields start at the top of their row rather than stretching. */
.fra-setup__answers {
  align-items: start;
}

/* ⚠ ONE DISABLED LOOK. The shell greys a disabled INPUT (inset ground, ink-3) and says
   nothing about a disabled SELECT, so a read-only setup showed two looks side by side. */
.fra-setup-page .ipi-field > select[disabled] {
  background-color: var(--ipi-bg-inset);
  color: var(--ipi-ink-3);
}

/* A dependent answer while its condition does not hold (forms.js `data-fra-enable-when`):
   the label reads as muted with its control, not as a live question. */
.fra-setup-page .ipi-field.is-inactive > span {
  color: var(--ipi-ink-3);
  font-weight: 500;
}

/* An issued record's setup, as values (setup.php): the chosen type as a plain card and
   the agreed answers as the shell's key/value list. */
.fra-setup__type.fra-setup__type--readonly {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  cursor: default;
}

.fra-setup__type--readonly > .fra-fra-type__label,
.fra-setup__type--readonly > .fra-fra-type__scope {
  grid-column: 1;
}

.fra-setup__none {
  margin: 0;
  color: var(--ipi-ink-3);
}

/* The A–D disclosure's toggle is a 32px line of text; a phone needs a 44px target. */
@media (max-width: 900px), (pointer: coarse) {
  .fra-setup-page .fra-field__guard-toggle {
    min-height: 44px;
  }
}

/* --------------------------------------------------------- the capture page */

/* ─────────────────────────────── item 5.3 — the section strip is ONE ROW ──
 *
 * 🔴 IT WAS `flex-wrap: wrap` WITH THE FULL TITLES AND NO MOBILE RULE AT ANY
 * WIDTH. Fourteen pills carrying titles up to 43 characters ("Emergency
 * response and evacuation procedures") is two rows at 1440px and roughly
 * eleven at 390px — more than a phone screen of navigation standing between
 * the assessor and the first question on the page.
 *
 * ⛔ THE SHAPE IS THE FIRE-DOOR STEPPER'S, COPIED DELIBERATELY.
 * `inspection_show.css:1023-1047` solved this exact problem and says why:
 * "Fourteen full-length component names at 11px was a wall of type… the thing
 * that made this strip look like a toolbar rather than progress." Horizontal
 * scroll, `scroll-snap-type: x proximity`, a hidden scrollbar, and a marker
 * that shows its label only when it is the step you are on.
 *
 * ⚠ THE PADDING IS LOAD-BEARING, and it is the same lesson that file records:
 * `overflow-x: auto` forces `overflow-y: auto`, which would clip the pills'
 * `::after` touch expansion to the strip's own height. The padding gives the
 * expansion somewhere to live.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 */
.fra-capture__nav {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  /* ⚠ 4px, not 16px: `.ipi-list-area`'s own gap already separates the strip from the
     offline row below it, and the two together were 32px of nothing (review R2-04). */
  margin-bottom: 4px;
  padding: 5px 0 7px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.fra-capture__nav::-webkit-scrollbar {
  display: none;
}

.fra-capture__nav-link {
  position: relative;
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 44px;
  min-height: 44px;
  padding: 0 12px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  text-decoration: none;
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
  scroll-snap-align: center;
}

/* ⚠ THE PILL ITSELF IS 44px NOW (review R2-12, 2026-09-23). A 34px pill widened by an
   `::after` inset measured a 42.4px hit area — the inset was clipped by the strip's own
   overflow. The gaps between pills stay tappable only sideways, which the 6px gap and the
   3px side inset below still cover. */
.fra-capture__nav-link::after {
  content: "";
  position: absolute;
  inset: 0 -3px;
}

/* ⚠ THE STRIP SAYS WHEN IT CONTINUES (review R2-12). Pills cut at the edge had no cue that
   more lay beyond. `fra-capture-steps.js` sets `data-fra-nav-fade` to the edges that still
   have somewhere to scroll; the mask fades only those. A mask uses alpha only, so the two
   keywords below are not colours. */
.fra-capture__nav[data-fra-nav-fade="end"] {
  -webkit-mask-image: linear-gradient(to right, black calc(100% - 36px), transparent);
  mask-image: linear-gradient(to right, black calc(100% - 36px), transparent);
}

.fra-capture__nav[data-fra-nav-fade="start"] {
  -webkit-mask-image: linear-gradient(to right, transparent, black 36px);
  mask-image: linear-gradient(to right, transparent, black 36px);
}

.fra-capture__nav[data-fra-nav-fade="start end"] {
  -webkit-mask-image: linear-gradient(to right, transparent, black 36px, black calc(100% - 36px), transparent);
  mask-image: linear-gradient(to right, transparent, black 36px, black calc(100% - 36px), transparent);
}

/* ⚠ ONLY THE SECTION YOU ARE ON SAYS ITS NAME. That is the whole reason the
   strip fits: the header above already names it and counts it, so a marker
   only has to say where you are, how far through it is, and be a jump target. */
.fra-capture__nav-title {
  display: none;
  white-space: nowrap;
}

.fra-capture__nav-link.is-current .fra-capture__nav-title {
  display: inline;
}

.fra-capture__nav-link.is-current {
  color: var(--ipi-ink);
  background: var(--ipi-accent-bg);
  border-color: var(--ipi-accent-border);
}

.fra-capture__nav-number {
  font-variant-numeric: tabular-nums;
  color: var(--ipi-ink-3);
}

.fra-capture__nav-link.is-current .fra-capture__nav-number {
  color: var(--ipi-ink);
}

/* ⚠ THE COUNT IS ON EVERY PILL — item 5.11 asks for a per-pill answered/total
   badge, and the "wall of type" item 5.3 removed was the fourteen full TITLES,
   not a five-character count. It is 11px and muted so the number of the step
   still reads first, and the strip scrolls, so the extra width costs nothing
   but scroll distance. */
.fra-capture__nav-count {
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--ipi-ink-4);
}

.fra-capture__nav-link.is-current .fra-capture__nav-count {
  color: var(--ipi-ink-2);
}

/* ⚠ `is-done` AND `is-todo` DIFFER IN MORE THAN COLOUR — the border weight
   changes too, so the strip still reads for anybody who cannot separate the
   two hues. The fire-door strip's own comment records that its pass and fail
   states were byte-identical for a while and said nothing. */
.fra-capture__nav-link[data-fra-nav-state="done"] {
  color: var(--ipi-green);
  background: var(--ipi-green-bg);
  border-color: var(--ipi-green);
}

.fra-capture__nav-link[data-fra-nav-state="part"] {
  color: var(--ipi-ink);
  background: var(--ipi-bg-card);
  border-color: var(--ipi-rule-h);
}

.fra-capture__nav-link[data-fra-nav-state="empty"] {
  color: var(--ipi-ink-4);
  background: var(--ipi-bg-inset);
}

/* ⛔ THE CURRENT PILL WINS OVER ITS OWN STATE. Both selectors are one class
   plus one attribute otherwise, and "which section am I on" is the thing the
   strip exists to answer. */
.fra-capture__nav-link.is-current[data-fra-nav-state] {
  color: var(--ipi-ink);
  background: var(--ipi-accent-bg);
  border-color: var(--ipi-accent-border);
}

.fra-capture__form {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* ⚠ THE STRIP IS THE PAGE'S OWN, not `.ipi-stats-strip`'s geometry — that
   primitive already scrolls horizontally at ≤900px and must not be
   overridden (tenant-ui.md). This only gives it the room the nav takes. */
.fra-capture__stats {
  margin-bottom: 4px;
}

/* ───────────────────────────── item 5.6 — Save is reachable from anywhere ──
 *
 * 🔴 IT WAS A PLAIN BUTTON AT THE END OF THE FORM. Before item 5.4 gave the
 * nine unblocked sections structure, section 7 put that several thousand
 * pixels below the first control; blocked, it is still a long way down on a
 * phone. Both sibling capture forms keep Save permanently reachable and both
 * offer a way out.
 *
 * ⛔ STICKY, NOT A TOPBAR ITEM. The mobile topbar budget is six pixels with
 * three modes in it, and a capture bar there would push the mode toggle off
 * the row on every page in the app.
 *
 * ⚠ `position: sticky` NEEDS AN UNCLIPPED ANCESTOR CHAIN. `.fra-capture__form`
 * is a plain flex column and `.ipi-list-area` sets no `overflow`, so the bar
 * sticks to the viewport. Anything that later adds `overflow: hidden` to
 * either would silently turn this back into a button at the bottom of a long
 * page, with nothing failing.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 */
.fra-capture__bar {
  position: sticky;
  bottom: 0;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 calc(var(--ipi-page-gutter) * -1);
  padding: 10px var(--ipi-page-gutter);
  background: var(--ipi-bg-card);
  border-top: 1px solid var(--ipi-rule);
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.06);
}

.fra-capture__save {
  min-height: 44px;
}

/* ⚠ AT 390px THE THREE STEP CONTROLS LOSE THEIR WORDS, NOT THEIR TARGETS.
   Previous / Save this section / Next / Exit is well over 390px of text; the
   arrows and Save keep their full 44px while the labels go.

   ⛔ SUPERSEDED FOR `.fra-capture-page` ON 2026-09-09 — the bar is now `position: fixed`
   there and pays `.content`'s padding instead, which is 16px below 1367px, not the 28px
   gutter. See the arithmetic on the unconditional `.fra-capture-page .fra-capture__bar`
   block further down. `fra/assessment/capture.php` is the ONLY view that renders this bar
   (`grep -rn 'fra-capture__bar' app/Views/`), so the `padding` line below is now inert —
   kept, not deleted, because the reasoning it carries is the thing that has to be obeyed
   by whatever pays the alignment next, and the `gap` beside it is still live:

   ⛔ THE HORIZONTAL PADDING STAYS `--ipi-page-gutter` AND MUST NOT BE SHRUNK.
   It is not decoration — it is the other half of the negative margin above it,
   and the two have to cancel. `.ipi-list-area` bleeds to
   `calc(var(--ipi-page-gutter) * -1)` and pads the gutter back, so its content
   sits 16px inside the phone's edge; the bar bleeds the same distance and must
   pad the same amount back or its buttons land somewhere else. This said
   `padding: 8px 12px`, and the measured result at 390px was the bar's first
   button starting at x=0 — flush against the screen edge, 16px out of line with
   every card above it, while the nav strip and the panels sat at 16..374.
   Measured 2026-09-08, not reasoned about. Only the VERTICAL padding is the
   mobile concession. */
@media (max-width: 620px) {
  .fra-capture__bar {
    gap: 6px;
    padding: 8px var(--ipi-page-gutter);
  }

  .fra-capture__save {
    flex: 1 1 auto;
  }
}

/* ─────────────────────────────── item 5.5 — the per-block bulk control ──
 *
 * ⚠ IT READS AS A SHORTCUT, NOT AS A PRIMARY ACTION. It is `.ipi-action-btn`
 * — the app's own secondary control — because the primary action on this page
 * is Save, and a shortcut that looked like the thing to press would get
 * pressed by somebody who had not read what it does.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 */
.fra-bulk {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  padding: 8px 10px;
  background: var(--ipi-bg-inset);
  border-radius: 8px;
}

.fra-bulk__btn {
  min-height: 36px;
}

.fra-bulk__btn[disabled] {
  opacity: 0.55;
  cursor: default;
}

.fra-bulk__note {
  flex: 1 1 220px;
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

/* ────── item 5.11 — the outstanding-items PANEL is gone; the tile is not ──
 * 🔴 REMOVED 2026-09-09 ON THE OWNER'S INSTRUCTION, along with the `<details>` it
 * skinned in `fra/assessment/capture.php`. `.fra-capture__blockers`, `__blockers-toggle`,
 * `__blockers-count`, `__blocker-list`, `__blocker`, `__blocker-link` and `__blocker-why`
 * had exactly one call site between them and it no longer exists.
 *
 * ⚠ THE COUNT SURVIVES ELSEWHERE AND IS NOT STYLED HERE. The "Outstanding items" tile
 * in the capture stats strip is an `_stats_strip.php` tile like every other one and
 * carries no class from this block — deleting these rules cannot have touched it.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md. */

/* --------------------------------------------------------------- the controls */

/* 🔴 STAGE 1, ITEM 1.3 — `.fra-control__input` AND `.fra-control__sub`'s INPUT
   SKIN ARE GONE. They were two of four hand-rolled skins of the same primitive
   (9/10 at 13.5px radius 8, and 8/10 at 13px radius 8, against the shell's 8/11 at
   13px radius 7), and neither carried the `:focus-visible` accent ring the shell
   gives every other input in the app. The controls are now direct children of an
   `.ipi-field` and the shell skins them. ⛔ Do not restate an input skin here. */

.fra-control__judgement-extras {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 10px;
  margin-top: 10px;
}

/* A mirrored field is written somewhere else, and it has to LOOK unwritable
   here or the assessor types into it and loses the answer. */
.fra-control--mirror {
  background: var(--ipi-bg-inset);
}

.fra-control__mirror-link {
  display: inline-block;
  margin-top: 6px;
  font-size: 12px;
  color: var(--ipi-blue);
}

.fra-field__definition {
  margin: 8px 0 0;
  padding: 8px 10px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-blue-bg);
  border-radius: 8px;
}

.fra-field__definition-term {
  display: block;
  font-weight: 700;
  color: var(--ipi-ink);
}

.fra-field__definition-source {
  display: block;
  margin-top: 4px;
  font-size: 11.5px;
  color: var(--ipi-ink-3);
}

/* ------------------------------------------------------------------ the blocks */

.fra-block {
  padding: 14px;
  background: var(--ipi-bg-hover);
  border: 1px solid var(--ipi-rule);
  border-radius: 12px;
}

.fra-block + .fra-block {
  margin-top: 12px;
}

.fra-block__title {
  margin: 0 0 6px;
  font-size: 14px;
  font-weight: 700;
  color: var(--ipi-ink);
}

.fra-block__note {
  margin: 0 0 12px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

/* ⚠ AT PHONE WIDTH "Back to premises" READS "Back". It pushed More onto a line of its own
   at 390px; the full name stays for screen readers. */
@media (max-width: 620px) {
  .fra-ph-back-long {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}

/* The in-section block index — a wrapping row of quiet chips naming what is below.
   ⚠ It is a `nav` of anchors, so it works with no JavaScript and survives Ctrl+F. */
.fra-section__index {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 16px;
  padding: 0 0 14px;
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-section__index-link {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  padding: 4px 10px;
  font-size: 12px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ipi-ink-3);
  text-decoration: none;
  background: var(--ipi-bg-inset);
  border-radius: 999px;
}

.fra-section__index-link:hover {
  color: var(--ipi-ink);
}

.fra-section__index-link:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
}

/* ⚠ The anchor lands the heading under the sticky topbar without this. */
.fra-block {
  scroll-margin-top: 76px;
}

/* 🔴 THE RECORD'S FINDING PILL CARRIES A TONE, BECAUSE IT IS NOW THE ANSWER.
   The record used to print all six states as identical grey pills — 594 of them on one
   assessment — so the pill said nothing and the reader had to hunt for the one with
   `.is-selected`. It now prints ONE pill: the finding. A single pill that is the answer
   can afford to be coloured, and has to be, because "Satisfactory" and "Not
   satisfactory" are the two facts a reader scans a fire risk assessment for.
   ⚠ TONES COME FROM THE SHELL'S OWN TOKENS (green/red/accent), not new hex. Contrast
   measured on the card: green 5.6:1, red 5.4:1, accent-ink 5.1:1.
   ⛔ Only on the record. The capture page's palette stays neutral — there the pills are
   radio options and colouring the unchosen ones would read as six verdicts at once. */
[data-fra-assessment-page="show"] .fra-field__finding {
  font-weight: 600;
  border: 1px solid transparent;
}

[data-fra-assessment-page="show"] .fra-field__finding--ok {
  background: var(--ipi-green-bg);
  color: var(--ipi-green);
  border-color: rgba(30, 122, 69, 0.22);
}

[data-fra-assessment-page="show"] .fra-field__finding--bad {
  background: var(--ipi-red-bg);
  color: var(--ipi-red);
  border-color: var(--ipi-red-border);
}

[data-fra-assessment-page="show"] .fra-field__finding--warn {
  background: var(--ipi-accent-bg);
  color: var(--ipi-ink-2);
  border-color: var(--ipi-accent-border);
}

/* not_applicable / not_inspected — true statements, but not verdicts. */
[data-fra-assessment-page="show"] .fra-field__finding--muted {
  background: var(--ipi-bg-inset);
  color: var(--ipi-ink-3);
}

/* 🔴 ON THE RECORD A BLOCK IS NOT A TINTED BOX. THIS IS THE COLOUR BUG.
   `.fra-block` fills with `var(--ipi-bg-hover)` — the colour a row takes WHILE THE
   POINTER IS OVER IT — and paints it permanently. Inside `.ipi-panel`, which is
   `--ipi-bg-card` (#ffffff), that puts a DARKER patch (rgb(242,239,233)) on a white
   card, and in this palette a darker inset is the recessed/disabled surface. So every
   block of actual data read as greyed-out while the empty card around it read as
   active — the surface hierarchy inverted, and the record looked switched off.
   ⛔ The stack everywhere else in this app is: tinted page ground -> WHITE CARD ->
   content on the card. Restored here: the block sits on the card, and blocks are
   separated by a rule and space rather than by a fill that means something else.
   ⚠ Scoped to the record. The capture page shares `.fra-block` and its tint is at
   least arguable there (it groups controls you are about to touch) — but it is the
   same hover token, so it is worth a look next time that page is open. */
[data-fra-assessment-page="show"] .fra-block {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}

[data-fra-assessment-page="show"] .fra-block + .fra-block {
  margin-top: 28px;
  padding-top: 24px;
  border-top: 1px solid var(--ipi-rule);
}

/* The heading has to carry the grouping now that the fill does not. */
[data-fra-assessment-page="show"] .fra-block__title {
  margin: 0 0 4px;
  font-size: 15px;
  font-weight: 700;
  color: var(--ipi-ink);
}

[data-fra-assessment-page="show"] .fra-block__note {
  margin: 0 0 14px;
  max-width: 70ch;
  color: var(--ipi-ink-3);
}

/* 🔴 ON THE RECORD, A FIELD IS A ROW — NOT A CARD.
   `.fra-field`'s bordered 12px card is the CAPTURE skin: there it wraps a control you
   are about to touch, and it earns its chrome. The read-only record reuses the same
   partial, so every one of 322 answers rendered as its own bordered box: measured a
   54px card around 28px of content, and 100 of them in section 1 alone. That is ~38px
   of border, padding and gap per answer, ~3,800px of pure chrome in one section.
   Here it becomes a ruled row, and the block body becomes two columns — a key/value
   list, which is what a record is.
   ⛔ SCOPED TO `[data-fra-assessment-page="show"]`. The capture page shares this
   partial and MUST keep the card; it is the target you tap.
   ⚠ No attempt to make long fields span: 37 of 100 exceed one line and the type does
   not predict which, so the grid sizes each row to its own tallest item instead. */
[data-fra-assessment-page="show"] .fra-block__body {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;
  gap: 0 24px;
}

[data-fra-assessment-page="show"] .fra-field {
  padding: 9px 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--ipi-rule);
  border-radius: 0;
}

/* 🔴 THE LABEL AND THE ANSWER GET THEIR OWN COLUMNS, AND EVERY ROW USES THE SAME ONES.
   They rendered as one run-on line — "Assessment scope The whole building" — where the
   only cue separating the question from the answer was font weight, and because every
   label is a different length NOTHING lined up. There was no column of answers to read
   down, which is the whole job of a record.
   ⚠ A fixed 44% label track, not `auto`: `auto` sizes to the longest label in each
   ROW, so the answers would still start at a different x on every line. This is the
   nearest thing to `subgrid` that works across the two block columns. */
[data-fra-assessment-page="show"] .fra-factual__values .ipi-keyvals__row {
  display: grid;
  grid-template-columns: minmax(0, 44%) minmax(0, 1fr);
  gap: 4px 14px;
  align-items: baseline;
}

[data-fra-assessment-page="show"] .fra-factual__values .ipi-keyvals__value {
  text-align: left;
  max-width: none;
}

/* ⚠ AN ABSENT ANSWER MUST NOT COMPETE WITH A REAL ONE. "Not recorded" appears 303
   times on this record and was rendering at the same size and weight as the answers,
   so a section of mostly-unanswered fields read as a wall of content. It is the
   absence of a fact; it should be legible and quiet.
   ⛔ `--ipi-ink-3`, NOT `--ipi-ink-4`, AND THAT IS A CONTRAST FLOOR RATHER THAN A
   PREFERENCE. `--ipi-ink-4` (#9a9188) is toned against `--ipi-bg-card` (white), where
   it measures 3.10:1 — already under AA. These sit on `.fra-block`'s inset
   (rgb(242,239,233)), where it collapses to **2.70:1**, under AA-LARGE as well. That
   is 80 unreadable values in section 1 alone. `--ipi-ink-3` measures 5.10:1 on the
   same ground and is still plainly quieter than the ink-2 answers at 9.87:1.
   ⚠ Quiet is a JOB FOR WEIGHT AND STYLE HERE, not for lightness — the italic and the
   400 weight already say "this is an absence". */
/* ⚠ THE MARKER CLASS IS `.is-unanswered` — see the rule further up this file, which
   is where the colour actually comes from. An earlier pass here guessed at
   `[data-fra-unanswered]` and `:has(em)`, neither of which the markup emits, so it
   matched NOTHING and the 300 failing values were reported as fixed when they were
   untouched. Only the weight belongs here; the colour is set at the source. */
[data-fra-assessment-page="show"] .fra-field .ipi-keyvals__value.is-unanswered {
  font-weight: 400;
}

/* 🔴 THE AUTHORING HELP IS SUBORDINATE ON A RECORD, NOT PEER TO THE ANSWER.
   35 help paragraphs render on this page, and at 12.5px in `--ipi-ink-2` they were
   within a hair of the answers' own colour — so the guidance for the person FILLING
   the form in outweighed the answers for the person READING it, and roughly doubled
   the length of every block.
   ⚠ Kept, not hidden: some of it carries real meaning ("Which fire safety regime
   applies…"). It moves into the answer column, at the muted tone, so it hangs under
   the answer it explains instead of breaking the scan line. */
[data-fra-assessment-page="show"] .fra-field__help {
  margin: 2px 0 0 44%;
  padding-left: 14px;
  font-size: 11.5px;
  line-height: 1.5;
  /* ⛔ `--ipi-ink-3`. See the contrast note above: `--ipi-ink-4` on the block inset is
     2.70:1, which fails AA-large, and 11.5px is the SMALLEST text on the page — the
     last thing that can afford to be the faintest as well. */
  color: var(--ipi-ink-3);
}

@media (max-width: 900px) {
  [data-fra-assessment-page="show"] .fra-factual__values .ipi-keyvals__row {
    grid-template-columns: minmax(0, 1fr);
  }

  [data-fra-assessment-page="show"] .fra-field__help {
    margin-left: 0;
    padding-left: 0;
  }
}

@media (max-width: 900px) {
  [data-fra-assessment-page="show"] .fra-block__body {
    grid-template-columns: minmax(0, 1fr);
  }
}

.fra-block__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.fra-block__mirrors {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}

.fra-mirror {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  padding: 8px 10px;
  background: var(--ipi-bg-inset);
  border-radius: 8px;
}

.fra-mirror__label {
  font-size: 12.5px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-mirror__value {
  font-size: 13px;
  color: var(--ipi-ink-2);
  overflow-wrap: anywhere;
}

.fra-mirror__link {
  /* ⚠ A 44px TARGET — it measured 18px tall, a line of 11.5px text (2026-09-23). */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 12.5px;
  color: var(--ipi-blue);
}

/* ------------------------------------------------- repeatable groups (item 6.5)

   ⛔ ONE BLOCK PER INSTANCE, NEVER A TABLE ROW. Every one of these groups
   carries free text, and a wrapping narrative in a table cell is this project's
   shipped-defect mechanism: the PDF row budget packs whole blocks and cannot
   split one, so an oversized row gets exactly one sheet and runs off it. */

.fra-group {
  padding: 12px;
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
}

.fra-group__title {
  margin: 0 0 8px;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ipi-ink);
}

/* ⛔ `--ipi-ink-3` for the same contrast reason as `.is-unanswered` above — this is the
   other place the module prints an absence, and it was `--ipi-ink-4` at 3.10:1 on
   white, under AA. The italic carries the "nothing here" meaning; the lightness was
   only making it hard to read. */
.fra-group__empty {
  margin: 0;
  font-size: 13px;
  font-style: italic;
  color: var(--ipi-ink-3);
}

.fra-group__instance {
  display: grid;
  grid-template-columns: 30px minmax(0, 1fr);
  gap: 10px;
  padding: 10px 0;
  border-top: 1px solid var(--ipi-rule);
}

.fra-group__instance > .fra-field,
.fra-group__instance > .fra-control {
  grid-column: 2;
}

.fra-group__instance-number {
  grid-column: 1;
  grid-row: 1 / -1;
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ipi-ink-3);
}

.fra-group__instance--new {
  background: var(--ipi-bg-inset);
  border-radius: 8px;
}

/* ------------------------------------------------------------------- actions */

.fra-action-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 10px;
  padding: 12px 0;
  border-top: 1px solid var(--ipi-rule);
}

.fra-action-row--new {
  background: var(--ipi-bg-inset);
  border-radius: 8px;
  padding: 12px;
}

@media (max-width: 900px) {
  .fra-setup__types,
  .fra-control__judgement-extras,
  .fra-action-row {
    grid-template-columns: minmax(0, 1fr);
  }

  .fra-group__instance {
    grid-template-columns: 24px minmax(0, 1fr);
  }
}

/* ───────────────────────────── item 7.6 — has anybody looked at this field? ──
 *
 * 🔴 A DIFFERENT FACT FROM "IS IT BLANK", AND THE PAGE HAS TO SAY WHICH. "Not
 * recorded" is ambiguous between *considered, and there was nothing to record*
 * and *never opened that step*; only the second is a defect, and issue is refused
 * while a required field carries no marker.
 *
 * 🔴 ITEM 5.7 REPLACED THE ACCENT BAR WITH A CHIP. This used to be a 3px accent
 * border down the left of every card whose state was "no" — which on a fresh
 * assessment is ONE HUNDRED PER CENT of them. Six hundred warning bars is not a
 * signal, it is the page's background colour, and the one thing the paint had to
 * say (which handful nobody has opened) was the thing it could not say. The chip
 * is per card and quiet; the SHAPE of the answer is carried by the counters on
 * the nav pills and the stats strip, which is where a reader can act on it.
 *
 * ⚠ THE TWO STATES DIFFER IN TEXT AND IN FILL, so a render check measures them
 * apart with a computed style rather than by reading back the attribute it is
 * supposed to be checking.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 */
.fra-field__assessed {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-field__assessed-chip {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 650;
  border-radius: 999px;
}

.fra-field[data-fra-assessed-state="no"] .fra-field__assessed-chip {
  color: var(--ipi-ink-3);
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
}

.fra-field[data-fra-assessed-state="no"] .fra-field__assessed-chip::after {
  content: "Not yet examined";
}

.fra-field[data-fra-assessed-state="yes"] .fra-field__assessed-chip {
  color: var(--ipi-green);
  background: var(--ipi-green-bg);
  border: 1px solid var(--ipi-green);
}

.fra-field[data-fra-assessed-state="yes"] .fra-field__assessed-chip::after {
  content: "Examined";
}

/* ⚠ ITEM 5.8 — the affordance for "considered, and there was nothing to
   record". 44px of target, because on this form it is pressed as often as any
   control on the card. */
.fra-field__considered {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  font-size: 12.5px;
  color: var(--ipi-ink-2);
  cursor: pointer;
}

.fra-field__considered input[disabled] {
  cursor: default;
}

.fra-field__considered:has(input[disabled]) {
  color: var(--ipi-ink-4);
  cursor: default;
}

/* ─────────────────────────────────── the offline outbox, on the capture form ──
 *
 * ⚠ SERVER-RENDERED HIDDEN AND UNHIDDEN BY THE MODULE, so there is no state where a
 * count flashes before anything has read the queue. `[hidden]` in this app is
 * `!important`, so nothing here may set `display` on the panel itself.
 *
 * ⚠ SITS INSIDE `.ipi-list-area`, above the form — anything between the stats strip
 * and the list area inherits `.inspections-proto`s full-bleed negative inline margin
 * and renders edge-to-edge.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 */
.fra-outbox {
  margin-bottom: 12px;
  border-left: 3px solid var(--ipi-accent, #b45309);
}

.fra-outbox__count {
  margin: 0;
  font-size: 13px;
  font-weight: 650;
  color: var(--ipi-ink-2);
}

/* The recovery export — register §2.5. A last resort, so it sits under the notes rather
   than beside the count, and its status line collapses when it has nothing to say. */
.fra-outbox__actions {
  margin: 10px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.fra-outbox__status {
  margin: 8px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

.fra-outbox__status:empty {
  display: none;
}

.fra-outbox__notes {
  margin: 8px 0 0;
  padding-left: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 13px;
  color: var(--ipi-ink-2);
}

.fra-outbox__notes:empty {
  display: none;
}

/* "Put this right", "Retry" and "Discard" (2026-09-28): one row. With a single note they join the
   panel's own row before "Download a recovery file"; with several, each note has this row. */
.fra-outbox__note-text {
  margin: 0;
}

.fra-outbox__note-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.fra-outbox__label-short {
  display: none;
}

/* ⚠ ONE ROW ON A PHONE TOO (owner, 2026-09-28): "Put this right · Discard · Download a recovery file"
   needs 362px and a 390px phone gives the panel 306, so the export button says "Recovery file"
   there (its aria-label keeps the full words) and the buttons share the row. */
@media (max-width: 900px) {
  .fra-outbox__label-long {
    display: none;
  }

  .fra-outbox__label-short {
    display: inline;
  }

  .fra-outbox__actions > .btn,
  .fra-outbox__note-actions > .btn {
    flex: 1 1 auto;
    justify-content: center;
  }
}

/* ─────────────────────────────────────── the risk matrix — Stage 8, items 8.2–8.4
 *
 * ⛔ NO RATING IS COLOUR-CODED, ANYWHERE IN THIS BLOCK, AND THAT IS THE RULE RATHER
 * THAN AN OMISSION.
 *
 * ⚠ ONE SCOPED EXCEPTION, AND IT IS THE CONFIGURATION SCREEN ONLY. The EDITOR grid on
 * `?page=fra-risk-matrix` tints its 25 cells on a rising ramp so an assessor can see at
 * a glance whether their own grid rises monotonically. Added greyscale on 2026-09-08 and
 * given hue on 2026-09-09, both by owner decision, the second scoped to the editor when
 * the owner was asked directly where colour should reach. It is scoped `.fra-matrix-page`
 * and does not reach this block, the assessment render or the client portal. Everything
 * below still applies wherever a rating is REPORTED. See "THE EDITOR GRID'S RATING
 * COLOURS" at the end of this file. In THIS block there is deliberately no
 * `[data-fra-matrix-cell-rating…]` selector, no red/amber/green, no severity chip and no
 * numeric badge. The one
 * calibration report that renders its rating as a ticked verdict rated the building
 * Substantial and then made all nineteen of its actions Priority 1 — the badge became
 * the message and the ranking underneath it stopped carrying information. The rating
 * renders as its word; the action plan carries the priorities.
 *
 * ⚠ THE ONLY CELL THAT DIFFERS IS THE SELECTED ONE, and it differs by WEIGHT and a
 * neutral inset ring rather than by hue — it is saying "this pair was chosen", not
 * "this is how bad it is". Ink-coloured on purpose so it cannot be read as a severity.
 *
 * ⚠ THE GRID SCROLLS RATHER THAN SQUEEZING AT 360px. Five columns of prose labels in
 * a 360px box is unreadable at any font size, and `.ipi-list-area` is where the scroll
 * has to be contained — `overflow-x` on `.page-section` or `.inspections-proto` clips
 * the page header.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 */
.fra-matrix__name {
  margin: 0 0 10px;
  font-size: 14px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-matrix__size {
  margin-left: 8px;
  font-size: 12px;
  font-weight: 550;
  color: var(--ipi-ink-3);
}

.fra-matrix__axis {
  margin: 0 0 14px;
}

.fra-matrix__axis-title {
  margin: 0 0 4px;
  font-family: var(--font-display, inherit);
  font-size: 13px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-matrix__axis-description {
  margin: 0 0 8px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

.fra-matrix__axis-values {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.fra-matrix__axis-value {
  padding: 8px 10px;
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
  background: var(--ipi-bg-inset);
}

.fra-matrix__axis-label {
  margin: 0;
  font-size: 13px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-matrix__axis-definition {
  margin: 2px 0 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

/* ⚠ The wrapper is what scrolls, not the table, so the sticky row header keeps its
 * box. `display: block` on the table itself would drop the row/column association
 * screen readers rely on. */
.fra-matrix__grid {
  width: 100%;
  margin: 12px 0 0;
  border-collapse: collapse;
  font-size: 13px;
}

.fra-matrix__caption {
  caption-side: top;
  margin-bottom: 8px;
  text-align: left;
  font-size: 12px;
  color: var(--ipi-ink-3);
}

.fra-matrix__grid th,
.fra-matrix__grid td {
  padding: 8px 10px;
  border: 1px solid var(--ipi-rule);
  text-align: left;
  vertical-align: top;
  color: var(--ipi-ink-2);
}

.fra-matrix__grid thead th {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ipi-ink-3);
}

.fra-matrix__grid tbody th {
  font-size: 12px;
  font-weight: 650;
  color: var(--ipi-ink-2);
}

.fra-matrix__cell.is-selected {
  font-weight: 700;
  color: var(--ipi-ink);
  box-shadow: inset 0 0 0 2px var(--ipi-ink-4);
}

.fra-matrix__outcome {
  margin-top: 14px;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.fra-matrix__outcome > p {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.fra-matrix__outcome-label {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ipi-ink-3);
}

.fra-matrix__outcome-value {
  font-family: var(--font-display, inherit);
  font-size: 16px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-matrix__override {
  flex: 1 1 100%;
}

.fra-matrix__override-text {
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

/* ───────────────────────────────────────────── the risk matrix settings page ── */
.fra-matrix-rules__list {
  margin: 0;
  padding-left: 18px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

/* The picker, at the foot. ⚠ It briefly moved to a tab strip under the header and the
   owner removed it from there on 2026-09-09 — two cards of chrome between the reader and
   the artefact, on every visit, for a choice made about once a year. Back to a list, in
   the row of things you consult rather than edit. */
.fra-matrix-list__items {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.fra-matrix-list__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
  font-size: 13px;
  color: var(--ipi-ink-2);
}

.fra-matrix-list__item.is-current {
  border-color: var(--ipi-accent-border);
  background: var(--ipi-accent-bg);
}

.fra-matrix-list__size,
.fra-matrix-list__state {
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* 🔴 STAGE 1, ITEM 1.3 — `.fra-matrix-form__field` IS GONE. It was the fourth
   skin of `.ipi-field`, and the only one of the four that styled the LABEL and left
   the control on browser defaults — so the matrix editor was the one FRA form whose
   inputs did not match any other form in the app. Its fields are `.ipi-field`s now,
   laid out by `.ipi-form-grid`, which also supplies the 14px gap the fork's
   `margin-bottom` was standing in for. */

/* ⛔ `.fra-matrix-form__axis-value` and `.fra-matrix-form__check` WENT WITH THE AXIS
   CARDS on 2026-09-09. The axis values are the grid's own headings now, and the
   make-default checkbox is `.fra-matrix-actionbar__check` in the sticky bar. */

/* ⚠ The grid is the one thing on either page that cannot be made to fit a 360px box,
 * so its WRAPPER scrolls. ⛔ Never `display: block` on the `<table>` itself — that is
 * the usual shortcut and it drops the row/column association a screen reader needs to
 * say which likelihood and which consequence a cell belongs to. And the overflow lives
 * here rather than on `.page-section` or `.inspections-proto`, either of which clips
 * the page header.
 *
 * ⚠ `min-width: 0` on the wrapper because it is a flex/grid child further up the
 * chain, and without it the scroll container is sized by its content and never
 * scrolls. */
.fra-matrix__grid-scroll {
  min-width: 0;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

@media (max-width: 900px) {
  .fra-matrix__grid td,
  .fra-matrix__grid th {
    min-width: 116px;
  }
}

/* Phone/tablet grids (table-cards plan 8.7). The matrix grid carries its own frame, so the white
   panel around it would be a card around a card: the panel drops its surface below 900. */
@media (max-width: 900px) {
  html:not(.rc-wide) .fra-page :is(.fra-matrix-gridpanel, .fra-matrix[data-fra-matrix-panel]):has([data-rc-gridframe]) {
    padding-left: 0;
    padding-right: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
  }

  /* The editor's rating cells hold a 16px select ("Substantial" is 84px, plus 56px of padding and
     arrow) and its headings hold a label input ("Moderate harm" is 135px), so each rating column is
     at least 162px and the grid scrolls sideways under its hint instead of cutting the words. */
  html:not(.rc-wide) .fra-matrix-page table.fra-matrix__grid--editor[data-rc="grid"] > :is(thead, tbody) > tr > :not(:first-child) {
    min-width: 162px;
  }

  html:not(.rc-wide) .fra-matrix-page table.fra-matrix__grid--editor[data-rc="grid"] :is(select, .fra-matrix-head__input) {
    width: 100%;
  }
}

/* ==========================================================================
   Stage 9 — the action plan
   ========================================================================== */

/* The band-to-timescale mapping, printed where the band is chosen. A priority
   band with no stated timescale is a word, and four assessors read "Medium"
   four ways. ⚠ Nothing here derives a date from a band — this is a printed
   recommendation, not a computation. */
.fra-actions__bands {
  margin: 0 0 14px;
  padding: 12px 14px;
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule-h);
  border-radius: 10px;
}

.fra-actions__preamble {
  margin: 0 0 10px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

.fra-actions__band-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 8px 14px;
  margin: 0;
}

.fra-actions__band {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.fra-actions__band dt {
  font-size: 12px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-actions__band dd {
  margin: 0;
  font-size: 12px;
  color: var(--ipi-ink-3);
}

.fra-actions__band-timescale {
  display: block;
  font-weight: 600;
  color: var(--ipi-ink-2);
}

.fra-actions__band-meaning {
  display: block;
  line-height: 1.45;
}

/* ⚠ Spans both columns of the two-up grid the action row falls into, because a
   finding label and an enumeration are both long strings and a 220px column
   truncates them into ambiguity.
   ⚠ Its base is `.ipi-field` now that `.fra-control__sub` is retired (item 1.3),
   so the modifier stands alone rather than hanging off a block that is gone. */
.fra-control__wide {
  grid-column: 1 / -1;
}

/* ---------------------------------------------------------------- the plan */

/* 2026-09-28: a to-do before issue, in the amber the rest of the page uses for "still to do" —
   it read as an error in red. */
.fra-action-plan__gaps {
  margin: 0 0 16px;
  padding: 12px 14px;
  background: var(--ipi-accent-bg);
  border: 1px solid rgba(196, 149, 16, 0.3);
  border-radius: 10px;
}

.fra-action-plan__gaps-title {
  margin: 0 0 8px;
  font-size: 13px;
  font-weight: 650;
  color: var(--accent-ink, #7a5c05);
}

.fra-page a.fra-action-plan__gap-go {
  display: inline-block;
  margin-top: 4px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-action-plan__notice {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin: 0 0 14px;
  padding: 10px 14px;
  border: 1px solid var(--ipi-rule-h);
  border-radius: 10px;
  background: var(--ipi-bg-inset);
}

.fra-action-plan__notice p {
  margin: 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ipi-ink-2);
}

.fra-action-plan__after-issue {
  margin: 8px 0 0 46px;
  font-size: 12px;
  font-style: italic;
  color: var(--ipi-ink-4);
}

@media (max-width: 900px) {
  .fra-action-plan__notice {
    flex-direction: column;
    align-items: stretch;
  }

  .fra-action-plan__notice .btn {
    min-height: 44px;
    justify-content: center;
  }

  .fra-action-plan__after-issue {
    margin-left: 0;
  }
}

.fra-action-plan__gap-list {
  margin: 0;
  padding-left: 18px;
}

.fra-action-plan__gap {
  margin-bottom: 8px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

.fra-action-plan__gap-label {
  display: block;
  font-weight: 620;
  color: var(--ipi-ink);
}

.fra-action-plan__gap-finding {
  display: block;
  font-size: 11.5px;
  color: var(--ipi-ink-3);
}

.fra-action-plan__gap-missing {
  display: block;
}

.fra-action-plan__empty {
  margin: 0;
  font-size: 13px;
  font-style: italic;
  color: var(--ipi-ink-3);
}

/* ---- The action plan tab (redesigned 2026-09-28, owner review) -----------------------------
   A summary strip, the non-empty priority bands, and one compact card per action. */
.fra-action-plan__heading {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* The "?" beside a heading: a native disclosure, its panel under the heading. */
.fra-help-q {
  position: relative;
}

.fra-help-q > summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 1px solid var(--ipi-rule-h);
  background: var(--ipi-card, #fff);
  color: var(--ipi-ink-3);
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  list-style: none;
}

.fra-help-q > summary::-webkit-details-marker {
  display: none;
}

.fra-page .ipi-panel .fra-help-q > summary {
  position: static;
  padding: 0;
  margin: 0;
}

.fra-page .ipi-panel .fra-help-q > summary::after {
  content: none;
  display: none;
}

.fra-help-q[open] > summary {
  background: var(--ipi-ink);
  border-color: var(--ipi-ink);
  color: var(--ipi-bg-card, #fff);
}

.fra-help-q__panel {
  position: absolute;
  z-index: 20;
  top: calc(100% + 8px);
  left: 0;
  width: min(520px, calc(100vw - 48px));
  padding: 14px 16px;
  border: 1px solid var(--ipi-rule-h);
  border-radius: 12px;
  background: var(--ipi-card, #fff);
  box-shadow: var(--ipi-shadow-md, 0 12px 32px rgba(0, 0, 0, 0.12));
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

.fra-help-q__lede {
  margin: 0 0 10px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

/* A glossary as a plain list — the assessment page carries no `<dl>`. */
.fra-help-q__bands {
  display: grid;
  gap: 2px 0;
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-help-q__bands > li {
  display: grid;
  gap: 2px;
  margin: 0;
}

.fra-help-q__term {
  display: block;
  margin-top: 6px;
  font-size: 12.5px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-help-q__term span {
  margin-left: 6px;
  font-weight: 500;
  color: var(--ipi-ink-3);
}

.fra-help-q__meaning {
  display: block;
  margin: 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ipi-ink-3);
}

.fra-action-plan__summary {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  margin: 0 0 16px;
  padding: 0;
  list-style: none;
}

.fra-action-plan__summary li {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 12px;
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
  background: var(--ipi-bg, #faf8f4);
}

.fra-action-plan__summary-value {
  font-family: var(--font-display, inherit);
  font-size: 20px;
  font-weight: 600;
  line-height: 1.1;
  color: var(--ipi-ink);
  font-variant-numeric: tabular-nums;
}

.fra-action-plan__summary-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ipi-ink-4);
}

.fra-action-plan__summary li.is-bad {
  border-color: var(--ipi-red-border, rgba(176, 48, 48, 0.25));
  background: var(--ipi-red-bg, rgba(176, 48, 48, 0.06));
}

.fra-action-plan__summary li.is-bad .fra-action-plan__summary-value {
  color: var(--ipi-red);
}

.fra-action-plan__band + .fra-action-plan__band {
  margin-top: 18px;
}

.fra-action-plan__band-title {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 0 8px;
  font-size: 13px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-action-plan__band-name {
  padding: 2px 10px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 650;
}

.fra-action-plan__band-name--bad {
  background: var(--ipi-red-bg, rgba(176, 48, 48, 0.08));
  color: var(--ipi-red);
}

.fra-action-plan__band-name--warn {
  background: var(--ipi-accent-bg);
  color: var(--accent-ink, #7a5c05);
}

.fra-action-plan__band-name--info {
  background: var(--ipi-blue-bg);
  color: var(--ipi-blue);
}

.fra-action-plan__band-name--neutral {
  background: var(--ipi-bg-inset);
  color: var(--ipi-ink-2);
}

.fra-action-plan__band-timescale {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--ipi-ink-3);
}

.fra-action-plan__band-count {
  margin-left: auto;
  min-width: 22px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--ipi-bg-inset);
  color: var(--ipi-ink-3);
  font-size: 11.5px;
  text-align: center;
}

.fra-action-plan__band-meaning {
  margin: -2px 0 8px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ipi-red);
}

.fra-action-plan__actions {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-action-plan__action {
  padding: 12px 14px;
  border: 1px solid var(--ipi-rule);
  border-radius: 12px;
  background: var(--ipi-card, #fff);
}

.fra-action-plan__action.is-overdue {
  border-left: 3px solid var(--ipi-red);
}

.fra-action-plan__action-top {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  gap: 10px;
}

.fra-action-plan__ref {
  min-width: 36px;
  padding: 2px 7px;
  border-radius: 6px;
  background: var(--ipi-bg-inset);
  color: var(--ipi-ink-2);
  font-size: 12px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.fra-action-plan__action-description {
  margin: 0;
  font-size: 14px;
  font-weight: 560;
  line-height: 1.45;
  color: var(--ipi-ink);
  overflow-wrap: anywhere;
}

.fra-action-plan__action.is-closed .fra-action-plan__action-description {
  color: var(--ipi-ink-3);
}

.fra-action-plan__status {
  white-space: nowrap;
}

.fra-action-plan__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 22px;
  margin: 8px 0 0;
  padding: 0 0 0 46px;
  list-style: none;
}

.fra-action-plan__facts li {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.fra-action-plan__fact-label {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ipi-ink-4);
}

.fra-action-plan__fact-value {
  font-size: 13px;
  color: var(--ipi-ink-2);
  overflow-wrap: anywhere;
}

.fra-action-plan__facts li.is-bad .fra-action-plan__fact-value {
  color: var(--ipi-red);
  font-weight: 600;
}

.fra-action-plan__action-justification {
  margin: 8px 0 0 46px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ipi-ink-3);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.fra-action-plan__action-justification span {
  font-weight: 600;
  color: var(--ipi-ink-2);
}

.fra-action-plan__action :is(.fra-action-plan__claims, .fra-action-plan__track) {
  margin-left: 46px;
}

@media (max-width: 600px) {
  .fra-action-plan__summary {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .fra-action-plan__action-top {
    grid-template-columns: auto minmax(0, 1fr);
  }

  .fra-action-plan__status {
    grid-column: 2;
    justify-self: start;
  }
}

@media (max-width: 900px) {
  .fra-action-plan__facts,
  .fra-action-plan__action-justification,
  .fra-action-plan__action :is(.fra-action-plan__claims, .fra-action-plan__track) {
    margin-left: 0;
    padding-left: 0;
  }

  .fra-help-q {
    position: static;
  }

  .fra-help-q__panel {
    left: 16px;
    right: 16px;
    width: auto;
    top: auto;
    margin-top: 8px;
  }
}

/* ==========================================================================
   POST-ISSUE TRACKING AND CLIENT CLAIMS — close-out Stage 3 item 3.5, Stage 4 item 4.5
   ==========================================================================

   ⛔ NO NEW PRIMITIVE AND NO NEW TOKEN. The forms below are `.ipi-field` labels and
   `.btn`/`.btn-primary`/`.btn-secondary`, which is what every other write surface in
   this module uses; what is here is only the layout that holds them inside an action
   card. Three inventions were caught and backed out of the actions list on 2026-09-20
   for exactly this reason.

   ⚠ EVERY COLOUR IS AN --ipi-* TOKEN. `fra-shell.css` owns them and the high-contrast
   and forced-colours blocks there redefine them, so a literal here would survive both. */

.fra-action-plan__claims {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 8px;
}

/* The claim is a client's assertion, so it reads as a quotation of somebody else rather
   than as part of the record: inset background, and the rule on the left is the same
   gesture `.fra-versions__frozen` uses for "this is fixed". */
.fra-action-plan__claim {
  padding: 10px 12px;
  border-left: 3px solid var(--ipi-blue);
  border-radius: 0 8px 8px 0;
  background: var(--ipi-bg-inset);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

/* ⚠ A PENDING CLAIM IS THE ONE THAT WANTS SOMETHING FROM THE READER, so it carries the
   accent border rather than the neutral one. The badge text says which state it is in —
   the colour is not the only carrier. */
.fra-action-plan__claim[data-fra-action-claim-status="pending"] {
  border-left-color: var(--ipi-accent);
  background: var(--ipi-accent-bg);
}

.fra-action-plan__claim[data-fra-action-claim-status="rejected"] {
  border-left-color: var(--ipi-red);
  background: var(--ipi-red-bg);
}

.fra-action-plan__claim-head {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

.fra-action-plan__claim-badge {
  display: inline-block;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--ipi-bg-card);
  color: var(--ipi-ink-3);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.fra-action-plan__claim-note,
.fra-action-plan__claim-verdict,
.fra-action-plan__claim-photo {
  margin: 6px 0 0;
}

.fra-action-plan__claim-verdict {
  color: var(--ipi-ink-3);
}

.fra-action-plan__claim-photo a {
  color: var(--ipi-blue);
  font-weight: 620;
  text-decoration: none;
}

.fra-action-plan__claim-photo a:hover,
.fra-action-plan__claim-photo a:focus-visible {
  text-decoration: underline;
}

.fra-action-plan__claim-form {
  margin: 8px 0 0;
  display: grid;
  gap: 8px;
}

.fra-action-plan__claim-actions,
.fra-action-plan__track-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* ⚠ A NATIVE `<details>`. It works with no JavaScript, which on a module whose capture
   side is used on a phone underground is the difference between a control and a
   decoration. The marker is left alone rather than restyled — the browser's own
   disclosure triangle is the affordance every user already knows. */
.fra-action-plan__track {
  margin: 10px 0 0;
  padding: 0;
  border-top: 1px solid var(--ipi-rule);
}

/* "Record progress" is a button (owner review, 2026-09-28); it was a small blue link. */
.fra-page .ipi-panel .fra-action-plan__track-summary {
  width: fit-content;
  max-width: 100%;
  margin-top: 10px;
  list-style: none;
}

.fra-action-plan__track-summary::-webkit-details-marker {
  display: none;
}

/* A `<summary>` dressed as a button (`.btn` — "Record a check…", "Send for check…",
   "Return for correction…", "Withhold from the client…", "Attach a document") is a button:
   its "…" already says more follows, and the shell's chevron was squeezed against the label
   inside the button's own padding. Narrow opt-out; the shell rule is untouched. */
.fra-page .ipi-panel summary.btn--compact {
  padding-right: 12px;
}

.fra-page .ipi-panel summary.btn::after {
  content: none;
}

/* One column at every width. The card it sits in is already narrow inside the record
   pane at 1440px, and two columns of date and select inputs wrap into a shape where the
   labels no longer line up with their controls. */
.fra-action-plan__track-form {
  margin: 10px 0 0;
  display: grid;
  gap: 10px;
}

.fra-action-plan__track-note {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ipi-ink-4);
}

/* ==========================================================================
   ISSUE, IMMUTABILITY AND THE SUPERSEDE CHAIN — Stage 11
   ========================================================================== */

/* The Issue and Revise controls live in `.ipi-ph-right`, which is a wrapping flex
   row with an 8px gap. A <form> is an ordinary flex item there, so it needs only
   its default margin removed — `display: contents` would work too and is what the
   drawers use, but it flattens the element out of the box tree and there is nothing
   here that needs flattening. The BUTTON keeps `.ipi-action-btn` verbatim so the two
   controls are visually identical to the four anchors beside them. */
.fra-inline-form {
  display: inline-flex;
  margin: 0;
}

.fra-versions__frozen {
  margin: 0 0 12px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-accent-bg);
  border: 1px solid var(--ipi-accent-border);
  border-radius: 8px;
}

.fra-versions__list,
.fra-versions__chain {
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One row per version, wrapping rather than truncating. At 360px the content-hash
   chip drops to its own line instead of pushing the row past the viewport — the
   assessment page is one of the surfaces the 360px budget is tightest on, and a
   nowrap row of three chips is exactly what overflows it. */
.fra-versions__item,
.fra-versions__link {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-versions__item:last-child,
.fra-versions__link:last-child {
  border-bottom: 0;
}

.fra-versions__no,
.fra-versions__link-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-versions__issued,
.fra-versions__link-status,
.fra-versions__link-date {
  font-size: 12px;
  color: var(--ipi-ink-2);
}

/* Monospace so two hashes line up character for character when a reader compares
   them, which is the only thing a truncated hash is for. `word-break` because a
   12-character token has no break opportunity of its own. */
.fra-versions__hash {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11px;
  color: var(--ipi-ink-3); /* R1-32: -4 is 3.10:1 on white at 11px */
  word-break: break-all;
}

.fra-versions__subtitle {
  margin: 16px 0 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-versions__link.is-current .fra-versions__link-label {
  color: var(--ipi-accent, var(--ipi-ink));
}

/* ==========================================================================
   REVIEW CYCLES AND THE THREE CLOCKS — Stage 12
   ========================================================================== */

/* One row per clock, wrapping rather than truncating. Each clock carries a label,
   a due date, a basis sentence and sometimes a rationale, and at 360px those have
   to stack: `flex-wrap` with a column-friendly basis is what keeps the panel
   inside the viewport on the narrowest phone this app supports. The basis takes a
   full row of its own at every width because it is a sentence, not a chip. */
.fra-clocks__list,
.fra-clocks__record-only-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-clocks__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-clocks__item:last-child {
  border-bottom: 0;
}

.fra-clocks__label {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-clocks__due {
  flex: 0 0 auto;
  font-size: 12px;
  font-weight: 600;
  color: var(--ipi-ink-2);
}

/* The basis is the sentence that says WHY this clock is what it is — statutory and
   annual, or the assessor's judgement with no prescribed period. It always takes a
   row of its own (`flex-basis: 100%`) so it can never be read as a continuation of
   the due date beside it. */
.fra-clocks__basis,
.fra-clocks__note {
  flex: 1 1 100%;
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

.fra-clocks__note {
  color: var(--ipi-ink-2);
}

/* A clock that is due now reads red; one that is merely not recorded does not.
   "Not recorded" is a legitimate state on a premises with no PCFRA duty, and
   colouring it as a failure would tell an assessor to fix something that is not
   broken. */
.fra-clocks__item[data-fra-clock-state="overdue"] .fra-clocks__due {
  color: var(--ipi-red);
}

.fra-clocks__item[data-fra-clock-state="not_recorded"] .fra-clocks__due,
.fra-clocks__item[data-fra-clock-state="not_applicable"] .fra-clocks__due {
  color: var(--ipi-ink-4);
}

/* The fourth date and the fifth obligation sit below the three clocks with a rule
   above them, because they are NOT clocks and the separation is the point. */
.fra-clocks__newfra {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  margin: 12px 0 0;
  padding-top: 12px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-clocks__obligation {
  margin: 12px 0 0;
  padding: 10px 12px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-accent-bg);
  border: 1px solid var(--ipi-accent-border);
  border-radius: 8px;
}

.fra-clocks__record-only {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-clocks__subtitle {
  margin: 0 0 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-clocks__record-only-list li {
  padding: 4px 0;
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* The chain metadata — item 3.5. It was a `<dl>` with its own dt/dd type scale, which
   made it FRA's fifth label/value language on a page whose sibling panels already used
   `.ipi-keyvals`. It is an `.ipi-keyvals` now and needs no type of its own; the class
   survives only as the panel's addressable hook. */
.fra-review-chain__list {
  margin: 0;
}

.fra-carry-forward__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-carry-forward__item {
  padding: 12px 0;
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-carry-forward__item:last-child {
  border-bottom: 0;
}

.fra-carry-forward__description {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink);
}

.fra-carry-forward__meta,
.fra-carry-forward__resolved {
  margin: 4px 0 0;
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* An item still awaiting a decision reads amber, which is the same accent the
   frozen note uses. It is not red: nothing is wrong yet, the review simply has not
   said what became of it. */
.fra-carry-forward__item[data-fra-carry-resolved="0"] {
  border-left: 3px solid var(--ipi-accent-border);
  padding-left: 10px;
}

/* The three decisions wrap onto their own lines at 360px rather than shrinking
   the labels, and the note box always takes a full row. */
.fra-carry-forward__decision {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 16px;
  margin-top: 8px;
}

.fra-carry-forward__choice {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--ipi-ink-2);
}

/* ⚠ ITEM 1.3 — `.ipi-field` OWNS `display` HERE. This was a `display: block` label
   wrapping a fifth hand-rolled textarea skin; the label is an `.ipi-field` now, so
   all this adds is the full-row flex basis its parent row needs. Restoring
   `display: block` would tie with the primitive on specificity, win on load order,
   and quietly drop the column layout and its 5px gap. */
.fra-carry-forward__note {
  flex: 1 1 100%;
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* The attestation conflict. Stated, never resolved — see the view's docblock. */
.fra-carry-forward__conflict,
.fra-review-blockers__list li {
  margin: 0 0 12px;
  padding: 10px 12px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-red-bg);
  border: 1px solid var(--ipi-red-border);
  border-radius: 8px;
}

.fra-review-blockers__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-review-blockers__list li:last-child {
  margin-bottom: 0;
}

.fra-review-blockers__label {
  display: block;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-review-blockers__detail {
  display: block;
  margin-top: 2px;
}

.fra-review__save {
  margin-top: 16px;
}
/* ==========================================================================
   Stage 3 — composition: the tab bar, the tab panel, and the header status chip.
   ========================================================================== */

/* ⛔ THE PANEL WRAPPER RESTATES THE LIST AREA'S OWN RHYTHM, and this is not optional.
   `.ipi-list-area` is `display:flex; flex-direction:column; gap:16px` and its ONE flex
   child is now this wrapper, so without a gap here the two or three panels inside the
   Record tab would sit flush against each other — item 3.6's 16px would apply to a
   single element and separate nothing. */
.fra-tab-panel {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

/* The title and its status chip on one baseline — item 3.4. The chip is a property of
   the record, so it reads beside the name rather than under it; it wraps below on a
   narrow phone rather than squeezing the title. */
.fra-ph-titlerow {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  min-width: 0;
}

/* The absence of an answer is a state, not a gap - item 6.9. It reads quieter than an
   answer and it is never empty.
   ⚠ RE-SCOPED BY ITEM 3.5. The value cell was `.fra-field__value`; it is
   `.ipi-keyvals__value` now, and this rule is scoped to `.fra-field` so it cannot reach
   the premises keyvals — a factual record has its own "Not recorded" wording and does
   not use this marker. */
/* ⛔ `--ipi-ink-3`, AND THIS IS A CONTRAST FLOOR, NOT A TONE PREFERENCE.
   `--ipi-ink-4` (#9a9188) is toned against `--ipi-bg-card` (white), where it measures
   3.10:1 — already under WCAG AA. These values sit on `.fra-block`'s inset
   (rgb(242,239,233)) on the record, where it collapses to 2.70:1, under AA-LARGE too.
   Audited on one assessment: 300 elements, 225 on the inset and 75 on white, all of
   them the word every unanswered field prints. `--ipi-ink-3` measures 5.10:1 on the
   inset and 6.4:1 on white.
   ⚠ IT STAYS QUIET — the italic is what says "this is an absence", and the answers
   beside it are `--ipi-ink` at 15.4:1, so the hierarchy is unchanged. Lightness was
   doing a job that style already does.
   ⚠ SHARED WITH THE CAPTURE PAGE on purpose: the same marker was 3.10:1 there. */
.fra-field .ipi-keyvals__value.is-unanswered {
  color: var(--ipi-ink-3);
  font-style: italic;
}

/* ⚠ THE PAIR IS A ROW, NOT A CARD WITHIN A CARD. `.fra-field` already supplies the
   12px padding and the border; the keyvals list inside it must not add its own top
   rule against the card edge. */
.fra-field__pair {
  margin: 0;
}

.fra-field__pair > li > .ipi-keyvals__row {
  padding-top: 0;
}

/* The guard badge rides in the label cell (item 3.5) and the label cell is
   `font-size: 12.5px`; without this the pill would inherit that and read as part of the
   question rather than as a qualifier on it. */
.fra-field__pair .ipi-keyvals__label {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 8px;
}

/* ───────────────────────────── Stage 6 — the photograph strips ──
 *
 * 🔴 ITEMS 6.1, 6.2 AND 6.5. The component is Fire Stopping's evidence zone
 * (`fire_stopping/surveys/show/_capture.php`) rendered with this module's own
 * tokens: a big tap target with a camera glyph and two CTA strings, and a
 * thumbs strip below it.
 *
 * ⛔ THE TARGET IS 44px MINIMUM AND THE ZONE IS 56. Per D-B this form is a
 * phone in a stairwell, one hand, gloves. The same reasoning is why the nav
 * pills carry a `::after` inset to 44px in item 5.3.
 *
 * ⛔ THE REAL `<input type="file">` IS VISUALLY HIDDEN, NOT `display:none`.
 * A `display:none` file input cannot be focused, so the label stops being
 * reachable by keyboard entirely — and it is the only way to attach a
 * photograph on a desktop. Clipped instead, which keeps it focusable.
 *
 * ⛔ THE TWO CTA STRINGS SWAP ON `.is-filled` AND BOTH ARE ALWAYS IN THE DOM.
 * That is the Fire Stopping device: swapping text nodes in JavaScript would
 * leave the zone saying "Photograph what you found" on a device where the
 * module graph did not load, over a strip that already holds three.
 *
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md. */
.fra-photos {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 8px;
}

.fra-photos__zone {
  position: relative;
  display: flex;
}

/* Clipped, not hidden: see the block comment above. */
.fra-photos__input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.fra-photos__action {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: 10px;
  min-height: 56px;
  padding: 10px 14px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  cursor: pointer;
  background: var(--ipi-bg-inset);
  border: 1px dashed var(--ipi-rule-h);
  border-radius: 10px;
}

.fra-photos__action:hover {
  background: var(--ipi-bg-hover);
  border-color: var(--ipi-ink-4);
}

/* ⚠ THE RING IS ON THE LABEL, DRIVEN BY THE INPUT'S FOCUS. The input is
   clipped, so its own ring would be invisible and a keyboard user would have
   no idea where they were. */
.fra-photos__input:focus-visible + .fra-photos__action {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
}

.fra-photos__icon {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  color: var(--ipi-ink-3);
  background: var(--ipi-card);
  border-radius: 8px;
}

.fra-photos__cta--filled,
.fra-photos.is-filled .fra-photos__cta--empty {
  display: none;
}

.fra-photos.is-filled .fra-photos__cta--filled {
  display: inline;
}

/* ⚠ DISABLED IS A REAL STATE HERE, NOT A GREY-OUT FOR TIDINESS — item 6.2.
   An action linked to no finding has nowhere to store evidence, and the note
   beneath says which control resolves it. */
.fra-photos__zone.is-disabled .fra-photos__action {
  cursor: not-allowed;
  opacity: 0.55;
}

.fra-photos__note {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}
/* 🔴 THE CAPTION BOX — STAGE 2 ITEM 2.1. What the assessor types here is what
   the issued report prints under the image. Before it existed the caption was the
   uploaded file name, so a statutory record carried "IMG_20260920_143512.jpg"
   beneath a photograph of a defect.

   ⛔ NOT A FOURTH INPUT SKIN. The note at the head of this file's control section
   records that `.fra-control__input` and `.fra-control__sub` were deleted for being
   two hand-rolled skins of one primitive, neither carrying the accent focus ring.
   The LITERAL VALUES below are copied from `inspections_shell.css`'s
   `.ipi-field > input` rule verbatim — 8/11 padding, 13px, radius 7, `--ipi-rule-h`
   — because this input is not a child of an `.ipi-field` and so the shell cannot
   reach it. If that rule changes, change this with it. */
.fra-photos__caption-label {
  display: block;
  margin-top: 8px;
}

.fra-photos__caption {
  font: inherit;
  font-size: 13px;
  font-weight: 400;
  color: var(--ipi-ink);
  padding: 8px 11px;
  border: 1px solid var(--ipi-rule-h);
  border-radius: 7px;
  background: var(--ipi-bg-card);
  width: 100%;
}

.fra-photos__caption:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 1px;
}

.fra-photos__caption[disabled] {
  background: var(--ipi-bg-inset);
  color: var(--ipi-ink-3);
}

/* 🔴 THE REMOVE CONTROL — STAGE 2 ITEM 2.2.

   ⚠ IT SITS ON THE THUMBNAIL, WHICH MEANS THE THUMBNAIL STOPS CLIPPING IT. The
   `.fra-photos__thumb` rule carries `overflow: hidden` to keep the image inside its
   radius, and an absolutely-positioned child of a clipping box is clipped too — so
   the thumb becomes the positioning context and the button is inset far enough to
   stay inside the corner radius rather than being allowed to overflow it.

   ⚠ 44px OF TOUCH TARGET ON A PHONE, 22px OF PAINT. The visible chip is small
   because it sits on a 72px thumbnail; the tappable area is the chip plus its
   padding, which is what stops a gloved hand on site deleting the wrong photograph
   or missing entirely. */
.fra-photos__thumb {
  position: relative;
}

.fra-photos__remove {
  position: absolute;
  top: 2px;
  right: 2px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  font-size: 15px;
  line-height: 1;
  /* ⛔ TOKENS, NOT LITERALS. FraStage1ForkRetirementTest strips var() fallbacks out of
     a copy of this file and then refuses ANY remaining hex — so `#ffffff` here is a
     failure even though it is white. `--ipi-bg-card` is the card white this shell already
     paints on, and the scrim is the ink colour at low alpha rather than an invented grey. */
  color: var(--ipi-bg-card);
  background: color-mix(in srgb, var(--ipi-ink) 62%, transparent);
  border: 0;
  border-radius: 999px;
  cursor: pointer;
}

.fra-photos__remove:hover {
  background: var(--ipi-red);
}

.fra-photos__remove:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 1px;
}

.fra-photos__remove[disabled] {
  cursor: default;
  opacity: 0.55;
}

/* ⚠ A PENDING THUMBNAIL HAS NOTHING TO REMOVE YET — it is a local object URL and
   the row does not exist, so there is no id to post. */
.fra-photos__thumb.is-pending .fra-photos__remove {
  display: none;
}

.fra-photos__thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 0;
  margin: 0;
  list-style: none;
}

/* ⚠ HIDDEN UNTIL THERE IS SOMETHING IN IT. A `<ul>` with no children still
   carries its own line box, which on a section of forty-four judgements is
   forty-four rows of nothing between the narrative and the next card.
   ⛔ NOT `:empty`, WHICH CANNOT WORK HERE. The partial renders the `<ul>` over
   several lines, so an empty one still contains a whitespace text node and
   `:empty` never matches — a rule that reads as applied and does nothing. The
   count attribute is server-rendered AND rewritten by attachPhoto(), so it is
   true before and after a photograph is chosen. */
.fra-photos[data-fra-photo-count="0"] .fra-photos__thumbs {
  display: none;
}

.fra-photos__thumb {
  width: 72px;
  height: 72px;
  overflow: hidden;
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
}

/* ⛔ `color: inherit`, AND IT IS NOT DECORATION. The global `a { color:
   var(--brand) }` paints every unstyled anchor gold, which is ~2.7:1 on white
   — the exact failure item 1.7 removed from the dashboard's in-card links, and
   the render lane caught this one being reintroduced by this stage on
   2026-09-08. The anchor wraps an image, so nothing about it LOOKS wrong; the
   check is right anyway, because a focus ring and a hover state both resolve
   off this colour. */
.fra-photos__thumb-link {
  display: block;
  width: 100%;
  height: 100%;
  color: inherit;
}

.fra-photos__thumb-link:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
}

.fra-photos__thumb-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ⚠ A PHOTOGRAPH THAT HAS NOT REACHED THE SERVER READS AS ONE THAT HAS NOT.
   Offline, that is the ordinary state for the length of a site visit, and a
   strip that looked identical either way would give an assessor no way to
   tell what is stored from what is still on the phone. */
.fra-photos__thumb.is-pending {
  border-style: dashed;
  border-color: var(--ipi-accent-border);
}

.fra-photos__thumb.is-pending .fra-photos__thumb-img {
  opacity: 0.75;
}

/* ⛔ 900px IS THE ONLY BREAKPOINT THIS FILE USES, and adding a second would
   break the discipline C-2 records as the best in the repo. Below it the
   thumbs get bigger rather than smaller: the screen is the only place an
   assessor can check what they photographed before they walk away. */
@media (max-width: 900px) {
  .fra-photos__thumb {
    width: 84px;
    height: 84px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * STAGE 7 — FORMS THAT DO NOT LOSE WORK
 *
 * ⛔ 900px IS STILL THE ONLY BREAKPOINT ADDED HERE. C-2 records this file's
 * breakpoint discipline as the best in the repo and it is not being spent on
 * a form. (The one 620px query above belongs to item 5.6's capture bar and
 * predates this block.)
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md.
 * ═══════════════════════════════════════════════════════════════════════ */

/* ───────────────────────────────────── item 7.4 — the required marker, ONCE ──
 *
 * ⛔ THIS WAS `.fra-setup__required` AND ONLY THE SETUP PAGE COULD USE IT.
 * `fra/premises/form.php` emitted the HTML `required` attribute and no visible
 * marker at all, so the two FRA forms disagreed about whether a required field
 * looks like anything. The rule is unchanged; only its name is, so both forms
 * and the capture judgements can carry the same pill.
 *
 * ⚠ IT IS A PILL, NOT AN ASTERISK. An asterisk needs a legend somewhere on the
 * page saying what it means, and this form has sixty-six fields and no room for
 * one; the word is its own legend. */
.fra-required {
  margin-left: 8px;
  padding: 2px 8px;
  font-size: 11px;
  font-weight: 650;
  color: var(--ipi-red);
  background: var(--ipi-red-bg);
  border: 1px solid var(--ipi-red-border);
  border-radius: 999px;
}

/* ─────────────────────────────── item 7.2 — the summary of what was refused ──
 *
 * Shared by the premises form and the capture form, deliberately: both answer a
 * refusal the same way and a second panel language would be the fork Stage 1
 * spent its time removing. */
.fra-form-errors {
  margin-bottom: 16px;
  border: 1px solid var(--ipi-red-border);
  background: var(--ipi-red-bg);
}

.fra-form-errors .ipi-panel__title {
  color: var(--ipi-red);
}

.fra-form-errors__lede {
  margin: 0 0 10px;
}

.fra-form-errors__list {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.fra-form-errors__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 8px;
  min-width: 0;
}

/* 44px of vertical target on a phone: this is a link an assessor aims at with a
   thumb, in the middle of correcting a form. */
.fra-form-errors__link {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  font-size: 13px;
  font-weight: 650;
  color: var(--ipi-red);
  text-decoration: underline;
}

.fra-form-errors__why {
  font-size: 12px;
  color: var(--ipi-ink-3);
  min-width: 0;
}

@media (max-width: 900px) {
  .fra-form-errors__link {
    min-height: 44px;
  }
}

/* ─────────────────────────── item 7.3 — the premises form's section rail ──
 *
 * ⚠ COPIED FROM `integrations-page.css` §3 DELIBERATELY, NOT SHARED. That file
 * records the same decision about Settings: extracting ~40 lines would put a
 * fourth stylesheet in two pages' load order, and the rails differ in what they
 * contain. If a fifth page grows one, share it then. */
/* ⚠ 16px, NOT INTEGRATIONS' 18. Item 3.6 declared `.fra-premises-form { gap: 16px }`
   because `style.css` styles the bare `form` tag `display: grid; gap: 12px` and the
   panels would otherwise drop to 12 — so the panes are inside a form that already has
   a rhythm, and a second value would show as a 2px step the moment JavaScript is absent
   and all five panes stack. Asserted by
   `fra-3-6-the-premises-form-declares-the-list-area-rhythm`. */
.fra-premises-sections__panes,
.fra-show-sections__panes {
  display: grid;
  gap: 16px;
  min-width: 0;
  align-content: start;
}

/* ⚠ `.ipi-sidenav__text` IS NOT IN ANY SHELL — `integrations-page.css` declares
   it, and that file is loaded on one page. Without this the label is the only
   child of a `justify-content: space-between` button, which is harmless until an
   error dot appears beside it and throws the label to the right-hand edge. */
.fra-premises-sections .ipi-sidenav__text,
.fra-show-sections .ipi-sidenav__text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* How many refusals a section is holding. The only number on this rail. */
.fra-premises-nav__count {
  flex: 0 0 auto;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--ipi-red-bg);
  color: var(--ipi-red);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.fra-premises-navbar,
.fra-rev-navbar,
.fra-show-navbar {
  display: none;
}

@media (max-width: 900px) {
  .fra-premises-navbar:not([hidden]),
  .fra-rev-navbar:not([hidden]),
  .fra-show-navbar:not([hidden]) {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 16px;
    margin-bottom: 16px;
    background: var(--ipi-bg-card);
    border-radius: 12px;
    box-shadow: var(--ipi-shadow-card);
  }

  .fra-premises-navbar .ipi-filter-rail__copy,
  .fra-rev-navbar .ipi-filter-rail__copy,
  .fra-show-navbar .ipi-filter-rail__copy {
    min-width: 0;
    flex: 1 1 auto;
  }

  .fra-premises-navbar .ipi-filter-rail__meta,
  .fra-rev-navbar .ipi-filter-rail__meta,
  .fra-show-navbar .ipi-filter-rail__meta {
    font-size: 14px;
    font-weight: 600;
    color: var(--ipi-ink);
  }

  .fra-premises-navbar .ipi-filter-rail__actions,
  .fra-rev-navbar .ipi-filter-rail__actions,
  .fra-show-navbar .ipi-filter-rail__actions {
    flex-shrink: 0;
  }

  /* ⛔ THE SHELL'S CHIP STRIP STILL APPLIES INSIDE THE BOTTOM SHEET, and inside
     a sheet it is wrong: the rail renders ~54px tall, scrolling sideways with
     its group labels hidden. The canonical map records the Help rail hitting
     exactly this. Undo it as `integrations-navdrawer` does. */
  .fra-premises-navdrawer .ipi-sidenav,
  .fra-rev-navdrawer .ipi-sidenav,
  .fra-show-navdrawer .ipi-sidenav {
    display: block;
    position: static;
    max-height: none;
    overflow: visible;
    background: transparent;
    border-radius: 0;
    box-shadow: none;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .fra-premises-navdrawer .ipi-sidenav__group,
  .fra-rev-navdrawer .ipi-sidenav__group,
  .fra-show-navdrawer .ipi-sidenav__group {
    display: block;
    padding: 10px 0;
  }

  .fra-premises-navdrawer .ipi-sidenav__label,
  .fra-rev-navdrawer .ipi-sidenav__label,
  .fra-show-navdrawer .ipi-sidenav__label {
    display: block;
  }

  /* 44px: the tap-target floor. The shell's chip height is smaller because a
     chip strip is scrolled, not aimed at. */
  .fra-premises-navdrawer .ipi-sidenav__item,
  .fra-rev-navdrawer .ipi-sidenav__item,
  .fra-show-navdrawer .ipi-sidenav__item {
    width: 100%;
    flex: 0 0 auto;
    white-space: normal;
    border-left: 2px solid transparent;
    border-radius: 0;
    min-height: 44px;
  }

  /* ⚠ THE ELLIPSIS RULE ABOVE IS FOR THE 236px DESKTOP COLUMN AND FIGHTS THE SHEET.
     `.ipi-sidenav__item` is given `white-space: normal` here so a long section name
     wraps rather than being cut — but the label's own `nowrap` + `ellipsis` wins
     inside it, so at 360px "Premises and occupants" would render clipped in a sheet
     with room to spare. Undo it where there is width to use. */
  .fra-premises-navdrawer .ipi-sidenav__text,
  .fra-rev-navdrawer .ipi-sidenav__text,
  .fra-show-navdrawer .ipi-sidenav__text {
    overflow: visible;
    text-overflow: clip;
    white-space: normal;
  }
}

/* ────────────────── item 7.3 — Save is reachable without scrolling ──
 *
 * The shape item 5.6 proved on the capture form, including the negative margin
 * that has to cancel `.ipi-list-area`'s gutter exactly — see that block's own
 * comment for what happens when the padding is shrunk and the margin is not. */
/* ═══════════════════════════════════════════════════════════════════════════
 * THE PREMISES FORM, REBUILT — 2026-09-09. Applies to all five panes.
 *
 * 🔴 THE FIELD CARD WAS INVISIBLE AND COST 92px A FIELD.
 * `$fraControl()` emits `<div class="ipi-field fra-field">`, and `.fra-field` is the
 * capture-page card: 12px padding, 10px radius, `--ipi-bg-card` fill, hairline border.
 * On this form the PANE is also `--ipi-bg-card`, so the card was WHITE ON WHITE with a
 * 7%-black outline — measured identical, `rgb(255,255,255)` on `rgb(255,255,255)`. It
 * drew nothing and bought nothing, while wrapping a 36px control in a 128px box.
 * Across the Clause 12 pane that is 40 fields at ~92px of chrome each and a 4,519px
 * pane — 5.9 screens to fill in one section.
 * Here a field is a label, a control and its help, separated by space. The pane's own
 * card is the surface; nothing inside it needs a second one.
 * ⛔ SCOPED TO `[data-fra-premises-page="form"]`. `.fra-field` is shared with the
 * capture page, where the card wraps a control you are about to tap on a phone and
 * earns its chrome.
 * ═══════════════════════════════════════════════════════════════════════════ */
[data-fra-premises-page="form"] .ipi-field.fra-field {
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
}

/* ⚠ THE PANE PAYS BACK THE INSET THE CARDS USED TO PROVIDE.
   `.ipi-panel` is `16px 20px`, and while every field was a 12px-padded card the form
   read as 32px in from the edge. Removing the card took that away and the controls
   ended up 20px from the panel edge at the sides and 16px at the foot — measured, and
   visibly cramped. The padding belongs on the surface, not on 40 boxes inside it. */
/* ⚠ THE GAP UNDER THE PAGE HEADER, which this page was missing exactly as the assessor
   and setup pages were. `.ipi-page-header` ends with 20px of padding and every FRA page
   that opens with `.ipi-stats-strip` gets 20px more from the strip — 41px measured on
   Premises. This form has no strip, so it had 21px. Third page with the same hole; the
   fix is identical and scoped the same way.
   ⛔ Not on `.ipi-list-area` generally: on a page that HAS a strip it would stack. */
.fra-premises-page .ipi-list-area {
  padding-top: 20px;
}

[data-fra-premises-page="form"] [data-fra-premises-section] {
  padding: 22px 26px 26px;
}

@media (max-width: 900px) {
  [data-fra-premises-page="form"] [data-fra-premises-section] {
    padding: 16px 16px 20px;
  }

}

/* ⚠ `align-items: start` on the two-up grid. Grid items stretch by default, so a field
   beside a taller one (help text, an error) grew to match and left a pool of empty card
   under its control — visible as the "Storeys" box running to the depth of "Floors
   entirely below ground" next to it. */
[data-fra-premises-page="form"] .ipi-form-grid {
  align-items: start;
  gap: 20px 24px;
}

/* The label carries the field now that no border does. */
[data-fra-premises-page="form"] .ipi-field.fra-field > label {
  display: block;
  margin-bottom: 5px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ipi-ink-2);
}

/* ⛔ `--ipi-ink-3`, NOT `--ipi-ink-4`, AND IT IS A CONTRAST FLOOR.
   Measured on this form: the field help rendered at **3.10:1**, under WCAG AA, on 23
   elements — and it is the smallest text on the page, so it was the faintest AND the
   smallest at once. `--ipi-ink-4` is toned against white where it is already 3.10; it
   has no headroom anywhere. `--ipi-ink-3` measures 6.4:1 on the same white. */
[data-fra-premises-page="form"] .ipi-field.fra-field > small {
  display: block;
  margin-top: 5px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

/* ⛔ Same floor, same reason — the "(m)" and "(storeys)" suffixes measured 3.10:1 on 8
   elements. A unit is part of the label; it cannot be the least readable thing in it. */
[data-fra-premises-page="form"] .fra-field__unit {
  color: var(--ipi-ink-3);
}

/* ═══ THE LETTERED CLAUSE 12 GROUPS ═══════════════════════════════════════
 * 🔴 THE (a) (b) (c) MARKER MEASURED 2.74:1 — BELOW AA-LARGE, ON 17 ELEMENTS.
 * It is `--ipi-accent` (#c49510) at 12px, and accent is a RING-AND-BORDER token in this
 * design system, not an ink — css-standards.md says so, and this is what using it as
 * text looks like. The letter is the only thing telling a reader which Clause 12 item
 * they are answering, so it cannot be the faintest mark on the page.
 * ⚠ It becomes ink at full weight, and the ACCENT moves to a left rule on the group,
 * where the token belongs and where it does the grouping job the marker was failing to
 * do on its own.
 * ⛔ The boxed `<fieldset>` goes for the same reason as the field card: a bordered box
 * inside a bordered pane, in the same colour, adding depth that is not there. */
/* ⚠ SEVENTEEN GROUPS, NOT A FEW. Clause 12 renders one lettered group per item and
   several hold only two fields, so the per-group chrome is paid 17 times: measured
   1,104px of legend and margin in a 3,868px pane — 28% of it, for 17 headings. The
   left rule already separates a group from the one above it, so the spacing only has
   to be enough to read as a break, not to substitute for one. */
[data-fra-premises-page="form"] .fra-form-clause {
  margin: 0 0 20px;
  padding: 0 0 0 14px;
  border: 0;
  border-left: 2px solid var(--ipi-accent-border);
  border-radius: 0;
}

[data-fra-premises-page="form"] .fra-form-clause:last-child {
  margin-bottom: 0;
}

[data-fra-premises-page="form"] .fra-form-clause > legend {
  display: block;
  padding: 0;
  margin-bottom: 10px;
  font-size: 13.5px;
  font-weight: 650;
  color: var(--ipi-ink);
}

[data-fra-premises-page="form"] .fra-form-clause > legend .fra-factual__letter {
  display: inline-block;
  margin-right: 6px;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ipi-ink);
}

/* An error still has to read as one without the card it used to tint. */
[data-fra-premises-page="form"] .ipi-field.fra-field.ipi-field--error > label {
  color: var(--ipi-red);
}

@media (max-width: 900px) {
  [data-fra-premises-page="form"] .fra-form-clause {
    padding-left: 12px;
  }
}

/* 🔴 THE SAME SAVE CARD AS THE ASSESSOR, SETUP AND RISK-MATRIX FORMS.
   This was a sticky strip — negative inline margins, a top rule and a shadow — and the
   only one of its shape in the module. Sticky is also what made it MOVE: it pins only
   when there is something to scroll, so on the two panes shorter than a screen it sat
   under the last field (y=495) while the long panes pinned it at the viewport foot
   (y=796). Three hundred pixels of travel between tabs of one form.
   ⛔ Holding it still needed a `min-height: 70vh` floor derived from one window size.
   Both are gone: in flow, it has the same relationship to the content on all five tabs
   at any window size, and it matches the rest of the module. */
/* ═══════════════════════════════════════════════════════════════════════════
 * THE PREMISES DETAIL PAGE — the record structure. All four tabs.
 *
 * 🔴 `.fra-rec` REPLACED `.ipi-keyvals` IN THE MARKUP; IT DOES NOT OVERRIDE IT.
 * The first attempt at this page kept the shell's narrow key/value component and put
 * twenty-five scoped rules on top of it to undo `justify-content: space-between`. That
 * is precisely what .claude/rules/tenant-ui.md forbids — "Do NOT override shared
 * primitives ... in page CSS" — and it left the sidebar component still deciding the
 * page's structure. The four partials now emit `<dl class="fra-rec">` and this block
 * owns it outright, so nothing is being fought.
 *
 * ⚠ WHAT IT FIXES: `.ipi-keyvals__row` is flex/space-between, which in a 1,589px panel
 * measured 1,309px of empty space between a label and its value on the Record tab and
 * 1,427px on Clause 12 — sixty-nine rows of eye-travel with nothing aligned, because
 * every label is a different length.
 * ═══════════════════════════════════════════════════════════════════════════ */
.fra-rec {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;
  column-gap: 40px;
  margin: 0;
  padding: 0;
}

/* ⚠ A row is a grid ITEM, so its rule sits under each one rather than between stacked
   siblings, and `min-width: 0` keeps a long address from widening its column. */
.fra-rec__row {
  display: grid;
  grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
  align-items: baseline;
  gap: 4px 20px;
  padding: 9px 0;
  border-bottom: 1px solid var(--ipi-rule);
  min-width: 0;
}

.fra-rec__key {
  margin: 0;
  font-size: 12.5px;
  font-weight: 400;
  color: var(--ipi-ink-3);
}

.fra-rec__val {
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink);
  min-width: 0;
  overflow-wrap: anywhere;
}

/* A person card is already narrow, so its record stays one column. */
.fra-rec--card {
  grid-template-columns: minmax(0, 1fr);
  column-gap: 0;
}

/* ═══ CLAUSE 12'S LETTERED ITEMS ═════════════════════════════════════════
 * 🔴 THE (a)…(s) MARKER MEASURED 2.74:1 ON 19 ELEMENTS — below AA-large.
 * It is `--ipi-accent` (#c49510) at 12px, and accent is a ring-and-border token in this
 * design system, not an ink; css-standards.md says so. The letter is the only thing
 * telling a reader which Clause 12 item they are looking at.
 * ⚠ Identical treatment to the premises FORM, which carried the same marker at the same
 * ratio: the letter becomes ink, and the accent moves to a left rule where the token
 * belongs and where it does the grouping the faint letter was failing at alone. */
[data-fra-premises-page="detail"] .fra-factual__list {
  margin: 0;
  padding: 0;
  list-style: none;
}

[data-fra-premises-page="detail"] .fra-factual__item {
  margin: 0 0 22px;
  padding: 0 0 0 14px;
  border-left: 2px solid var(--ipi-accent-border);
}

[data-fra-premises-page="detail"] .fra-factual__item:last-child {
  margin-bottom: 0;
}

[data-fra-premises-page="detail"] .fra-factual__clause {
  margin-bottom: 10px;
}

[data-fra-premises-page="detail"] .fra-factual__letter {
  margin-right: 6px;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--ipi-ink);
}

[data-fra-premises-page="detail"] .fra-factual__text {
  font-size: 13.5px;
  font-weight: 650;
  color: var(--ipi-ink);
}

/* ═══ THE ASSESSMENTS TAB ════════════════════════════════════════════════
 * 🔴 THREE ROWS THAT READ THE SAME. Every row was type + FRA type, so a premises with
 * three drafts showed "Initial · TYPE 1" three times, separated only by a date at the
 * far right — on the one panel whose job is to let you open the right one. The row now
 * leads with its reference and carries the type and assessor underneath; see the note
 * in _assessments.php, and the LEFT JOIN added to `forPremises()` that supplies the
 * name. */
/* 🔴 R4-03 / R4-18 — A GRID, NOT ONE FLEX LINE. As a single flex row the chip moved with
   the width of the date beside it, the Continue button pushed every row's chip left, and
   at 390 the row squeezed to "FRA-00…" and a button broken over three lines. The status
   and date now sit in FIXED columns (and the action column is reserved even on a row
   without the button, so the chips line up down the list); below 900 the row stacks. */
.fra-row--assessment {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 116px 96px 150px;
  grid-template-areas: "lead status meta action";
  align-items: center;
  gap: 4px 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-row--assessment:last-child {
  border-bottom: 0;
}

.fra-row__lead {
  grid-area: lead;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.fra-row--assessment .fra-row__main {
  font-size: 13.5px;
  font-weight: 650;
  white-space: normal;
}

.fra-row__sub {
  font-size: 12px;
  color: var(--ipi-ink-3);
}

.fra-row__status {
  grid-area: status;
  min-width: 0;
}

.fra-row--assessment .fra-row__meta {
  grid-area: meta;
  font-size: 12px;
  color: var(--ipi-ink-3);
  white-space: nowrap;
}

.fra-row--assessment .fra-row__action {
  grid-area: action;
  justify-self: end;
  white-space: nowrap;
}

@media (max-width: 900px) {
  .fra-row--assessment {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      "lead lead"
      "status meta"
      "action action";
    gap: 8px 10px;
  }

  .fra-row--assessment .fra-row__action {
    justify-self: stretch;
    justify-content: center;
    min-height: 44px;
  }
}

/* ⚠ THE PANEL FOOT AND THE ACTION UNDER IT WERE 2px APART. `.fra-table__foot` is a
   paragraph and `.fra-inline-form` is a form; neither carries a margin against the
   other, so "See these in the assessments list" sat directly on top of "Start a draft
   assessment" — measured, a 2px gap between a link and a primary button. */
[data-fra-premises-page="detail"] .fra-table__foot + .fra-inline-form,
[data-fra-premises-page="detail"] .fra-rowlist + .fra-table__foot {
  margin-top: 14px;
}

@media (max-width: 900px) {
  .fra-rec,
  .fra-rec__row {
    grid-template-columns: minmax(0, 1fr);
  }

  [data-fra-premises-page="detail"] .fra-factual__item {
    padding-left: 12px;
  }
}

.fra-premises__bar {
  margin-top: 16px;
}

/* ⚠ `.ipi-panel__foot--actions` normally CLOSES a panel that has content above it, so
   its own top margin, padding and rule separate it from nothing when it is the panel's
   only child. Same as `.fra-assessor-savebar`. */
.fra-premises__bar .ipi-panel__foot--actions {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.fra-premises__save,
.fra-premises__cancel {
  min-height: 40px;
}

/* R4-10 — the save bar's two buttons are 44px targets below the split. */
@media (max-width: 900px) {
  .fra-premises__save,
  .fra-premises__cancel {
    min-height: 44px;
  }
}

/* R4-11 — "Required" is a label, not an alarm. Red at rest put a warning on three fields
   nobody had got wrong yet; the red is kept for a field that was actually refused. Scoped
   to the premises form (the setup page and capture own their own use of the pill). */
[data-fra-premises-page="form"] .fra-required {
  color: var(--ipi-ink-3);
  background: var(--ipi-bg-inset);
  border-color: var(--ipi-rule-h);
}

[data-fra-premises-page="form"] .ipi-field--error .fra-required {
  color: var(--ipi-red);
  background: var(--ipi-red-bg);
  border-color: var(--ipi-red-border);
}

/* ─────────────────────────────────── the risk matrix EDITOR — rebuilt 2026-09-09
 *
 * 🔴 "ATTACHED AXES", BY OWNER DECISION. The previous layout stacked six panels and
 * measured 2,171px in a 1,144px viewport; 790px of that was two axis-editor cards
 * whose only job was to word the ten categories that ALREADY appear as the grid's own
 * headers. The labels are now those headers, the definitions open one at a time
 * beneath the grid, and the picker and the save button have swapped ends of the page.
 *
 * ⛔ THIS PAGE CARRIED A SECTION RAIL FOR ONE REVISION. DO NOT PUT IT BACK, and the
 * reason survives this rebuild intact: THE GRID'S COLUMN HEADERS ARE THE
 * CONSEQUENCE-AXIS LABELS, so checking that a category is worded right while seeing
 * where it lands in the grid is the actual job, and one-pane-at-a-time made that
 * impossible. A rail suits Settings, whose seventeen sections never refer to one
 * another. It does not suit one object seen from four angles. This layout answers the
 * same problem by putting the label IN the header rather than in a pane at all.
 *
 * ⛔ AND THE TWO-COLUMN AXIS GRID IS GONE WITH IT. `.fra-matrix-axes`,
 * `.fra-matrix-axisvalue`, `.fra-matrix-identbar` and `.fra-matrix-savebar` were
 * retired here on 2026-09-09; `.fra-matrix-list__items` / `__item` went with the
 * picker's move to a tab strip. `.fra-matrix-list__size` and `__state` survive
 * because the tab strip still prints both.
 */

/* ── the sticky action bar ──────────────────────────────────────────────────
 * 🔴 ONE COMMIT POINT. Name, size, state and Save were four things in three places
 * — an identity card at the top, a save bar in the middle, an availability panel at
 * the foot — so the control that commits every label, every definition and all 25
 * cells sat 900px from the control that decides whether the matrix is used at all.
 *
 * ⚠ TOP-STICKY IS SAFE WHERE BOTTOM-STICKY IS NOT. `.fra-premises__bar` records why
 * it refuses to stick: `layout/app.php` pins a 125x38px "Need help?" button
 * bottom-right and a bottom bar collides with it. Nothing is pinned to the TOP of the
 * scroller, so this bar has no such neighbour.
 *
 * ⚠ `top: 0` RESOLVES AGAINST `.content`, which is the scrolling ancestor (the app
 * shell itself is `overflow: hidden` and does not scroll). Do not add `overflow` to
 * `.page-section` or `.inspections-proto` — either would make them the scroll parent
 * and strand this bar mid-page, and `.inspections-proto`'s negative inline margin
 * means an overflow there also clips the page header.
 *
 * ⚠ IT CARRIES THE PAGE GUTTER ITSELF. It sits OUTSIDE `.ipi-list-area` so it can be
 * full-bleed under the header, so nothing else supplies its horizontal padding. */
.fra-matrix-actionbar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 10px var(--ipi-page-gutter);
  background: var(--ipi-bg-card);
  border-bottom: 1px solid var(--ipi-rule);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}

/* ⚠ The two controls in the bar are `.ipi-field`s so a refusal still paints them, but
   an `.ipi-field` is a flex COLUMN with a label above — and here the label is
   `.sr-only`, so the column would still reserve its gap. Zeroing it is what keeps the
   bar one line tall. */
.fra-matrix-actionbar > .ipi-field {
  gap: 0;
  flex: 0 0 auto;
}

/* ⚠ R5-09 (2026-09-23): AT ≤1366px `.content` HAS 16px OF TOP PADDING (tablet.css /
   mobile.css) and a sticky box stops at the padding edge, so `top: 0` pinned the bar
   16px below the scroller's top with the grid scrolling through the strip above it.
   Pulling it up by the same 16px seats it on the edge. Above 1366px the padding is 0. */
@media (max-width: 1366px) {
  .fra-matrix-actionbar {
    top: -16px;
  }
}

/* R5-10: the pending-change flag. Hidden (the `hidden` attribute) until something has
   changed; `features/fra-risk-matrix.js` shows it. */
.fra-matrix-dirty {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 11.5px;
  font-weight: 600;
  background: var(--ipi-accent-bg);
  color: var(--accent-ink, var(--ipi-ink-2));
  white-space: nowrap;
}

/* ⚠ THE ≤900px SAVE (R5-10). Absent above 900px, where the top bar sticks. */
.fra-matrix-mobilebar {
  display: none;
}

@media (max-width: 900px) {
  .fra-matrix-mobilebar {
    position: sticky;
    bottom: 0;
    z-index: 5;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 14px;
    background: var(--ipi-bg-card);
    border-radius: 12px;
    box-shadow: var(--ipi-shadow-card), 0 -2px 10px rgba(26, 24, 21, 0.06);
  }

  .fra-matrix-mobilebar__state {
    min-width: 0;
    font-size: 13px;
    color: var(--ipi-ink-3);
  }

  .fra-matrix-mobilebar__state.is-dirty {
    font-weight: 600;
    color: var(--ipi-ink);
  }

  .fra-matrix-mobilebar .btn {
    flex: 0 0 auto;
    min-height: 44px;
  }

  /* One Save per page: the bottom bar's. */
  .fra-matrix-actionbar__save,
  .fra-matrix-actionbar__hint,
  .fra-matrix-actionbar .fra-matrix-dirty {
    display: none !important;
  }
}

.fra-matrix-actionbar__name {
  font-family: var(--font-display, inherit);
  font-size: 15px;
  font-weight: 650;
  letter-spacing: -0.01em;
  min-width: 220px;
}

/* ── size, at the foot of the thing it rebuilds ─────────────────────────────
 * 🔴 IT IS NOT IN THE ACTION BAR (owner instruction, 2026-09-09). Changing the size
 * REPLACES every label, every definition and all 25 cells, and it is used about once
 * in a matrix's life — so a pill beside the name gave the page's most destructive
 * control its most prominent position, one tab-stop from Save. It sits with the
 * sentence that says what it costs instead. */
.fra-matrix-sizerow {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-matrix-sizerow > .ipi-field {
  flex: 0 0 auto;
}

.fra-matrix-sizerow__select {
  width: auto;
  min-width: 96px;
}

.fra-matrix-sizerow__warning {
  margin: 0;
  padding-top: 22px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

.fra-matrix-actionbar__spacer {
  flex: 1 1 auto;
  min-width: 0;
}

.fra-matrix-actionbar__hint {
  margin: 0;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--ipi-ink-4);
  max-width: 26ch;
}

.fra-matrix-actionbar__check {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  color: var(--ipi-ink-2);
  white-space: nowrap;
}

/* ⚠ The state chip is NOT `.ipi-status-pill`. That primitive maps a document status
   (draft / sent / paid), and a matrix's "in use" is a different axis of meaning that
   would inherit the wrong tone from any of them. */
.fra-matrix-state-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px;
  border-radius: 999px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  background: var(--ipi-green-bg);
  color: var(--ipi-green);
  white-space: nowrap;
}

.fra-matrix-state-chip::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
}

.fra-matrix-state-chip--muted {
  background: var(--ipi-bg-inset);
  color: var(--ipi-ink-3);
}

/* ⚠ `.ipi-list-area` supplies 28px of bottom padding, and the page carries two of
   them because the availability panel's own `<form>`s cannot nest inside the editor
   form. Zeroing the first one's is what makes the pair read as a single column.

   🔴 `min-width: 0` IS LOAD-BEARING AND IT IS NOT DEFENSIVE BOILERPLATE. `style.css`
   styles the bare `form` TAG as `display: grid`, and item 1.1's FRA reset strips that
   rule's card chrome but DELIBERATELY LEAVES THE `display` ALONE — so this element is a
   GRID ITEM, and a grid item's default `min-width: auto` refuses to shrink below its
   content. The 5x5 grid is 697px wide at the 116px-per-column mobile floor, so without
   this the whole column blew out to 785px at 390px: `.fra-matrix__grid-scroll` was
   sized by its content, never scrolled, and the panel was simply CLIPPED mid-column by
   `.content`'s `overflow: hidden`. Measured on a Pixel 8 UA at 360 and 390 on
   2026-09-09 — it does not reproduce at 810px and above, because the table fits. */
.fra-matrix-main,
.fra-matrix-footarea {
  min-width: 0;
}

.fra-matrix-main {
  padding-bottom: 0;
}

.fra-matrix-footarea {
  padding-top: 16px;
}

/* ── the grid's editable headings ──────────────────────────────────────────
 * 🔴 THE HEADER *IS* THE LABEL INPUT. It used to be static text with the input that
 * changes it 600px below, in an axis card — so renaming a category meant scrolling
 * away from the only place its effect is visible.
 *
 * ⚠ THE INPUT IS BORDERLESS UNTIL TOUCHED. Five bordered boxes across a header row
 * read as a filter bar rather than as headings; the border appears on hover and focus
 * so the control is still discoverable. `.ipi-field`'s focus ring still applies
 * because the shell scopes it on the element, not on the wrapper. */
.fra-matrix-head {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.fra-matrix-head__input {
  font: inherit;
  font-size: 12px;
  font-weight: 650;
  color: var(--ipi-ink);
  width: 100%;
  padding: 5px 7px;
  /* ⚠ R5-24: A FAINT RESTING BORDER, NOT NONE. Fully transparent, nothing said these
     headings could be typed into until a pointer happened to cross one — and a touch
     screen has no hover. The rule is the lightest the shell has, so the row still reads
     as headings. */
  border: 1px solid var(--ipi-rule);
  border-radius: 6px;
  background: transparent;
}

.fra-matrix-head__input[disabled] {
  border-color: transparent;
}

.fra-matrix-head__input:hover:not([disabled]) {
  border-color: var(--ipi-rule-h);
  background: var(--ipi-bg-card);
}

.fra-matrix-head__input:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 1px;
}

/* ⚠ A refusal has to be visible on a control that is normally borderless, so the
   error border is stated here rather than inherited — `.ipi-field--error` targets
   `.ipi-field > input` and this input has no `.ipi-field` around it. */
.fra-matrix-head--error .fra-matrix-head__input {
  border-color: var(--ipi-red);
  background-color: var(--ipi-red-bg);
}

/* ⚠ R5-24 (2026-09-23): 12px IN `--ipi-ink-3`, WITH A REAL HIT BOX. It was 10px in the
   faint ink (3.1:1 on white) on a 63×16 target — the smallest, palest control on a page
   whose whole job is to be edited. 44px tall at ≤900, the tap floor. */
.fra-matrix-head__define {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 7px;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ipi-ink-3);
  cursor: pointer;
}

@media (max-width: 900px) {
  .fra-matrix-head__define {
    min-height: 44px;
  }
}

.fra-matrix-head__define:hover,
.fra-matrix-head__define[aria-expanded="true"] {
  color: var(--ipi-accent);
}

.fra-matrix-head__define::after {
  content: " \25BE";
  font-size: 9px;
}

.fra-matrix-head__define[aria-expanded="true"]::after {
  content: " \25B4";
}

/* ⚠ The corner cell used to read `Likelihood \ Consequence`. A backslash there reads
   as a path separator, and it was the only thing on the page saying which axis ran
   which way. */
.fra-matrix__corner {
  width: 148px;
  display: table-cell;
  /* R5-24: 12px in `--ipi-ink-3` — it is the only thing saying which way each axis runs. */
  font-size: 12px !important;
  line-height: 1.35;
  letter-spacing: 0.01em !important;
  text-transform: none !important;
  color: var(--ipi-ink-3) !important;
  background: var(--ipi-bg-inset);
  vertical-align: bottom;
}

.fra-matrix__corner span {
  display: block;
}

.fra-matrix__rowhead {
  width: 148px;
  background: var(--ipi-bg-inset);
  vertical-align: middle;
}

/* Headings sit at the bottom of a header cell so the row of inputs lines up when one
   label wraps to two lines. */
.fra-matrix__grid--editor thead th {
  vertical-align: bottom;
}

/* ── the definition drawer ─────────────────────────────────────────────────
 * ⛔ ALL TEN DEFINITIONS ARE ALWAYS IN THE DOM. They post as `definitions[axis][i]`,
 * so the nine not on screen would post EMPTY if they were not rendered — a drawer
 * swapping one shared textarea would wipe nine paragraphs on every save. The collapse
 * below is presentation only, added by `features/fra-risk-matrix.js`, and the page is
 * complete and saveable without it.
 *
 * ⚠ `.is-enhanced` IS SET BY THE SCRIPT, NOT BY PHP. Hiding these in the stylesheet
 * would mean a page whose JS failed to load renders ten definitions that cannot be
 * reached — the module graph failure mode `app.js` warns about, turned into data loss.
 * With no script the stack simply renders open, which is what it did before. */
.fra-matrix-defs {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 14px;
}

.fra-matrix-defs.is-enhanced > .fra-matrix-def {
  display: none;
}

.fra-matrix-defs.is-enhanced > .fra-matrix-def.is-open {
  display: grid;
}

.fra-matrix-def {
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  gap: 14px;
  align-items: start;
  padding: 12px 14px;
  border: 1px solid var(--ipi-accent-border);
  border-radius: 9px;
  background: var(--ipi-accent-bg);
  /* R5-25: opened by scrolling it into view — clear of the 58px sticky action bar. */
  scroll-margin-top: 80px;
  scroll-margin-bottom: 16px;
}

/* R5-25: "Done" exists only once the stack is a drawer; without the script every
   definition is open and there is nothing to close. */
.fra-matrix-def__close {
  display: none;
  margin-top: 10px;
}

.fra-matrix-defs.is-enhanced .fra-matrix-def__close {
  display: inline-flex;
}

/* A refused definition wears the refusal, and the stack it is in never collapses —
   the view sets `data-fra-matrix-defs-locked` so a message in the summary panel can
   never point at a field nothing on screen reveals. */
.fra-matrix-def--error {
  border-color: var(--ipi-red);
  background: var(--ipi-red-bg);
}

.fra-matrix-def__axis {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ipi-accent);
  margin-bottom: 3px;
}

.fra-matrix-def__label {
  display: block;
  font-size: 13px;
  font-weight: 650;
  color: var(--ipi-ink);
  line-height: 1.35;
}

@media (max-width: 900px) {
  .fra-matrix-def {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── what each axis measures ───────────────────────────────────────────────
 * ⛔ THE STANDARD'S WORDS, AND THEY ARE NOT IN THE DRAWER. Cl. 17.1 requires the
 * consequence axis to cover life safety AND the potential for injury; a statement of
 * that which is only visible when a drawer happens to be open is not a statement of
 * it. Two-up on desktop, stacked below 900px. */
.fra-matrix-axisnotes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-matrix-axisnote__title {
  display: block;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ipi-ink-3);
  margin-bottom: 4px;
}

.fra-matrix-axisnotes .fra-matrix__axis-description {
  margin: 0;
  font-size: 12.5px;
}

@media (max-width: 900px) {
  .fra-matrix-axisnotes {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ── the foot: availability and the standard's rules ───────────────────────
 * ⚠ Availability is wrapped in `<?php if ($isFraAssessor)`, so it is ABSENT for a
 * reader who cannot edit — not hidden. Two auto columns rather than named areas,
 * because there are only two panels now and the third (the picker) has moved up. */
/* 🔴 NAMED AREAS, NOT AUTO-PLACEMENT, and it is the same device and the same reason as
   `.fra-assessor-grid`: the three panels differ in height, and row-major auto-flow would
   start the short Availability card below the tall rules card's full height and leave a
   hole. The two short ones stack in column one beside the tall one.
   ⚠ `align-items: start` — without it the shorter card in a row stretches to match the
   taller and grows exactly the whitespace this removes. */
.fra-matrix-foot {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-areas:
    'list  rules'
    'avail rules';
  align-items: start;
  gap: 16px;
}

.fra-matrix-foot > .ipi-panel {
  min-width: 0;
}

.fra-matrix-foot > .fra-matrix-list {
  grid-area: list;
}

.fra-matrix-foot > .fra-matrix-rules {
  grid-area: rules;
}

/* ⚠ Availability is wrapped in `<?php if ($isFraAssessor)`, so it is ABSENT for a reader
   who cannot edit — not hidden. `grid-template-areas` tolerates an unused area name, so
   the other two keep their places and no rule is needed for its absence. */
.fra-matrix-foot > [data-fra-matrix-state] {
  grid-area: avail;
}

.fra-matrix-availability {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 12px;
}

/* ⚠ Retire is not the twin of "Use for new assessments". Both were `.btn-secondary`,
   so the action that takes a matrix out of service looked exactly like the one that
   puts it in. `.btn-ghost` is an existing variant (style.css) — quieter, not red,
   because retiring is reversible and destroys nothing. */
.fra-matrix-retire {
  margin-left: 4px;
}

@media (max-width: 900px) {
  .fra-matrix-foot {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      'list'
      'avail'
      'rules';
  }

  /* ⚠ The bar wraps to two or three rows at this width, and a sticky element taller
     than a third of the viewport stops being a convenience. It scrolls away on
     mobile; Save is then found where it always was, at the end of the form. */
  .fra-matrix-actionbar {
    position: static;
  }

  /* ⛔ THE SPACER MUST NOT SURVIVE THE WRAP. `flex: 1 1 auto` on a zero-content item
     eats the whole remainder of the first line, which pushes the state chip and Save
     onto a line of their own AND leaves a 200px hole above them. Wrapping is the
     mobile answer; a spacer is the desktop one, and the two do not compose. */
  .fra-matrix-actionbar__spacer {
    display: none;
  }

  /* ⚠ A FLEX BASIS, NOT `1 1 100%`. Forcing the name to a full row is right at 390px
     and wasteful at 810px, where the name, the state chip and Save fit on one line
     with room to spare — and 900px is the app's only breakpoint, so this one rule has
     to serve both. A 220px basis wraps when it must and shares a row when it can. */
  .fra-matrix-actionbar > .ipi-field {
    flex: 1 1 220px;
  }

  .fra-matrix-actionbar__name {
    min-width: 0;
  }

  .fra-matrix-actionbar__hint {
    max-width: none;
    flex: 1 1 100%;
    order: 1;
  }

  .fra-matrix-sizerow {
    flex-direction: column;
    gap: 10px;
  }

  .fra-matrix-sizerow__warning {
    padding-top: 0;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
 * 🔴 THE EDITOR GRID'S RATING COLOURS — OWNER DECISION, 2026-09-09.
 *
 * ⛔ THIS DOES NOT LIFT THE COLOUR BAN, AND MUST NOT BE READ AS DOING SO. The rule in
 * the "the risk matrix — Stage 8" block above still holds everywhere a rating is
 * REPORTED: no colour, no severity chip, no numeric badge, on the assessment render,
 * in the client portal, or anywhere a client sees a rating. That reasoning — a
 * calibration report whose colour badge became the message while its nineteen action
 * priorities stopped carrying information — is about the OUTPUT, and is unchanged.
 * The owner was asked directly on 2026-09-09 and scoped this to the editor.
 *
 * ⚠ THIS SUPERSEDES THE GREYSCALE RAMP OF 2026-09-08, which answered the same problem
 * one step short. The task on THIS page is checking that your own 25-cell grid rises
 * monotonically, and twenty-five identical white selects make that impossible to
 * eyeball. Greyscale proved the point; hue does the job better, and the objection that
 * killed hue was never about this surface.
 *
 * ⛔ SCOPED TO `.fra-matrix-page`. The `.fra-matrix__grid` class is shared with the
 * READ-ONLY render inside an assessment; an unscoped selector would tint that too and
 * would be exactly the thing the ban forbids. `FraRiskEvaluationHttpTest` asserts that
 * every `cell-rating` rule in this file carries that scope.
 *
 * ⚠ DERIVED WITH `color-mix()` FROM THE SEMANTIC TOKENS, NOT HARDCODED. fra-shell.css
 * ships six palettes and a fixed hex ramp would be wrong under five of them. Mixing
 * `--ipi-green` / `--ipi-accent` / `--ipi-red` over `--ipi-bg-card` follows whichever
 * is active, and satisfies the project's own "never hardcode a colour" rule.
 *
 * ⚠ THE MIX PERCENTAGE RISES MONOTONICALLY — 8, 18, 22, 38, 44 — so the ramp carries
 * its order in LIGHTNESS as well as in hue. Green-to-red alone is the one ramp a
 * red-green colour-blind reader cannot follow; the darkening is the second channel,
 * and the rating's own word is in the select regardless. Text stays `--ipi-ink` at
 * every step rather than flipping to white, which is what keeps contrast honest
 * across all six palettes without a per-palette override.
 * ═══════════════════════════════════════════════════════════════════════════ */
.fra-matrix-page [data-fra-matrix-cell-rating] > select {
  background-color: var(--ipi-bg-card);
  color: var(--ipi-ink);
}

/* ⚠ A READ-ONLY MATRIX MUST LOOK READ-ONLY. For a reader without `is_fra_assessor`
   all 25 grid cells are genuinely `disabled` server-side, but at the browser default
   they kept a white field and a dropdown arrow and read as editable — the only signal
   was a line of small grey text in the corner. The cursor is the cheap half of the
   answer; the rest is already carried by the 0.7 opacity the disabled state applies. */
.fra-matrix-page [data-fra-matrix-cell-rating] > select:disabled {
  cursor: not-allowed;
}

.fra-matrix-page [data-fra-matrix-cell-rating='trivial'] > select {
  background-color: color-mix(in srgb, var(--ipi-green) 8%, var(--ipi-bg-card));
  border-color: color-mix(in srgb, var(--ipi-green) 22%, var(--ipi-rule-h));
}

.fra-matrix-page [data-fra-matrix-cell-rating='tolerable'] > select {
  background-color: color-mix(in srgb, var(--ipi-green) 18%, var(--ipi-bg-card));
  border-color: color-mix(in srgb, var(--ipi-green) 34%, var(--ipi-rule-h));
}

.fra-matrix-page [data-fra-matrix-cell-rating='moderate'] > select {
  background-color: color-mix(in srgb, var(--ipi-accent) 22%, var(--ipi-bg-card));
  border-color: color-mix(in srgb, var(--ipi-accent) 38%, var(--ipi-rule-h));
}

.fra-matrix-page [data-fra-matrix-cell-rating='substantial'] > select {
  background-color: color-mix(in srgb, var(--ipi-accent) 38%, var(--ipi-bg-card));
  border-color: color-mix(in srgb, var(--ipi-accent) 55%, var(--ipi-rule-h));
}

.fra-matrix-page [data-fra-matrix-cell-rating='intolerable'] > select {
  background-color: color-mix(in srgb, var(--ipi-red) 44%, var(--ipi-bg-card));
  border-color: color-mix(in srgb, var(--ipi-red) 60%, var(--ipi-rule-h));
}

/* ⚠ An unset cell is a HOLE, not a low rating, so it is marked as absence — a dashed
 * rule — rather than being given the lightest step of the ramp, which would read as
 * "trivial" and is the one wrong answer here. */
.fra-matrix-page [data-fra-matrix-cell-rating=''] > select {
  background-color: var(--ipi-bg-card);
  border-style: dashed;
  color: var(--ipi-ink-4);
}

/* ── R5-26: the phone's at-a-glance map ────────────────────────────────────
 * The editor grid is ~930px wide; a 343px frame showed a column and a half of it.
 * This is the same grid as small tiles, the same ramp as the selects above, so the
 * shape of the grading can be read without scrolling. ≤900px only; `aria-hidden`
 * in the view because the grid below says the same thing to a screen reader.
 * ⛔ Scoped `.fra-matrix-page`, like every rating-keyed rule — it is the editor's
 * ramp and must never reach the assessment render. */
.fra-matrix-page .fra-matrix-minimap {
  display: none;
}

@media (max-width: 900px) {
  .fra-matrix-page .fra-matrix-minimap {
    display: block;
    margin-bottom: 14px;
  }

  .fra-matrix-page .fra-matrix-minimap__cap {
    margin: 0 0 8px;
    font-size: 12px;
    color: var(--ipi-ink-3);
  }

  .fra-matrix-page .fra-matrix-minimap__grid {
    display: grid;
    grid-template-columns: minmax(64px, 88px) repeat(5, minmax(0, 1fr));
    gap: 3px;
  }

  .fra-matrix-page .fra-matrix-minimap[data-fra-mm-cols="3"] .fra-matrix-minimap__grid {
    grid-template-columns: minmax(64px, 88px) repeat(3, minmax(0, 1fr));
  }

  .fra-matrix-page .fra-matrix-minimap[data-fra-mm-cols="7"] .fra-matrix-minimap__grid {
    grid-template-columns: minmax(56px, 76px) repeat(7, minmax(0, 1fr));
  }

  .fra-matrix-page .fra-matrix-minimap__colhead {
    font-size: 12px;
    font-weight: 600;
    text-align: center;
    color: var(--ipi-ink-3);
  }

  .fra-matrix-page .fra-matrix-minimap__rowhead {
    display: flex;
    align-items: center;
    min-width: 0;
    font-size: 12px;
    font-weight: 600;
    line-height: 1.2;
    color: var(--ipi-ink-2);
    overflow-wrap: anywhere;
  }

  .fra-matrix-page .fra-matrix-minimap__cell {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 32px;
    min-width: 0;
    border: 1px solid var(--ipi-rule-h);
    border-radius: 5px;
    font-size: 12px;
    font-weight: 600;
    color: var(--ipi-ink);
    background: var(--ipi-bg-card);
  }

  .fra-matrix-page .fra-matrix-minimap__cell[data-fra-mm-rating='trivial'] {
    background: color-mix(in srgb, var(--ipi-green) 8%, var(--ipi-bg-card));
    border-color: color-mix(in srgb, var(--ipi-green) 22%, var(--ipi-rule-h));
  }

  .fra-matrix-page .fra-matrix-minimap__cell[data-fra-mm-rating='tolerable'] {
    background: color-mix(in srgb, var(--ipi-green) 18%, var(--ipi-bg-card));
    border-color: color-mix(in srgb, var(--ipi-green) 34%, var(--ipi-rule-h));
  }

  .fra-matrix-page .fra-matrix-minimap__cell[data-fra-mm-rating='moderate'] {
    background: color-mix(in srgb, var(--ipi-accent) 22%, var(--ipi-bg-card));
    border-color: color-mix(in srgb, var(--ipi-accent) 38%, var(--ipi-rule-h));
  }

  .fra-matrix-page .fra-matrix-minimap__cell[data-fra-mm-rating='substantial'] {
    background: color-mix(in srgb, var(--ipi-accent) 38%, var(--ipi-bg-card));
    border-color: color-mix(in srgb, var(--ipi-accent) 55%, var(--ipi-rule-h));
  }

  .fra-matrix-page .fra-matrix-minimap__cell[data-fra-mm-rating='intolerable'] {
    background: color-mix(in srgb, var(--ipi-red) 44%, var(--ipi-bg-card));
    border-color: color-mix(in srgb, var(--ipi-red) 60%, var(--ipi-rule-h));
  }

  .fra-matrix-page .fra-matrix-minimap__cell[data-fra-mm-rating=''] {
    border-style: dashed;
    color: var(--ipi-ink-4);
  }

  .fra-matrix-page .fra-matrix-minimap__key {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 12px;
    margin: 8px 0 0;
    font-size: 12px;
    line-height: 1.4;
    color: var(--ipi-ink-3);
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE PREMISES RECORD — one document with one spine. Rebuilt 2026-09-09.
 *
 * 🔴 IT WAS FOUR TABS AND THEY WERE NOT PEERS. Measured at 1440x1200, where the
 * scroller is 1,144px: Record 1,092px, Clause 12 3,405px (three viewports),
 * Responsible persons 1,397px, Assessments 408px. The tab bar navigated BETWEEN
 * the four and offered nothing for moving INSIDE one, so Clause 12's nineteen
 * lettered items were three screens with no index.
 *
 * ⛔ THE RAIL IS NOT `.ipi-sidenav`. That primitive shows ONE pane at a time,
 * which is the opposite of what this page is — everything is present at once and
 * the rail addresses positions within it. Reusing it would have meant fighting
 * its pane-hiding on every rule.
 * ══════════════════════════════════════════════════════════════════════════ */

.fra-record-layout {
  display: grid;
  grid-template-columns: 268px minmax(0, 1fr);
  align-items: start;
  gap: 20px;
}

/* ⚠ `min-width: 0` ON BOTH TRACKS AND IT IS LOAD-BEARING. The document holds a
   40-row factual record whose long prose values would otherwise set a min-content
   floor wider than the column, widening the grid instead of wrapping inside it. */
.fra-record-layout > * {
  min-width: 0;
}

/* R4-07 — the ≤900px "Contents" bar. Nothing above the split: there the rail is the
   layout's first column (the drawer wrapper is `display: contents`). */
.fra-record-navbar {
  display: none;
}

/* ── the rail ───────────────────────────────────────────────────────────────
 * ⚠ STICKY, AND THE OFFSET IS NOT ZERO. `.content` is the scroller; 12px keeps
 * the rail clear of the list area's own top edge rather than welding it to it.
 * ⚠ ITS OWN MAX-HEIGHT AND SCROLL. Five open groups would otherwise make the
 * rail taller than the viewport, at which point the bottom of it is unreachable
 * — the exact failure the flat 23-row list had. */
/* ⚠ R4-19 — THE CAP HAS TO FIT BESIDE THE END OF THE DOCUMENT, NOT ONLY THE WINDOW. A
   sticky box cannot outrun its grid area, and the area ends at the document's foot, which
   sits ~100px above the scroller's bottom (list-area padding and the help-launcher clearance).
   At `100vh - 140px` the rail was taller than that, so at the end of the page it rode up
   under the topbar (measured top −20px at 1440×780) and its last entries were unreachable.
   `100dvh - 220px` leaves the rail whole at every scroll position; it scrolls by itself
   and the script keeps the marked entry in view. */
.fra-rail {
  position: sticky;
  top: 12px;
  max-height: calc(100dvh - 220px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--ipi-bg-card);
  border-radius: 12px;
  box-shadow: var(--ipi-shadow-card);
  padding: 16px 12px 12px;
  scrollbar-width: thin;
}

.fra-rail::-webkit-scrollbar {
  width: 5px;
}

.fra-rail::-webkit-scrollbar-thumb {
  background: var(--ipi-bg-deep);
  border-radius: 3px;
}

/* ── the completeness meter ─────────────────────────────────────────────────
 * ⛔ IT IS A COUNT, NOT A SCORE. A blank Clause 12 item is a legitimate answer —
 * Annex A.2 turns on the item having been CONSIDERED — so the fill is the ink
 * colour and NOT a severity ramp. ⛔ Never make this red at low values. */
.fra-rail__meter {
  padding: 0 8px 14px;
  margin-bottom: 6px;
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-rail__meter-top {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 8px;
}

.fra-rail__meter-num {
  font-family: var(--font-display, inherit);
  font-size: 17px;
  font-weight: 700;
  color: var(--ipi-ink);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.fra-rail__meter-label {
  font-size: 11.5px;
  color: var(--ipi-ink-3);
  line-height: 1.35;
}

.fra-rail__meter-track {
  height: 6px;
  border-radius: 999px;
  background: var(--ipi-bg-inset);
  overflow: hidden;
}

.fra-rail__meter-fill {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: var(--ipi-ink-3);
}

.fra-rail__meter-note {
  margin: 8px 0 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--ipi-ink-4);
}

/* ── rail entries ──────────────────────────────────────────────────────── */
.fra-rail__heading {
  padding: 12px 10px 5px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ipi-ink-4);
}

.fra-rail__link {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 7px 10px;
  border-radius: 7px;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--ipi-ink-2);
  text-decoration: none;
  min-width: 0;
}

.fra-rail__link:hover {
  background: var(--ipi-bg-hover);
  color: var(--ipi-ink);
}

/* ⚠ `aria-current="location"`, NOT `="page"` — these address positions within ONE
   document, not separate pages. The tab bar's `role="tab"` wiring went with the
   tabs; "tab 3 of 4" announced for an anchor into a document is worse than
   nothing. */
.fra-rail__link[aria-current='location'] {
  background: var(--ipi-accent-bg);
  color: var(--ipi-ink);
  font-weight: 600;
}

/* ⚠ THE LETTER TEXT IS CLIPPED TO ONE LINE ON PURPOSE. These are the standard's
   own item texts and several run past 100 characters; wrapped, the five groups
   would be the 3,405px problem in a 268px column. The full text is in the
   document, and `title` carries it on hover. */
/* ⚠ TWO LINES, THEN CLIPPED — module rule for rails (2026-09-23). A one-line ellipsis cut
   the standard's own item text to "Type and use of the premis…" with nothing to read the
   rest from; two lines carry nearly every item whole. */
.fra-rail__text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  white-space: normal;
}

.fra-rail__letter {
  flex: 0 0 auto;
  font-family: var(--font-display, inherit);
  font-size: 11px;
  font-weight: 650;
  color: var(--ipi-ink-4);
}

.fra-rail__link[aria-current='location'] .fra-rail__letter {
  color: var(--ipi-accent);
}

/* ⚠ ABSENCE OR PRESENCE, NOT GOOD OR BAD. A hollow ring means nothing was
   recorded, which is legitimate — deliberately not red, and deliberately not the
   green/amber the status chips use. */
.fra-rail__dot {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ipi-ink-3);
  align-self: center;
}

.fra-rail__dot--blank {
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--ipi-ink-4);
}

/* ── the clause groups ─────────────────────────────────────────────────────
 * 🔴 FIVE GROUPS THAT OPEN ONE AT A TIME, so the rail is about ten rows at rest
 * rather than twenty-three. A rail listing a) to s) flat is the same wall of text
 * moved to the left and is not a tidier answer than no rail. */
.fra-rail__grouphead {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  padding: 7px 10px;
  border: 0;
  border-radius: 7px;
  background: none;
  font: inherit;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--ipi-ink-2);
  cursor: pointer;
  text-align: left;
}

.fra-rail__grouphead:hover {
  background: var(--ipi-bg-hover);
  color: var(--ipi-ink);
}

.fra-rail__grouphead.is-open {
  color: var(--ipi-ink);
  font-weight: 600;
}

/* ⚠ The scroll-spy marks a clause GROUP on its head, because a group has no link of
   its own — its rail entry is the disclosure button. Same treatment as a marked link so
   the rail reads as one thing. */
.fra-rail__grouphead[aria-current='location'] {
  background: var(--ipi-accent-bg);
  color: var(--ipi-ink);
  font-weight: 600;
}

.fra-rail__grouphead-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  white-space: normal;
}

.fra-rail__grouphead-range {
  flex: 0 0 auto;
  font-family: var(--font-display, inherit);
  font-size: 10.5px;
  color: var(--ipi-ink-4);
}

.fra-rail__grouphead-count {
  flex: 0 0 auto;
  font-size: 10.5px;
  color: var(--ipi-ink-4);
  font-variant-numeric: tabular-nums;
}

/* ⚠ A FIXED WIDTH AND A REAL SIZE. At 9px and auto width the caret rendered as a stray
   dot after the count and did not line up down the five group rows, which read as a typo
   rather than as a control. */
.fra-rail__grouphead::after {
  content: '\25BE';
  flex: 0 0 12px;
  font-size: 11px;
  line-height: 1;
  text-align: right;
  color: var(--ipi-ink-4);
  transition: transform 0.15s ease;
}

.fra-rail__grouphead.is-open::after {
  transform: rotate(180deg);
}

/* ⚠ `[hidden]` IS `!important` GLOBALLY (`style.css` `:where([hidden])`), so the
   closed state needs no rule of its own — and an `is-open` class could not
   override it if it tried. The list is shown by REMOVING the attribute. */
.fra-rail__letters {
  margin: 0 0 4px;
  padding: 0 0 0 10px;
  list-style: none;
  border-left: 1px solid var(--ipi-rule);
  margin-left: 12px;
}

.fra-rail__link--letter {
  padding: 5px 8px;
  font-size: 12px;
}

/* ── the document ─────────────────────────────────────────────────────────── */
.fra-record-doc {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* ⚠ MORE PADDING THAN A LIST PANEL, because this is a document being READ rather
   than a table being scanned. `.ipi-panel`'s 16px/20px is right for a list card;
   at 40 rows of factual record it crowds the content against the card edge. */
.fra-record-sec {
  padding: 20px 24px 22px;
  scroll-margin-top: 16px;
}

/* ⚠ A SECTION THE RAIL JUST JUMPED TO SAYS SO, briefly. Without it a fragment
   jump on a page of near-identical cards gives no confirmation anything moved. */
.fra-record-sec.is-targeted {
  box-shadow: var(--ipi-shadow-card), inset 0 0 0 2px var(--ipi-accent-border);
}

/* ── the factual rows, rebuilt ─────────────────────────────────────────────
 * 🔴 LABEL ABOVE VALUE, NOT BESIDE IT. `.fra-rec__row` was `minmax(0, 22rem)`
 * then `1fr` — a 352px label track inside a 590px column, leaving the value about
 * 215px. Every prose answer on Clause 12 runs to ~120 characters, so it wrapped to
 * five or six lines beside 352px of empty label. And because rows were placed
 * row-major into two columns, a tall row stretched its partner's band and left a
 * hole: items e), f) and i) each carried 100-150px of blank column.
 *
 * ⚠ `auto-fill` RATHER THAN A FIXED COLUMN COUNT, so short facts pack three-up on
 * a wide document and one-up on a narrow one with no media query.
 *
 * ⚠ AND THE STACKED VARIANT IS SCOPED. `.fra-rec` without `--stacked` is still the
 * beside-label form, because `_persons.php` uses `.fra-rec--card` and the person
 * cards read better as label/value pairs at their width. */
.fra-rec--stacked {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  align-items: start;
  column-gap: 24px;
  row-gap: 0;
}

.fra-rec--stacked .fra-rec__row {
  display: block;
  padding: 9px 0;
  border-bottom: 1px solid var(--ipi-rule);
  min-width: 0;
}

/* R4 wave 3 (9) — `--ipi-ink-3`, not `-4`: the faint ink measured 3.1:1 on white at 11.5px,
   under AA. Premises-only (`.fra-rec--stacked` renders nowhere else). */
.fra-rec--stacked .fra-rec__key {
  display: block;
  margin-bottom: 2px;
  font-size: 11.5px;
  color: var(--ipi-ink-3);
  line-height: 1.4;
}

.fra-rec--stacked .fra-rec__val {
  display: block;
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink);
  line-height: 1.5;
  /* ⚠ A READING MEASURE ON THE PROSE. Without it a 120-character answer runs the
     full 900px of the document and is a single unbroken line to track back from. */
  max-width: 62ch;
}

/* ⚠ A LONG ANSWER TAKES THE WHOLE ROW rather than setting the column width for
   every two-word fact beside it. The view decides at 64 characters. */
.fra-rec__row--wide {
  grid-column: 1 / -1;
}

/* 🔴 A BLANK IS SAID AS A BLANK. Sixteen of the Record tab's twenty-nine rows read
   "Not recorded" in the same weight and colour as a fact, so absence and answer
   were indistinguishable on a page whose whole job is to say what is known.
   ⛔ QUIETER, NOT REDDER. A blank Clause 12 item is a legitimate answer.
   R4 wave 3 (5) — `.fra-rec .fra-rec__val.fra-rec__val--blank` (0,3,0): the bare modifier
   (0,1,0) lost to `.fra-rec--stacked .fra-rec__val` (0,2,0) above, so every blank on the
   premises record rendered bold, upright and in full ink. */
.fra-rec .fra-rec__val.fra-rec__val--blank {
  font-weight: 400;
  font-style: italic;
  /* `-3`, not `-4` (3.1:1) — quiet by weight and style, still legible at AA. */
  color: var(--ipi-ink-3);
}

/* ── the clause item ──────────────────────────────────────────────────────── */
.fra-factual__item {
  scroll-margin-top: 20px;
}

/* ⚠ The item text is the standard's own wording and is the thing being answered,
   so it gets room above its answers rather than sitting tight against them. */
.fra-factual__clause {
  margin-bottom: 10px;
}

@media (max-width: 1200px) {
  .fra-record-layout {
    grid-template-columns: 232px minmax(0, 1fr);
    gap: 16px;
  }
}

/* ⚠ BELOW THE TENANT BREAKPOINT THE RAIL STOPS BEING A RAIL. A 268px sticky
   column beside a document is not available at 390px, and a sticky element that
   tall would eat the viewport. It becomes an ordinary block at the top of the
   page — a contents list, which is what it is — with its groups closed. */
@media (max-width: 900px) {
  /* R4 wave 3 (2) — A FLEX COLUMN, NOT A ONE-COLUMN GRID. A grid item's containing block
     is its own grid AREA, so the sticky Contents bar below was pinned inside a row exactly
     its own height and could never stick. As a flex item it sticks against the whole
     layout, which spans the document. */
  .fra-record-layout {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 16px;
  }

  /* R4-07 — inside the Contents sheet the rail is the sheet's content: no card of its
     own, no sticky, no scroller of its own (the sheet body scrolls). */
  .fra-rail {
    position: static;
    max-height: none;
    overflow: visible;
    background: transparent;
    border-radius: 0;
    box-shadow: none;
    padding: 4px 0 8px;
  }

  /* R4 wave 3 (2) — STICKY AT THE TOP OF THE SCROLLER. `.content` pads its top 16px and
     Chrome insets a sticky box by the scroller's padding, so `top: 0` pinned the bar 16px
     down with the record showing through the strip above it; `-16px` welds it to the
     scroller's edge (the matrix's R5-09 fix). The sections' scroll margin clears it. */
  .fra-record-navbar:not([hidden]) {
    position: sticky;
    top: -16px;
    z-index: 6;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 16px;
    background: var(--ipi-bg-card);
    border-radius: 12px;
    box-shadow: var(--ipi-shadow-card);
  }

  .fra-record-page .fra-record-sec {
    scroll-margin-top: 76px;
  }

  .fra-record-page .fra-factual__item {
    scroll-margin-top: 80px;
  }

  .fra-record-navbar .ipi-filter-rail__copy {
    min-width: 0;
    flex: 1 1 auto;
  }

  .fra-record-navbar .ipi-filter-rail__meta {
    font-size: 14px;
    font-weight: 600;
    color: var(--ipi-ink);
  }

  .fra-record-navbar .ipi-filter-rail__actions {
    flex-shrink: 0;
  }

  /* 44px targets in the sheet; the desktop rail's 30px rows are aimed with a mouse. */
  .fra-record-navdrawer .fra-rail__link,
  .fra-record-navdrawer .fra-rail__grouphead {
    min-height: 44px;
    align-items: center;
  }

  .fra-record-sec {
    padding: 16px 16px 18px;
  }

  /* Premises → Fire stopping surveys is a card list (table-cards.css): its section loses the white
     panel chrome so the cards are not cards in a card. The heading keeps its layout. */
  html:not(.rc-wide) .fra-record-sec:has(table[data-rc="card"]) {
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
  }

  .fra-rec--stacked {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ⚠ THE EDIT FORM RAIL'S CLAUSE HEADING. NOT an `.ipi-*` name, deliberately — a new
 * `ipi-*` class has to land in all THREE shells (inspections, fire-stopping, fra) or it
 * resolves to nothing in whichever mode was missed, and this heading is FRA-only. It
 * matches `.fra-rail__heading` on the reading page so the two rails read as one system. */
.fra-rail-heading {
  margin: 12px 0 4px;
  padding: 0 10px;
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  /* R1-32: `--ipi-ink-3`, not `-4`. The faint ink measures 3.10:1 on white — under AA for
     10px text. */
  color: var(--ipi-ink-3);
}

.fra-rail-heading:first-child {
  margin-top: 0;
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE ASSESSMENT RECORD — brought into line with the premises record, 2026-09-09.
 *
 * ⚠ THE SAME READING PADDING. `.ipi-panel` ships 16px/20px, which is right for a list
 * card and crowds a section of a report against its own edge — an assessment section
 * holds up to a hundred answers. `.fra-record-sec` made the same change on the premises
 * document; these two pages are the same record read from two angles and should not
 * differ in how much room the text gets.
 * ══════════════════════════════════════════════════════════════════════════ */
/* 🔴 A PANE THAT HOLDS MORE THAN ONE PANEL MUST SPACE THEM, and this was missed on the
 * first pass: `.fra-show-sections__panes` is a grid with a 16px gap, so it spaces the
 * PANES — but "About this record" is a plain `<div>` holding THREE sibling `.ipi-panel`s
 * with no margin between them, and the risk-rating, action-plan and limitations panes are
 * the same shape. The three cards rendered welded into one continuous white slab with no
 * seam, which is what padding on each of them individually cannot fix.
 *
 * ⚠ SAME 16px AS THE GRID GAP ABOVE, so a multi-panel pane and a single-panel one read
 * identically. ⚠ `[hidden]` is `display: none !important` globally, so making these flex
 * cannot stop the rail hiding them.
 */
.fra-show-sections__panes > div {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

/* ⚠ BOTH SHAPES. The catalogue sections are `<section class="ipi-panel fra-section">`
   DIRECT children of the pane wrapper; the record, risk-rating, action-plan and
   limitations panes are each wrapped in a `<div id="fra-show-…">` first. A selector for
   one of the two silently misses the other — measured: the first cut only named the
   wrapped shape and every catalogue section kept its list-card padding. */
.fra-show-sections__panes > .ipi-panel,
.fra-show-sections__panes > .fra-section,
.fra-show-sections__panes > div > .ipi-panel,
.fra-show-sections__panes > div > .fra-section {
  padding: 20px 24px 22px;
}

/* ⚠ ONE LINE, NOT A 250px EMPTY STATE, when the panel has a supersede chain under it.
 * See the note in show.php: a rich empty state directly above three rows of real content
 * announces "nothing here" over a panel that plainly has something in it. */
.fra-versions__none {
  margin: 0 0 4px;
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--ipi-ink-3);
  max-width: 68ch;
}

@media (max-width: 900px) {
  .fra-show-sections__panes > .ipi-panel,
  .fra-show-sections__panes > .fra-section,
  .fra-show-sections__panes > div > .ipi-panel,
  .fra-show-sections__panes > div > .fra-section {
    padding: 16px 16px 18px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE ASSESSMENT RECORD — UI review fixes, 2026-09-23 (R1-*).
 * Everything here is scoped to the record page; none of it touches a shell primitive.
 * ══════════════════════════════════════════════════════════════════════════ */

/* R1-16 — A LONG SECTION NAME WRAPS TO TWO LINES instead of losing its end to an ellipsis
   ("1. Premises, scope and limitati…"). ⚠ The rail's height cap and keeping the active item
   in view (R1-03) are module-wide now — fra-shell.css `.fra-page .ipi-sidenav` and
   section-rail.js `revealInRail()` — so this page adds only the wrap. Desktop only: below
   900px the rail is the bottom sheet, which already wraps. */
@media (min-width: 901px) {
  .fra-show-sections .ipi-sidenav__text {
    white-space: normal;
    text-overflow: clip;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    line-height: 1.35;
  }
}

/* R1-17 — the chosen type, one column, with a written marker rather than a border only. */
.fra-fra-type--record .fra-fra-type__options--chosen {
  grid-template-columns: minmax(0, 1fr);
}

.fra-fra-type--record .fra-fra-type__label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.fra-fra-type__chosen {
  display: inline-flex;
  align-items: center;
  min-height: 20px;
  padding: 0 8px;
  border-radius: 999px;
  background: var(--ipi-accent-bg);
  border: 1px solid var(--ipi-accent-border);
  color: var(--accent-ink, #7d5800);
  font-size: 11px;
  font-weight: 600;
}

.fra-fra-type__none {
  margin: 0;
  font-size: 13px;
  color: var(--ipi-ink-3);
}

.fra-fra-type__compare {
  margin-top: 12px;
}

.fra-fra-type__compare > .fra-fra-type__numbering {
  margin-top: 10px;
}

/* The disclosure toggles on this page read as quiet text links, not as buttons: they fold
   detail, they do not act. */
.fra-fra-type__compare-toggle,
.fra-versions__hashtoggle,
.fra-show-unanswered__summary {
  display: inline-flex;
  align-items: center;
  min-height: 32px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  text-decoration: underline;
  text-decoration-color: var(--ipi-rule-h);
  text-underline-offset: 3px;
  cursor: pointer;
  list-style: none;
}

.fra-fra-type__compare-toggle::-webkit-details-marker,
.fra-versions__hashtoggle::-webkit-details-marker,
.fra-show-unanswered__summary::-webkit-details-marker {
  display: none;
}

/* R1-13 — the content hash is a verification detail, one click away. */
.fra-versions__hashbox {
  display: inline-flex;
  flex-direction: column;
  gap: 2px;
}

.fra-versions__hashtoggle {
  min-height: 0;
  font-size: 12px;
  font-weight: 500;
  color: var(--ipi-ink-3);
}

/* R1-30 — the folded run of unanswered questions at the end of a section or block. */
.fra-show-unanswered {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--ipi-rule);
}

.fra-show-unanswered[open] > .fra-show-unanswered__summary {
  margin-bottom: 6px;
}

/* R1-27 — THE CHECK FORMS OPEN UNDER THE ROW OF BUTTONS. Opening one used to make its
   <details> a full-width flex item, which pushed the buttons after it onto a new line.
   features/fra-show.js moves each form into one tray under the row (`is-split`) and shows
   the open one there, so the buttons never move. Without the script the forms stay inside
   their <details> and the old full-width rule above still applies. */
.fra-check__actions.is-split > .fra-check__disclosure[open] {
  flex: 0 0 auto;
}

.fra-check__actions.is-split > .fra-check__disclosure[open] > summary {
  border-color: var(--ipi-ink);
}

.fra-check__tray > .fra-check__form {
  margin-top: 10px;
}

/* The done chip's green is 4.4:1 on a zebra row at 12px — a touch of ink takes it past
   AA without changing its hue family (final verification, 2026-09-24). */
.fra-page .ipi-chip--done {
  color: color-mix(in srgb, var(--ipi-green) 80%, var(--ipi-ink));
}

/* The FRA terms dialog's summary (fra/_terms_modal.php). The dialog sits outside
   `.fra-page`, so it uses no --ipi-* token. */
.fra-terms-modal__points {
  display: grid;
  gap: 8px;
  margin: 0;
  padding-left: 18px;
}

/* Gold is not a text colour (css-standards: --accent-ink for text). */
#fra-terms-modal .modal__body a {
  color: var(--accent-ink);
}

/* The 44px floor for the two style.css button primitives fra-shell.css's floor cannot
   name: a shell may only declare the ipi-* classes all three shells declare
   (FraModeWiringTest), and these two are style.css's, not the shells'. */
@media (max-width: 900px), (pointer: coarse) {
  .fra-page :is(.ipi-action-btn, .ipi-row-action-btn) {
    min-height: 44px;
  }
}

/* Setup's selects, text and date inputs stood 35 / 38 / 40px in one row on desktop. */
@media (min-width: 901px) {
  .fra-setup-page .ipi-field > :is(select, input:not([type='checkbox']):not([type='radio'])) {
    box-sizing: border-box;
    height: 40px;
    min-height: 40px;
  }
}

/* R1-10 — the app's upload card, in this module's palette and without the lift-on-hover
   (no motion on these pages). */
.fra-assessment-page .fra-upload {
  border: 1px dashed var(--ipi-rule-h);
  border-radius: 12px;
  background: var(--ipi-bg-inset);
  padding: 4px;
}

.fra-assessment-page .fra-upload .file-upload-card__action,
.fra-assessment-page .fra-upload .file-upload-card__action:hover,
.fra-assessment-page .fra-upload.file-upload-card--has-value .file-upload-card__action {
  transform: none;
  transition: none;
  box-shadow: none;
  border-radius: 9px;
  background: var(--ipi-bg-card);
  min-height: 44px;
}

.fra-assessment-page .fra-upload input[type='file']:focus-visible + .file-upload-card__action {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
}

.fra-upload__icon {
  width: 20px;
  height: 20px;
  flex: 0 0 20px;
  color: var(--ipi-ink-3);
}

.fra-upload__copy {
  display: grid;
  gap: 1px;
  min-width: 0;
}

.fra-upload__copy strong {
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-upload__hint {
  font-size: 12px;
  color: var(--ipi-ink-3);
  overflow-wrap: anywhere;
}

/* R1-11 — attachments: a one-line empty state and the add form behind a button. */
.fra-attachments__none {
  margin: 0 0 12px;
  font-size: 13px;
  line-height: 1.55;
  color: var(--ipi-ink-3);
  max-width: 72ch;
}

.fra-attachments__add > summary {
  list-style: none;
  cursor: pointer;
  width: max-content;
}

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

.fra-attachments__add[open] > .fra-attachments__form {
  margin-top: 10px;
}

/* One field ground per form: white, like every other form field. The record's forms
   borrow the filter-bar primitives (inset beige) for their fields, and below 768px
   mobile.css repaints only the selects white — so at 390px one form showed two field
   colours. ⚠ `background-color`, not the shorthand, so a select keeps its chevron. */
.fra-assessment-page :is(.fra-check__form, .fra-attachments__form, .fra-portal-visibility__form)
  :is(input.ipi-filter-search, input.ipi-filter-date, textarea.ipi-filter-search, select.ipi-filter-select) {
  background-color: var(--ipi-bg-card);
}

/* R1-08 — every control in the record's own forms meets the touch floor on a phone and
   tablet: 44px tall and 16px text (no iOS focus zoom), a 20px checkbox in a 44px row.
   These are the controls the module-wide floor in fra-shell.css does not name. */
@media (max-width: 900px) {
  .fra-assessment-page :is(.fra-check__form, .fra-attachments__form, .fra-portal-visibility__form)
    :is(input[type='text'], input[type='date'], select, textarea) {
    min-height: 44px;
    font-size: 16px;
    box-sizing: border-box;
    width: 100%;
  }

  .fra-assessment-page .fra-attachments__check {
    min-height: 44px;
    align-items: center;
  }

  .fra-assessment-page .fra-attachments__check input[type='checkbox'] {
    width: 20px;
    height: 20px;
    flex: 0 0 20px;
  }

  .fra-assessment-page :is(.fra-check__form, .fra-attachments__form, .fra-portal-visibility__form, .fra-cover-photo__form) > .btn,
  .fra-assessment-page :is(.fra-check__disclosure, .fra-attachments__add, .fra-portal-visibility__disclosure) > summary.btn {
    min-height: 44px;
  }

  /* The record page's quiet disclosure toggles still get a thumb-sized target. */
  .fra-fra-type__compare-toggle,
  .fra-versions__hashtoggle,
  .fra-show-unanswered__summary {
    min-height: 44px;
  }

  /* R1-07's delete dialog: the password box measured 39px. ⚠ `!important` because
     style.css's canonical sheet-modal input spec sets `min-height: 0 !important` with a
     9px padding; this selector is more specific, so its `!important` wins, here only. */
  .fra-assessment-page [data-fra-assessment-delete-modal] .qdm-field input[type='password'] {
    min-height: 44px !important;
    box-sizing: border-box;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE REVIEW PAGE — rebuilt 2026-09-09.
 *
 * 🔴 EVERY CONTROL ON IT WAS 1,098px WIDE. Measured: 19 selects, 10 textareas, 9 text
 * inputs and 2 date pickers, all of them the full width of the list area, because
 * `.ipi-field > input|select|textarea` sets `width: 100%` and nothing on this page ever
 * narrowed it. A yes/no dropdown and a `dd/mm/yyyy` picker rendered eleven hundred pixels
 * wide is the single biggest reason the page looked wrong, and no amount of spacing fixes
 * a control whose size tells the reader nothing about what goes in it.
 *
 * ⛔ SIZED BY FIELD TYPE, AND SCOPED TO THIS PAGE. `_control.php` already emits
 * `data-fra-field-type`, so the width comes from what the field IS. It is scoped
 * `.fra-review-page` deliberately: the same partial renders the CAPTURE form, which is a
 * different surface with its own layout decisions, and a global change here would move
 * every control on it without anyone having looked at that page.
 *
 * ⚠ `max-width`, NOT `width`. Below 900px the columns collapse and a fixed width would
 * overflow; a max-width simply stops applying once the container is narrower.
 * ══════════════════════════════════════════════════════════════════════════ */

.fra-review-page [data-fra-field-type='bool'] > select,
.fra-review-page [data-fra-field-type='enum'] > select {
  max-width: 26rem;
}

.fra-review-page [data-fra-field-type='date'] > input {
  max-width: 14rem;
}

.fra-review-page [data-fra-field-type='int'] > input,
.fra-review-page [data-fra-field-type='decimal'] > input {
  max-width: 10rem;
}

.fra-review-page [data-fra-field-type='text'] > input {
  max-width: 34rem;
}

/* ⚠ A READING MEASURE ON THE PROSE BOXES. A textarea 1,098px wide invites a single
   unbroken line the assessor cannot scan back through, and the report renders it far
   narrower than that anyway. */
.fra-review-page [data-fra-field-type='textarea'] > textarea {
  max-width: 72ch;
}

/* ── the field cards ───────────────────────────────────────────────────────
 * 🔴 TWENTY-FOUR BORDERED CARDS IN ONE COLUMN, each repeating its own "Not yet examined"
 * chip and "Considered — nothing to record" checkbox. At twenty-four repetitions the
 * chrome says nothing and the answers are what the reader came for.
 *
 * ⚠ THE CARD IS NOT REMOVED — the state chip and the checkbox are per-field facts and
 * `_control.php` owns them. What changes is that a field no longer draws a full border
 * box around itself inside a panel that already has one; a rule between fields is enough
 * to separate them and costs 2px instead of 24 boxes. */
/* 🔴 A FIELD IS A LABEL, AN ANSWER AND A STATE — AND IT WAS THREE STACKED ROWS.
 * `.ipi-field` is a flex COLUMN, so every field put its label on line one, its control on
 * line two and the "Not yet examined / Considered — nothing to record" row on line three,
 * measured at 142-256px each. Twenty-two of those is four thousand pixels in which the
 * repeated state chrome is as tall and as loud as the answers.
 *
 * ⚠ TWO COLUMNS: the answer on the left, its state on the right, on ONE line. The state
 * row is identical on every field and repeats twenty-two times — putting it in a fixed
 * right-hand column is what lets the eye skip it and read down the answers instead.
 *
 * ⛔ THE DEFAULT IS FULL WIDTH, and that matters more than the two placed children. A
 * field may also carry help text, a guidance disclosure, a mirror link or a refusal
 * message; `> *` spanning both tracks means anything this rule has not heard of keeps its
 * own row instead of being auto-placed into the state column.
 */
.fra-review-page .fra-section__body > .fra-control {
  display: grid;
  /* ⚠ `auto` THEN `1fr`, NOT `1fr` THEN `auto`. With the flexible track FIRST the state
     chip was pushed to the far right edge of the panel — a 416px select on the left and
     its own state six hundred pixels away on the right, which is the label/value gulf
     this rebuild removed from the clocks panel reappearing inside a field. Sizing the
     control track to its content puts the state immediately beside the answer it is
     about, and the flexible track becomes trailing space rather than a gap. */
  grid-template-columns: minmax(0, 1fr);
  column-gap: 0;
  row-gap: 4px;
  align-items: start;
  border: 0;
  border-radius: 0;
  border-top: 1px solid var(--ipi-rule);
  padding: 12px 0 14px;
  margin: 0;
  background: none;
  box-shadow: none;
}

.fra-review-page .fra-section__body > .fra-control > * {
  grid-column: 1 / -1;
  min-width: 0;
}

.fra-review-page .fra-section__body > .fra-control > :is(select, input, textarea) {
  grid-column: 1;
}

/* ⛔ ITS OWN `border-top` AND `padding-top` HAVE TO GO WITH THE MOVE. `.fra-field__assessed`
   draws a rule above itself, which was right when it was a full-width row UNDER the
   control and is a stray 470px stub now that it sits BESIDE one — measured as a short line
   floating between every label and its answer. The separator between fields is the
   control's own `border-top`; this one has nothing left to separate. */
.fra-review-page .fra-section__body > .fra-control > .fra-field__assessed {
  grid-column: 1;
  margin: 4px 0 0;
  padding-top: 0;
  border-top: 0;
  justify-self: start;
  white-space: nowrap;
}

/* ⚠ QUIETER, BECAUSE IT REPEATS. The chip and the checkbox are per-field facts and stay —
 * but at twenty-two repetitions they are chrome, and they were competing with the answers
 * at the same size and weight. */
.fra-review-page .fra-field__considered {
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* ── the read-only label/value panels ──────────────────────────────────────
 * 🔴 THE VALUE SAT 1,100px FROM ITS LABEL. `.ipi-keyvals__row` is
 * `justify-content: space-between`, which is right in a 300px card and wrong across the
 * full width of a list area: "Review number" at x=268 and "2" at x=1,390, with a
 * thousand pixels of nothing between a label and the thing it labels.
 *
 * ⚠ CAPPED RATHER THAN RE-LAID-OUT. `.ipi-keyvals` is a shell primitive shared with
 * three other modules; narrowing the row's container on this page keeps the primitive's
 * own behaviour and stops it stretching. */
.fra-review-page .fra-review-chain__list {
  max-width: 52rem;
}

/* ── the clocks ───────────────────────────────────────────────────────────
 * ⚠ SAME FAULT, DIFFERENT PANEL. A clock's state was pushed to the far right of an
 * 1,100px row, so "Not recorded" sat a full screen away from the clock it described.
 * The state goes on the same line as the label, and the basis wraps underneath as the
 * explanation it is. */
.fra-review-page .fra-clocks__item {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 2px 16px;
  padding: 12px 0;
  border-top: 1px solid var(--ipi-rule);
}

.fra-review-page .fra-clocks__list > .fra-clocks__item:first-child {
  border-top: 0;
  padding-top: 0;
}

.fra-review-page .fra-clocks__label {
  font-size: 13px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-review-page .fra-clocks__due {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  white-space: nowrap;
  text-align: right;
}

/* "Not recorded" / "Not applicable" stay quieter than a date, but at ink-3, not ink-4
   (3.10:1 on white at 12.5px). */
.fra-review-page .fra-clocks__item:is([data-fra-clock-state='not_recorded'], [data-fra-clock-state='not_applicable']) .fra-clocks__due {
  color: var(--ipi-ink-3);
}

/* ⚠ The basis is the sentence explaining the clock, so it spans both tracks and takes a
   reading measure rather than running the full width beside a right-aligned state. */
.fra-review-page .fra-clocks__basis,
.fra-review-page .fra-clocks__note {
  grid-column: 1 / -1;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
  max-width: 78ch;
}

/* ⛔ A CLOCK THAT IS DUE SAYS SO IN WEIGHT, NOT IN COLOUR. The rating colour ban is about
   a REPORTED risk rating and does not reach a due date — but a red "Due now" beside four
   neutral rows would still be the loudest thing on a page whose subject is a statutory
   deadline, and `--ipi-red` is already carrying refusal on this same page. Ink and weight
   separate it without competing with the error state. */
.fra-review-page [data-fra-clock-state='overdue'] .fra-clocks__due {
  color: var(--ipi-ink);
  font-weight: 700;
}

/* ⚠ R3-17 — THE FOURTH DATE LINES UP WITH THE THREE CLOCKS ABOVE IT. A `max-width: 78ch`
   on the whole row pulled its due date in from the right edge the clocks' due dates sit
   on. It is laid out as a clock row; only its basis sentence takes the reading measure,
   which the basis rule above already gives it. */
.fra-review-page .fra-clocks__newfra,
.fra-review-page .fra-clocks__obligation {
  margin: 14px 0 0;
  padding-top: 14px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-review-page .fra-clocks__newfra {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 2px 16px;
}

.fra-review-page .fra-clocks__obligation {
  max-width: 78ch;
}

.fra-review-page .fra-clocks__record-only {
  margin-top: 16px;
  padding: 14px 16px;
  border-radius: 10px;
  background: var(--ipi-bg-inset);
}

.fra-review-page .fra-clocks__subtitle {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 650;
  color: var(--ipi-ink);
}

/* ⚠ The four record-only duties were four bare list items with no marker and no shape.
   They are a list of named regulations, so they read as one. */
.fra-review-page .fra-clocks__record-only-list {
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 6px 20px;
  font-size: 12.5px;
  color: var(--ipi-ink-2);
}

/* ── the rail layout ─────────────────────────────────────────────────────── */
.fra-rev-sections__panes {
  display: grid;
  gap: 16px;
  min-width: 0;
}

/* ⚠ A pane that holds more than one panel must space them — the fault found on the
   assessment record, where three panels welded into one slab. None of this page's panes
   does today; the rule is here so none of them can. */
.fra-rev-sections__panes > div {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

/* ⚠ Same reading padding as the premises record and the assessment record. */
.fra-rev-sections__panes > div > .ipi-panel {
  padding: 20px 24px 22px;
}

.fra-rev-savebar {
  margin-top: 16px;
}

.fra-rev-savebar .ipi-panel__foot--actions {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

@media (max-width: 900px) {
  .fra-rev-sections__panes > div > .ipi-panel {
    padding: 16px 16px 18px;
  }

  .fra-review-page .fra-clocks__item,
  .fra-review-page .fra-clocks__newfra {
    grid-template-columns: minmax(0, 1fr);
  }

  .fra-review-page .fra-clocks__due {
    text-align: left;
  }
}

/* ⚠ BELOW 1200px THE TWO TRACKS STOP FITTING. A 590px textarea plus a ~330px state row
 * needs 936px and the pane is narrower than that once the rail takes its column, so they
 * stack rather than squeezing the answer. */
@media (max-width: 1200px) {
  .fra-review-page .fra-section__body > .fra-control {
    grid-template-columns: minmax(0, 1fr);
  }

  .fra-review-page .fra-section__body > .fra-control > .fra-field__assessed {
    grid-column: 1;
  }
}

/* 🔴 THE SECTION IS TWO COLUMNS, AND THAT IS WHAT KILLS THE SCROLL. `.fra-section__body`
 * is a flex COLUMN, so twenty-two fields stacked into one 860px-wide strip while using
 * about five hundred pixels of it — a four-thousand-pixel scroll down a half-empty page.
 * Most of these answers are a yes/no or a short number; two fit side by side with room to
 * spare, and the section halves.
 *
 * ⚠ `auto-fill` WITH A FLOOR, not a fixed two. Below the floor it becomes one column on
 * its own, so the rail's narrower pane and every viewport under 900px are handled by the
 * same rule rather than by a media query that has to guess the pane width.
 *
 * ⛔ A PROSE FIELD TAKES THE FULL ROW. A 72ch textarea in a 415px column is a five-line
 * box for a paragraph; these span, and so does anything carrying a list of options or a
 * judgement's extra controls, because none of them reads in half a row.
 */
/* ⛔ `min(360px, 100%)`, NOT A BARE 360px FLOOR (UI review R3-01). At 390px the pane body
 * is ~311px wide, and a 360px track floor made every field 49px wider than its own panel.
 * `min()` lets the single column shrink to the body it is in. */
.fra-review-page .fra-section__body {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(360px, 100%), 1fr));
  column-gap: 32px;
  align-items: start;
}

/* ⚠ A FIELD WITH HELP OR A GUIDANCE DISCLOSURE TAKES THE FULL ROW (R3-23). Paired with a
 * field that has neither, its control sat a paragraph lower than its neighbour's and the
 * row read as misaligned. A block is a group of questions with its own heading and takes
 * the row too (R3-03) — as the premises pane's only child it had been left in one column
 * with the other empty. */
.fra-review-page .fra-section__body > .fra-control:is(
  [data-fra-field-type='textarea'],
  [data-fra-field-type='multi'],
  [data-fra-field-type='judgement'],
  [data-fra-field-type='group'],
  :has(> .fra-field__help, > .fra-field__guard, > details)
),
.fra-review-page .fra-section__body > .fra-block,
.fra-review-page .fra-section__body > .fra-field:is(
  [data-fra-field-type='textarea'],
  [data-fra-field-type='group'],
  :has(> .fra-field__definition, > .fra-field__guard-help)
) {
  grid-column: 1 / -1;
}

/* ⚠ THE CONTROL FILLS ITS COLUMN rather than keeping the width it was given for a
 * full-width row. The per-type caps still apply to the fields that span. */
.fra-review-page .fra-section__body > .fra-control:not([data-fra-field-type='textarea']) > :is(select, input) {
  max-width: none;
  width: 100%;
}

/* ⚠ A FIELD IS A GRID CELL NOW, so the rule between rows is its own top border and the
 * first TWO cells must not draw one — `:first-child` alone leaves a stray rule over the
 * second column's opening field. */
/* ⚠ WHICH CELLS FORM THE FIRST ROW DEPENDS ON WHICH ONES SPAN (R3-23 makes that vary), so
 * `:nth-child(-n + 2)` is not the answer any more — it took the rule off a second-row
 * field whenever the first one spanned. Every cell keeps its top rule and the body clips
 * the one pixel along its own top edge, which is exactly the first row's rules. The
 * negative insets keep focus rings on the other three sides unclipped. */
.fra-review-page .fra-section__body {
  clip-path: inset(1px -8px -8px -8px);
  /* The cells' own padding and rules space the rows; the base flex gap doubled it. */
  row-gap: 0;
}

/* ⚠ READ-ONLY, A FIELD IS A RULED ROW (R3-04/05/06). The view renders `_field.php` when
 * nothing can be edited; its `.fra-field` is the capture card by default — a bordered box
 * per answer inside a panel that is already a box. Same flattening as the assessment
 * record, scoped to this page. */
.fra-review-page .fra-section__body > .fra-field:not(.fra-control),
.fra-review-page .fra-section__body .fra-block .fra-field:not(.fra-control) {
  padding: 10px 0;
  background: none;
  border: 0;
  border-top: 1px solid var(--ipi-rule);
  border-radius: 0;
}

/* ⚠ The value track is `max-content` capped by the label's 40% floor: a date or a "No"
   keeps one line (an `auto` track let "2031-09-15" break at its hyphen), and a sentence
   still wraps inside the 60% it is given. */
.fra-review-page .fra-field:not(.fra-control) .fra-factual__values .ipi-keyvals__row {
  display: grid;
  grid-template-columns: minmax(40%, 1fr) minmax(0, max-content);
  gap: 4px 16px;
  align-items: baseline;
  padding-bottom: 0;
}

/* `max-width: none` — the `.fra-factual__values` cap is a percentage, which a grid item
   resolves against its own track and so squeezed a date onto two lines. */
.fra-review-page .fra-field:not(.fra-control) .fra-factual__values .ipi-keyvals__value {
  text-align: right;
  max-width: none;
}

/* ⚠ A PARAGRAPH ANSWER READS UNDER ITS QUESTION, left-aligned at body weight — right-
   aligned bold prose beside a label is not something anyone can read. */
.fra-review-page .fra-field--textarea:not(.fra-control) .fra-factual__values .ipi-keyvals__row {
  grid-template-columns: minmax(0, 1fr);
}

.fra-review-page .fra-field--textarea:not(.fra-control) .fra-factual__values .ipi-keyvals__value {
  text-align: left;
  font-weight: 400;
  max-width: 72ch;
  white-space: pre-line;
}

.fra-review-page .fra-field:not(.fra-control) .ipi-keyvals__value.is-unanswered {
  font-weight: 400;
}

/* ⚠ The judgement's answer is already the row's value ("Satisfactory"); the finding pill
   `_field.php` adds under it said the same word a second time, in a capture-pill skin. */
.fra-review-page .fra-field:not(.fra-control) > .fra-field__findings {
  display: none;
}

/* ⚠ The authoring help ("tick … below") is an instruction for the person filling the
   field in; on a page nobody can edit it only instructs. */
.fra-review-page .fra-field:not(.fra-control) > .fra-field__help {
  display: none;
}

.fra-review-page .fra-field:not(.fra-control) .fra-field__guard-help {
  margin: 4px 0 0;
  max-width: 72ch;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

/* ── Header "More actions" dropdown — VERBATIM COPY of the inspection-show menu
   (inspection_show.css `.show-more-actions`), scoped to this page: same class names, same
   literal values, nothing new. `.show-more-actions` is forked per page by house rule (see
   css-standards.md) — the values are literals rather than tokens precisely so this menu
   cannot drift from the inspection one when a token moves.

   ⛔ NO DARK OVERRIDE, no disabled-grey, no hint span. Those were the three things an
   earlier fork of this menu invented and they are what made it stop matching.

   🔴 IT IS A `<details>`/`<summary>` AND NEEDS NO JAVASCRIPT. Six controls in one row was
   the defect; a menu that only opens once a module has loaded would be a worse one. ── */
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions {
    position: relative;
}
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions > summary {
    list-style: none;
    cursor: pointer;
}
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions > summary::-webkit-details-marker {
    display: none;
}
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__panel {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    min-width: 220px;
    padding: 8px;
    border: 1px solid rgba(0, 0, 0, 0.11);
    border-radius: 10px;
    background: var(--ipi-bg-card);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
    z-index: 12;
}
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions:not([open]) .show-more-actions__panel {
    display: none;
}
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__item {
    width: 100%;
    display: flex;
    align-items: center;
    padding: 9px 10px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: var(--ipi-ink-2);
    font: 500 12.5px/1.4 "Inter", sans-serif;
    text-decoration: none;
    cursor: pointer;
    text-align: left;
}
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__item:hover,
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__item:focus {
    background: var(--ipi-bg-hover);
    color: var(--ipi-ink);
}
/* ⚠ THE ONE IRREVERSIBLE ITEM IN THIS MENU READS AS ONE — close-out Stage 2. It sits
   below a divider and carries the red the rest of the module uses for a shortcoming, so a
   reader scanning the panel does not land on it by shape alone. It is NOT a filled red
   button: this deletes a DRAFT, which is a recoverable kind of mistake, and painting it
   like the issue control would overstate it. */
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__item--danger {
    color: var(--ipi-red);
}
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__item--danger:hover,
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__item--danger:focus {
    background: var(--ipi-red-bg);
    color: var(--ipi-red);
}

/* ⚠ A FORM WRAPPING A MENU ITEM MUST NOT NARROW IT. The delete control posts, so it is
   a <button> inside a <form>, and without this the form is a shrink-to-fit block and the
   item's own `width: 100%` resolves against it rather than against the panel. */
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__panel .fra-inline-form {
    display: block;
    width: 100%;
}

:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__divider {
    height: 1px;
    margin: 6px 0;
    background: rgba(0, 0, 0, 0.07);
}

/* ⚠ THE DELETE PASSWORD BOX BORROWS `.ipi-filter-select` FOR ITS SIZE AND BORDER, and with
   it the select chevron, so it read as a dropdown. A text box has no chevron. */
:is(.fra-assessment-page, .fra-premises-page) .show-more-actions__panel input[type="password"].ipi-filter-select {
    background-image: none;
}

/* ⚠ ON A PHONE THE MENU SPANS THE ACTION ROW. The header's controls wrap there, so "More"
   can sit mid-row and a panel pinned to its right edge opened 36px off the left of a 390px
   screen (premises, 2026-09-23). Anchored to the whole cluster instead, it opens beneath it
   at the cluster's width whichever slot the button landed in. */
@media (max-width: 620px) {
    :is(.fra-assessment-page, .fra-premises-page) .ipi-ph-right {
        position: relative;
    }
    :is(.fra-assessment-page, .fra-premises-page) .show-more-actions {
        position: static;
    }
    :is(.fra-assessment-page, .fra-premises-page) .show-more-actions__panel {
        left: 0;
        right: 0;
        min-width: 0;
    }
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE CAPTURE FORM — the guided run. Owner decision, 2026-09-09.
 *
 * 🔴 WHAT WAS MEASURED, on the fixture, before any of this. Section 1 of 14 —
 * 96 questions in 12 blocks, 148 field elements, 351 controls in one form:
 *
 *     phone   390 × 788    37,359px   47.4 screens   266 of 351 targets under 44px
 *     tablet  810 × 1024   30,663px   29.9 screens   304 of 350 under 44px
 *     desktop 1440 × 1144  29,402px   25.7 screens
 *
 * ⛔ THIS IS THE PAGE USED ON A TABLET, IN A STAIRWELL, SOMETIMES IN GLOVES. Every
 * rule below is either about the size of a thing a finger has to hit or about how
 * much of the answer is on screen at once. Nothing here is decoration.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── 44px, everywhere, once ────────────────────────────────────────────────
 * 🔴 THE SINGLE MOST IMPORTANT RULE ON THIS PAGE. Both Apple's and Google's guidance
 * put the floor at 44px and three quarters of this form was under it. Declared once,
 * on the page root, so a control added later cannot quietly go below.
 *
 * ⚠ `font-size: 16px` ON THE TEXT CONTROLS IS NOT A TYPE CHOICE — iOS Safari zooms the
 * whole page when a focused input is smaller, and on this form that means the assessor
 * loses their place on every single question. The canonical modal rules do the same at
 * ≤900px; this page needs it at every width because tablets are the normal device. */
.fra-capture-page :is(input, select, textarea, button, .fra-seg__opt) {
  min-height: 44px;
}

.fra-capture-page :is(
  input[type='text'], input[type='number'], input[type='date'],
  input[type='email'], input[type='tel'], select, textarea
) {
  font-size: 16px;
  padding: 11px 13px;
  border-radius: 10px;
}

/* ⚠ A SELECT DOES NOT GROW WITH LINE-HEIGHT, so it sat at the 44px floor while the text
   inputs beside it are 48px (16px × 1.55 + padding). In a judgement's extras row the
   benchmark select read as a different, smaller control. Match the text inputs. */
.fra-capture-page select:not([multiple]) {
  min-height: 48px;
  /* ⚠ THE CHEVRON IS 18px PAINTED 14px IN FROM THE RIGHT (the shell's select rule), and the
     13px padding above let a long option run under it — "Standards at the time of
     construction" at 390px (2026-09-24). 42px is the shell's own mobile select padding.
     A native select cannot wrap, and that option is wider than a 390px card at 16px, so it
     ends in an ellipsis before the chevron; the open list shows it whole. */
  padding-right: 42px;
  text-overflow: ellipsis;
}

.fra-capture-page textarea {
  min-height: 96px;
  line-height: 1.5;
}

/* ⚠ A checkbox cannot be 44px without becoming a huge box, so its LABEL carries the
   target instead and the box stays 18px inside it. */
.fra-capture-page .fra-field__considered {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-height: 44px;
  font-size: 14px;
  color: var(--ipi-ink-3);
  cursor: pointer;
}

.fra-capture-page .fra-field__considered input[type='checkbox'] {
  min-height: 0;
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
}

/* ── controls sized to what they hold ──────────────────────────────────────
 * 🔴 EVERY CONTROL WAS FULL WIDTH. Section 1 holds 27 integers, 8 decimals and 10
 * dates; "Storeys" and "Number of lifts" were given the same 700px box as a paragraph
 * of prose, which tells the assessor nothing about what goes in it and wastes the
 * width a tablet actually has.
 *
 * ⛔ SPINNERS OFF. A number input's step arrows are ~15px of target sitting inside a
 * 44px control, they fire on an accidental swipe, and there is no sensible step for a
 * floor area. The keypad comes from `inputmode` on the element instead. */
.fra-capture-page .fra-field--int > input,
.fra-capture-page .fra-field--decimal > input {
  max-width: 11rem;
  -moz-appearance: textfield;
  appearance: textfield;
}

.fra-capture-page .fra-field--int > input::-webkit-outer-spin-button,
.fra-capture-page .fra-field--int > input::-webkit-inner-spin-button,
.fra-capture-page .fra-field--decimal > input::-webkit-outer-spin-button,
.fra-capture-page .fra-field--decimal > input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.fra-capture-page .fra-field--date > input {
  max-width: 15rem;
}

.fra-capture-page .fra-field--textarea > textarea {
  max-width: 70ch;
}

/* ── the segmented yes / no ────────────────────────────────────────────────
 * 🔴 A TWO-VALUE DROPDOWN IS FOUR ACTIONS ON A PHONE — tap, sheet, scroll, tap — and
 * section 1 asks seventeen of them. Segmented, it is one tap and the answer is legible
 * without being opened.
 *
 * ⛔ RADIOS UNDER THE PAINT. The native input is what posts, what the keyboard reaches
 * and what a screen reader announces; it is moved off-screen rather than
 * `display: none`, which would take it out of the accessibility tree with it. */
.fra-capture-page .fra-seg {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.fra-capture-page .fra-seg__opt {
  position: relative;
  flex: 1 1 7rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px 14px;
  border: 1px solid var(--ipi-rule-h);
  border-radius: 10px;
  background: var(--ipi-bg-card);
  color: var(--ipi-ink-2);
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  text-align: center;
}

/* ⚠ The third option is the way back out of a wrong answer, and it is the one pressed
   least — so it is present, and narrower. */
.fra-capture-page .fra-seg__opt--clear {
  flex: 0 1 auto;
  font-weight: 500;
  color: var(--ipi-ink-3);
}

.fra-capture-page .fra-seg__opt input {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.fra-capture-page .fra-seg__opt:has(input:checked) {
  background: var(--ipi-ink);
  border-color: var(--ipi-ink);
  /* Inverse text on the ink fill. `--ipi-bg-card` is the established spelling for
     this across activity.css, analytics.css and fire-stopping-analytics.css — there
     is no separate on-ink token, and inventing one here would be a fourth. */
  color: var(--ipi-bg-card);
}

/* ⚠ THE RING IS ON THE LABEL, because the input it belongs to has no box to draw one
   on. Without this the segmented control is invisible to a keyboard. */
.fra-capture-page .fra-seg__opt:has(input:focus-visible) {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
}

/* ── one card layer, not three ─────────────────────────────────────────────
 * 🔴 A BLOCK WAS A TINTED CARD, EACH QUESTION A WHITE CARD INSIDE IT, AND THE CONTROL
 * INSIDE THAT. Three nested borders cost about 60px of a 390px screen and give the eye
 * two frames to cross before it reaches the answer. The step is the card; a question is
 * a rule between rows. */
.fra-capture-page .fra-step {
  background: var(--ipi-bg-card);
  border-radius: 14px;
  box-shadow: var(--ipi-shadow-card);
  padding: 18px 20px 20px;
}

.fra-capture-page .fra-step__head {
  margin-bottom: 6px;
}

.fra-capture-page .fra-step__count {
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ipi-ink-4);
}

.fra-capture-page .fra-step__title {
  margin: 2px 0 0;
  font-family: var(--font-display, inherit);
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ipi-ink);
  line-height: 1.2;
}

.fra-capture-page .fra-step__title:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 3px;
  border-radius: 4px;
}

/* ⚠ The block inside a step no longer draws its own card — the step is the card. Its
   heading is the step's heading, so it does not repeat either. */
.fra-capture-page .fra-step .fra-block {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  margin: 0;
}

.fra-capture-page .fra-step .fra-block > .fra-block__title,
.fra-capture-page .fra-step .fra-block > h3:first-child {
  display: none;
}

/* ⚠ A CHILD block keeps a heading — it is a real subdivision the step's title does not
   name — but it is a rule and a label, not a second card. */
.fra-capture-page .fra-step .fra-block .fra-block {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-capture-page .fra-step .fra-block .fra-block > .fra-block__title,
.fra-capture-page .fra-step .fra-block .fra-block > h3:first-child {
  display: block;
  margin: 0 0 4px;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ipi-ink-4);
}

/* ── one question ─────────────────────────────────────────────────────────── */
.fra-capture-page .fra-step .fra-field {
  display: block;
  padding: 16px 0;
  margin: 0;
  border: 0;
  border-top: 1px solid var(--ipi-rule);
  border-radius: 0;
  background: none;
  box-shadow: none;
}

.fra-capture-page .fra-step .fra-field:first-of-type {
  border-top: 0;
  padding-top: 8px;
}

.fra-capture-page .fra-field__label {
  display: block;
  margin-bottom: 4px;
}

.fra-capture-page .fra-field__label-text {
  font-size: 16px;
  font-weight: 650;
  color: var(--ipi-ink);
  line-height: 1.3;
}

/* ⚠ The help text sits BETWEEN the question and the control, because it qualifies the
   question. It used to sit wherever the partial put it. */
.fra-capture-page .fra-field__help,
.fra-capture-page .fra-field__hint {
  display: block;
  margin: 0 0 10px;
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--ipi-ink-3);
  max-width: 68ch;
}

/* 🔴 THE STATE ROW WAS A CHIP AND A CHECKBOX ON ALL 148 FIELDS — roughly 6,500px of
 * identical chrome in one section, at which repetition it stops being read. The chip is
 * gone and the checkbox carries the whole meaning.
 *
 * ⛔ THE CHIP IS HIDDEN, NOT DELETED. `fra-capture.js` writes to it
 * (`data-fra-assessed-chip`) and `markAssessed()` reads the row; removing the element
 * would be a change to that module's contract from a stylesheet. */
.fra-capture-page .fra-field__assessed {
  margin: 8px 0 0;
  padding: 0;
  border: 0;
}

.fra-capture-page .fra-field__assessed-chip {
  display: none;
}

/* ── the bar ──────────────────────────────────────────────────────────────
 * 🔴 IT IS FIXED TO THE BOTTOM OF THE SCREEN AT EVERY WIDTH (2026-09-09). Phone,
 * tablet and desktop: the assessor's next action is always on the bottom edge.
 *
 * ⚠ STICKY COULD NOT DO THIS, AND THE REASON IS THE CONTAINING BLOCK. `position:
 * sticky` never leaves its own parent, and this bar's parent is `.fra-capture__form`,
 * so it could only reach the bottom of the screen while the FORM reached the bottom of
 * the screen. `features/fra-capture-steps.js` hides every step but one, so a step of two
 * questions is a form a few hundred pixels tall on an 844px screen — sticky had nothing
 * to stick to and the bar simply sat where the form ended, hovering mid-screen.
 * ⛔ THIS WAS FIXED FOR ≤900px FIRST AND THAT WAS NOT ENOUGH, which is the whole reason
 * this block is no longer inside a media query. The app's real breakpoints are the three
 * `<link media>` gates in `layout/app.php` — mobile ≤768, tablet 769–1366, desktop ≥1367
 * — so a 900px ceiling left every tablet from 901px up, and every desktop, on the old
 * sticky behaviour with the same gap under the bar. Derive them, don't assume 900:
 * `grep -n 'assets/css/.*\.css' app/Views/layout/app.php | grep media=`.
 *
 * ⚠ IT RESOLVES AGAINST THE VIEWPORT because nothing on the chain — `.content`,
 * `.page-layout`, `.page-main`, `.page-section`, `.inspections-proto`, `.ipi-list-area`,
 * the form — sets `transform`, `filter`, `backdrop-filter`, `perspective`, `contain` or
 * `will-change`. Any ONE of those anywhere above this element turns `fixed` into "pinned
 * to that ancestor" and the gap comes straight back, with nothing failing and no error.
 * `.app-shell { overflow: hidden }` does NOT clip it; fixed descendants escape overflow.
 * ⛔ THE NEGATIVE MARGIN GOES WITH THE FLOW POSITION. In flow the bar bleeds
 * `calc(var(--ipi-page-gutter) * -1)` to cancel `.ipi-list-area`'s gutter; fixed to a
 * left edge it has no gutter to cancel and the same margin would drag it off-screen. The
 * gutter PADDING stays — that is what keeps its buttons in line with the cards above.
 * ⚠ z-index 1050 is deliberate: over the page (nothing there exceeds 100), UNDER the
 * mobile nav overlay at 1100 and the drawer at 1150, so an open drawer covers it.
 *
 * ⛔ THE INLINE PADDING IS `.content`'s PADDING, NOT `--ipi-page-gutter`, AND SWAPPING IT
 * BACK MISALIGNS THE BAR BY 12px BELOW 1367px. This is the one thing about the bar that
 * changed meaning when it left the flow, so it is worth the arithmetic:
 *
 *   · IN FLOW the two cancelled and the gutter was the right answer. `.inspections-proto`
 *     bleeds `margin-inline: calc(var(--ipi-page-gutter) * -1)` and `.ipi-list-area` pays
 *     the same gutter back, so a card's left edge is simply `.content`'s padding-left —
 *     the gutter drops out of the sum entirely. The bar bled and paid back the same 28px,
 *     so it landed on the cards wherever they were.
 *   · FIXED, IT IS MEASURED FROM THE VIEWPORT and the gutter no longer cancels anything.
 *     Its padding has to equal `.content`'s padding-left outright.
 *
 * ⚠ AND THOSE TWO NUMBERS ARE NOT THE SAME ON THIS PAGE. `.content` pads 16px below 1367px
 * (`mobile.css` and `tablet.css` both set `padding: 16px 16px 72px`) and 28px above it
 * (`style.css`), while `--ipi-page-gutter` is 28px above 1024px and 16px at ≤1024px
 * (`fra-shell.css` added `.fra-page` to that rule on 2026-09-23) — so between 1025 and 1366
 * they still differ. Measured on a 390px screen while the bar was still in flow: cards at
 * 16..374. Derive rather than trust this:
 * `grep -n -- '--ipi-page-gutter:' public/assets/css/fra-shell.css`. */
.fra-capture-page .fra-capture__bar {
  position: fixed;
  left: var(--fra-cap-bar-left, 0px);
  right: 0;
  bottom: 0;
  margin: 0;
  z-index: 1050;
  gap: 10px;
  flex-wrap: wrap;
  padding-inline: var(--fra-cap-bar-pad, 16px);
  padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
}

/* ⚠ A FIXED BAR RESERVES NO SPACE, so without this the last question of a section sits
 * under it with no way to scroll past. `features/fra-capture-steps.js` measures the bar
 * and publishes `--fra-cap-bar-h`, because the height is NOT constant — that module
 * reveals the step controls, which add a whole row below 900px and widen the single row
 * above it.
 * ⛔ THE FALLBACK IS THE NO-JAVASCRIPT HEIGHT, DERIVED, NOT A GUESS AT THE OTHER ONE.
 * With the module absent the step controls stay `hidden` and the bar is one row: a 44px
 * `min-height` control between 10px of padding over a 1px `border-top` — 65px. The
 * ≤900px block below re-declares it as 61px because the padding there is 8px, not 10px.
 * ⚠ THE 16px IS SEPARATION, NOT SLACK IN THE MEASUREMENT — it keeps the last card off
 * the bar's edge exactly as `.ipi-list-area`'s own gap does between cards. */
.fra-capture-page .ipi-list-area {
  padding-bottom: calc(
    var(--fra-cap-bar-h, calc(65px + env(safe-area-inset-bottom, 0px))) + 16px
  );
}

/* ⚠ THE 165px RESERVATION FOR THE "Need help?" LAUNCHER ABOVE 900px IS GONE (review R2-28,
 * 2026-09-23). The launcher is now hidden on the capture page at every width — see the
 * `.support-fab` rule at the foot of the capture block — so there is no corner to reserve,
 * and the black launcher no longer sits beside the black Next. */

/* 🔴 DESKTOP KEEPS ITS SIDEBAR, SO THE BAR MUST NOT START AT THE SCREEN EDGE.
 *
 * ⛔ 1367px IS NOT AN ARBITRARY NUMBER AND MUST MATCH `layout/app.php`. It is the exact
 * min-width of the `desktop.css` link, and `desktop.css` is the ONLY place the shell is
 * still a two-column grid: `mobile.css` (≤768) and `tablet.css` (769–1366) both re-declare
 * `.app-shell` as `display: flex; flex-direction: column` with the sidebar as a fixed
 * off-canvas drawer, so below 1367px the content really is full-bleed and `left: 0` is
 * right. Change one of the three links and this query has to move with it.
 * ⚠ 220px IS THE GRID TRACK, NOT THE SIDEBAR'S OWN WIDTH. `.app-shell` is
 * `grid-template-columns: 220px minmax(0, 1fr)` in both `style.css` and `desktop.css`;
 * `.sidebar` is a grid ITEM in column 1 with `width: auto`, so the track is what decides
 * where `.content` begins. `desktop.css`'s `.sidebar--collapsed { width: 84px }` cannot
 * move it — and nothing in the app adds that class anyway (`grep -rn sidebar--collapsed
 * public/assets/js/ app/Views/layout/app.php` returns nothing). */
@media (min-width: 1367px) {
  .fra-capture-page {
    --fra-cap-bar-left: 220px;
    --fra-cap-bar-pad: 28px;
  }
}

.fra-capture-page .fra-capture__steps {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 1 1 auto;
  min-width: 0;
  justify-content: flex-end;
}

.fra-capture-page .fra-capture__step-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 46px;
  padding: 10px 18px;
}

.fra-capture-page .fra-capture__step-of {
  margin: 0;
  font-size: 12.5px;
  color: var(--ipi-ink-3);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* ⚠ A disabled step button stays legible. `style.css` fades a disabled `.btn` to a grey
   that reads as broken rather than as "you are at the end". */
.fra-capture-page .fra-capture__step-btn[disabled] {
  opacity: 1;
  background: var(--ipi-bg-inset);
  border-color: var(--ipi-rule);
  color: var(--ipi-ink-4);
  box-shadow: none;
  cursor: default;
}

@media (max-width: 900px) {
  /* The help button is 125px wide plus its 16px inset; this keeps the bar's own controls
     out from under it without moving the button, which is global chrome. */
  /* ⚠ MEASURED, NOT GUESSED. The bar was 173px — 20% of a 390x844 screen — once the step
     row was added to the section row. The step row is what gets pressed; the section row
     (Exit / Save / next section) is compacted rather than removed, because
     FraStage5CaptureHttpTest requires Save and Exit inside `[data-fra-capture-bar]`.
     ⚠ THE HELP BUTTON NEEDS NO PADDING RESERVED FOR IT. Below 900px `.support-fab` is
     hidden outright (rule at the foot of this file, owner instruction 2026-09-09), and
     before that it was pinned to the viewport while this bar was not, so reserving 64px
     for it cost a fifth of the screen and cleared nothing.
     ⛔ THAT MEASUREMENT — "bar bottom 712, help button top 790" on a 390x844 screen — WAS
     THE BUG, READ AS REASSURANCE. A bar whose bottom edge is 132px above the bottom of the
     screen is not clearing the launcher, it is failing to reach the screen. See the
     unconditional `position: fixed` block above this query for why sticky could never get
     there and what replaced it. */
  .fra-capture-page .fra-capture__bar {
    padding-top: 8px;
    padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
    row-gap: 8px;
  }

  .fra-capture-page .fra-capture__save {
    padding: 8px 12px;
    font-size: 13.5px;
  }

  .fra-capture-page .fra-step {
    padding: 16px 14px 18px;
  }

  .fra-capture-page .fra-capture__step-of {
    flex: 0 0 auto;
    padding: 0 4px;
  }

  /* ⚠ THE NO-JAVASCRIPT FALLBACK IS 4px SHORTER HERE THAN THE ONE ABOVE, and that is the
   * whole content of this rule. The bar's vertical padding drops from 10px to 8px in this
   * query, so a one-row bar measures 8 + 44 + 8 + 1 = 61px rather than 65px. Everything
   * else about the reservation — the measured `--fra-cap-bar-h`, the 16px separation — is
   * declared once, unconditionally, above.
   * ⛔ THE `position: fixed` BLOCK IS NOT REPEATED HERE. It used to live in this query and
   * that was the bug: tablets from 901px and every desktop were left on the old sticky
   * behaviour. It is now unconditional. */
  .fra-capture-page .ipi-list-area {
    padding-bottom: calc(
      var(--fra-cap-bar-h, calc(61px + env(safe-area-inset-bottom, 0px))) + 16px
    );
  }
}

/* ── the calmer save bar — 2026-09-23 ──────────────────────────────────────
 * The owner found the bar "a little too busy": seven controls in two rows on a phone.
 * It is now ONE row at every width —
 *   [Save] [⋯] ........ [←] [Step 2 of 12] [Next →]
 * and on the last step Next gives way to "Save & next section". Previous section, Next
 * section and Exit live in the ⋯ menu (a native <details>, so it opens with no script).
 * `fra/assessment/capture.php` carries the reasoning; this is only the layout.
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md. */
.fra-capture-page .fra-capture__bar {
  flex-wrap: nowrap;
  align-items: center;
  gap: 8px;
}

.fra-capture-page .fra-capture__steps {
  flex: 0 1 auto;
  margin-left: auto;
  justify-content: flex-end;
  gap: 8px;
}

.fra-capture-page .fra-capture__steps[hidden] + .fra-capture__advance {
  margin-left: auto;
}

.fra-capture-page .fra-capture__step-btn--back {
  min-width: 46px;
  padding-inline: 12px;
  justify-content: center;
}

.fra-capture-page .fra-capture__advance {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: 46px;
  white-space: nowrap;
}

.fra-capture-page .fra-capture__save {
  white-space: nowrap;
}

.fra-capture-page .fra-capture__save-short {
  display: none;
}

/* The ⋯ menu. */
.fra-capture-page .fra-capture__more {
  position: relative;
  flex: 0 0 auto;
}

.fra-capture-page .fra-capture__more-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 46px;
  min-height: 46px;
  padding: 0 12px;
  list-style: none;
  cursor: pointer;
  font-weight: 700;
  letter-spacing: 0.05em;
}

.fra-capture-page .fra-capture__more-btn::-webkit-details-marker {
  display: none;
}

.fra-capture-page .fra-capture__more-btn::marker {
  content: '';
}

.fra-capture-page .fra-capture__more-menu {
  position: absolute;
  left: 0;
  bottom: calc(100% + 8px);
  z-index: 2;
  display: grid;
  min-width: 250px;
  max-width: calc(100vw - 32px);
  padding: 6px;
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule);
  border-radius: 12px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.16);
}

.fra-capture-page .fra-capture__more-item {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 48px;
  padding: 6px 12px;
  border-radius: 8px;
  color: var(--ipi-ink);
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
}

.fra-capture-page .fra-capture__more-item:hover,
.fra-capture-page .fra-capture__more-item:focus-visible {
  background: var(--ipi-bg-hover);
}

.fra-capture-page .fra-capture__more-item small {
  display: block;
  font-size: 12px;
  font-weight: 400;
  color: var(--ipi-ink-3);
}

/* A phone keeps one row by shortening words, never by dropping a control. */
@media (max-width: 620px) {
  .fra-capture-page .fra-capture__save-long,
  .fra-capture-page .fra-capture__step-word,
  .fra-capture-page .fra-capture__step-answered {
    display: none;
  }

  .fra-capture-page .fra-capture__save-short {
    display: inline;
  }

  .fra-capture-page .fra-capture__save {
    flex: 0 0 auto;
  }

  /* 🔴 SAVE STAYS ON THE LAST STEP (review R2-11, 2026-09-23). This used to hide it there,
     on the reasoning that "Save & next section" saves too — but that one LEAVES, so on a
     phone the action plan and every one-step section had no way to save and stay put. Save
     keeps its compact "Save"; the advance button drops to "Save & next" to make the room,
     and on the last step the "12 of 12" counter steps aside (Back is still there). */
  .fra-capture-page .fra-capture__advance-long {
    display: none;
  }

  .fra-capture-page .fra-capture__bar.is-last-step .fra-capture__step-of {
    display: none;
  }

  .fra-capture-page .fra-capture__advance {
    padding-inline: 14px;
  }

  .fra-capture-page .fra-capture__step-btn {
    padding-inline: 14px;
  }
}

/* ── the capture frame, review fixes — 2026-09-23 ─────────────────────────
 * ⛔ Dark mode is out of scope — see .claude/rules/css-standards.md. */

/* R2-04 — ONE CARD LAYER. The section panel was a white card holding the step's white card,
   so a question sat 30px inside two frames (283px of a 390px screen). The step is the card;
   the section panel keeps its element and data hooks and loses only its chrome. */
.fra-capture-page .fra-capture__form > .fra-section {
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
}

/* R2-09 — TOASTS SIT ABOVE THE BAR, NOT ON IT. The stack is fixed 22px off the bottom at
   z-index 9999, so for seven seconds after every save it covered Save, Back and Next.
   `--fra-cap-bar-h` is the bar's measured height (published by fra-capture-steps.js); the
   fallback is the no-script bar. `body:has()` because the stack is a child of <body>. */
body:has(.fra-capture-page) .toast-stack {
  bottom: calc(var(--fra-cap-bar-h, calc(65px + env(safe-area-inset-bottom, 0px))) + 12px);
}

/* R2-21 — A FOCUSED FIELD IS NEVER LEFT UNDER THE FIXED BAR. Tabbing or the phone's "next"
   key scrolls `main.content` just far enough to show the control, and a fixed bar reserves no
   space, so the control could land behind it. `scroll-padding` is what focus scrolling and
   anchor jumps honour. */
body:has(.fra-capture-page) main.content {
  scroll-padding-bottom: calc(var(--fra-cap-bar-h, calc(65px + env(safe-area-inset-bottom, 0px))) + 16px);
  scroll-padding-top: 16px;
}

/* R2-10 — THE FOCUS RING GOES ON THE PILL, NOT THE DOT. A refused save focuses the chosen
   finding's radio; the 44px floor on every input made its own outline a tall gold box
   round a 13px dot. The ring is drawn on the pill (the label) instead. */
.fra-capture-page .fra-field__finding input[type='radio']:focus-visible {
  outline: none;
  box-shadow: none;
}

.fra-capture-page .fra-field__finding > label:has(input:focus-visible) {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
}

/* R2-10 — one row per refused answer; its messages stack under the label. */
.fra-capture-page .fra-form-errors__item:has(.fra-form-errors__why + .fra-form-errors__why) .fra-form-errors__why {
  flex-basis: 100%;
}

/* R2-25 — a one-step section prints no step heading, so its card needs no head room. */
.fra-capture-page .fra-step--only {
  padding-top: 8px;
}

/* ── "Save to show them", and "Add another action" — 2026-09-23 ─────────── */
.fra-capture-page .fra-field__reveal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-top: 10px;
  padding: 10px 12px;
  border-left: 3px solid var(--ipi-accent);
  border-radius: 8px;
  background: var(--ipi-accent-bg);
}

.fra-capture-page .fra-field__reveal p {
  flex: 1 1 220px;
  margin: 0;
  font-size: 13px;
  color: var(--ipi-ink-2);
}

.fra-capture-page .fra-actions__add {
  margin-top: 12px;
  min-height: 44px;
}

/* ── tablet: use the width ────────────────────────────────────────────────
 * ⚠ TWO COLUMNS OF QUESTIONS FROM 700px, which is where an iPad in portrait lands.
 * A step of sixteen short answers is one screen at two columns and two at one.
 * ⛔ PROSE AND ANYTHING WITH ITS OWN STRUCTURE SPANS. A 70ch textarea in a half column
 * is a five-line box for a paragraph, and a photo strip or a repeatable group has its
 * own internal layout that a half column would break. */
/* 🔴 REBUILT 2026-09-23 (review R2-02, R2-13, R2-26) — A FIXED TWO-COLUMN GRID ABOVE 900px,
 * ONE COLUMN AT AND BELOW IT.
 *
 * ⛔ `auto-fill, minmax(300px, 1fr)` WAS THREE COLUMNS AT 1440px, while the rule that drops
 * the top border only knew about TWO opening cells (`:nth-child(-n + 2)`), so every third
 * cell carried a stray rule; and at 1024/820 the count depended on the sidebar. Two columns
 * is what a question plus its answer can use; the breakpoint is the app's own 900px.
 *
 * ⛔ THE SPAN LIST MISSED THE REPEATABLE GROUP. A group renders as `section.fra-group`, not
 * as `.fra-field[data-fra-field-type='group']`, so the old selector never matched and every
 * group was squeezed into one column (335/432/340px). Anything that is not a plain question
 * now spans: groups, the bulk control, the mirror summary, reveal notes, judgements, the
 * multi checklist, photograph fields and any select whose options are sentences.
 * ⚠ A plain textarea no longer spans: at two columns a half row is ~500px, a comfortable box.
 *
 * ⚠ THE ROW RULES ARE CLIPPED, NOT COUNTED. Every question draws its rule above itself and
 * the container clips its own first pixel row (`clip-path` with the other three edges pushed
 * far outwards), so whichever cells open the first row lose their rule and every later row
 * keeps one — whatever spans, and whatever the planner hides. */
.fra-capture-page :is(.fra-step__body, .fra-block__body) {
  clip-path: inset(2px -100vmax -100vmax -100vmax);
}

/* ⚠ SO NO QUESTION IS SPECIAL ANY MORE. `:first-of-type` gave the first card 8px of top
   padding and no rule, which in two columns put its label 8px above its neighbour's. */
.fra-capture-page .fra-step .fra-field:first-of-type {
  padding-top: 16px;
  border-top: 1px solid var(--ipi-rule);
}

@media (min-width: 901px) {
  /* ⚠ `.fra-block__body` IS THE CONTAINER, NOT `.fra-step__body`. A step whose content is
     a block nests its questions one level deeper, so a grid on the step body alone made
     grid items of the BLOCK — one item, one column. A CHILD block's body is a container too. */
  .fra-capture-page .fra-step__body > .fra-block > .fra-block__body,
  .fra-capture-page .fra-block--child > .fra-block__body,
  .fra-capture-page .fra-step__body {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 32px;
    align-items: start;
  }

  /* ⚠ PROSE SPANS AGAIN (review R2-13, 2026-09-24). A textarea in a half row left the other
     half empty whenever its neighbour was also prose or a spanning card — "Purpose of the
     assessment" sat beside a hole. A paragraph box takes the row. */
  .fra-capture-page :is(.fra-step__body, .fra-block__body) > :not(.fra-field),
  .fra-capture-page :is(.fra-step__body, .fra-block__body) > .fra-field:is(
    [data-fra-field-type='multi'],
    [data-fra-field-type='judgement'],
    [data-fra-field-type='group'],
    [data-fra-field-type='photo'],
    [data-fra-field-type='textarea'],
    .fra-field--wide,
    :has(> [data-fra-photos])
  ) {
    grid-column: 1 / -1;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE CAPTURE CONTROLS — review fixes, 2026-09-23 (R2-02 … R2-31).
 * Every rule here is scoped to `.fra-capture-page`; the record and review pages that share
 * these partials are untouched. Colours are `--ipi-*` tokens, all declared on `.fra-page` in
 * fra-shell.css. No motion.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── one question: its help UNDER the control (R2-13) ──────────────────────
 * In two columns, help text above one control and none above its neighbour put the two
 * controls at different heights. The card is a column and the help (and a value's definition)
 * is ordered after the answer; the DOM order — label, help, control — is unchanged, so a
 * screen reader still hears the help before the control. A judgement keeps its help above the
 * findings: there it qualifies the question being judged. */
.fra-capture-page .fra-step .fra-field.fra-control {
  display: flex;
  flex-direction: column;
}

.fra-capture-page .fra-step .fra-field:not(.fra-field--judgement) > :is(.fra-field__help, .fra-field__definition) {
  order: 1;
  margin: 8px 0 0;
}

.fra-capture-page .fra-step .fra-field > .fra-field__assessed {
  order: 2;
}

.fra-capture-page .fra-step .fra-field > :is(.fra-control__mirror-link, .fra-attach-point, a) {
  align-self: flex-start;
}

/* ── read-only mirrors, compact (R2-13, R2-19) ───────────────────────────── */
.fra-capture-page .fra-control--mirror .fra-control__mirror-value {
  font-size: 14px;
  color: var(--ipi-ink-2);
}

.fra-capture-page .fra-mirrorset {
  padding: 12px 14px;
  margin: 8px 0;
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
}

.fra-capture-page .fra-mirrorset__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 4px 12px;
  margin: 0 0 6px;
}

.fra-capture-page .fra-mirrorset__title {
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ipi-ink-3);
}

.fra-capture-page .fra-mirrorset__link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-blue);
}

.fra-capture-page .fra-mirrorset__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(240px, 100%), 1fr));
  gap: 8px 24px;
  margin: 0;
}

.fra-capture-page .fra-mirrorset__row {
  min-width: 0;
  padding: 0;
  background: none;
}

.fra-capture-page .fra-mirrorset__label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ipi-ink-3);
}

.fra-capture-page .fra-mirrorset__value {
  margin: 2px 0 0;
  font-size: 14px;
  color: var(--ipi-ink);
  overflow-wrap: anywhere;
}

.fra-capture-page .fra-mirrorset__value.is-blank {
  font-style: italic;
  color: var(--ipi-ink-3);
}

/* ── the answer choices: ONE pill design (R2-20) ─────────────────────────────
 * The segmented yes/no and the six finding states were two designs — the findings kept a
 * visible blue native radio in a gold-tinted pill. Both are now the segmented control's pill,
 * with the native radio moved off-screen (still what posts, what the keyboard reaches and what
 * a screen reader announces) and the focus ring on the pill rather than a 13px dot. */
.fra-capture-page .fra-field__findings {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: 8px;
}

@media (min-width: 621px) {
  .fra-capture-page .fra-field__findings {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.fra-capture-page .fra-field__finding {
  position: relative;
  font-size: 15px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule-h);
  border-radius: 10px;
  box-shadow: none;
}

.fra-capture-page .fra-field__finding > label {
  justify-content: center;
  padding: 8px 10px;
  text-align: center;
  line-height: 1.25;
}

.fra-capture-page .fra-field__finding input[type='radio'] {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.fra-capture-page .fra-field__finding.is-selected,
.fra-capture-page .fra-field__finding:has(input:checked) {
  color: var(--ipi-bg-card);
  font-weight: 650;
  background: var(--ipi-ink);
  border-color: var(--ipi-ink);
  box-shadow: none;
}

.fra-capture-page .fra-field__finding:has(input:focus-visible) {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
}

/* ⚠ "NOT RECORDED" CHOSEN IS THE ABSENCE OF AN ANSWER, so it must not look like one. It was
   the same solid black as a chosen Yes; it is a quiet outline now. */
.fra-capture-page .fra-seg {
  flex-wrap: nowrap;
}

.fra-capture-page .fra-seg__opt {
  flex: 1 1 0;
  min-width: 0;
  padding-inline: 10px;
}

.fra-capture-page .fra-seg__opt--clear {
  flex: 0 1 auto;
}

.fra-capture-page .fra-seg__opt--clear:has(input:checked) {
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-card);
  border-color: var(--ipi-ink-3);
  box-shadow: inset 0 0 0 1px var(--ipi-ink-3);
}

/* ── the multi-choice checklist (R2-18) ──────────────────────────────────── */
.fra-capture-page .fra-checklist {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
  gap: 8px;
}

.fra-capture-page .fra-checklist__opt {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 8px 12px;
  font-size: 15px;
  line-height: 1.3;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule-h);
  border-radius: 10px;
  cursor: pointer;
}

.fra-capture-page .fra-checklist__opt input[type='checkbox'] {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  min-height: 0;
  margin: 0;
  accent-color: var(--ipi-ink);
}

.fra-capture-page .fra-checklist__opt:has(input:checked) {
  color: var(--ipi-ink);
  font-weight: 600;
  border-color: var(--ipi-ink);
}

/* ── a judgement's secondary parts (R2-15, R2-23) ──────────────────────── */
.fra-capture-page .fra-field__action-link {
  margin: 6px 0 0;
}

.fra-capture-page .fra-field__action-link a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-blue);
}

.fra-capture-page .fra-control__more-toggle {
  align-self: flex-start;
  margin-top: 10px;
  padding: 8px 14px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-card);
  border: 1px dashed var(--ipi-rule-h);
  border-radius: 10px;
  cursor: pointer;
}

.fra-capture-page .fra-control__more-toggle:hover {
  border-style: solid;
  color: var(--ipi-ink);
}

.fra-capture-page .fra-control__more-toggle:focus-visible {
  outline: 2px solid var(--ipi-accent);
  outline-offset: 2px;
}

.fra-capture-page .fra-control__limitations-known {
  display: block;
  margin-top: 4px;
  color: var(--ipi-ink-2);
}

.fra-capture-page .fra-field__sublabel {
  display: block;
  margin: 10px 0 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink-2);
}

/* ── the P-guard badge and toggle (R2-27) ─────────────────────────────────
   "Role preferred" wrapped to two lines, 41px tall. A badge is one line. */
.fra-capture-page .fra-field__guard-badge {
  white-space: nowrap;
  cursor: help;
}

@media (max-width: 900px) {
  .fra-capture-page .fra-field__guard-toggle,
  .fra-capture-page .fra-attach-point__link {
    min-height: 44px;
  }

  .fra-capture-page .fra-attach-point__link {
    display: inline-flex;
    align-items: center;
  }
}

/* ── the bulk control (R2-24) ───────────────────────────────────────────── */
.fra-capture-page .fra-bulk {
  align-items: center;
  margin-bottom: 4px;
}

.fra-capture-page .fra-bulk__btn {
  min-height: 44px;
}

.fra-capture-page .fra-bulk__done {
  flex: 1 1 100%;
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-green);
}

/* ── repeatable groups (R2-02, R2-29) ─────────────────────────────────────
 * The instance padding was `10px 0`, so the spare row's inputs touched its inset edge. And on a
 * phone a group was four frames deep (step card › group card › instance › spare-row tint); the
 * group's own card is dropped below 900px and the rule above it separates it instead. */
.fra-capture-page .fra-group__instance {
  padding: 10px 12px 10px 0;
}

.fra-capture-page .fra-group__instance--new {
  padding-left: 8px;
}

/* ⚠ The spare row is not an answer anybody has considered — it is where a new one goes — so
   its "considered" boxes are not offered (R2-06). They stay in the form, and are ignored by the
   writer when the row posts blank. */
.fra-capture-page .fra-group__instance--new .fra-field__assessed {
  display: none;
}

.fra-capture-page .fra-group__instance > :not(.fra-group__instance-number) {
  grid-column: 2;
  min-width: 0;
}

.fra-capture-page .fra-group__new-title {
  margin: 0;
  font-size: 14px;
  font-weight: 650;
  color: var(--ipi-ink);
}

.fra-capture-page .fra-group__new-hint {
  display: block;
  font-size: 13px;
  font-weight: 400;
  color: var(--ipi-ink-3);
}

.fra-capture-page .fra-group__instance-tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 8px 12px;
}

.fra-capture-page .fra-group__clear {
  min-height: 44px;
  padding: 0 4px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-red);
  background: none;
  border: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.fra-capture-page .fra-group__cleared {
  margin: 0;
  font-size: 13.5px;
  color: var(--ipi-ink-2);
}

@media (max-width: 900px) {
  .fra-capture-page .fra-step .fra-group {
    padding: 16px 0 0;
    background: none;
    border: 0;
    border-top: 1px solid var(--ipi-rule);
    border-radius: 0;
  }
}

/* ── the photograph strip's removal question (R2-31) ─────────────────── */
.fra-capture-page .fra-photos__confirm {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  margin: 10px 0 0;
  padding: 10px 12px;
  font-size: 14px;
  color: var(--ipi-ink);
  background: var(--ipi-bg-inset);
  border-radius: 10px;
}

.fra-capture-page .fra-photos__confirm > span {
  flex: 1 1 200px;
}

.fra-capture-page .fra-photos__confirm .btn {
  min-height: 44px;
}

.fra-capture-page .fra-photos__confirm-yes {
  color: var(--ipi-red);
}

/* ── the action rows (R2-14) ────────────────────────────────────────────
 * No heading, no gap between new rows, saved rows white and new rows grey. Every action is
 * one card now, numbered, the same colour saved or not, with a gap between them. */
.fra-capture-page .fra-action-row,
.fra-capture-page .fra-action-row--new {
  margin-top: 14px;
  padding: 14px 16px 16px;
  background: var(--ipi-bg-card);
  border: 1px solid var(--ipi-rule-h);
  border-radius: 12px;
}

/* ⚠ On a phone a card inside the panel card costs 32px of a 390px screen; the rows are ruled
   off instead, each still headed by its number. */
@media (max-width: 900px) {
  .fra-capture-page .fra-action-row,
  .fra-capture-page .fra-action-row--new {
    margin-top: 8px;
    padding: 14px 0 16px;
    border: 0;
    border-top: 1px solid var(--ipi-rule-h);
    border-radius: 0;
  }
}

.fra-capture-page .fra-action-row__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 32px;
}

.fra-capture-page .fra-action-row__title {
  margin: 0;
  font-family: var(--font-display, inherit);
  font-size: 16px;
  font-weight: 700;
  color: var(--ipi-ink);
}

.fra-capture-page .fra-action-row__remove {
  min-height: 44px;
  padding: 0 4px;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-red);
  background: none;
  border: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* A SAVED action's remove (R2-14 remainder): a checkbox posted with the section's own save,
   in the red of the unsaved row's Remove so the two read as the same act. */
.fra-capture-page .fra-action-row__remove-saved {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-red);
  cursor: pointer;
  white-space: nowrap;
}

.fra-capture-page .fra-action-row__remove-saved input {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--ipi-red);
}

.fra-capture-page .fra-action-row__removing {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-red);
}

/* The row marked for removal keeps its values (they still post) but reads as going. */
.fra-capture-page .fra-action-row[data-fra-action-removing-row] > :not(.fra-action-row__head):not(.fra-action-row__removing):not(input[type="hidden"]) {
  opacity: 0.45;
}

/* ⚠ `display: contents` so the lifecycle fields stay items of the row's own grid. `[hidden]`
   is `!important` in this app, so hiding the wrapper still wins. */
.fra-capture-page .fra-action-row__lifecycle {
  display: contents;
}

.fra-capture-page .fra-actions__bands {
  padding: 0;
  background: none;
  border: 0;
}

.fra-capture-page .fra-actions__bands-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  cursor: pointer;
  list-style: none;
}

.fra-capture-page .fra-actions__bands-toggle::-webkit-details-marker {
  display: none;
}

/* ⚠ `fra-shell.css` gives every `.ipi-panel summary` a chevron `::after`; this toggle carries
   its own triangle, so the shell's is switched off (same fix as `.fra-field__guard-toggle`). */
.ipi-panel .fra-actions__bands-toggle {
  padding-right: 0;
}

.ipi-panel .fra-actions__bands-toggle::after {
  content: none;
}

.fra-capture-page .fra-actions__bands-toggle::before {
  content: "▸";
  font-size: 11px;
  color: var(--ipi-ink-3);
}

.fra-capture-page .fra-actions__bands-more[open] > .fra-actions__bands-toggle::before {
  content: "▾";
}

.fra-capture-page .fra-actions__bands-more[open] {
  padding: 4px 14px 12px;
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule-h);
  border-radius: 10px;
}

/* ── the risk matrix panel (R2-05, R2-08) ────────────────────────────────
 * The level definitions were ten bordered, filled tiles that looked like buttons. They are a
 * plain definition list: the label, and its meaning beside it. The caption sits above the grid
 * frame now, where the frame's radius cannot clip it. */
.fra-matrix__axis-values {
  gap: 4px;
}

.fra-matrix__axis-value {
  display: grid;
  grid-template-columns: minmax(7rem, max-content) minmax(0, 1fr);
  column-gap: 12px;
  padding: 4px 0;
  background: none;
  border: 0;
  border-radius: 0;
}

.fra-matrix__axis-definition {
  margin: 0;
}

@media (max-width: 620px) {
  .fra-matrix__axis-value {
    grid-template-columns: minmax(0, 1fr);
  }
}

p.fra-matrix__caption {
  margin: 14px 0 8px;
  line-height: 1.5;
}

.fra-matrix__outcome-value.is-blank {
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--ipi-ink-2);
}

/* 🔴 THE STATS STRIP IS HIDDEN ON THE CAPTURE PAGE, AND ONLY ON THIS PAGE. Four tiles —
 * two of them cut off at 390px — sat between the header and the form, and with the
 * section nav and the blockers panel there was about 560px of chrome before the first
 * question on a 788px screen. Every figure they carried is still on the page: the section
 * nav pills carry each section's count, and the step control carries this step's.
 *
 * ⛔ HIDDEN RATHER THAN REMOVED FROM THE VIEW, deliberately. The strip is built from the
 * same page model the other FRA pages use and `_stats_strip.php` is shared; dropping the
 * require would fork this page away from that model for a layout reason.
 * ⚠ Capture is NOT in `FraStage3CompositionHttpTest`'s stat-tile sweep — that walks the
 * four pages that still show one — so this does not make any assertion vacuous.
 */
.fra-capture-page .ipi-stats-strip {
  display: none;
}

/* ⚠ THE MIRROR LINK IS A DESTINATION, SO IT IS A TARGET. "Answered on setup" sends the
 * assessor to the page where a mirrored field is actually written; it rendered as a 19px
 * inline link, which is the only genuine sub-44px hit area left in the capture form once
 * the segmented labels and the checkbox labels carry their own.
 * ⚠ `inline-flex` with vertical padding rather than a min-height on an inline element —
 * `min-height` does nothing to an inline box. */
.fra-capture-page .fra-control__mirror-link {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 2px 0;
}

/* 🔴 THE HELP LAUNCHER IS HIDDEN ON CAPTURE AT EVERY WIDTH (owner-authorised review fix
 * R2-28, 2026-09-23; phones and tablets since the owner's 2026-09-09 instruction). Above
 * 900px it sat as a black pill beside the black Next on the fixed bar. The topbar's Help
 * remains on every page. The ≤900px history is kept below for the reasoning.
 *
 * `.support-fab` is pinned to the viewport bottom-right by `layout/app.php`
 * and is a direct child of `<body>`, so it sits over the one page in the module where the
 * bottom of the screen is the working area: Back, Save and Next all live there, and an
 * assessor on site is reaching past a help bubble to press them.
 *
 * ⚠ `body:has()` BECAUSE THE BUTTON IS OUTSIDE THE PAGE. It is a sibling of the page
 * section rather than a descendant, so there is no scope to reach it from — `:has()` is
 * what lets a page-level rule apply to shell chrome without the layout growing a body
 * class for one page.
 *
 * ⚠ It was kept at ≤900px only until the owner authorised the review fix above; the 165px
 * corner reservation that made room for it on the bar above 900px went with it.
 * ⚠ It is still reachable where it is hidden — the topbar carries Help on every page, and
 * the section nav is above the form. Nothing is removed, only the floating duplicate.
 */
/* The Scope step is capture's first step since workflow rebuild Stage 5, and its Save sits in the
   same bottom-right corner — the button covered it at 1536px (review fix, 2026-09-28). */
body:has(.fra-capture-page) .support-fab,
body:has(.fra-setup-page) .support-fab {
  display: none;
}

/* ══════════════════════════════════════════════════════════════════════════
 * THE CAPTURE FORM — wave-3 review fixes, 2026-09-24. Scoped to `.fra-capture-page`; every
 * colour is an `--ipi-*` token declared on `.fra-page`. No motion.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── read-only mirrors: one compact summary per source (R2-04 / R2-13 / R2-19) ──
 * The head is one line — "RECORDED ON YOUR ASSESSOR PROFILE ....... Change" — and on a phone
 * each row is label | value on one line, ruled, instead of a stacked pair. Seven assessor
 * fields were 448px as a card of stacked pairs at 390px. */
.fra-capture-page .fra-mirrorset__head {
  flex-wrap: nowrap;
}

.fra-capture-page .fra-mirrorset__title {
  flex: 1 1 auto;
  min-width: 0;
}

.fra-capture-page .fra-mirrorset__link {
  flex: 0 0 auto;
}

@media (max-width: 620px) {
  .fra-capture-page .fra-mirrorset {
    padding: 2px 12px 8px;
  }

  .fra-capture-page .fra-mirrorset__head {
    margin: 0;
  }

  .fra-capture-page .fra-mirrorset__list {
    gap: 0;
  }

  .fra-capture-page .fra-mirrorset__row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: 12px;
    align-items: baseline;
    padding: 5px 0;
    border-top: 1px solid var(--ipi-rule);
  }

  .fra-capture-page .fra-mirrorset__value {
    margin: 0;
    font-size: 13.5px;
  }
}

/* ── a repeat group's entry number, inline on a phone (R2-02) ─────────────
 * The 24px number column plus its gap took ~34px of a 311px card from every row of every
 * entry. On a phone the number is a small chip on the entry's first line, beside its title
 * (the spare row's "Add another entry") or its "Remove this entry", and the answers take the
 * full width under it. */
@media (max-width: 620px) {
  .fra-capture-page .fra-group__instance {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 10px;
    padding: 10px 0;
  }

  .fra-capture-page .fra-group__instance--new {
    padding: 10px 8px;
  }

  .fra-capture-page .fra-group__instance > * {
    flex: 1 1 100%;
    min-width: 0;
  }

  .fra-capture-page .fra-group__instance > .fra-group__instance-number {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 26px;
    height: 26px;
    padding: 0 7px;
    color: var(--ipi-ink-2);
    background: var(--ipi-bg-card);
    border: 1px solid var(--ipi-rule-h);
    border-radius: 13px;
  }

  .fra-capture-page .fra-group__instance > :is(.fra-group__new-title, .fra-group__instance-tools) {
    flex: 1 1 0;
  }
}

/* ── the risk matrix's level definitions fold on a phone or tablet (R2-08) ──
 * `fra-capture.js` marks the chosen level `.is-chosen` and the axis `[data-fra-axis-folded]`
 * until "Show every level" is pressed. Above 900px every definition stays beside its label. */
.fra-capture-page .fra-matrix__axis-toggle {
  display: none;
}

@media (max-width: 900px) {
  .fra-capture-page .fra-matrix__axis[data-fra-axis-folded] .fra-matrix__axis-value:not(.is-chosen) {
    display: none;
  }

  .fra-capture-page .fra-matrix__axis-toggle {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    color: var(--ipi-blue);
    background: none;
    border: 0;
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  .fra-capture-page .fra-matrix__axis-toggle:focus-visible {
    outline: 2px solid var(--ipi-accent);
    outline-offset: 2px;
  }
}

.fra-capture-page .fra-matrix__axis-value.is-chosen .fra-matrix__axis-label {
  color: var(--ipi-ink);
}

/* ── an action's optional fields, folded (R2-14) ──────────────────────────
 * The same disclosure shape as the priority-band legend above the rows. */
.fra-capture-page .fra-action-row__more {
  grid-column: 1 / -1;
}

.fra-capture-page .fra-action-row__more-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  cursor: pointer;
  list-style: none;
}

.fra-capture-page .fra-action-row__more-toggle::-webkit-details-marker {
  display: none;
}

/* ⚠ `fra-shell.css` gives every `.ipi-panel summary` a chevron `::after`; this toggle draws
   its own triangle, as `.fra-actions__bands-toggle` does. */
.ipi-panel .fra-action-row__more-toggle {
  padding-right: 0;
}

.ipi-panel .fra-action-row__more-toggle::after {
  content: none;
}

.fra-capture-page .fra-action-row__more-toggle::before {
  content: "▸";
  font-size: 11px;
  color: var(--ipi-ink-3);
}

.fra-capture-page .fra-action-row__more[open] > .fra-action-row__more-toggle::before {
  content: "▾";
}

.fra-capture-page .fra-action-row__more-hint {
  font-weight: 400;
  color: var(--ipi-ink-3);
}

.fra-capture-page .fra-action-row__more-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 10px;
  padding-top: 4px;
}

@media (max-width: 900px) {
  .fra-capture-page .fra-action-row__more-grid {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* W24 (2026-09-27): the standard limitations, offered above the numbered list. */
.fra-capture-page .fra-std-limitations {
  margin: 0 0 12px;
}

.fra-capture-page .fra-std-limitations > summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
}

.fra-capture-page .fra-std-limitations__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 8px;
}

.fra-capture-page .fra-std-limitations__list li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: start;
  padding: 8px 0;
  border-top: 1px solid var(--ipi-rule);
}

.fra-capture-page .fra-std-limitations__list p {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
}

/* W7 (2026-09-27): which of the finding's photographs an action shows in the report.
   44px rows so each tick is a thumb target on a phone. */
.fra-capture-page .fra-action-row__photo-choice {
  margin: 8px 0 0;
  padding: 0;
  border: 0;
  display: grid;
  gap: 2px;
}

.fra-capture-page .fra-action-row__photo-choice legend {
  padding: 0;
  margin-bottom: 4px;
  font-size: 13px;
  font-weight: 600;
}

.fra-capture-page .fra-action-row__photo-tick {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-size: 14px;
  cursor: pointer;
}

.fra-capture-page .fra-action-row__photo-tick input {
  width: 18px;
  height: 18px;
  margin: 0;
  flex: 0 0 auto;
}

.fra-capture-page .fra-action-row__photo-choice small {
  font-size: 12px;
  line-height: 1.45;
}

@media (max-width: 620px) {
  .fra-capture-page .fra-action-row__more-hint {
    display: none;
  }
}

/* An action linked to no finding cannot take a photograph yet (the note under the strip says
   why), so its caption box — disabled, 48px of nothing to type into — is not shown until the
   finding is chosen. `fra-capture.js` toggles `.is-disabled` on the zone live. */
.fra-capture-page .fra-photos:has(> .fra-photos__zone.is-disabled) > .fra-photos__caption-label {
  display: none;
}

/* A refusal written straight into the row (a saved action that could not be removed) takes
   the row's width, not one ~250px grid column. */
.fra-capture-page .fra-action-row > .ipi-field__error {
  grid-column: 1 / -1;
}

/* ── a judgement's extras get room for their longest option (R2-26) ───────
 * At 1440px four 270px columns cut "Standards at the time of construction" in the
 * Benchmark select. 320px holds it; narrower cards fall to fewer columns. */
.fra-capture-page .fra-control__judgement-extras {
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
}

/* ── the current section pill always fits the strip (R2-12) ───────────────
 * "9 Emergency response and evacuation procedures 1/14" was 359px in a 343px strip, so its
 * number and count sat under the edge fades. On a phone the current pill is capped to the
 * strip less both fades and its title ellipsises; the full title is the pill's `title`. */
@media (max-width: 620px) {
  .fra-capture-page .fra-capture__nav-link.is-current {
    max-width: calc(100% - 72px);
  }

  .fra-capture-page .fra-capture__nav-link.is-current .fra-capture__nav-title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }
}

/* ==========================================================================
   THE RECORD OF CHECK — close-out Stage 5 item 5.8
   `app/Views/fra/assessment/show/_check.php`, inside the "About this record" pane.

   ⛔ NO NEW PRIMITIVES. The panel is `.ipi-panel`, the buttons are `.btn`, the inputs are
   `.ipi-filter-search` / `.ipi-filter-select` and the disclosure is `<details>` — all of
   which already exist on this page. What is declared here is only this block's own
   spacing and the two state notes, which nothing else has.

   ⚠ EVERY COLOUR IS AN `--ipi-*` TOKEN, and all of them are declared on `.fra-page`'s
   token block in fra-shell.css. A bare `var()` with no fallback resolves to nothing
   silently, so no literal hex appears here.
   ========================================================================== */

/* ⛔ The head is a bare `.ipi-panel__head` (FraStage1ForkRetirementTest), reached through the panel. */
.fra-check > .ipi-panel__head {
  align-items: center;
}

/* 2026-09-28: a status card — a bold line saying where the check stands, a grey line saying what
   it means. */
.fra-check__state {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 12px;
  padding: 12px 14px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
}

.fra-check__state-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-ink, #1a1815);
}

.fra-check__state-text {
  color: var(--ipi-ink-3, #6b6458);
}

.fra-check__state--todo {
  background: var(--ipi-accent-bg, rgba(196, 149, 16, 0.08));
  border-color: rgba(196, 149, 16, 0.3);
}

/* The checked state reads as the violet the chip uses for the same status, so the panel
   and the header chip are saying one thing in one colour. */
.fra-check__state--done {
  color: var(--ipi-violet);
  background: var(--ipi-violet-bg);
  border-color: rgba(107, 79, 168, 0.18);
}

.fra-check__state--done .fra-check__state-title {
  color: var(--ipi-violet);
}

/* ⛔ SPACING ONLY — the rows are `.ipi-keyvals`, which the shell owns.
   Corrected 2026-09-21: this used to declare `.fra-check__fact dt/dd`, an FRA-local
   label/value pair for a shape item 3.5 already answers. `FraStage3CompositionTest` caught
   it. ⚠ The wrapping concern that justified the old two-line layout is the primitive's
   already: `.ipi-keyvals__value` wraps, so a long qualifications string does not get
   squeezed into a 90px column at 360px. */
.fra-check__facts {
  margin: 0 0 12px;
}

/* ⚠ The checker's name and qualifications are free text, and `.ipi-keyvals__value` is
   `nowrap` (written for figures). At 390px a 90-character qualification ran ~225px out of
   the card and squeezed "Checker qualifications" into a mid-word break. The label keeps its
   width; the value wraps, as `.fra-factual__values` does. */
.fra-check__facts .ipi-keyvals__label {
  flex: 0 0 auto;
}

.fra-check__facts .ipi-keyvals__value {
  min-width: 0;
  white-space: normal;
  overflow-wrap: anywhere;
  text-align: right;
}

/* The sentence that keeps a recorded check from reading as independent review (D-J). It
   is quieter than the facts above it on purpose — it is a qualification of them, not a
   warning about them. */
.fra-check__caveat {
  margin: 0 0 12px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--ipi-ink-3);
}

/* A live return is the loudest thing in the panel, because it is the one thing on it that
   somebody has to act on. */
.fra-check__returned {
  margin: 0 0 12px;
  padding: 10px 12px;
  background: var(--ipi-red-bg);
  border: 1px solid var(--ipi-red-border);
  border-radius: 8px;
}

.fra-check__returned-head {
  margin: 0 0 4px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ipi-red);
}

.fra-check__returned-body {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  white-space: pre-line;
}

.fra-check__meta {
  margin: 0 0 12px;
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* Wrapping, not a nowrap row: three disclosures at 360px is two lines and must be. */
.fra-check__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.fra-check__disclosure > summary {
  list-style: none;
  cursor: pointer;
}

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

/* The open form sits UNDER the row of summaries rather than beside them, so opening one
   does not reflow the other two out from under the pointer. */
.fra-check__disclosure[open] {
  flex: 1 1 100%;
}

.fra-check__form {
  display: grid;
  gap: 6px;
  margin-top: 10px;
  padding: 12px;
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 9px;
}

.fra-check__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  /* ink-3: ink-4 at 11px was 2.58:1 on the inset ground (under AA). */
  color: var(--ipi-ink-3);
}

/* `.ipi-filter-search` is sized for one line; a textarea borrowing it needs its own
   height and the resize handle restricted to the axis that cannot break the grid. */
.fra-check__multiline {
  min-height: 60px;
  resize: vertical;
  font: inherit;
  line-height: 1.5;
}

/* The submit is the only full-weight control in the form and it goes on its own row,
   left-aligned with the fields above it rather than stretched across them. */
.fra-check__form > .btn {
  justify-self: start;
  margin-top: 4px;
}

/* ==========================================================================
   ATTACHED DOCUMENTS — close-out Stage 6
   `app/Views/fra/assessment/show/_attachments.php` and the `file`-type attach point
   in `app/Views/fra/assessment/_control.php`.

   ⛔ NO NEW PRIMITIVES. Panel is `.ipi-panel`, buttons are `.btn` / `.ipi-row-action-btn`,
   inputs are `.ipi-filter-search` / `.ipi-filter-select` — all already on these pages.
   Only this block's own spacing is declared here.

   ⚠ EVERY COLOUR IS AN `--ipi-*` TOKEN declared on `.fra-page`'s block in fra-shell.css.
   A bare `var()` with no fallback resolves to nothing, silently.
   ========================================================================== */

.fra-attachments__empty,
.fra-attachments__frozen {
  margin: 0 0 12px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
}

.fra-attachments__summary {
  margin: 0 0 10px;
  font-size: 12px;
  color: var(--ipi-ink-3);
}

.fra-attachments__list {
  margin: 0 0 12px;
  padding: 0;
  list-style: none;
}

/* Wrapping, not nowrap: the title, the kind, the file name and the page count are four
   things and at 360px they are three lines. A nowrap row of four chips is exactly what
   overflows the narrowest budget this module has. */
.fra-attachments__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-attachments__item:last-child {
  border-bottom: 0;
}

.fra-attachments__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  min-width: 0;
}

.fra-attachments__title {
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-attachments__kind,
.fra-attachments__pages,
.fra-attachments__file {
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* A file name has no break opportunity of its own and a customer's can be very long. */
.fra-attachments__file {
  word-break: break-all;
}

/* "Not appended" is a fact the reader needs and not a warning, so it reads as a quiet chip
   rather than as an error. The document IS on the record; it is only out of the report. */
.fra-attachments__flag {
  font-size: 11px;
  font-weight: 600;
  padding: 1px 7px;
  border-radius: 999px;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule-h);
}

.fra-attachments__form {
  display: grid;
  gap: 6px;
  padding: 12px;
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 9px;
}

.fra-attachments__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  /* ink-3: ink-4 at 11px was 2.58:1 on the inset ground (under AA). */
  color: var(--ipi-ink-3);
}

/* The action plan printed on the record page: its small grey notes used the faint ink-4
   (3.10:1 on white at 11.5-12px). Scoped to the record page — the capture form's own
   copy of these rules belongs to that page. */
.fra-assessment-page .fra-action-plan__track-note {
  color: var(--ipi-ink-3);
}

.fra-attachments__check {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 4px;
  font-size: 13px;
  color: var(--ipi-ink-2);
}

/* 2026-09-28 — the cover photograph is one control: the upload card, no box around it. */
.fra-cover-photo__form {
  padding: 0;
  background: none;
  border: 0;
  max-width: 460px;
}

.fra-assessment-page .fra-cover-photo__form .fra-upload {
  border: 1px dashed var(--ipi-rule-h);
  background: var(--ipi-bg-card);
  padding: 0;
}

.fra-attachments__form > .btn {
  justify-self: start;
  margin-top: 4px;
}

/* ==========================================================================
   COVER PHOTOGRAPH — `app/Views/fra/assessment/show/_cover_photo.php`.

   ⛔ NO NEW PRIMITIVES. The panel is `.ipi-panel`, the upload form is the attachments
   panel's `.fra-attachments__form` verbatim, the remove button is `.ipi-row-action-btn`.
   Only the hint and the thumbnail are declared here, in `--ipi-*` tokens.
   ========================================================================== */

.fra-cover-photo__hint,
.fra-cover-photo__withheld {
  margin: 0 0 10px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

/* Thumbnail then the Remove control, wrapping under it at 360px. */
.fra-cover-photo__current {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px 12px;
  margin: 0 0 12px;
}

.fra-cover-photo__link {
  display: block;
  max-width: 100%;
}

/* A fixed box rather than the image's own size: a 4000px phone photograph must not set
   the width of the pane, and `object-fit: cover` crops rather than distorts. */
.fra-cover-photo__img {
  display: block;
  width: 240px;
  max-width: 100%;
  height: 160px;
  object-fit: cover;
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
  background: var(--ipi-bg-inset);
}

/* The attach point inside a capture group instance. Quieter than the panel — it is a
   pointer to the panel, not a second copy of it. */
.fra-attach-point {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 12px;
}

.fra-attach-point__empty {
  margin: 0;
  font-size: 12px;
  color: var(--ipi-ink-4);
}

.fra-attach-point__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
}

.fra-attach-point__item {
  font-size: 12px;
}

.fra-attach-point__pages {
  color: var(--ipi-ink-4);
}

/* ==========================================================================
   WORK OFFLINE — the FRA capture page's offline row. `app/Views/fra/assessment/capture.php`,
   beside the outbox. Since 2026-09-24 it only SAYS what the device is doing (the assessment is
   stored automatically; saves are kept when there is no signal) — there is no button.
   ========================================================================== */

/* 🔴 ONE LINE, NOT A CARD (review R2-04, 2026-09-23): title and status on one row at 13px.
   The two notes below it appear only when there is something to say. */
.fra-offline__copy {
  margin: 0;
  min-width: 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ipi-ink-3);
}

.fra-offline__title {
  font-weight: 650;
  color: var(--ipi-ink-2);
  margin-right: 4px;
}

.fra-offline__status[data-fra-offline-state="error"],
.fra-offline__status[data-fra-offline-state="none"] {
  color: var(--ipi-ink-2);
}

/* Answers on this section waiting on the device. */
.fra-offline__pending {
  margin: 8px 0 0;
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ipi-ink-2);
  background: var(--ipi-accent-bg);
  border: 1px solid var(--ipi-accent-border);
}

/* What the last save did — floated just above the sticky save bar, where the assessor pressed
   Save. ⚠ ABSOLUTE, NOT A ROW OF THE BAR: the bar does not wrap at desktop widths, and a
   message inside it pushed the step controls off the right edge. The bar is sticky, so it is
   the containing block. A solid background, because the form scrolls underneath it. */
.fra-capture__saved {
  position: absolute;
  left: var(--ipi-page-gutter);
  right: var(--ipi-page-gutter);
  bottom: calc(100% + 8px);
  margin: 0;
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ipi-ink-2);
  background: var(--ipi-card);
  border: 1px solid var(--ipi-accent-border);
  border-left: 3px solid var(--ipi-accent);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
}

.fra-capture__saved[data-fra-save-tone="ok"] {
  color: var(--ipi-green);
  border-color: var(--ipi-green-bg);
  border-left-color: var(--ipi-green);
}

.fra-capture__saved[data-fra-save-tone="error"] {
  color: var(--ipi-red);
  border-color: var(--ipi-red-border);
  border-left-color: var(--ipi-red);
}

.fra-offline__pending[hidden],
.fra-capture__saved[hidden] {
  display: none;
}

/* ==========================================================================
   ASSESSMENT TEMPLATE — `app/Views/fra/template/`, `?page=fra-template`.
   Rebuilt 2026-09-23 on the property workspace's layout: a grouped rail in a
   sticky column, one section per request, each question a table row, and
   editing in a `.set-modal` dialog (settings-shell.css, loaded for this page).

   ⛔ THE RAIL LAYOUT IS A COPY OF `properties.css`'s, SCOPED TO THIS PAGE, with
   its literal values. That file is not loaded here and must not be: every rule
   in it is scoped `.property-page`, and wearing that class would couple this
   page to another page's stylesheet. The meter is `jobs.css`'s
   `.jobs-railmeter`, copied for the same reason, with its `--jobs-*` tokens
   resolved to the `--ipi-*` tokens they alias. Change the property rail and
   this copy does not follow — that is the cost of not coupling them.

   ⛔ TOKENS ONLY — every colour is an `--ipi-*` from fra-shell.css — and NO
   DARK-MODE BLOCK: every customer-facing surface is light-locked.
   ========================================================================== */

.fra-template__notice {
  margin: 0 0 16px;
  padding: 12px 14px;
  border: 1px solid var(--ipi-accent-border);
  border-radius: 10px;
  background: var(--ipi-accent-bg);
  color: var(--ipi-ink-2);
  font-size: 13px;
  line-height: 1.55;
}

/* ⛔ `style.css` styles the bare `form` TAG as a padded white card, and only
   `.modal form` undoes it. Every form on this page outside a dialog is a
   wrapper, not a card. */
.fra-template-page .fra-template-doc form {
  background: transparent;
  padding: 0;
  border-radius: 0;
  display: block;
}

/* ── the record: the rail column and the document ──────────────────────── */

.fra-template-page .fra-template-record {
  display: grid;
  grid-template-columns: 250px minmax(0, 1fr);
  align-items: start;
  gap: 20px;
}

/* ⚠ `min-width: 0` on both tracks, and it is load-bearing: a long question
   would otherwise set a min-content floor wider than the column. */
.fra-template-page .fra-template-record > *,
.fra-template-page .fra-template-doc,
.fra-template-page .fra-template-railcol {
  min-width: 0;
}

/* THE COLUMN IS STICKY, NOT THE NAV — the property rail's fix, for its reason:
   the nav sits inside the grid item, which is exactly its own height, so a
   sticky nav has no room to move. The column carries the cap and scrolls. */
/* ⚠ R5-07 (2026-09-23): `top: 16px`, NOT 76px. The scroller already starts BELOW the
   56px topbar, so 76 floated the rail 70px down the column with a gap above it. At
   ≤1366px `.content` has 16px of top padding, where a sticky box already stops, so
   `top: 0` there gives the same 16px. The cap follows the same arithmetic: the
   scroller's top (56px) + 16px above + 16px below. */
@media (min-width: 901px) {
  .fra-template-page .fra-template-railcol {
    position: sticky;
    top: 16px;
    max-height: calc(100vh - 88px);
    max-height: calc(100dvh - 88px);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    padding: 4px;
    margin: -4px;
  }

  .fra-template-page .fra-template-railcol::-webkit-scrollbar {
    width: 5px;
  }

  .fra-template-page .fra-template-railcol::-webkit-scrollbar-thumb {
    background: var(--ipi-bg-deep);
    border-radius: 3px;
  }

  /* R5 wave 3 (13) — ONE SCROLLER. The shell's `.ipi-sidenav` carries its own max-height
     and `overflow-y: auto`, so the nav scrolled INSIDE the scrolling column: two nested
     scrollbars, and `revealActiveRailItem` moved the outer while the entry sat clipped in
     the inner. The column is the only scroller; the nav is plain content in it. The
     column's inline max-height (fra-template.js) fits it to the room on screen. */
  .fra-template-page .fra-template-railcol .fra-template-sidenav {
    position: static;
    max-height: none;
    overflow: visible;
  }
}

@media (min-width: 901px) and (max-width: 1366px) {
  .fra-template-page .fra-template-railcol {
    top: 0;
  }
}

/* ⚠ R5-07: TWO LINES, THEN AN ELLIPSIS — "1. Premises, scope and limitati…" cut the one
   word that says what the section is. */
.fra-template-page .fra-template-sidenav .ipi-sidenav__text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  white-space: normal;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  line-height: 1.3;
}

.fra-template-page .fra-template-sidenav .ipi-sidenav__label {
  display: flex;
  align-items: baseline;
  gap: 8px;
}

.fra-template-page .fra-template-sidenav__num {
  flex: 0 0 auto;
  font-size: 11px;
  color: var(--ipi-ink-4);
  font-variant-numeric: tabular-nums;
}

.fra-template-page .fra-template-sidenav__item.is-active .fra-template-sidenav__num {
  color: var(--ipi-accent);
}

.fra-template-page .fra-template-sidenav__grouptotal {
  margin-left: auto;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}

/* ── the meter ─────────────────────────────────────────────────────────────
   ⛔ A PROPORTION, NOT A SCORE. Four tones and no severity: a template that is
   mostly fixed follows the standard closely. Never a red/amber ramp. */
.fra-template-page .fra-template-railmeter {
  background: var(--ipi-bg-card);
  border-radius: 12px;
  box-shadow: var(--ipi-shadow-card);
  padding: 14px 16px 15px;
  margin-bottom: 12px;
}

.fra-template-page .fra-template-railmeter__top {
  display: flex;
  align-items: baseline;
  gap: 7px;
  flex-wrap: wrap;
}

/* R5-30: the meter's own figure is gone — the stats strip's first tile prints it. */
.fra-template-page .fra-template-railmeter__label {
  font-size: 12px;
  font-weight: 600;
  line-height: 1.35;
  color: var(--ipi-ink-2);
}

.fra-template-page .fra-template-railmeter__bar {
  display: flex;
  gap: 2px;
  height: 6px;
  margin-top: 10px;
  border-radius: 999px;
  overflow: hidden;
}

.fra-template-page .fra-template-railmeter__seg {
  display: block;
  height: 100%;
}

.fra-template-page .fra-template-railmeter__legend {
  display: flex;
  flex-wrap: wrap;
  gap: 3px 11px;
  margin: 9px 0 0;
  padding: 0;
  list-style: none;
}

.fra-template-page .fra-template-railmeter__key {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  line-height: 1.4;
  color: var(--ipi-ink-4);
}

.fra-template-page .fra-template-railmeter__key::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 2px;
  background: var(--ipi-ink-4);
}

.fra-template-page .fra-template-railmeter__keynum {
  font-variant-numeric: tabular-nums;
  color: var(--ipi-ink-3);
}

.fra-template-page .fra-template-railmeter__seg--fixed          { background: var(--ipi-ink-3); }
.fra-template-page .fra-template-railmeter__seg--optional       { background: var(--ipi-blue); }
.fra-template-page .fra-template-railmeter__seg--left-out       { background: var(--ipi-bg-deep); }
.fra-template-page .fra-template-railmeter__seg--yours          { background: var(--ipi-green); }
.fra-template-page .fra-template-railmeter__key--fixed::before    { background: var(--ipi-ink-3); }
.fra-template-page .fra-template-railmeter__key--optional::before { background: var(--ipi-blue); }
.fra-template-page .fra-template-railmeter__key--left-out::before { background: var(--ipi-bg-deep); }
.fra-template-page .fra-template-railmeter__key--yours::before    { background: var(--ipi-green); }

/* Above 900px the drawer is `display: contents` (tnt-filter-drawer.css), so
   the rail column is laid out by `.fra-template-record` as if the wrapper were
   not there. There is only ever ONE rail in the DOM. */
.fra-template-page .fra-template-navbar {
  display: none;
}

/* ── the document ──────────────────────────────────────────────────────── */

.fra-template-page .fra-template-section-pane {
  display: grid;
  gap: 12px;
  min-width: 0;
}

.fra-template-page .fra-template-pane-header {
  align-items: flex-start;
  margin: 0;
}

.fra-template-page .fra-template-pane-header + .fra-template-pane-header,
.fra-template-page .fra-template-intro + .fra-template-pane-header,
.fra-template-page .ipi-table-wrap + .fra-template-pane-header {
  margin-top: 10px;
}

.fra-template-page .fra-template-pane-header__copy {
  min-width: 0;
}

.fra-template-page .fra-template-pane-header__copy .ipi-result-copy {
  margin-top: 3px;
  line-height: 1.5;
}

.fra-template-page .fra-template-pane-header__actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  flex-wrap: wrap;
  flex: 0 0 auto;
}

.fra-template-page .fra-template-intro .ipi-panel__lede + .ipi-panel__lede {
  margin-top: 8px;
}

.fra-template__policy {
  margin-top: 12px;
  padding: 10px 12px;
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
  background: var(--ipi-bg-inset);
  font-size: 12.5px;
}

.fra-template__policy > summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--ipi-ink);
}

/* R5-16: a reading measure — the reasons ran the full panel width. */
.fra-template__policy dl {
  margin: 10px 0 0;
  max-width: 68ch;
}

.fra-template__policy dt {
  margin-top: 8px;
  font-weight: 600;
  color: var(--ipi-ink);
}

.fra-template__policy dd {
  margin: 2px 0 0;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

.fra-template__schemes {
  margin: 12px 0 0;
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* The rows. The table is the shell's `.ipi-table`; these only shape the cells. */
.fra-template-page .fra-template-table__name {
  display: block;
  font-weight: 600;
  color: var(--ipi-ink);
  text-decoration: none;
}

.fra-template-page a.fra-template-table__name:hover {
  text-decoration: underline;
}

.fra-template-page .fra-template-table__num {
  display: inline-block;
  min-width: 22px;
  margin-right: 6px;
  color: var(--ipi-ink-4);
  font-variant-numeric: tabular-nums;
}

.fra-template-page .fra-template-table__sub {
  display: block;
  margin-top: 3px;
  max-width: 72ch;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ipi-ink-3);
}

.fra-template-page .fra-template-table__source {
  font-size: 12px;
  color: var(--ipi-ink-2);
}

.fra-template-page .fra-template-table__topic {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  color: var(--ipi-ink-4);
}

.fra-template-page .fra-template-table td[data-rc="pill"] .ipi-chip + .ipi-chip {
  margin-left: 4px;
}

/* R5-04: why a fixed question is fixed, under "Asked for by". */
.fra-template-page .fra-template-table__why {
  color: var(--ipi-ink-3);
}

/* ⚠ R5-06 (2026-09-23): A MOVE THAT CANNOT HAPPEN LOOKS LIKE IT. The first row's "up"
   and the last shipped row's "down" are `disabled`, and at the button's default they
   kept full ink — at 1440 the disabled pair read as pressed. Faded and not-allowed,
   but still visible, so the column keeps its shape. */
.fra-template-page .fra-template-table__move:disabled {
  opacity: 0.4;
  cursor: not-allowed;
  box-shadow: none;
  background: var(--ipi-bg-card);
}

/* R5 wave 3 (17) — AND ON THE CARDS. table-cards.css's footer-button rule (0,5,4) sets
   `opacity: 1; cursor: pointer` and outranks both the rule above and its own `:disabled`
   rule, so at 820 and 390 a move that cannot happen looked live. This selector (0,6,5)
   puts the 1440 treatment back. */
.fra-template-page table.fra-template-table[data-rc] > tbody > tr > td[data-rc="actions"] button.fra-template-table__move:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

/* The destructive controls (delete a section, reset the template) read as such. */
.fra-template-page .fra-template-danger {
  color: var(--ipi-red);
}

/* ⚠ R5-20 (2026-09-23): FIXED COLUMNS ABOVE 900px. Each answer list is its own table,
   and with `auto` layout each sized its Status column to its own chips — twenty-two
   tables, twenty-two different column edges down one page. */
@media (min-width: 901px) {
  .fra-template-page .fra-template-set .fra-template-table {
    table-layout: fixed;
    width: 100%;
  }

  .fra-template-page .fra-template-set .fra-template-table th:nth-child(2) {
    width: 220px;
  }

  .fra-template-page .fra-template-set .fra-template-table th:nth-child(3) {
    width: 104px;
  }
}

/* ── R5-08: find / filter / jump, above a section's questions ──────────── */
.fra-template-page .fra-template-finder {
  display: grid;
  gap: 8px;
  padding: 10px 12px;
  background: var(--ipi-bg-card);
  border-radius: 12px;
  box-shadow: var(--ipi-shadow-card);
}

.fra-template-page .fra-template-finder__row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.fra-template-page .fra-template-finder__search {
  flex: 1 1 220px;
  max-width: 320px;
}

.fra-template-page .fra-template-finder__search input {
  width: 100%;
}

.fra-template-page .fra-template-finder__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.fra-template-page .fra-template-finder__chip[aria-pressed="true"] {
  background: var(--ipi-ink);
  border-color: var(--ipi-ink);
  color: var(--ipi-bg-card);
}

.fra-template-page .fra-template-finder__jump {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 14px;
  font-size: 12.5px;
}

.fra-template-page .fra-template-finder__jumplabel {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ipi-ink-3);
}

.fra-template-page .fra-template-finder__jump a {
  color: var(--accent-ink, var(--ipi-ink));
  text-decoration: underline;
  text-underline-offset: 2px;
}

.fra-template-page .fra-template-finder__none {
  margin: 0;
  font-size: 13px;
  color: var(--ipi-ink-3);
}

/* ⚠ STICKY ABOVE 900px ONLY, AND AGAINST THE SAME PADDING ARITHMETIC AS THE RAIL. On a
   phone the bar would take a fifth of the screen for the whole section. */
/* R5 wave 3 (14) — WELDED TO THE SCROLLER'S TOP EDGE, with nothing showing above it. The
   bar stuck 16px below the scroller at every width (`top: 16px` over 1366, `top: 0` under
   `.content`'s 16px top padding below it — Chrome insets a sticky box by the scroller's
   padding), and the rows scrolled through that strip. The matrix's R5-09 arithmetic: cancel
   whatever padding the scroller has, so the bar's top IS the scroller's top.
   `--fra-tpl-finder-h` is the bar's measured height (fra-template.js), so a jump lands
   below it however many lines its "Jump to" row wraps to. */
@media (min-width: 901px) {
  .fra-template-page .fra-template-finder {
    position: sticky;
    top: 0;
    z-index: 4;
  }

  /* A jump — or a save returning to its row — lands below the sticky bar, not under it. */
  .fra-template-page .fra-template-questions tr[id] {
    scroll-margin-top: var(--fra-tpl-finder-h, 150px);
  }
}

@media (min-width: 901px) and (max-width: 1366px) {
  .fra-template-page .fra-template-finder {
    top: -16px;
  }
}

.fra-template-page .fra-template-table__actions,
.fra-template-page .fra-template-table td.ipi-actions {
  white-space: nowrap;
  text-align: right;
}

.fra-template-page .fra-template-table__actions .btn + .btn {
  margin-left: 4px;
}

/* A question that is left out, or an option no longer offered, reads as
   quieter — it still exists and can be switched back on. */
.fra-template-page tr.is-left-out .fra-template-table__name,
.fra-template-page tr.is-left-out .fra-template-table__sub {
  color: var(--ipi-ink-4);
}

@media (min-width: 901px) {
  .fra-template-page .fra-template-table tbody tr[data-fra-tpl-kind] {
    cursor: pointer;
  }

  .fra-template-page .fra-template-table tbody tr[data-fra-tpl-kind]:hover {
    background: var(--ipi-bg-hover);
  }

  .fra-template-page .fra-template-questions th:nth-child(2) {
    width: 30%;
  }
}

/* The panel band inside the questions table. */
.fra-template-page .fra-template-band > td {
  padding: 10px 20px 8px;
  background: var(--ipi-bg-inset);
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-template-page .fra-template-band__label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ipi-ink-2);
}

.fra-template-page .fra-template-band__hint,
.fra-template-page .fra-template-band__count {
  margin-left: 8px;
  font-size: 11.5px;
  color: var(--ipi-ink-4);
}

/* ⚠ A SAVE RETURNS TO THE ROW (`#fra-tpl-q-…`), and the app topbar is fixed —
   without this the row lands underneath it. */
.fra-template-page tr[id] {
  scroll-margin-top: 90px;
}

.fra-template-page .fra-template-set {
  scroll-margin-top: 90px;
}

/* An answer list: its own card, head above the table. */
.fra-template-page .fra-template-set__head {
  align-items: flex-start;
}

.fra-template-page .fra-template-set__copy {
  min-width: 0;
}

.fra-template-page .fra-template-set__title {
  margin: 0;
  font-family: var(--font-display, inherit);
  font-size: 15px;
  font-weight: 700;
  color: var(--ipi-ink);
}

.fra-template-page .fra-template-fixed-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
}

.fra-template-page .fra-template-fixed__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
}

.fra-template-page .fra-template-fixed__reason {
  margin: 8px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
}

.fra-template-page .fra-template-fixed__values {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 10px 0 0;
  padding: 0;
  list-style: none;
}

.fra-template-page .fra-template-fixed__values li {
  padding: 3px 8px;
  border-radius: 6px;
  background: var(--ipi-bg-inset);
  font-size: 12px;
  color: var(--ipi-ink-2);
}

.fra-template-page .fra-template-footpanel .ipi-panel__head {
  margin-bottom: 10px;
}

/* ⚠ R5-15 (2026-09-23): `form.` IN THE SELECTOR, AND IT IS THE FIX. The page-wide
   `.fra-template-page .fra-template-doc form { display: block }` above is 0,2,1 and beat
   this rule's 0,2,0, so "Add a section" and "Start again" stacked their controls. */
.fra-template-page .fra-template-doc form.fra-template-inline-form,
.fra-template-page .fra-template-inline-form {
  display: flex;
  align-items: flex-end;
  flex-wrap: wrap;
  gap: 10px;
}

.fra-template-page .fra-template-inline-form .ipi-field {
  flex: 1 1 260px;
  min-width: 0;
}

.fra-template-page .fra-template-inline-form .ipi-panel__lede {
  flex: 1 1 100%;
  margin: 0;
}

/* ── the dialogs ───────────────────────────────────────────────────────── */

.fra-template-dialog__refusal {
  margin: 10px 0 0;
  padding: 10px 12px;
  border-left: 3px solid var(--ipi-accent);
  border-radius: 6px;
  background: var(--ipi-accent-bg);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
}

.fra-template-dialog__source-text {
  margin: 4px 0 0;
  font-size: 13px;
  color: var(--ipi-ink);
}

.fra-template-dialog__changes {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.fra-template-dialog__changes[hidden],
.fra-template-dialog [data-fra-tpl-position-wrap][hidden] {
  display: none;
}

.fra-template-dialog .fra-template-dialog__danger {
  color: var(--ipi-red);
}

/* ⚠ R5-05 (2026-09-23): A REFUSED SWITCH LOOKS REFUSED. "Leave this question out" is
   `disabled` when the question may not be left out, and it rendered at full ink beside
   the refusal that explains it — so it read as a switch that simply did not work. */
.fra-template-dialog .set-toggle:has(input:disabled) {
  cursor: not-allowed;
}

.fra-template-dialog .set-toggle:has(input:disabled) input {
  opacity: 0.5;
  cursor: not-allowed;
}

.fra-template-dialog .set-toggle:has(input:disabled) > span {
  color: var(--ipi-ink-3, #6b6458);
}

/* ⚠ R5-22 (2026-09-23): THE ONE-FIELD DIALOG SIZES TO ITS FIELD. `.set-modal__dialog--narrow`
   carries a 380px floor meant for a form with several sections, which left 59px of empty
   body under a single input against 20px above it. Desktop only — ≤900 is full-screen. */
@media (min-width: 901px) {
  .set-modal.fra-template-dialog[data-fra-template-dialog="add-option"] .set-modal__dialog--narrow {
    min-height: 0;
  }
}

/* ── ≤900px: the rail becomes a bar and a bottom sheet ─────────────────── */

@media (max-width: 900px) {
  .fra-template-page .fra-template-record {
    grid-template-columns: minmax(0, 1fr);
    gap: 14px;
  }

  .fra-template-page .fra-template-navbar {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 16px;
    margin-bottom: 14px;
    background: var(--ipi-bg-card);
    border-radius: 12px;
    box-shadow: var(--ipi-shadow-card);
  }

  .fra-template-page .fra-template-navbar .ipi-filter-rail__copy {
    min-width: 0;
    flex: 1 1 auto;
  }

  .fra-template-page .fra-template-navbar .ipi-filter-rail__eyebrow,
  .fra-template-page .fra-template-navbar .ipi-filter-rail__meta {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .fra-template-page .fra-template-navbar .ipi-filter-rail__actions {
    flex: 0 0 auto;
  }

  .fra-template-page .fra-template-navbar .ipi-filter-railbtn {
    min-height: 44px;
  }

  /* UNDO THE SHELL'S CHIP STRIP INSIDE THE SHEET — ALL OF IT. At ≤900px both
     shells turn `.ipi-sidenav` into a horizontal chip scroller; undoing only
     the nav left the Client Hub's sheet a sideways strip. */
  .fra-template-page .fra-template-navdrawer .ipi-sidenav {
    display: block;
    position: static;
    max-height: none;
    overflow: visible;
    background: transparent;
    border-radius: 0;
    box-shadow: none;
    -webkit-mask-image: none;
    mask-image: none;
  }

  .fra-template-page .fra-template-navdrawer .ipi-sidenav__group {
    display: block;
    padding: 10px 0;
  }

  .fra-template-page .fra-template-navdrawer .ipi-sidenav__group + .ipi-sidenav__group {
    border-top: 1px solid var(--ipi-rule);
    border-left: 0;
  }

  .fra-template-page .fra-template-navdrawer .ipi-sidenav__label {
    display: flex;
  }

  /* 44px is the tap-target floor. */
  .fra-template-page .fra-template-navdrawer .ipi-sidenav__item {
    width: 100%;
    flex: 0 0 auto;
    white-space: normal;
    border-left: 2px solid transparent;
    border-radius: 0;
    min-height: 44px;
  }

  .fra-template-page .fra-template-navdrawer .fra-template-railmeter {
    box-shadow: none;
    border: 1px solid var(--ipi-rule);
  }

  .fra-template-page .fra-template-pane-header {
    flex-direction: column;
    align-items: stretch;
    gap: 10px;
  }

  .fra-template-page .fra-template-pane-header__actions {
    justify-content: flex-start;
  }

  .fra-template-page .fra-template-pane-header__actions .btn {
    min-height: 44px;
  }

  /* ⛔ NO CARD LIST INSIDE A SECOND CARD (.claude/rules/table-cards.md). An answer
     list's white panel holds a table that becomes cards here, so the panel drops
     its chrome and its head sits on the page ground above them. */
  .fra-template-page .fra-template-set:has(table[data-rc]) {
    padding: 0;
    background: transparent;
    border-radius: 0;
    box-shadow: none;
  }

  .fra-template-page .fra-template-set:has(table[data-rc]) .ipi-panel__head {
    padding-inline: 0;
  }

  .fra-template-page .fra-template-set + .fra-template-set {
    margin-top: 10px;
  }

  /* ⚠ R5-06: THREE BUTTONS, ONE ROW. The shared card footer is two columns, so Move up,
     Move down and Open wrapped to two rows on every one of the sixteen cards. The
     selector outranks table-cards.css's footer rule (0,5,5) rather than using !important. */
  .fra-template-page table.fra-template-table[data-rc="card"] > tbody > tr[data-fra-template-order] > td.fra-template-table__actions[data-rc="actions"]:has(.fra-template-table__move) {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  /* ⚠ R5-23: THIS PAGE'S OWN SMALL TARGETS, which the module floor does not reach — the
     22 "Add an option" buttons, the "Edit its wording there" links, the policy
     disclosure's summary and the dialogs' close button. 44px is the tap floor. */
  .fra-template-page .fra-template-set .ipi-panel__head-tools .btn,
  .fra-template-page .fra-template-pane-header__actions .btn {
    min-height: 44px;
  }

  .fra-template-page .fra-template-fixed__reason a {
    display: inline-flex;
    align-items: center;
    min-height: 44px;
  }

  .fra-template__policy > summary {
    display: flex;
    align-items: center;
    min-height: 44px;
  }

  .fra-template-dialog .modal__close,
  .fra-template-page .fra-template-navdrawer .modal__close {
    min-width: 44px;
    min-height: 44px;
  }

  .fra-template-page .fra-template-finder__search {
    flex: 1 1 100%;
    max-width: none;
    min-height: 44px;
  }

  .fra-template-page .fra-template-finder__chip {
    min-height: 44px;
  }

  /* R5 wave 3 (12) — "JUMP TO" IS ONE SWIPEABLE ROW OF 44px CHIPS. As wrapped 19px text
     links it took ~240px of section 1 at 390 before the first question, and every target
     was under half the tap floor. The label stays at the start of the row. */
  .fra-template-page .fra-template-finder__jump {
    flex-wrap: nowrap;
    align-items: center;
    gap: 8px;
    overflow-x: auto;
    scrollbar-width: none;
    margin-inline: -12px;
    padding-inline: 12px;
  }

  .fra-template-page .fra-template-finder__jump::-webkit-scrollbar {
    display: none;
  }

  .fra-template-page .fra-template-finder__jumplabel {
    flex: 0 0 auto;
  }

  .fra-template-page .fra-template-finder__jump a {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    min-height: 44px;
    padding: 0 14px;
    border: 1px solid var(--ipi-rule-h);
    border-radius: 999px;
    background: var(--ipi-bg-card);
    color: var(--ipi-ink-2);
    font-size: 13px;
    white-space: nowrap;
    text-decoration: none;
  }

  /* R5 wave 3 (14) — THE STICKY PANEL BANDS ("IN THIS SECTION 28") WELD TO THE SCROLLER'S
     EDGE. table-cards pins a group heading at `--rc-sticky-top` (0), and `.content`'s 16px
     top padding insets a sticky box by that much, so cards showed through a 16px strip
     above the band. */
  .fra-template-page {
    --rc-sticky-top: -16px;
  }
}

/* ==========================================================================
   CLIENT PORTAL VISIBILITY — close-out items 15.2 and 15.3
   `app/Views/fra/assessment/show/_portal_visibility.php`, inside the "About this record"
   pane, below the record of check and the attachments.

   ⛔ NO NEW PRIMITIVES, AND NO NEW VALUES. The panel is `.ipi-panel`, the chip is
   `.ipi-chip`, the buttons are `.btn`, the note is `.ipi-filter-search` and the disclosure
   is `<details>`. Every length and colour below is copied VERBATIM from the record-of-check
   block above — same 10px/12px inset padding, same 8px/9px radii, same 13px/12px type,
   same `--ipi-*` tokens — because the two panels sit one above the other in the same pane
   and any difference reads as a mistake rather than as a distinction.

   ⚠ EVERY COLOUR IS AN `--ipi-*` TOKEN DECLARED IN fra-shell.css. A bare `var()` with no
   fallback resolves to nothing silently, so no literal hex appears here. The one literal
   `rgba()` is the amber border, and it is the accent token's own value at the weight the
   check panel's violet border uses — `--ipi-accent-border` is 0.2 and the pattern here is
   0.18.

   ⛔ NO DARK MODE. Out of scope permanently, and every customer-facing surface is hard
   light-locked.
   ========================================================================== */

/* The withheld state is amber rather than red. Red is for something that is wrong and has
   to be put right — the check panel spends it on a live return. A withheld report is a
   deliberate, correct state that somebody chose, and it needs to be noticed rather than
   alarmed about. */
.fra-portal-visibility__state {
  margin: 0 0 12px;
  padding: 10px 12px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ipi-ink-2);
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 8px;
}

[data-fra-portal-visibility="withheld"] .fra-portal-visibility__state {
  color: var(--ipi-accent);
  background: var(--ipi-accent-bg);
  border-color: var(--ipi-accent-border);
}

/* What ELSE stops when a report is withheld. Quieter than the state above it, because it
   is a qualification of that sentence rather than a second warning — the same relationship
   `.fra-check__caveat` has to the facts above it, and the same values. */
.fra-portal-visibility__consequence {
  margin: 0 0 12px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--ipi-ink-3);
}

/* Wrapping, not a nowrap row: the summary and an open form at 360px is two lines and must
   be. One control here rather than the check panel's three, and it still wraps. */
.fra-portal-visibility__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.fra-portal-visibility__disclosure > summary {
  list-style: none;
  cursor: pointer;
}

.fra-portal-visibility__disclosure > summary::-webkit-details-marker {
  display: none;
}

.fra-portal-visibility__disclosure[open] {
  flex: 1 1 100%;
}

.fra-portal-visibility__form {
  display: grid;
  gap: 6px;
  margin-top: 10px;
  padding: 12px;
  background: var(--ipi-bg-inset);
  border: 1px solid var(--ipi-rule);
  border-radius: 9px;
}

.fra-portal-visibility__label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  /* ink-3, not ink-4: 11px #9a9188 on the inset ground measured 2.58:1. */
  color: var(--ipi-ink-3);
}

/* `.ipi-filter-search` is sized for one line; a textarea borrowing it needs its own height
   and the resize handle restricted to the axis that cannot break the grid. */
.fra-portal-visibility__multiline {
  min-height: 60px;
  resize: vertical;
  font: inherit;
  line-height: 1.5;
}

.fra-portal-visibility__form > .btn {
  justify-self: start;
  margin-top: 4px;
}

/* The history is `.ipi-keyvals`, which the shell owns — spacing only, exactly as
   `.fra-check__facts` does it. ⚠ `margin-bottom: 0` rather than 12px: it is the last thing
   in the panel, and the panel already has its own padding. */
.fra-portal-visibility__history {
  margin: 12px 0 0;
}

/* A reason sits under its own row, indented to the value column's reading line rather than
   to the label's, so a list of three decisions reads as three entries and not six. */
.fra-portal-visibility__note {
  display: block;
  margin: 2px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--ipi-ink-3);
  white-space: pre-line;
}

/* ==========================================================================
   `.fra-page-note` — the paragraphs under an FRA page's lede.

   🔴 FOUND UNSTYLED ON 2026-09-21 WHILE ADDING THE THIRD ONE. The class had TWO users on
   `app/Views/fra/actions/index.php` (the immutability note and the client-claims line) and
   NO rule anywhere in this stylesheet or in fra-shell.css, so both rendered at the browser's
   default 16px paragraph with a 1em margin — directly under a 13px 300-weight lede. Nothing
   reported it, because no test in this suite computes a style and a bare `<p>` is never a
   visible error.

   ⛔ IT IS NOT A NEW PRIMITIVE AND DECLARES NO NEW VALUES. Every value below is `.ipi-ph-sub`'s
   own — the lede these paragraphs sit under — so the group reads as one block of page
   introduction rather than as a heading followed by three different kinds of small print.
   ========================================================================== */

.fra-page-note {
  margin: 8px 0 0;
  font-size: 13px;
  font-weight: 300;
  line-height: 1.5;
  color: var(--ipi-ink-4);
}

/* The Action plan's two notes, moved out of the header to sit above the list. */
/* ── FRA settings lists (memberships, standard limitations) — 2026-09-27 ────────────────────
   One row per item with its own Remove button, and an Add button with a running count.
   Markup: app/Views/partials/_fra_list_editor.php. ⚠ Twinned in settings-tabs.css and fra.css,
   because the Settings page and the FRA pages each load only their own sheet. */
.fra-list__rows {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* `align-items: start` keeps Remove level with the top of a two-line textarea row. */
.fra-list__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: 8px 12px;
  min-width: 0;
}

.fra-list__field {
  min-width: 0;
  margin: 0;
}

.fra-list__field > input,
.fra-list__field > textarea {
  width: 100%;
}

/* A limitation shows whole: the box grows with its text (where the browser supports it). */
.fra-list__field > textarea {
  field-sizing: content;
  max-height: 14em;
}

.fra-list__remove {
  white-space: nowrap;
}

.fra-list__foot {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 10px;
}

.fra-list__count {
  font-size: 12px;
  color: var(--ipi-ink-3);
}

/* Setup: adding the other standard for parts of the building it covers (2026-09-27). */
.fra-setup__widen {
  margin-top: 16px;
  padding: 12px 14px;
  border: 1px solid var(--ipi-rule, rgba(0, 0, 0, 0.1));
  border-radius: 10px;
}

.fra-setup__widen-label {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-weight: 600;
}

.fra-setup__widen-label input {
  margin-top: 3px;
  flex: 0 0 auto;
}

.fra-setup__widen-hint {
  margin: 6px 0 0 26px;
  font-size: 13px;
  color: var(--ipi-ink-3);
}

/* Capture: pick a building or area for a finding (2026-09-27). */
.fra-area-chips {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 6px;
}

.fra-area-chips__lead {
  font-size: 12.5px;
  color: var(--ipi-ink-3);
}

.fra-area-chip {
  min-height: 32px;
  padding: 4px 10px;
  border: 1px solid var(--ipi-rule, rgba(0, 0, 0, 0.12));
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

/* ==========================================================================
 * THE ASSESSMENT PAGE AS A TABBED RECORD — FRA workflow rebuild Stage 4 (2026-09-27)
 *
 * The Fire Doors inspection page's shape: three info cards, a dark-active tab bar and one panel
 * at a time. Forked from `inspection_show.css` (`.show-info-row` / `.ir-*`, `.inspection-tabs`,
 * `.capture-handoff`) with its LITERAL values, as the house rule for matching a reference page
 * requires (css-standards.md) — renamed `fra-*` because the FD rules are scoped to
 * `.page-section--inspection-show` and must not be widened.
 * ⚠ Every colour is written `var(--ipi-*, <the literal>)`: this file may carry no bare hex
 * (FraStage1ForkRetirementTest), and each `--ipi-*` token below resolves to exactly the
 * inspection page's literal in the default palette, which the fallback keeps on the page.
 *
 * ⚠ EVERYTHING HERE SITS INSIDE `.ipi-list-area`, which already carries the page gutter and a 16px
 * column gap — so nothing below sets an outer inline margin.
 * ======================================================================= */

/* ---- The three cards: Property · Building · Visit ---------------------- */
.fra-info-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
}

.fra-ir-card {
  min-width: 0;
  background: var(--ipi-card, #ffffff);
  border-radius: 10px;
  padding: 16px 18px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.05);
}

.fra-ir-label {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-bottom: 6px;
  color: var(--ipi-ink-4, #9a9188);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.fra-ir-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--ipi-bg-inset, #edeae3);
  border: 1px solid rgba(0, 0, 0, 0.11);
}

.fra-ir-dot.is-set {
  background: var(--ipi-green, #1e7a45);
}

.fra-ir-dot.is-warn {
  background: var(--ipi-accent, #c49510);
}

.fra-ir-title {
  margin-bottom: 2px;
  font-size: 15px;
  font-weight: 600;
  color: var(--ipi-ink, #1a1815);
  overflow-wrap: anywhere;
}

.fra-ir-sub {
  color: var(--ipi-ink-4, #9a9188);
  font-size: 12px;
  overflow-wrap: anywhere;
}

.fra-ir-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 10px;
}

.fra-page .fra-ir-action {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 10px;
  border-radius: 6px;
  border: 1px solid rgba(0, 0, 0, 0.11);
  background: var(--ipi-card, #ffffff);
  color: var(--ipi-ink-3, #6b6458);
  font: 500 11.5px/1 "Inter", sans-serif;
  text-decoration: none;
  cursor: pointer;
}

.fra-page .fra-ir-action:hover {
  background: var(--ipi-bg-hover, #f2efe9);
  color: var(--ipi-ink, #1a1815);
}

/* ---- The tab bar -------------------------------------------------------- */
.fra-tabs {
  display: flex;
  gap: 0;
  width: fit-content;
  max-width: 100%;
  padding: 4px;
  border-radius: 10px;
  border: 1px solid rgba(0, 0, 0, 0.07);
  background: var(--ipi-card, #ffffff);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.05);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.fra-tabs::-webkit-scrollbar {
  display: none;
}

.fra-page a.fra-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  padding: 8px 18px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--ipi-ink-4, #9a9188);
  font: 500 13px/1 "Inter", sans-serif;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
}

.fra-page a.fra-tab:hover {
  color: var(--ipi-ink-2, #3d3a34);
}

.fra-page a.fra-tab.is-active {
  background: var(--ipi-ink, #1a1815);
  color: var(--ipi-bg-card, #ffffff);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
}

.fra-tab__count {
  min-width: 18px;
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.07);
  font-size: 11px;
  font-weight: 600;
  line-height: 14px;
  text-align: center;
}

.fra-tab.is-active .fra-tab__count {
  background: rgba(255, 255, 255, 0.18);
}

.fra-tab-panels,
.fra-tab-panel {
  min-width: 0;
}

.fra-tab-panel {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.fra-tab-panel[hidden] {
  display: none;
}

.fra-tab-panel:focus {
  outline: none;
}

.fra-tab-panel:focus-visible {
  outline: 2px solid var(--ipi-ink, #1a1815);
  outline-offset: 4px;
  border-radius: 8px;
}

/* ---- Capture tab (redesigned 2026-09-28) --------------------------------
   What is left, grouped In progress / To do / Done, and what stands before issue. One row per
   section: the position number the capture screen prints, the title, one grey line of what is left,
   a mini bar on wide screens, and a chevron. The whole row is the link.
   ⛔ The head is a bare `.ipi-panel__head` (FraStage1ForkRetirementTest) — it is reached through
   the panel's own class, never given a modifier of its own. */
.fra-capture-entry > .ipi-panel__head {
  align-items: center;
}

.fra-capture-entry__offline {
  display: block;
  margin: 0 0 12px;
  font-size: 12.5px;
}

.fra-capture-entry__offline:empty {
  display: none;
}

/* The scope step's offline note (setup.php) shares this look. */
.fra-capture-entry__note {
  margin: 0 0 12px;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--ipi-accent-bg, rgba(196, 149, 16, 0.08));
  color: var(--accent-ink, #7a5c05);
  font-size: 13px;
  line-height: 1.45;
}

.fra-capture-entry__overview {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin: 0 0 14px;
}

.fra-capture-entry__fig {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  padding: 12px 14px;
  border: 1px solid var(--ipi-rule, rgba(0, 0, 0, 0.07));
  border-radius: 10px;
  background: var(--ipi-bg, #faf8f4);
}

.fra-page a.fra-capture-entry__fig--link {
  color: inherit;
  text-decoration: none;
}

.fra-page a.fra-capture-entry__fig--link:hover {
  border-color: rgba(0, 0, 0, 0.16);
}

.fra-capture-entry__fig--link.is-warn {
  border-color: rgba(196, 149, 16, 0.35);
  background: var(--ipi-accent-bg, rgba(196, 149, 16, 0.08));
}

.fra-capture-entry__fig--link.is-ok {
  border-color: rgba(30, 122, 69, 0.3);
  background: var(--ipi-green-bg, rgba(30, 122, 69, 0.08));
}

.fra-capture-entry__fig-label {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ipi-ink-4, #9a9188);
}

.fra-capture-entry__fig-value {
  font-size: 13.5px;
  color: var(--ipi-ink-3, #6b6458);
  font-variant-numeric: tabular-nums;
}

.fra-capture-entry__fig-value strong {
  font-family: var(--font-display, inherit);
  font-size: 18px;
  font-weight: 600;
  color: var(--ipi-ink, #1a1815);
}

.fra-capture-entry__fig-sub {
  font-size: 12px;
  line-height: 1.4;
  color: var(--ipi-ink-3, #6b6458);
}

.fra-capture-entry__bar {
  display: block;
  height: 5px;
  margin-top: 4px;
  border-radius: 999px;
  background: var(--ipi-bg-inset, #edeae3);
  overflow: hidden;
}

.fra-capture-entry__bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--ipi-green, #1e7a45);
}

.fra-capture-sections {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.fra-capture-sections__heading {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ipi-ink-4, #9a9188);
}

.fra-capture-sections__heading-count {
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--ipi-bg-inset, #edeae3);
  color: var(--ipi-ink-3, #6b6458);
  font-size: 11px;
  letter-spacing: 0;
  text-align: center;
}

.fra-capture-sections__list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--ipi-rule, rgba(0, 0, 0, 0.07));
  border-radius: 10px;
  overflow: hidden;
  background: var(--ipi-card, #ffffff);
}

.fra-capsec + .fra-capsec {
  border-top: 1px solid var(--ipi-rule, rgba(0, 0, 0, 0.07));
}

.fra-capsec__row {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) 96px 16px;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  padding: 8px 14px;
  color: inherit;
}

.fra-page a.fra-capsec__row {
  color: inherit;
  text-decoration: none;
}

.fra-page a.fra-capsec__row:hover {
  background: var(--ipi-bg-hover, #f6f3ee);
}

.fra-page a.fra-capsec__row:focus-visible {
  outline: 2px solid var(--ipi-ink, #1a1815);
  outline-offset: -2px;
}

.fra-capsec__num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--ipi-bg-inset, #edeae3);
  color: var(--ipi-ink-3, #6b6458);
  font-size: 12px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.fra-capsec.is-part .fra-capsec__num {
  background: var(--ipi-accent-bg, rgba(196, 149, 16, 0.08));
  color: var(--accent-ink, #7a5c05);
  box-shadow: inset 0 0 0 1px rgba(196, 149, 16, 0.35);
}

.fra-capsec.is-done .fra-capsec__num {
  background: var(--ipi-green-bg, rgba(30, 122, 69, 0.08));
  color: var(--ipi-green, #1e7a45);
}

.fra-capsec__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.fra-capsec__title {
  font-size: 14px;
  font-weight: 560;
  color: var(--ipi-ink, #1a1815);
  overflow-wrap: anywhere;
}

.fra-capsec__next {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--ipi-ink, #1a1815);
  color: var(--ipi-bg-card, #ffffff);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: 2px;
}

.fra-capsec__meta {
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--ipi-ink-4, #9a9188);
  font-variant-numeric: tabular-nums;
}

.fra-capsec__open {
  color: var(--accent-ink, #7a5c05);
  font-weight: 560;
}

.fra-capsec__open::before {
  content: "·";
  margin: 0 6px;
  color: var(--ipi-ink-4, #9a9188);
  font-weight: 400;
}

.fra-capsec__bar {
  display: block;
  height: 5px;
  border-radius: 999px;
  background: var(--ipi-bg-inset, #edeae3);
  overflow: hidden;
}

.fra-capsec__bar > span {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--ipi-green, #1e7a45);
}

.fra-capsec__go {
  display: inline-flex;
  color: var(--ipi-ink-4, #9a9188);
}

@media (max-width: 900px) {
  .fra-capture-entry > .ipi-panel__head {
    align-items: stretch;
  }

  .fra-capture-entry > .ipi-panel__head .ipi-panel__head-tools {
    width: 100%;
  }

  .fra-capsec__open {
    display: block;
  }

  .fra-capsec__open::before {
    content: none;
  }

  .fra-capture-entry > .ipi-panel__head .ipi-panel__head-tools .btn {
    width: 100%;
    min-height: 44px;
    justify-content: center;
  }

  .fra-capture-entry__overview {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .fra-capture-entry__fig--link {
    grid-column: 1 / -1;
  }

  .fra-capsec__row {
    grid-template-columns: 28px minmax(0, 1fr) 16px;
    padding: 10px 12px;
  }

  .fra-capsec__bar {
    display: none;
  }
}

/* "Continue on your phone" — true desktops only, like the Fire Doors gate: a phone or a touch
   tablet is already the capture device. One compact row (owner review, 2026-09-28): a 96px code,
   one line of copy, the link and Copy. */
.fra-handoff {
  display: flex;
  align-items: center;
  gap: 18px;
  padding-top: 14px;
  padding-bottom: 14px;
}

.fra-handoff__qr {
  flex: 0 0 auto;
}

.fra-handoff__qr img {
  display: block;
  width: 96px;
  height: 96px;
  border: 1px solid var(--rule, rgba(26, 24, 21, 0.1));
  border-radius: 10px;
  background: var(--ipi-card, #ffffff);
}

.fra-handoff__body {
  flex: 1 1 auto;
  min-width: 0;
}

.fra-handoff__title {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--ipi-ink, #1a1815);
}

.fra-handoff__lead {
  margin: 2px 0 10px 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-3, #6b6458);
}

.fra-handoff__row {
  display: flex;
  gap: 8px;
  align-items: center;
  max-width: 560px;
}

.fra-handoff__input {
  flex: 1 1 auto;
  min-width: 0;
  height: 36px;
  padding: 0 12px;
  font-size: 12.5px;
  font-family: inherit;
  color: var(--ink-2, #3d3a34);
  border: 1px solid var(--rule, rgba(26, 24, 21, 0.1));
  border-radius: 8px;
  background: var(--surface-subtle, #f2efe9);
}

@media (max-width: 1024px), (any-pointer: coarse) {
  .fra-handoff {
    display: none;
  }
}

/* ---- Check & issue, Timeline, Building ---------------------------------- */

/* The Timeline tab (rebuilt 2026-09-28 to match Fire Doors and Fire Stopping). The event list is
   the fire-stopping-survey-show.css `.tl-*` block, values copied verbatim and scoped to `.fra-tl`
   (fra.css loads on every FRA page, and those names are generic). Key dates stay the shell's
   `.ipi-keyvals`, as a short strip of label/value pairs. */
.fra-tl {
  display: grid;
  gap: 16px;
  min-width: 0;
}

.fra-tl__dates {
  padding: 14px 18px;
}

.fra-tl__heading {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ipi-ink-3, #6b6458);
}

.fra-tl__keyvals {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 28px;
}

.fra-tl__keyvals > li + li {
  border-top: 0;
}

.fra-tl__keyvals .ipi-keyvals__row {
  display: grid;
  gap: 1px;
  padding: 4px 0;
}

.fra-tl__keyvals .ipi-keyvals__label {
  font-size: 12px;
  color: var(--ipi-ink-4, #9a9188);
}

.fra-tl__keyvals .ipi-keyvals__value {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--ipi-ink, #1a1815);
  white-space: normal;
}

.fra-tl .tl-timeline-wrap { padding: 4px 2px; }
.fra-tl .timeline-empty { margin: 18px 0; text-align: center; font-size: 0.9rem; color: var(--text-muted, #6b6458); }

.fra-tl .tl-date-group { margin-bottom: 16px; }
.fra-tl .tl-date-label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 0 10px;
  color: var(--text-muted, #9a9188);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.fra-tl .tl-date-label::after { content: ""; flex: 1; height: 1px; background: rgba(0, 0, 0, 0.07); }
.fra-tl .tl-events { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }

.fra-tl .tl-event { display: flex; align-items: flex-start; gap: 14px; padding: 14px 18px; border: 0; border-radius: 12px; background: var(--card, #ffffff); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.05); }
.fra-tl .tl-icon {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.11);
  background: var(--ipi-bg-inset, #edeae3);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.fra-tl .tl-icon.added { background: rgba(30, 122, 69, 0.08); border-color: rgba(30, 122, 69, 0.2); }
.fra-tl .tl-icon.update { background: rgba(37, 99, 168, 0.08); border-color: rgba(37, 99, 168, 0.15); }
.fra-tl .tl-icon svg { width: 13px; height: 13px; color: var(--text-muted, #9a9188); }

.fra-tl .tl-body { min-width: 0; flex: 1 1 auto; }
.fra-tl .tl-top { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; flex-wrap: wrap; }
.fra-tl .tl-time { color: var(--text-muted, #9a9188); font-size: 11.5px; }
.fra-tl .tl-type-chip {
  display: inline-flex;
  align-items: center;
  min-height: 20px;
  padding: 2px 8px;
  border-radius: 5px;
  border: 1px solid rgba(0, 0, 0, 0.11);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
}
.fra-tl .tl-chip-update { background: rgba(37, 99, 168, 0.08); color: var(--ipi-blue, #2563a8); border-color: rgba(37, 99, 168, 0.16); }
.fra-tl .tl-chip-added { background: rgba(30, 122, 69, 0.08); color: var(--ipi-green, #1e7a45); border-color: rgba(30, 122, 69, 0.2); }
.fra-tl .tl-desc { color: var(--text, #1a1815); font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }
.fra-tl .tl-actor-byline { color: var(--text-muted, #9a9188); }

.fra-tl__note {
  margin: 6px 0 0;
  padding: 6px 10px;
  border-radius: 8px;
  background: var(--ipi-bg-hover, #f2efe9);
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ipi-ink-3, #6b6458);
  overflow-wrap: anywhere;
}

.fra-building-tab {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

.fra-building-tab__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}

.fra-building-tab__lede {
  margin: 0;
  flex: 1 1 320px;
  min-width: 0;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ipi-ink-3, #6b6458);
}

/* ---- Tablet and phone ----------------------------------------------------
   The Fire Doors page switches its tab strip at the same gate as its capture content (1024px or a
   coarse pointer). Cards stack one to a row at ≤900px, where three at a third of a phone would
   squeeze every title onto three lines. */
@media (max-width: 1024px), (any-pointer: coarse) {
  .fra-tabs {
    width: auto;
    border-radius: 12px;
    scroll-snap-type: x proximity;
  }

  .fra-page a.fra-tab {
    min-height: 40px;
    padding: 10px 16px;
    border-radius: 8px;
    scroll-snap-align: start;
  }
}

@media (max-width: 620px) {
  /* On a phone the three cards swipe sideways like the stats strip above them (the canonical KPI
     pattern, tenant-ui.md) rather than stacking — stacked, they pushed the tab bar a full phone
     screen down. A tablet keeps all three side by side. */
  .fra-info-row {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
    gap: 12px;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .fra-info-row::-webkit-scrollbar {
    display: none;
  }

  .fra-ir-card {
    flex: 0 0 min(280px, 80vw);
    scroll-snap-align: start;
  }

}

@media (max-width: 900px) {
  .fra-ir-card {
    border-radius: 12px;
    padding: 16px;
  }

  .fra-ir-title {
    font-size: 16px;
  }

  .fra-page .fra-ir-action {
    min-height: 36px;
    padding: 6px 12px;
    border-radius: 8px;
  }

  .fra-tl .tl-event {
    gap: 10px;
    padding: 12px 14px;
  }
}

/* ==========================================================================
   Settings → Assessments (FRA workflow rebuild Stage 10, D9)
   The template's four numbers, and the risk matrix editor inside a Settings
   section rather than on a page of its own.
   ========================================================================== */

.fra-settings-facts {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
  margin: 0 0 12px;
}

.fra-settings-facts > div {
  padding: 12px 14px;
  border: 1px solid var(--rule, rgba(26, 24, 21, 0.11));
  border-radius: 12px;
  background: var(--canvas, #f7f5f0);
}

.fra-settings-facts dt {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-3, #6b645c);
}

.fra-settings-facts dd {
  margin: 4px 0 0;
  font-family: var(--font-display);
  font-size: 22px;
  font-weight: 600;
  color: var(--ink, #1a1815);
}

@media (max-width: 620px) {
  .fra-settings-facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The matrix editor inside a Settings section: the section already pads its content, so the
   page gutter (which the editor's full-bleed bands pull back out with negative margins) is
   zero here — otherwise the sticky bar and the grid hang past the section's edges. */
.fra-page.fra-matrix-page--settings {
  --ipi-page-gutter: 0px;
}

.fra-matrix-page--settings .fra-matrix-actionbar {
  padding-block: 10px;
}

/* Review (plan Stage 4): Check & issue lists what stands before issue, by section; the Timeline
   carries a history of the portal holds and the action plan's trail. */
/* 2026-09-28 (owner review): the issue panel leads with whether it can be issued; the outstanding
   items are typed and grouped by section, each with a way to it. */
.fra-issue__head {
  align-items: flex-start;
}

.fra-issue__title {
  margin: 2px 0 4px;
  font-family: var(--font-display, inherit);
  font-size: 20px;
  font-weight: 600;
  line-height: 1.25;
  color: var(--ipi-ink, #1a1815);
}

.fra-issue.is-ready .fra-issue__title {
  color: var(--ipi-green, #1e7a45);
}

.fra-issue__tools {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: flex-end;
}

.fra-issue__tools .btn[disabled] {
  opacity: 1;
  background: var(--ipi-bg-inset, #edeae3);
  border-color: var(--ipi-bg-inset, #edeae3);
  color: var(--ipi-ink-4, #9a9188);
  cursor: not-allowed;
  box-shadow: none;
}

.fra-issue__kinds {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 16px;
  padding: 0;
  list-style: none;
}

.fra-issue__kind {
  padding: 4px 10px;
  border-radius: 999px;
  background: var(--ipi-accent-bg, rgba(196, 149, 16, 0.08));
  box-shadow: inset 0 0 0 1px rgba(196, 149, 16, 0.3);
  color: var(--accent-ink, #7a5c05);
  font-size: 12.5px;
  font-weight: 560;
}

.fra-issue__kind--check {
  background: var(--ipi-violet-bg, rgba(107, 79, 168, 0.08));
  box-shadow: inset 0 0 0 1px rgba(107, 79, 168, 0.25);
  color: var(--ipi-violet, #6b4fa8);
}

.fra-issue__list-title {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ipi-ink-4, #9a9188);
}

.fra-issue__blocker-list {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
  overflow: hidden;
}

.fra-issue__blocker {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  padding: 8px 12px 8px 14px;
  min-width: 0;
}

.fra-issue__blocker + .fra-issue__blocker {
  border-top: 1px solid var(--ipi-rule);
}

.fra-issue__blocker-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
}

.fra-issue__blocker-name {
  font-size: 14px;
  font-weight: 560;
  color: var(--ipi-ink, #1a1815);
  overflow-wrap: anywhere;
}

.fra-issue__blocker-what {
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--accent-ink, #7a5c05);
}

.fra-issue__go {
  flex: 0 0 auto;
}

.fra-issue__ok {
  margin: 0;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--ipi-green-bg, rgba(30, 122, 69, 0.08));
  color: var(--ipi-green, #1e7a45);
  font-size: 13px;
}

@media (max-width: 900px) {
  .fra-issue__head {
    flex-direction: column;
    align-items: stretch;
  }

  .fra-issue__tools {
    justify-content: stretch;
  }

  .fra-issue__tools > .btn,
  .fra-issue__tools > .fra-inline-form {
    flex: 1 1 0;
  }

  .fra-issue__tools .btn {
    width: 100%;
    min-height: 44px;
    justify-content: center;
  }

  .fra-issue__go {
    min-height: 40px;
  }
}

/* Review (plan 5b.4): the assessment page, offline — a note above the tabs, and every server action
   shown disabled (links carry aria-disabled). */
.fra-offline-note {
  margin: 0 0 12px;
  padding: 10px 14px;
  border: 1px solid var(--ipi-rule);
  border-radius: 10px;
  background: var(--ipi-accent-bg, #fbf3e0);
  font-size: 14px;
}

/* Record progress kept on the device (2026-09-28): the note inside the form, and the small mark
   beside the row's own Record progress button until it syncs. */
.fra-track-waiting {
  font-size: 13px;
}

.fra-track-waiting-mark {
  display: inline-flex;
  align-items: center;
  margin-left: 8px;
  padding: 2px 8px;
  border: 1px dashed var(--ipi-accent-border);
  border-radius: 999px;
  font-size: 12px;
  font-weight: 600;
  color: var(--ipi-ink-2);
  white-space: nowrap;
}

.fra-assessment-page a[aria-disabled="true"] {
  opacity: 0.55;
  cursor: not-allowed;
}

/* ---- Edit details dialog (owner review, 2026-09-28: "no padding at bottom") ----------------
   The form sits INSIDE the scrolling body, and the shared `.set-modal__formfields` rule makes it a
   shrinkable flex item (the fire door PIN dialog hit the same shape — inspection_show.css): its
   sections overflowed the form and the last card ran into the footer. The form is exactly as tall
   as its content, and the body keeps its padding below the last card. */
.fra-assessment-page [data-fra-edit-details-modal] .set-modal__body > form.set-modal__formfields {
  display: flex;
  flex-direction: column;
  gap: 16px;
  flex: 0 0 auto;
  min-height: 0;
  width: 100%;
  margin: 0;
}

.fra-assessment-page [data-fra-edit-details-modal] .set-modal__body {
  padding-bottom: 24px;
}

@media (max-width: 900px) {
  .fra-assessment-page [data-fra-edit-details-modal] .set-modal__body > form.set-modal__formfields {
    gap: 12px;
  }
}

/* ---- The Actions page (redesigned 2026-09-28, owner review) ---------------------------------
   The table used to run 1,867px inside a 1,245px list area (overflow hidden), cutting off six
   columns. It is fixed-layout now: the action takes the spare width and wraps to two lines, every
   other column has a set width, and nothing is wider than the area it sits in. */
.fra-actions-export-note {
  margin: 0 0 12px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--ipi-ink-3);
}

@media (min-width: 901px) {
  .fra-actions-page .fra-actions-table {
    table-layout: fixed;
    width: 100%;
  }

  .fra-actions-table__col-priority { width: 108px; }
  .fra-actions-table__col-status { width: 140px; }
  .fra-actions-table__col-due { width: 124px; }
  .fra-actions-table__col-who { width: 18%; }
  .fra-actions-table__col-open { width: 250px; }

  .fra-actions-page .fra-actions-table td {
    vertical-align: top;
    overflow-wrap: anywhere;
  }

  .fra-actions-page .fra-actions-table__desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    white-space: normal;
    font-weight: 560;
    line-height: 1.4;
    border-bottom: 0;
    text-decoration: none;
    box-shadow: none;
    background-image: none;
  }

  .fra-actions-page .fra-actions-table__desc:hover {
    text-decoration: underline;
  }

  .fra-actions-page .fra-actions-table .fra-table__sub {
    display: block;
    margin-top: 3px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .fra-actions-page .fra-actions-table tr.is-overdue td:first-child {
    box-shadow: inset 3px 0 0 var(--ipi-red);
  }
}

/* The priority, as a small coloured label — the same four tones the assessment page's bands use. */
.fra-priority {
  display: inline-block;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 650;
  white-space: nowrap;
}

.fra-priority--bad {
  background: var(--ipi-red-bg);
  color: var(--ipi-red);
}

.fra-priority--warn {
  background: var(--ipi-accent-bg);
  color: var(--accent-ink, #7a5c05);
}

.fra-priority--info {
  background: var(--ipi-blue-bg);
  color: var(--ipi-blue);
}

.fra-priority--neutral {
  background: var(--ipi-bg-inset);
  color: var(--ipi-ink-2);
}

.fra-actions-table__overdue {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  font-weight: 650;
  color: var(--ipi-red);
}

.fra-actions-table__muted {
  color: var(--ipi-ink-3);
}

.fra-actions-table__claim {
  display: block;
  margin-top: 4px;
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ipi-violet);
}

/* ── My Jobs (inspector workspace redesign, owner sign-off 2026-09-28) ───────────────────────────
   fra/my_jobs.php + fra/_my_jobs_track_sheet.php. A small grey (or red) line under a desktop cell,
   and the "Record progress" panel: a side sheet at ≥901px, a bottom sheet on a phone. On a card the
   notes are table-cards' own [data-rc-note], so these rules only shape the desktop table.
   ⚠ .fra-mj-note is also used by fra/assessments/index.php (the inspector's Open actions note) -
   keep it when changing My Jobs. */
.fra-page .fra-mj-note {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  font-weight: 500;
  color: var(--ipi-ink-3, #6b6458);
}

.fra-page .fra-mj-note--bad {
  color: var(--danger);
  font-weight: 600;
}

.fra-mj-sheet[hidden] {
  display: none;
}

.fra-mj-sheet {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  justify-content: flex-end;
}

.fra-mj-sheet__overlay {
  position: absolute;
  inset: 0;
  background: rgba(26, 24, 21, 0.36);
}

.fra-mj-sheet__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  width: min(460px, 100%);
  height: 100%;
  background: var(--surface-card);
  box-shadow: var(--shadow-md);
}

.fra-mj-sheet__head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 18px 20px 14px;
  border-bottom: 1px solid var(--rule);
}

.fra-mj-sheet__heading {
  flex: 1 1 auto;
  min-width: 0;
}

.fra-mj-sheet__eyebrow {
  margin: 0 0 4px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent-ink);
}

.fra-mj-sheet__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.fra-mj-sheet__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 8px 0 0;
  font-size: 12.5px;
}

.fra-mj-sheet__band {
  display: inline-flex;
  align-items: center;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--surface-inset);
  color: var(--ink-2);
  font-weight: 600;
}

.fra-mj-sheet__late {
  color: var(--danger);
  font-weight: 600;
}

.fra-mj-sheet__where {
  margin: 4px 0 0;
  font-size: 12.5px;
  color: var(--ink-3);
}

.fra-mj-sheet__form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  gap: 0;
  min-height: 0;
  margin: 0;
  padding: 0;
  background: transparent;
  border-radius: 0;
}

.fra-mj-sheet__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 16px 20px;
}

.fra-mj-sheet__field {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.fra-mj-sheet__field > span,
.fra-mj-sheet__field > legend {
  padding: 0;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-2);
}

.fra-mj-sheet__field > legend {
  margin-bottom: 6px;
}

.fra-mj-sheet__field :is(input[type="date"], input[type="text"], select, textarea) {
  width: 100%;
  min-height: 44px;
  padding: 9px 12px;
  border: 1px solid var(--rule-strong);
  border-radius: 9px;
  background: var(--surface-card);
  color: var(--ink);
  font: inherit;
  font-size: 16px;
}

.fra-mj-sheet__field textarea {
  min-height: 88px;
  resize: vertical;
}

.fra-mj-sheet__field small {
  font-size: 12px;
  color: var(--ink-3);
}

.fra-mj-sheet__choices {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}

.fra-mj-sheet__choice {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 12px;
  border: 1px solid var(--rule-strong);
  border-radius: 9px;
  font-size: 14px;
  color: var(--ink-2);
  cursor: pointer;
}

.fra-mj-sheet__choice input {
  margin: 0;
  accent-color: var(--ink);
}

.fra-mj-sheet__choice:has(input:checked) {
  border-color: var(--ink);
  background: var(--surface-subtle);
  color: var(--ink);
  font-weight: 600;
}

.fra-mj-sheet__choice:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.fra-mj-sheet__note {
  margin: 0;
  padding: 10px 12px;
  border-radius: 10px;
  background: var(--surface-subtle);
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink-2);
}

.fra-mj-sheet__foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--rule);
}

.fra-mj-sheet__foot .btn {
  min-height: 44px;
}

body.fra-mj-sheet-open {
  overflow: hidden;
}

/* The fixed "Need help?" launcher sits above everything and covered the panel's Save button
   (found in the render check). While a panel is open it steps aside, as it does for every
   page's open filter drawer (clients.css, fire-stopping-rams.css). */
body.fra-mj-sheet-open .support-fab,
body.fra-mj-sheet-open button[data-support-open] {
  pointer-events: none !important;
  opacity: 0 !important;
  visibility: hidden !important;
}

@media (max-width: 900px) {
  .fra-mj-sheet {
    align-items: flex-end;
    justify-content: stretch;
  }

  .fra-mj-sheet__panel {
    width: 100%;
    height: auto;
    max-height: 92dvh;
    border-radius: 18px 18px 0 0;
  }

  .fra-mj-sheet__head,
  .fra-mj-sheet__body,
  .fra-mj-sheet__foot {
    padding-left: 16px;
    padding-right: 16px;
  }

  .fra-mj-sheet__foot .btn {
    flex: 1 1 0;
  }
}

/* ==========================================================================
   COMPLETION PHOTOGRAPHS ON AN ACTION — 2026-09-28
   `fra/_action_photos.php` (thumbnails), `_action_photo_inputs.php` (Add a photo). Used on the
   Action plan tab, the My Jobs progress panel and the Actions page's panel. Tokens are the
   global ones the panel already uses, so the block reads the same inside and outside `.ipi-panel`.
   ========================================================================== */
.fra-aphotos {
  margin: 10px 0 0;
  min-width: 0;
}

.fra-aphotos__label {
  margin: 0 0 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-2);
}

.fra-aphotos__count {
  margin-left: 4px;
  font-weight: 500;
  color: var(--ink-3);
}

.fra-aphotos__hidden {
  margin: 0;
  font-size: 13px;
  color: var(--ink-3);
}

.fra-aphotos__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px;
}

.fra-aphotos__item {
  display: grid;
  grid-template-columns: 56px minmax(0, 1fr);
  grid-template-areas: "thumb meta" "thumb remove";
  align-items: start;
  column-gap: 8px;
  padding: 6px;
  border: 1px solid var(--rule);
  border-radius: 9px;
  background: var(--surface-card);
  min-width: 0;
}

.fra-aphotos__thumb {
  grid-area: thumb;
  display: block;
  width: 56px;
  height: 56px;
  border-radius: 6px;
  overflow: hidden;
  background: var(--surface-inset);
}

.fra-aphotos__thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.fra-aphotos__meta {
  grid-area: meta;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  font-size: 12px;
  line-height: 1.35;
}

.fra-aphotos__caption {
  color: var(--ink);
  overflow-wrap: anywhere;
}

.fra-aphotos__by {
  color: var(--ink-3);
}

/* A text button, 44px tall on touch — it sits under the caption, not over the image. */
.fra-aphotos__remove {
  grid-area: remove;
  justify-self: start;
  min-height: 32px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--danger);
  cursor: pointer;
}

.fra-aphotos__remove:hover,
.fra-aphotos__remove:focus-visible {
  text-decoration: underline;
}

.fra-aphotos__client {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  margin: 8px 0 0;
  font-size: 13px;
}

.fra-aphotos__client a {
  font-weight: 600;
}

.fra-aphotos-add {
  display: grid;
  gap: 8px;
  min-width: 0;
}

.fra-aphotos-add__rows {
  display: grid;
  gap: 10px;
}

.fra-aphotos-add__row {
  display: grid;
  gap: 6px;
  min-width: 0;
}

.fra-aphotos-add__file,
.fra-aphotos-add__caption {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-2);
}

.fra-aphotos-add__file input[type="file"] {
  max-width: 100%;
  min-height: 44px;
  font-size: 14px;
  font-weight: 400;
}

.fra-aphotos-add__caption input[type="text"] {
  width: 100%;
  min-height: 44px;
  padding: 9px 12px;
  border: 1px solid var(--rule-strong);
  border-radius: 9px;
  background: var(--surface-card);
  color: var(--ink);
  font: inherit;
  font-size: 16px;
  font-weight: 400;
}

.fra-aphotos-add__more {
  justify-self: start;
}

.fra-aphotos-add__more[hidden] {
  display: none;
}

@media (max-width: 900px), (any-pointer: coarse) {
  .fra-aphotos__remove {
    min-height: 44px;
  }
}

/* ==========================================================================
   THE ACTIONS PAGE, GROUPED BY BUILDING — 2026-09-28 (Option A)
   `fra/actions/index.php` + `_group.php` + `_row.php`. One card per assessment; the table inside a
   card keeps the flat table's column rules above, and becomes table-cards' record cards at ≤900px.
   ========================================================================== */
.fra-actions-priorities {
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  margin: 0 0 12px;
  overflow-x: auto;
  scrollbar-width: none;
}

.fra-actions-priorities::-webkit-scrollbar {
  display: none;
}

.fra-page a.fra-actions-priorities__btn {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  padding: 0 12px;
  border: 1px solid var(--ipi-rule);
  border-radius: 999px;
  background: var(--ipi-bg-card);
  color: var(--ipi-ink-2);
  font-size: 13px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.fra-page a.fra-actions-priorities__btn:hover {
  background: var(--ipi-bg-hover);
}

.fra-page a.fra-actions-priorities__btn.is-active {
  border-color: var(--ipi-ink);
  background: var(--ipi-ink);
  color: var(--ipi-bg-card);
}

.fra-actions-priorities__count {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

.fra-actions-priorities__btn--bad:not(.is-active) .fra-actions-priorities__count {
  color: var(--ipi-red);
  opacity: 1;
}

.fra-agroups {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-width: 0;
}

.fra-agroups__section {
  margin: 8px 0 -4px;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ipi-ink-3);
}

/* A native <details open>: the summary is the card's header, and closes it to that header. */
.fra-agroup {
  min-width: 0;
  border: 1px solid var(--ipi-rule);
  border-radius: 12px;
  background: var(--ipi-bg-card);
  box-shadow: var(--ipi-shadow-sm);
  overflow: hidden;
}

.fra-agroup--draft {
  border-style: dashed;
  box-shadow: none;
}

.fra-agroup__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 20px;
  padding: 14px 18px;
  cursor: pointer;
  list-style: none;
}

.fra-agroup__head::-webkit-details-marker {
  display: none;
}

.fra-agroup[open] > .fra-agroup__head {
  border-bottom: 1px solid var(--ipi-rule);
}

.fra-agroup__head:hover {
  background: var(--ipi-bg-hover);
}

.fra-agroup__title {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 1 1 260px;
  min-width: 0;
}

.fra-agroup__name {
  font-family: var(--font-display);
  font-size: 17px;
  font-weight: 600;
  line-height: 1.3;
  color: var(--ipi-ink);
  overflow-wrap: anywhere;
}

.fra-agroup__meta {
  font-size: 12.5px;
  color: var(--ipi-ink-3);
  overflow-wrap: anywhere;
}

.fra-agroup__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 8px 14px;
  flex: 0 1 auto;
  min-width: 0;
}

.fra-agroup__progress {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 220px;
  max-width: 100%;
  min-width: 0;
}

.fra-agroup__bar {
  flex: 1 1 auto;
  min-width: 60px;
  height: 8px;
  border: 0;
  border-radius: 999px;
  background: var(--ipi-bg-inset);
  overflow: hidden;
  appearance: none;
  -webkit-appearance: none;
}

.fra-agroup__bar::-webkit-progress-bar {
  background: var(--ipi-bg-inset);
  border-radius: 999px;
}

.fra-agroup__bar::-webkit-progress-value {
  background: var(--ipi-green);
  border-radius: 999px;
}

.fra-agroup__bar::-moz-progress-bar {
  background: var(--ipi-green);
  border-radius: 999px;
}

.fra-agroup__done {
  flex: 0 0 auto;
  font-size: 12.5px;
  color: var(--ipi-ink-3);
  white-space: nowrap;
}

.fra-agroup__done strong {
  color: var(--ipi-ink);
}

/* The open/closed cue: a chevron that turns when the card closes. */
.fra-agroup__chevron {
  width: 10px;
  height: 10px;
  margin: 0 4px;
  border-right: 2px solid var(--ipi-ink-3);
  border-bottom: 2px solid var(--ipi-ink-3);
  transform: rotate(45deg) translateY(-3px);
}

.fra-agroup:not([open]) .fra-agroup__chevron {
  transform: rotate(-45deg);
}

.fra-agroup__note {
  margin: 0;
  padding: 10px 18px;
  border-bottom: 1px solid var(--ipi-rule);
  background: var(--ipi-bg-inset);
  font-size: 13px;
  color: var(--ipi-ink-2);
}

.fra-agroup__note a {
  margin-left: 6px;
  font-weight: 600;
}

/* The card is the frame, so the table inside it loses its own. */
.fra-agroup .fra-actions-table-wrap {
  margin: 0;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

.fra-agroup__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  padding: 10px 18px;
  border-top: 1px solid var(--ipi-rule);
}

.fra-page a.fra-agroup__more {
  font-size: 13px;
  font-weight: 600;
  color: var(--ipi-blue);
  text-decoration: none;
}

.fra-page a.fra-agroup__more:hover {
  text-decoration: underline;
}

.fra-agroup__open {
  margin-left: auto;
}

.fra-actions-table__buttons {
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* A laptop or a landscape tablet (901–1279px): the two buttons stack so the action keeps its width. */
@media (min-width: 901px) and (max-width: 1279px) {
  .fra-actions-table__col-status { width: 124px; }
  .fra-actions-table__col-who { width: 15%; }
  .fra-actions-table__col-open { width: 158px; }

  .fra-actions-table__buttons {
    flex-direction: column;
    align-items: stretch;
  }
}

.fra-actions-table__photos {
  color: var(--ipi-ink-2);
}

@media (max-width: 900px) {
  .fra-agroups {
    gap: 12px;
  }

  /* On a phone an OPEN card loses its frame and its header sits on the page ground above the list —
     table cards must not sit inside a second card (table-cards.md, "Cards inside a panel"). A closed
     card keeps a light frame, so a page of closed buildings reads as a list of them. */
  .fra-agroup {
    box-shadow: none;
  }

  .fra-agroup[open] {
    border: 0;
    border-radius: 0;
    background: transparent;
    overflow: visible;
  }

  .fra-agroup__head {
    min-height: 56px;
    padding: 12px 14px;
    gap: 8px 12px;
  }

  .fra-agroup[open] > .fra-agroup__head {
    padding: 4px 0 10px;
    border-bottom: 0;
  }

  .fra-agroup__head:hover {
    background: transparent;
  }

  .fra-agroup__facts {
    flex: 1 1 100%;
    justify-content: flex-start;
  }

  .fra-agroup__progress {
    flex: 1 1 auto;
    width: auto;
  }

  .fra-agroup__note {
    margin: 0 0 10px;
    padding: 10px 12px;
    border: 0;
    border-radius: 9px;
  }

  .fra-agroup__foot {
    padding: 10px 0 0;
    border-top: 0;
  }

  .fra-agroup__open {
    min-height: 44px;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
 * CAPTURE POLISH — owner, 2026-09-28 ("some fields have no padding under them, pills have no
 * spacing, adding an image not polished"). Measured at 390 / 810 / 1440 against every one of the
 * fourteen sections before and after (storage/logs/fra-polish/survey.mjs): the finding pills sat
 * flush on the narrative box in nine sections, the narrative flush on its "more" fields in eight,
 * and the photo tiles were broken everywhere. Scoped to `.fra-capture-page`; tokens only.
 * ══════════════════════════════════════════════════════════════════════════ */

/* ── room between the parts of one question ─────────────────────────────── */
.fra-capture-page .fra-field__findings,
.fra-capture-page .fra-seg {
  gap: 10px;
}

.fra-capture-page .fra-field--judgement > .fra-field__findings + :not(.fra-field__requires),
.fra-capture-page .fra-field--judgement > .fra-field__requires + textarea,
.fra-capture-page .fra-field--judgement > .fra-field__findings ~ textarea {
  margin-top: 12px;
}

/* `~`, not `+`: the "Add a photo or details" toggle sits between them in the DOM, hidden once open. */
.fra-capture-page .fra-field--judgement > textarea ~ .fra-control__more {
  margin-top: 12px;
}

.fra-capture-page .fra-field__guard[open] > .fra-field__guard-panel {
  margin-top: 8px;
}

.fra-capture-page .fra-std-limitations[open] > .fra-std-limitations__list {
  margin-top: 10px;
}

/* ── a paragraph box takes its whole field; a date or a number is not cramped on a phone ── */
.fra-capture-page .fra-field--textarea > textarea {
  max-width: none;
}

@media (max-width: 900px) {
  .fra-capture-page .fra-field--date > input,
  .fra-capture-page .fra-field--int > input,
  .fra-capture-page .fra-field--decimal > input {
    max-width: none;
  }
}

/* ── the photograph strip ──────────────────────────────────────────────────
 * 🔴 THE TILES WERE BROKEN BY TWO RULES FROM ELSEWHERE, and both are answered with specificity,
 * not `!important`: `.content img { height: auto }` (style.css) outranked the tile's
 * `height: 100%`, so the photograph stopped short of its square with a white band beneath; and
 * the capture page's "every control is 44px" rule made the 22px remove chip a 44px bar down the
 * image. The chip is now a 26px circle whose ::before carries the 44px touch target.
 * ⚠ THE PHOTOGRAPHS COME FIRST, then "Add a photo", and the caption box shows only once a new
 * photograph is attached — it is that photograph's caption, and an empty caption box under a
 * finding with nothing attached read as a field to fill. */
.fra-capture-page .fra-photos {
  gap: 10px;
  margin-top: 12px;
}

.fra-capture-page .fra-photos > .fra-photos__thumbs {
  order: -2;
  gap: 10px;
}

/* The new photograph's caption sits straight under it, before "Add another photograph". */
.fra-capture-page .fra-photos > .fra-photos__caption-label {
  order: -1;
}

.fra-capture-page .fra-photos .fra-photos__thumb {
  width: 96px;
  height: 96px;
  border-radius: 10px;
  background: var(--ipi-bg-inset);
}

.fra-capture-page .fra-photos .fra-photos__thumb-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.fra-capture-page .fra-photos .fra-photos__remove {
  top: 5px;
  right: 5px;
  width: 26px;
  height: 26px;
  min-height: 0;
  min-width: 0;
  font-size: 16px;
  box-shadow: 0 1px 3px color-mix(in srgb, var(--ipi-ink) 30%, transparent);
}

.fra-capture-page .fra-photos .fra-photos__remove::before {
  content: '';
  position: absolute;
  inset: -9px;
}

.fra-capture-page .fra-photos:not(:has([data-fra-photo-pending])) > .fra-photos__caption-label {
  display: none;
}

.fra-capture-page .fra-photos > .fra-photos__caption-label {
  margin-top: 0;
}

.fra-capture-page .fra-photos .fra-photos__action {
  min-height: 48px;
  padding: 6px 14px 6px 8px;
  font-size: 14px;
  border-radius: 10px;
}

.fra-capture-page .fra-photos .fra-photos__icon {
  width: 32px;
  height: 32px;
}

@media (min-width: 901px) {
  /* A button, not a bar across the whole row — and a caption line, not a paragraph box. */
  .fra-capture-page .fra-photos > .fra-photos__zone {
    align-self: flex-start;
  }

  .fra-capture-page .fra-photos > .fra-photos__caption-label {
    max-width: 520px;
  }
}

@media (max-width: 900px) {
  /* Four to a row on a phone; on a tablet as many 104px tiles as fit, never a 180px one. */
  .fra-capture-page .fra-photos > .fra-photos__thumbs {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(72px, 104px));
    gap: 8px;
  }

  .fra-capture-page .fra-photos .fra-photos__thumb {
    width: auto;
    height: auto;
    aspect-ratio: 1;
  }
}

/* ⚠ STILL HIDDEN WHEN EMPTY — the phone grid above would otherwise outrank the count rule. */
.fra-capture-page .fra-photos[data-fra-photo-count="0"] > .fra-photos__thumbs {
  display: none;
}

/* ── Record progress: the completion photograph and the form's shape (2026-09-28 polish) ──
 * The photograph was the browser's own "Choose File / No file chosen" box. Once the script has
 * bound the form (`[data-fra-aphoto-bound]`) the input is clipped — still focusable, still what
 * posts — and its label is the capture strip's camera button; a chosen photograph shows as a
 * thumbnail with its name, and only then its caption box. With no script the native input stays. */
[data-fra-aphoto-bound] .fra-aphotos-add__file {
  position: relative;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  align-self: start;
  width: fit-content;
  max-width: 100%;
  min-height: 48px;
  padding: 6px 14px 6px 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink-2);
  cursor: pointer;
  background: var(--surface-subtle);
  border: 1px dashed var(--rule-strong);
  border-radius: 10px;
}

[data-fra-aphoto-bound] .fra-aphotos-add__file:hover {
  border-color: var(--ink-3);
}

[data-fra-aphoto-bound] .fra-aphotos-add__file:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

[data-fra-aphoto-bound] .fra-aphotos-add__file input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  min-height: 0;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.fra-aphotos-add__icon {
  display: none;
}

[data-fra-aphoto-bound] .fra-aphotos-add__icon {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  color: var(--ink-3);
  background: var(--surface-card);
  border-radius: 8px;
}

.fra-aphotos-add__name {
  min-width: 0;
  overflow: hidden;
  font-weight: 400;
  color: var(--ink-3);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.fra-aphotos-add__name:empty {
  display: none;
}

.fra-aphotos-add__preview {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: 8px;
}

/* A chosen photograph replaces the camera icon with itself. */
[data-fra-aphoto-bound] .fra-aphotos-add__row.is-filled .fra-aphotos-add__icon {
  display: none;
}

[data-fra-aphoto-bound] .fra-aphotos-add__row.is-filled .fra-aphotos-add__file {
  border-style: solid;
}

/* Chosen: the thumbnail and its name, not the invitation as well (it wrapped on a phone). */
[data-fra-aphoto-bound] .fra-aphotos-add__row.is-filled .fra-aphotos-add__file-label {
  display: none;
}

[data-fra-aphoto-bound] .fra-aphotos-add__row.is-filled .fra-aphotos-add__name {
  font-weight: 500;
  color: var(--ink-2);
}

[data-fra-aphoto-bound] .fra-aphotos-add__row:not(.is-filled) .fra-aphotos-add__caption {
  display: none;
}

@media (min-width: 901px) {
  /* ⚠ TWO COLUMNS SINCE THE PAGE IS TABBED. "One column at every width" (above) held while the
     form sat in the narrow record pane; in the Action plan tab it is ~1,050px wide, and one column
     made a date box as wide as the page. Status beside the date, the assignee beside who owes the
     work; the evidence, the photographs, the note and the button take the row. */
  .fra-assessment-page .fra-action-plan__track-form {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: 16px;
    max-width: 880px;
  }

  .fra-assessment-page .fra-action-plan__track-form > :not(label.ipi-field),
  .fra-assessment-page .fra-action-plan__track-form > label.ipi-field:has(textarea) {
    grid-column: 1 / -1;
  }

  [data-fra-aphoto-bound] .fra-aphotos-add__caption {
    max-width: 520px;
  }
}

/* FRA My Jobs (2026-09-28 polish): a status chip is one line. "In progress" wrapped into a
   two-line bubble in the desktop table's narrow Status column. */
[data-fra-my-jobs-index] .badge {
  white-space: nowrap;
}

/* Wide-screen rows (2026-10-08 table redesign plan) — FRA-only touches. None applies below 901px
   or to a table without data-rc-rows (the demo renders the assessments and actions views without it).
   - Assessments: the row's controls sit on one line with its ⋯ and chevron. An inspector's next
     step sits behind ⋯ (2026-10-08 vocabulary: the name is the open, nothing else in the row); it
     carries data-rc-wide-keep, so table-rows.css's rule hiding a row-opening primary link leaves it.
   - The building record's fire stopping surveys: the Surveyor CELL carries [data-rc-sub] (it is the
     card's sub-line), so it keeps its table-cell box rather than table-rows.css's one-line block. */
html.rc-wide table[data-fra-assessments-table][data-rc][data-rc-rows] .ipi-row-actions {
  display: inline-flex;
  vertical-align: middle;
}

/* - Actions: Record progress and the ⋯ sit on one line (the stack below 1280px was for two buttons). */
html.rc-wide table[data-fra-actions-table][data-rc][data-rc-rows] .fra-actions-table__buttons {
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
}

@media (max-width: 1279px) {
  html.rc-wide table[data-fra-actions-table][data-rc][data-rc-rows] > colgroup > .fra-actions-table__col-open {
    width: 204px;
  }
}

/* - The template's question list: a panel's band heading keeps its own height (a band is not a record row). */
html.rc-wide table.fra-template-questions[data-rc][data-rc-rows] > tbody > tr[data-rc-group] > td {
  height: auto;
}

html.rc-wide table[data-fra-record-fs-surveys][data-rc][data-rc-rows] > tbody > tr > td[data-rc-sub] {
  display: table-cell;
  max-width: none;
  margin-top: 0;
  overflow: visible;
  white-space: nowrap;
  text-overflow: clip;
  font-size: 13.5px;
  color: var(--ink-2);
}

/* The scannable vocabulary (2026-10-08): one line per row.
   - A record's name and its grey line sit side by side. These cells hold them as direct children,
     and the pages draw the name as a block (the actions' two-line clamp, the template's names); the
     building record's survey reference is a block <div> (a card fact). */
html.rc-wide table[data-fra-actions-table][data-rc][data-rc-rows] > tbody > tr > td[data-rc="title"] > .fra-actions-table__desc,
html.rc-wide table.fra-template-table[data-rc][data-rc-rows] > tbody > tr > td[data-rc="title"] > .fra-template-table__name {
  display: inline-block;
  vertical-align: middle; /* a clipping inline-block's baseline is its bottom edge — the name rode high */
}

/*   Held as direct children (no flex wrapper to shrink them), the name and grey line share the cell
     by proportion so neither runs into the next column; the cell clips what is left. */
html.rc-wide :is(table[data-fra-actions-table], table.fra-template-table, table[data-fra-assessments-table], table[data-fra-record-fs-surveys])[data-rc][data-rc-rows] > tbody > tr > td[data-rc="title"] {
  overflow: hidden;
}

html.rc-wide :is(table[data-fra-actions-table], table.fra-template-table, table[data-fra-assessments-table], table[data-fra-record-fs-surveys])[data-rc][data-rc-rows] > tbody > tr > td[data-rc="title"] > [data-rc-name] {
  max-width: min(260px, 62%);
}

html.rc-wide :is(table[data-fra-actions-table], table.fra-template-table, table[data-fra-assessments-table], table[data-fra-record-fs-surveys])[data-rc][data-rc-rows] > tbody > tr > td[data-rc="title"] > [data-rc-sub] {
  max-width: min(200px, calc(38% - 8px));
}

@media (max-width: 1366px) {
  html.rc-wide :is(table[data-fra-actions-table], table.fra-template-table, table[data-fra-assessments-table], table[data-fra-record-fs-surveys])[data-rc][data-rc-rows] > tbody > tr > td[data-rc="title"] > [data-rc-name] {
    max-width: min(190px, 62%);
  }

  html.rc-wide :is(table[data-fra-actions-table], table.fra-template-table, table[data-fra-assessments-table], table[data-fra-record-fs-surveys])[data-rc][data-rc-rows] > tbody > tr > td[data-rc="title"] > [data-rc-sub] {
    max-width: min(110px, calc(38% - 8px));
  }
}

html.rc-wide table[data-fra-record-fs-surveys][data-rc][data-rc-rows] > tbody > tr > td[data-rc="title"] > div {
  display: inline-block;
  margin-left: 8px;
  font-size: 12.5px;
  color: var(--rw-ink-3);
}

/* - Actions: every fact cell is one line, in the middle of the row (the page tops them for its
     two-line clamp), and the Due column holds "Within 6 months" whole. */
html.rc-wide table[data-fra-actions-table][data-rc][data-rc-rows] > tbody > tr > td {
  vertical-align: middle;
}

html.rc-wide table[data-fra-actions-table][data-rc][data-rc-rows] > tbody > tr > td:not([data-rc="title"], [data-rc="actions"]) {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

html.rc-wide table[data-fra-actions-table][data-rc][data-rc-rows] > colgroup > .fra-actions-table__col-due {
  width: 140px;
}
