/* ============================================================================
   THE COMPONENT LAYER
   ============================================================================

   One definition per component. If a component is defined here, no template
   may redefine it — that rule is what the whole rework exists to establish.

   Built from docs/design/samples/E-components.html (Direction C, owner-confirmed
   2026-09-04) and the 36px input geometry (owner-confirmed 2026-09-04).

   ⛔ WHY THIS FILE EXISTS. The app had 76 distinct button classes for what is
   really one button with different fills -- btn-approve, btn-accept, btn-claim
   and btn-activate are the same control under four names. `.btn`, the base class
   the samples approved, did not exist at all. Deduplicating CSS rules did not fix
   that, because the problem was never duplicate rules: it was the absence of a
   component to point at.

   ⚠️ Loads AFTER _01..._13 so it wins over the older per-page rules it replaces,
   and BEFORE any template <style>, which still wins by source order. That is
   deliberate: a page can still override, but now it has to mean it.
   ========================================================================= */



/* ⛔ SPECIFICITY, NOT SOURCE ORDER. `_11-theme-chrome.css` scopes its rules as
   `body.laser-theme .card` -- specificity (0,2,1). A bare `.card` here is
   (0,1,0) and LOSES no matter how late this file loads. That is why the first
   build measured 3px button / 6px card / 6px input radii instead of the
   approved 8 / 12 / 12, and read "edgy" rather than round.

   ⛔ AND IT MUST NOT REQUIRE THE THEME CLASS. `LASER_THEME_ENABLED` defaults to
   "False" (base.py:1285) and is set in NEITHER `.env.example` NOR
   `env_vars_prod.yaml` -- so `body.laser-theme` is never emitted in staging or
   production. Scoping components on it ALONE meant this entire file would render
   nothing outside a developer's laptop, where a local .env happens to set it True.
   That is why every rule is written TWICE: `body.laser-theme .x` keeps the
   (0,2,1) needed to outrank the theme layer when it is on, and `body .x` makes
   the component work when it is off.

   So every component rule below is scoped `body.laser-theme .x` -- the SAME
   (0,2,1) the theme layer uses, and this file loads after it, so source order
   decides and the component wins. `body .x` at (0,1,1) was tried first and still
   lost: measured 6px card radius against the approved 12px.

   ⚠️ That means these components are theme-scoped like everything else here. A
   page's own <style> still wins, which is the intended escape hatch. The
   alternative was the priority flag, which the IMPORTANT_BASELINE guard exists
   to keep out.
   ------------------------------------------------------------------------ */

/* ---------------------------------------------------------------- BUTTON ---
   One height (32px), one shape. The FILL carries the meaning, never the size.
   Semantic variants are fills over the same base, so a new intent is one line
   here rather than a new class in a template.
   ------------------------------------------------------------------------ */
body.laser-theme .btn,
body .btn {
    height: var(--h-button);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    padding: 0 var(--s-3);
    border: 1px solid var(--border-color);
    border-radius: var(--r-button);
    background: var(--surface-alt-color);
    color: var(--text-primary-color);
    font-family: inherit;
    font-size: var(--font-size-body, 14px);
    font-weight: 600;
    line-height: 1;
    text-transform: none;
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;

    /* A button is inline-flex, but as a flex ITEM (in .actions, or dropped into
       `main`) `align-self: stretch` is the default and it grows to the full row
       width -- measured 1518px for a "Process Payment" button. Pin both axes:
       the button sizes to its label, and .btn--block is the deliberate opt-in. */
    flex: 0 0 auto;
    align-self: flex-start;
    transition: background var(--transition, 250ms), border-color var(--transition, 250ms);
}
body.laser-theme .btn:hover,
body .btn:hover { background: var(--row-hover-color, rgba(255, 255, 255, 0.06)); text-decoration: none; }body.laser-theme .btn:focus-visible,
body .btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }

/* A disabled control must look unavailable AND refuse the pointer. Opacity
   alone still accepts clicks in some browsers when the element is an <a>. */body.laser-theme .btn:disabled,
body .btn:disabled,
body.laser-theme .btn[aria-disabled="true"],
body .btn[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; pointer-events: none; }
body.laser-theme .btn--primary,
body .btn--primary { background: var(--primary-color); border-color: var(--primary-color); color: var(--on-primary-color, #003038); }body.laser-theme .btn--primary:hover,
body .btn--primary:hover { background: var(--primary-hover-color, var(--primary-color)); border-color: var(--primary-hover-color, var(--primary-color)); }
/* ⛔ `success` PAINTS CYAN, NOT GREEN (owner, 2026-09-07). The app had a green fill
   and a cyan fill side by side -- "Process Payment" cyan next to "Approve" green --
   and the green did not belong to the palette. One accent, not two.

   The VARIANT NAME is kept: 98 call sites say `variant="success"`, and they are
   right about the MEANING (this is the affirmative action). Re-pointing the paint
   here changes all 98 at once; renaming them would be 98 edits to say the same
   thing. Semantic name, single accent.

   ⚠️ `--success-color` itself is untouched -- status badges, note tones and money
   deltas still use green, where it means "good outcome" rather than "press this". */
body.laser-theme .btn--success,
body .btn--success { background: var(--primary-color); border-color: var(--primary-color); color: var(--on-primary-color, #003038); }body.laser-theme .btn--danger,
body .btn--danger  { background: transparent; border-color: var(--error-color); color: var(--error-color); }body.laser-theme .btn--danger:hover,
body .btn--danger:hover { background: color-mix(in srgb, var(--error-color) 12%, transparent); }

/* Ghost: no chrome until you interact with it. For the third and fourth action
   in a row, where four bordered buttons would read as four equal choices. */body.laser-theme .btn--ghost,
body .btn--ghost { background: transparent; border-color: transparent; color: var(--text-secondary-color); }body.laser-theme .btn--ghost:hover,
body .btn--ghost:hover { background: var(--row-hover-color, rgba(255, 255, 255, 0.06)); color: var(--text-primary-color); }
body.laser-theme .btn--sm,
body .btn--sm { height: 26px; padding: 0 var(--s-2); font-size: var(--font-size-caption); }body.laser-theme .btn--lg,
body .btn--lg { height: var(--h-touch); padding: 0 var(--s-4); }

/* Full width is a LAYOUT choice, so it is a modifier and not a variant. */body.laser-theme .btn--block,
body .btn--block { width: 100%; }

/* ⚠️ Touch only. 32px is right for a mouse and 12px short for a finger
   (WCAG 2.5.8). Gating on pointer keeps desktop rows dense, which is what the
   compact ruling asked for -- raising every toolbar to 44px costs a row of
   vertical space on every list page. */
@media (pointer: coarse), (max-width: 640px) {
    body.laser-theme .btn { height: var(--h-touch); }
    body.laser-theme .btn--sm { height: var(--h-touch-sm); }
}


/* ------------------------------------------------------------ FORM FIELD ---
   191 hand-rolled label+input blocks across 33 pages, and no component for the
   single most repeated markup in the app. That is why one page's field carries
   a help line and the next one's does not.

   Geometry is the owner's ruling: 36px tall, 16px text. ⚠️ 16px is not a style
   choice -- below it, iOS auto-zooms the field on focus, and the payment form
   is the worst place for the viewport to jump.

   ⛔ A fixed HEIGHT, never symmetric padding. In every padding-based variant the
   <select> rendered 1-2px taller than its own <input> (measured 43/45, 34/36,
   40/41, 42/44) because the two controls have different intrinsic content
   boxes. Equal padding cannot produce equal height; a height can. Measured 36/36.
   ------------------------------------------------------------------------ */
body.laser-theme .field,
body .field { margin-bottom: var(--s-4); }
body.laser-theme .field-label,
body .field-label {
    display: block;
    margin-bottom: var(--s-1, var(--s-1));
    font-size: var(--font-size-caption);
    font-weight: 600;
    color: var(--text-secondary-color);
}

/* The marker distinguishes required from optional among SEVERAL fields. On a
   single-field form it says nothing, so it is opt-in per field, not automatic. */body.laser-theme .field-required,
body .field-required { color: var(--error-color); margin-left: 2px; }

/* ⚠️ The theme targets fields by ATTRIBUTE -- `body.laser-theme input[type="text"]`
   and friends, which is (0,2,1) exactly like `body.laser-theme .field-input`.
   Source order should then decide, and this file loads later... but the theme
   lists ~10 attribute selectors and the LAST matching one wins within its own
   sheet, so the tie resolved against the class for <input> while <select>
   passed. Adding `input.field-input` makes it (0,2,2) and settles it.
   MEASURED: input radius 6px -> 12px, select already 12px. */body.laser-theme input.field-input,
body input.field-input,
body.laser-theme select.field-select,
body select.field-select,
body.laser-theme textarea.field-textarea,
body textarea.field-textarea,
body.laser-theme .field-input,
body .field-input,
body.laser-theme .field-select,
body .field-select,
body.laser-theme .field-textarea,
body .field-textarea {
    width: 100%;
    max-width: 100%;
    height: var(--h-input);
    padding: 0 var(--s-3);
    border: 1px solid var(--border-color);
    border-radius: var(--r-input);
    background: var(--surface-color);
    color: var(--text-primary-color);
    font-family: inherit;
    font-size: var(--fs-input);
    box-sizing: border-box;
    transition: border-color var(--transition, 250ms);
}

/* ⚠️ A textarea is sized by `rows`. A fixed height would collapse a multi-line
   note to a single line -- the one deliberate exception to the rule above. */body.laser-theme .field-textarea,
body .field-textarea { height: auto; min-height: calc(var(--h-input) * 2); padding: var(--s-2) var(--s-3); line-height: 1.5; }
body.laser-theme .field-input:focus,
body .field-input:focus,
body.laser-theme .field-select:focus,
body .field-select:focus,
body.laser-theme .field-textarea:focus,
body .field-textarea:focus { outline: none; border-color: var(--primary-color); }
/* WCAG 2.4.7 -- the rule above is the ONLY focus treatment the shared field component
   has, and a border-colour swap is not an indicator: it recolours an edge that was
   already drawn instead of adding anything, so there is no new 3:1 boundary for a
   low-vision keyboard user to find. This is also the layer that matters most -- this
   file loads LAST and `components/field.html` is the migration target for every
   converted form, so one missing ring here is every converted field at once.
   It cannot fall through to the global `*:focus-visible` ring in
   `_02-buttons-and-icons.css`: that selector is (0,1,0) and the rule above is (0,2,1). */
body.laser-theme .field-input:focus-visible,
body .field-input:focus-visible,
body.laser-theme .field-select:focus-visible,
body .field-select:focus-visible,
body.laser-theme .field-textarea:focus-visible,
body .field-textarea:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; }
body.laser-theme .field-input:disabled,
body .field-input:disabled,
body.laser-theme .field-select:disabled,
body .field-select:disabled,
body.laser-theme .field-textarea:disabled,
body .field-textarea:disabled { opacity: 0.6; cursor: not-allowed; }
body.laser-theme .field-help,
body .field-help { margin-top: var(--s-1, var(--s-1)); font-size: var(--font-size-caption); color: var(--text-secondary-color); }

/* ⚠️ Named `.field-msg-error`, NOT `.field-error`. `_02-buttons-and-icons.css`
   already owns `.field-error` and uses it for the INPUT -- it sets a red border
   with the priority flag and an error background. Reusing the name painted a red
   bar behind the message text. Two different things needed two names.

   An error must not be colour-only -- colour alone fails WCAG 1.4.1 for a
   colour-blind user. Callers pair this with aria-invalid + aria-describedby so
   a screen reader hears the message, and the text itself names the problem. */body.laser-theme .field-msg-error,
body .field-msg-error { margin-top: var(--s-1, var(--s-1)); font-size: var(--font-size-caption); color: var(--error-color); }body.laser-theme .field-input[aria-invalid="true"],
body .field-input[aria-invalid="true"],
body.laser-theme .field-select[aria-invalid="true"],
body .field-select[aria-invalid="true"],
body.laser-theme .field-textarea[aria-invalid="true"],
body .field-textarea[aria-invalid="true"] { border-color: var(--error-color); }

/* ⚠️ A <select> sizes itself to its LONGEST <option>. "Partially Refunded" made
   one control 355px and wrapped a six-control toolbar over 4px. The popup opens
   at its own width regardless, so the closed control only ever needs to show
   the selected value. */body.laser-theme .field-select,
body .field-select { max-width: 100%; text-overflow: ellipsis; }

@media (pointer: coarse), (max-width: 640px) {
    body.laser-theme .field-input,
    body.laser-theme .field-select { height: var(--h-touch); }
}


/* ------------------------------------------------- CHECKBOX, RADIO, TOGGLE ---
   ⛔ Real controls, restyled with accent-color -- never a <div> pretending to be
   a checkbox. A div loses keyboard focus, the space key, and the screen reader's
   "checked" state, and every one of those has to be rebuilt by hand and will be
   rebuilt wrong.
   ------------------------------------------------------------------------ */
body.laser-theme .check,
body .check { display: inline-flex; align-items: center; gap: var(--s-2); cursor: pointer; }body.laser-theme .check input[type="checkbox"],
body .check input[type="checkbox"],
body.laser-theme .check input[type="radio"],
body .check input[type="radio"] {
    width: 18px;
    height: 18px;
    margin: 0;
    accent-color: var(--primary-color);
    cursor: pointer;
    flex-shrink: 0;
}body.laser-theme .check-label,
body .check-label { font-size: var(--font-size-body, 14px); color: var(--text-primary-color); }

body.laser-theme .notif-preference-row,
body .notif-preference-row {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    cursor: pointer;
}

@media (pointer: coarse), (max-width: 640px) {
    body.laser-theme .notif-preference-row,
    body .notif-preference-row { min-height: var(--h-touch); }
}

/* ⚠️ The visible pill is NOT the tap target -- the input behind it is 0x0, so
   measuring the input reports height 0 and looks like a bug. The LABEL is the
   target. Measured at 26x50, which is 18px under the touch floor, so the hit
   area is extended with a centred ::after rather than by growing the pill: a
   44px pill reads as a button, not a switch. */body.laser-theme .toggle,
body .toggle { position: relative; display: inline-flex; align-items: center; cursor: pointer; }body.laser-theme .toggle input,
body .toggle input { position: absolute; opacity: 0; width: 0; height: 0; }body.laser-theme .toggle-track,
body .toggle-track {
    width: 44px;
    height: 24px;
    border-radius: 999px;
    background: var(--border-color);
    transition: background var(--transition, 250ms);
    flex-shrink: 0;
}body.laser-theme .toggle-thumb,
body .toggle-thumb {
    position: absolute;
    left: 3px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--text-primary-color);
    transition: transform var(--transition, 250ms);
    pointer-events: none;
}body.laser-theme .toggle input:checked ~ .toggle-track,
body .toggle input:checked ~ .toggle-track { background: var(--primary-color); }body.laser-theme .toggle input:checked ~ .toggle-thumb,
body .toggle input:checked ~ .toggle-thumb { transform: translateX(20px); }body.laser-theme .toggle input:focus-visible ~ .toggle-track,
body .toggle input:focus-visible ~ .toggle-track { outline: 2px solid var(--primary-color); outline-offset: 2px; }

/* The hit area, invisible and centred. This is what makes a 24px control
   satisfy a 44px floor without changing how it looks. */body.laser-theme .toggle::after,
body .toggle::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--h-touch);
    height: var(--h-touch);
    transform: translate(-50%, -50%);
}



/* ⛔ BADGE, PAGE HEADER and EMPTY STATE ARE NOT DEFINED HERE ON PURPOSE.
   `templates/partials/` already had them -- `_status_badge.html`,
   `_page_header.html`, `_empty_state.html` -- with 4, 12 and 11 real adopters
   and better parameters than the versions this file first shipped.

   Building a second set beside them was the exact defect this rework exists to
   remove: two definitions of one thing, free to drift. The duplicates were
   deleted and the incumbents kept. Their CSS lives with them
   (`.laser-badge`, `.page-header`, `.laser-empty`).

   ★ Before adding a component here, check `templates/partials/` first.
   ------------------------------------------------------------------------ */

/* ----------------------------------------------------------------- TABLE ---
   ⛔ THERE IS NO TABLE COMPONENT HERE, AND THAT IS DELIBERATE.

   `_10b-components-surfaces.css:299` already single-sources every table in the
   app -- 22 legacy class names aliased onto one rule set for width, borders,
   header treatment, cell padding, hover and the last-row rule. The convergence
   this file was going to perform had already happened.

   A `.table` component was written here and then DELETED, because it painted
   LESS than the classes it replaced and would have silently dropped:
     * `table-layout: fixed` -- without it the 141 `.col-*` width helpers are inert
     * the owner-ruled header/cell wrap (#2119, #2289)
     * sortable headers and the `:has(> .sort-link)` fix (#2168)
     * the compact density toggle, `td.empty-row`, and the print sticky reset
     * `.numeric` right-align (71 uses) -- the component had renamed it `.num`
     * the measured 10-column width ladder on the accounts table

   ★ The lesson: a "component" that is newer is not automatically the canonical
   one. `_10b` was the real component; this would have been a THIRTEENTH
   vocabulary. Check what already converged before converging it again.

   ⚠️ A stale note here previously claimed `.data-table` disagreed with itself
   (headers at one padding, its own cells at another). `_10b` fixed that; the
   note was describing a defect that no longer existed.
   ------------------------------------------------------------------------ */


/* ------------------------------------------------------------------ CARD ---
   ⛔ `.card` IS NOT DEFINED HERE. It is aliased onto `.section-card` in
   `_10-components.css` -- one rule paints both, plus `.panel`, `.vendor-status`
   and the other section boxes.

   They were the same box with two radii: `.section-card` had drifted to
   `--border-radius-md` (6px) while the approved sample's section box
   (`samples/_tokens.css` `.section`) is `--r-card` (12px). Every other
   declaration -- background, border, margin, overflow -- was already identical.

   ★ So the fix was ONE TOKEN, not 23 template conversions. Defining `.card`
   again here would have re-created the two-definitions-of-one-thing defect this
   rework exists to remove; `.card-head` is aliased onto the head family the same
   way. `components/card.html` is the markup; `_10` is the paint.
   ------------------------------------------------------------------------ */


/* ------------------------------------------------- KPI TILE / METRIC CARD ---
   ⛔ `.metric-card` had NO base rule in any stylesheet -- it was defined three
   separate times inside template <style> blocks and rendered at 24/12/10/8px
   padding depending on which page you were on, with no min-height, so tiles went
   uneven the moment one label wrapped to a second line.
   ------------------------------------------------------------------------ */
body.laser-theme .kpis,
body .kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); gap: var(--s-3); }
body.laser-theme .kpi,
body .kpi {
    display: flex;
    flex-direction: column;
    gap: var(--s-1, var(--s-1));
    min-height: 84px;
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--border-color);
    border-radius: var(--r-card);
    background: var(--surface-color);
}
body.laser-theme .kpi-label,
body .kpi-label { font-size: var(--font-size-caption); font-weight: 600; color: var(--text-secondary-color); }body.laser-theme .kpi-value,
body .kpi-value { font-size: var(--fs-metric, 28px); font-weight: 600; color: var(--text-primary-color); font-variant-numeric: tabular-nums lining-nums; }body.laser-theme .kpi-hint,
body .kpi-hint { font-size: var(--font-size-caption); color: var(--text-secondary-color); }


/* ------------------------------------------------------------- DETAIL ROW ---
   81 uses, hand-rolled on every detail page. The read-only alternative to a
   form: labels 12px uppercase, values tabular so money columns align.
   ------------------------------------------------------------------------ */
/* ⛔ THIS IS WHY DETAIL PAGES FELT EMPTY AND STRETCHED.

   `body .facts` (0,1,1) declared `display: grid` with NO COLUMNS, beating
   `_10-components.css:566`'s `.facts` (0,1,0) which sets the real track list — so the
   grid collapsed to a single column. `body .fact` then made each fact a full-width
   `space-between` row: label pinned left, value pinned right, a rule under each, one per
   line. On a 1500px page that is a label and a value separated by a metre of nothing,
   repeated seven times.

   ★ The approved sample (`docs/design/samples/_tokens.css:539`) is an AUTO-FIT GRID at a
   200px minimum, so a wide screen shows four to six facts per row and the page reads
   compact. The label sits ABOVE its value, not across the page from it — that is what
   makes the pair read as one object instead of two things at opposite edges.

   Restored to the sample. `.fact-value` keeps `text-align: left` because in a grid cell
   the value belongs under its own label; right-alignment only made sense while each fact
   was a full-width row. */
/* ⚠️ 240px, not the sample's 200px — measured 2026-09-08. At a 200px floor a 1460px card
   resolved to SIX 223px columns, and "local_enterprise@example.com" (a perfectly ordinary
   value) broke mid-word across two lines because an email address offers the line breaker
   no opportunity. 240px fits the common long values -- an email, a timestamp, a full name
   -- and a wide screen still gets five or six columns. The inner `min()` keeps the track
   from overflowing a viewport narrower than the floor. */
body.laser-theme .facts,
body .facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
    gap: var(--s-4) var(--s-5);
    margin: 0;
}
body.laser-theme .fact,
body .fact { display: block; }body.laser-theme .fact-label,
body .fact-label { display: block; margin-bottom: 2px; font-size: var(--font-size-caption); font-weight: 600; color: var(--text-secondary-color); text-transform: uppercase; letter-spacing: 0.02em; }body.laser-theme .fact-value,
body .fact-value { display: block; font-size: var(--font-size-body, 14px); color: var(--text-primary-color); font-variant-numeric: tabular-nums lining-nums; overflow-wrap: break-word; hyphens: none; }


/* ----------------------------------------------------------------- MODAL ---
   ⛔ THERE IS NO MODAL COMPONENT HERE, AND THAT IS DELIBERATE -- the same finding
   as the table.

   `_10e-modals.css` already owns the dialog: `.modal-content`, `.modal-header`,
   `.modal-body`, `.modal-footer`, `.modal-actions`, `.modal-close`, `.modal-form`.
   The app uses that vocabulary (10 `.modal-content`, 12 `.modal-actions`, …) and
   so does the shared destructive gate, `partials/_confirm_modal.html`.

   A `components/modal.html` was written here with its OWN names -- `.modal-backdrop`,
   `.modal-head`, `.modal-foot` -- and adopted by exactly ZERO pages. It was a THIRD
   vocabulary for one dialog, which is the defect this rework exists to remove, so it
   was deleted along with its CSS.

   ★ Same lesson as the table: check what already converged before converging it
   again. The thing with adopters is the real component.
   ------------------------------------------------------------------------ */


/* -------------------------------------------------- MESSAGES AND CALLOUTS ---
   A message is transient and reports what just happened. A callout is part of
   the page and explains a standing condition -- most often the gate chain, why
   something cannot proceed. Same shape, different lifetime, so both live here
   and neither is rebuilt per page.

   ⛔ A 3px left mark carries the meaning. No coloured box: a fully tinted panel
   competes with the content it is explaining.
   ------------------------------------------------------------------------ */
body.laser-theme .note,
body .note {
    display: flex;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    margin-bottom: var(--s-3);
    border: 1px solid var(--border-color);
    border-left: 3px solid var(--text-secondary-color);
    border-radius: var(--border-radius-md);
    background: var(--surface-alt-color);
    font-size: var(--font-size-body, 14px);
    color: var(--text-primary-color);
}
body.laser-theme .note-success,
body .note-success { border-left-color: var(--success-color); }body.laser-theme .note-warning,
body .note-warning { border-left-color: var(--warning-color); }body.laser-theme .note-danger,
body .note-danger  { border-left-color: var(--error-color); }body.laser-theme .note-error,
/* ⛔ `.note-error` is an ALIAS, added 2026-09-08. The component documents its tone set as
   `success | warning | danger | info`, but DESIGN.md's badge section uses `error` for the
   same semantic, and two agents independently reached for `tone="error"` — which rendered
   an UNSTYLED note, silently, because a wrong variant name still produces a class.
   The badge family already answers to `error`, so the two components disagreed on the name
   of one idea. Accepting both is cheaper than a rename across every call site, and it means
   the failure mode of guessing is a correct note rather than a grey one. */
body .note-error   { border-left-color: var(--error-color); }body.laser-theme .note-info,
body .note-info    { border-left-color: var(--primary-color); }body.laser-theme .note-title,
body .note-title { font-weight: 600; margin: 0 0 var(--s-1, var(--s-1)); }


/* ----------------------------------------------------------- EMPTY STATE ---
   ⚠️ An empty table that says nothing is indistinguishable from one that failed
   to load. The empty state is what tells them apart.
   ------------------------------------------------------------------------ */


/* The skeleton reuses the real row's padding, so nothing shifts when data lands. */body.laser-theme .skeleton,
body .skeleton { height: 12px; border-radius: var(--border-radius-sm); background: var(--surface-alt-color); animation: skeleton-pulse 1.4s ease-in-out infinite; }
@keyframes skeleton-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } }


/* --------------------------------------------------------------- STEPPER ---
   5 templates run a wizard -- payment capture, LOA signing, onboarding -- with
   no shared progress indicator between them. This is the component the MFA
   onboarding pages were missing, which is why their four steps did not match.

   ⛔ The step row must NOT use Tailwind utilities. This project has no Tailwind:
   `.flex` happens to exist, `.justify-between` does not, so the four labels
   rendered flush as one run-on string. `.items-center` and `.py-3` are likewise
   undefined here and style nothing.
   ------------------------------------------------------------------------ */
body.laser-theme .stepper,
body .stepper { margin-bottom: var(--s-5); }body.laser-theme .stepper-steps,
body .stepper-steps { display: flex; justify-content: space-between; gap: var(--s-2); flex-wrap: wrap; margin-bottom: var(--s-2); font-size: var(--font-size-caption); color: var(--text-secondary-color); }body.laser-theme .stepper-step-current,
body .stepper-step-current { font-weight: 700; color: var(--primary-color); }body.laser-theme .stepper-track,
body .stepper-track { height: 6px; border-radius: 999px; background: var(--surface-alt-color); overflow: hidden; }body.laser-theme .stepper-fill,
body .stepper-fill { height: 100%; width: 100%; border-radius: 999px; background: var(--primary-color); transform-origin: left center; transform: scaleX(0); transition: transform var(--transition, 250ms); }

/* Fixed widths, because the step count is a fixed set -- so no inline style is
   needed and style-src never has to allow 'unsafe-inline' for a progress bar. */body.laser-theme .stepper-w-25,
body .stepper-w-25 { transform: scaleX(0.25); }body.laser-theme .stepper-w-50,
body .stepper-w-50 { transform: scaleX(0.5); }body.laser-theme .stepper-w-75,
body .stepper-w-75 { transform: scaleX(0.75); }body.laser-theme .stepper-w-100,
body .stepper-w-100 { transform: scaleX(1.0); }


/* ------------------------------------------------------------ PAGINATION ---
   7 templates paginate, each with its own markup. Sits inside the section's own
   border, so it needs no box of its own.
   ------------------------------------------------------------------------ */
body.laser-theme .pager,
body .pager { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); padding: var(--s-3) var(--s-4); border-top: 1px solid var(--border-color); font-size: var(--font-size-caption); color: var(--text-secondary-color); }body.laser-theme .pager-links,
body .pager-links { display: flex; align-items: center; gap: var(--s-1, var(--s-1)); }


/* ------------------------------------------------------------------ TABS ---
   3px indicator, radius 3px 3px 0 0. The active delta is weight 500->600 plus
   colour ONLY, so nothing reflows when you switch tab -- a size change would
   shift every tab beside it.

   Behaviour comes from record-tabs.js, already loaded globally: arrow keys,
   Home and End. ⚠️ Declaring roving tabindex without a key handler, which the
   app's own pill tabs do today, promises keyboard support that is not there.
   ------------------------------------------------------------------------ */
body.laser-theme .tabs,
body .tabs { display: flex; gap: var(--s-4); border-bottom: 1px solid var(--border-color); margin-bottom: var(--s-4); }body.laser-theme .tab,
body .tab {
    padding: var(--s-2) 0;
    border: none;
    border-bottom: 3px solid transparent;
    border-radius: 3px 3px 0 0;
    background: none;
    font-family: inherit;
    font-size: var(--font-size-body, 14px);
    font-weight: 500;
    color: var(--text-secondary-color);
    cursor: pointer;
}body.laser-theme .tab[aria-selected="true"],
body .tab[aria-selected="true"] { border-bottom-color: var(--primary-color); font-weight: 600; color: var(--primary-color); }body.laser-theme .tab:focus-visible,
body .tab:focus-visible { outline: 2px solid var(--primary-color); outline-offset: -2px; }


/* --------------------------------------------------------------- TOOLTIP ---
   ⚠️ An element must NEVER carry both aria-label and title -- a guard test fails
   the build on it, and a screen reader announces one of them twice.
   ------------------------------------------------------------------------ */
body.laser-theme .tip,
body .tip { position: relative; display: inline-flex; align-items: center; border-bottom: 1px dotted var(--primary-color); cursor: help; }


/* ----------------------------------------------------------- FILE UPLOAD ---
   8 templates take a file: LOA templates, POA documents, remittance uploads.
   ------------------------------------------------------------------------ */
body.laser-theme .dropzone,
body .dropzone { padding: var(--s-6, var(--s-6)) var(--s-4); border: 1px dashed var(--border-color); border-radius: var(--r-card); background: var(--surface-alt-color); text-align: center; color: var(--text-secondary-color); }body.laser-theme .dropzone:hover,
body .dropzone:hover,
body.laser-theme .dropzone.is-dragover,
body .dropzone.is-dragover { border-color: var(--primary-color); color: var(--text-primary-color); }body.laser-theme .file-row,
body .file-row { display: flex; align-items: center; justify-content: space-between; gap: var(--s-3); padding: var(--s-2) var(--s-3); border: 1px solid var(--border-color); border-radius: var(--border-radius-md); background: var(--surface-color); }


/* ---------------------------------------------------------------- TOASTS ---
   9 templates show a notification. Fixed to the viewport so it never shifts the
   page it is reporting on.
   ------------------------------------------------------------------------ */
body.laser-theme .toasts,
body .toasts {
    position: fixed;
    right: var(--s-4);
    bottom: var(--s-4);
    z-index: 1100;
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
}
body.laser-theme .toast,
body .toast {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-width: 240px;
    max-width: 360px;
    min-height: var(--h-touch);
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--border-color);
    border-left: 3px solid var(--primary-color);

    /* r-card, not the 6px legacy step: a toast is a small card, and at 6px it
       read as a different shape family from everything around it. */
    border-radius: var(--r-card);
    background: var(--surface-color);
    color: var(--text-primary-color);
    font-size: var(--font-size-body, 14px);
    line-height: 1.4;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

/* A toast shown inline (the gallery, or a page that stacks them in flow) must
   not be torn out to the viewport corner. */body.laser-theme .toasts-inline,
body .toasts-inline { position: static; display: flex; flex-direction: column; gap: var(--s-2); align-items: flex-start; }
body.laser-theme .toast-success,
body .toast-success { border-left-color: var(--success-color); }body.laser-theme .toast-danger,
body .toast-danger  { border-left-color: var(--error-color); }body.laser-theme .toast-warning,
body .toast-warning { border-left-color: var(--warning-color); }


/* -------------------------------------------------------------- BAR LIST ---
   There are zero charts in the app and no charting library. This shows
   proportion without adding a dependency -- the answer to "proper information
   instead of click cards" (sample F, Option 2).
   ------------------------------------------------------------------------ */
body.laser-theme .bars,
body .bars { display: grid; gap: var(--s-4); }body.laser-theme .bar-row,
body .bar-row { display: grid; grid-template-columns: 1fr auto; gap: var(--s-2) var(--s-3); align-items: baseline; }body.laser-theme .bar-label,
body .bar-label { font-size: var(--font-size-body, 14px); color: var(--text-primary-color); }body.laser-theme .bar-value,
body .bar-value { font-size: var(--font-size-body, 14px); color: var(--text-secondary-color); font-variant-numeric: tabular-nums lining-nums; }body.laser-theme .bar-track,
body .bar-track { grid-column: 1 / -1; height: 6px; border-radius: 999px; background: var(--surface-alt-color); overflow: hidden; }body.laser-theme .bar-fill,
body .bar-fill { height: 100%; border-radius: 999px; background: var(--primary-color); }


/* -------------------------------------------------------------- TIMELINE ---
   The audit-trail shape. Every activity list is a table today; a sequence of
   events reads better as a timeline than as rows.
   ------------------------------------------------------------------------ */

/* ⛔ RESET THE LIST MARKER. This is an <ol> -- the order of events is meaningful,
   so an ordered list is the right element -- but the default decimal marker
   rendered "1. 2. 3." right on top of the dots. MEASURED: list-style-type
   computed `decimal` while the ::before dot sat at left:-20px, so every row
   showed a number AND a dot. `list-style: none` on both the list and the item;
   the item alone is not enough in every browser. */body.laser-theme .timeline,
body .timeline {
    display: grid;
    gap: var(--s-5);
    margin: 0;
    padding-left: var(--s-5);
    list-style: none;
    border-left: 1px solid var(--border-color);
}
body.laser-theme .timeline-item,
body .timeline-item { position: relative; list-style: none; }

/* The dot sits ON the rail, not beside it: half its width past the border so it
   is visually centred on the 1px line. */body.laser-theme .timeline-item::before,
body .timeline-item::before {
    content: "";
    position: absolute;
    left: calc(var(--s-5) * -1 - 4px);
    top: 5px;
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--primary-color);

    /* A ring in the page ground so the rail appears to pass BEHIND the dot
       rather than through it. */
    box-shadow: 0 0 0 3px var(--background-color, #0A0A0A);
}
body.laser-theme .timeline-time,
body .timeline-time {
    display: block;
    margin-bottom: 2px;
    font-size: var(--font-size-caption);
    color: var(--text-secondary-color);
}
body.laser-theme .timeline-text,
body .timeline-text { font-size: var(--font-size-body, 14px); color: var(--text-primary-color); line-height: 1.5; }


/* ------------------------------------------------------------- ACCORDION ---
   ⛔ A <details> opened inside {% if %} and closed outside it is a
   TemplateSyntaxError that takes down every page for every role. Keep the whole
   element inside one branch.
   ------------------------------------------------------------------------ */
body.laser-theme .accordion,
body .accordion { border: 1px solid var(--border-color); border-radius: var(--r-card); background: var(--surface-color); overflow: hidden; }body.laser-theme .accordion > summary,
body .accordion > summary {
    display: flex;
    align-items: center;
    min-height: var(--h-touch);
    padding: var(--s-2) var(--s-4);
    font-size: var(--font-size-body, 14px);
    font-weight: 600;
    color: var(--text-primary-color);
    cursor: pointer;
    list-style: none;
}body.laser-theme .accordion > summary::-webkit-details-marker,
body .accordion > summary::-webkit-details-marker { display: none; }body.laser-theme .accordion > summary:focus-visible,
body .accordion > summary:focus-visible { outline: 2px solid var(--primary-color); outline-offset: -2px; }body.laser-theme .accordion[open] > summary,
body .accordion[open] > summary { border-bottom: 1px solid var(--border-color); }body.laser-theme .accordion-body,
body .accordion-body { padding: var(--s-4); }


/* ------------------------------------------------------------ ACTION ROW ---
   29 hand-rolled groups. Wraps rather than overflowing: a toolbar that scrolls
   sideways hides the action at its end.
   ------------------------------------------------------------------------ */
body.laser-theme .actions,
body .actions { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }body.laser-theme .actions-end,
body .actions-end { justify-content: flex-end; }body.laser-theme .actions-between,
body .actions-between { justify-content: space-between; }


/* ---------------------------------------------------- GALLERY (DEBUG ONLY) ---
   Spacing for the component gallery itself. It lives here rather than in an
   inline <style> so the gallery is subject to exactly the same cascade as every
   real page -- a gallery styled by its own rules would prove nothing about how
   the components behave in the app.
   ------------------------------------------------------------------------ */
body.laser-theme .tip-demo,
body .tip-demo { margin-top: var(--s-4); }

body.laser-theme .card .kpis,
body .card .kpis { margin: 0; }

body.laser-theme .card + .card,
body .card + .card { margin-top: 0; }



/* ------------------------------------------------ RADIOGROUP + ADORNED FIELD ---
   Two markup SHAPES that a one-label-one-control field cannot express. Each was
   refused repeatedly by conversion agents (radiogroup 5 times, adornment 4) --
   which is the signal that they are components, not parameters.
   ------------------------------------------------------------------------ */

body.laser-theme .radiogroup,
body .radiogroup { display: flex; flex-wrap: wrap; gap: var(--s-2); }

body.laser-theme .radio-option,
body .radio-option {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    min-height: var(--h-input);
    padding: 0 var(--s-3);
    border: 1px solid var(--border-color);
    border-radius: var(--r-input);
    background: var(--surface-color);
    cursor: pointer;
}

body.laser-theme .radio-option input[type="radio"],
body .radio-option input[type="radio"] {
    width: 18px;
    height: 18px;
    margin: 0;
    accent-color: var(--primary-color);
    cursor: pointer;
    flex-shrink: 0;
}

/* The selected option reads as selected without relying on the dot alone -- a
   1px border change is easy to miss at a glance on a dense form. */
body.laser-theme .radio-option:has(input:checked),
body .radio-option:has(input:checked) { border-color: var(--primary-color); }

body.laser-theme .radio-option:focus-within,
body .radio-option:focus-within { outline: 2px solid var(--primary-color); outline-offset: 2px; }

body.laser-theme .radio-option-label,
body .radio-option-label { font-size: var(--font-size-body, 14px); color: var(--text-primary-color); }

/* --- adorned field --- */

body.laser-theme .field-adorned,
body .field-adorned { position: relative; display: flex; align-items: center; }

/* The symbol sits INSIDE the control's box, so the input takes the padding rather
   than the wrapper -- otherwise the text would start under the symbol. */
body.laser-theme .field-adornment,
body .field-adornment {
    position: absolute;
    color: var(--text-secondary-color);
    font-size: var(--fs-input);
    pointer-events: none;
}

body.laser-theme .field-adornment-pre,
body .field-adornment-pre { left: var(--s-3); }

body.laser-theme .field-adornment-post,
body .field-adornment-post { right: var(--s-3); }

body.laser-theme .field-adorned-pre .field-input,
body .field-adorned-pre .field-input { padding-left: var(--s-6, var(--s-6)); }

body.laser-theme .field-adorned-post .field-input,
body .field-adorned-post .field-input { padding-right: var(--s-7, var(--s-7)); }


/* ---------------------------------------------- TWO CLASSES IN SHARED PARTIALS ---
   Both are used by partials with many adopters and were defined NOWHERE, so they
   rendered unstyled on every page that includes them. Found by sweeping each class
   in the markup against every stylesheet -- not by looking at a page.

   `.section-card-body` is the body of `partials/_section_card_open.html` (23 adopters).
   ------------------------------------------------------------------------ */

body .section-card-body { padding: var(--s-4); }

/* ⛔ `.laser-back-link` REMOVED 2026-09-10 — the back link is now `.page-back` (design E:
   an icon square plus a label), and the DSC console's sideways nav row, which had borrowed
   this class only for its muted colour, has its own `.console-links`. Both replacements
   are at the end of this file.
   ⚠️ Verified before deleting: ZERO occurrences in markup. The two that a grep still finds
   are prose inside `{% comment %}` blocks describing this very change — the literal-in-its-
   own-documentation trap this repo records. */


/* ------------------------------------------------- SECTION BOXES: THE BORDER ---
   ⛔ THE HAIRLINE IS THE WHOLE POINT OF DIRECTION C, so it must not depend on a
   feature flag.

   `.section-card` (19 uses, via partials/_section_card_open.html) sets ONLY padding
   and margin. Its border and background come from `.laser-glass`, and BOTH are
   scoped `body.laser-theme` -- a class emitted only when LASER_THEME_ENABLED is on.
   That flag defaulted to False and was set in neither .env.example nor
   env_vars_prod.yaml, so for months those 11 pages rendered as bare, borderless
   boxes everywhere except a developer's laptop.

   The flag now defaults on, which fixes it -- but a structural border that exists
   only because of an env var is one deploy away from vanishing again. These rules
   give the section boxes their hairline unconditionally; the theme layer still
   restyles them where it wants to, because it loads with higher specificity.

   ⚠️ `.panel` (46 uses) had NO border rule of its own in any unscoped stylesheet
   either. Same fix, same reason.
   ------------------------------------------------------------------------ */

body .section-card,
body .panel {
    border: 1px solid var(--border-color);
    border-radius: var(--r-card);
    background: var(--surface-color);
    overflow: hidden;
}

/* The head band, matching `.card-head` so a section looks the same whichever
   partial drew it. */
/* ⛔ NO `justify-content: space-between` HERE — it was the bug, on 51 templates.
   `.section-card-head` holds an ICON and its OWN TITLE (`partials/_section_card_open.html`
   emits `.section-card-icon` then `.section-card-title`). `space-between` pushed those two to
   opposite ends of the card, so every panel read as a lone glyph far-left and its heading
   far-right — "🛡  ................  Consumer identity" — instead of one labelled heading.

   ⚠️ `space-between` is right for a header carrying a title AND a trailing action, which
   is what it was presumably copied from. It is wrong for a header whose children ARE the
   label. `gap` alone keeps the icon beside the words it names; a count badge that needs to
   sit at the far end should be pushed with `margin-left: auto` on that element, so the
   default stays "these belong together". */
body .section-card-head {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-3) var(--s-4);
    border-bottom: 1px solid var(--border-color);
}


/* --------------------------------------------------------- SORTABLE HEADER ---
   ⛔ THE PAINT IS NOT HERE. `_06-tables-and-tooltips.css:1330` already owns the
   sortable header, including the #2168 fix -- `padding: 0` on the cell so the
   ANCHOR owns it and the whole header is the click target, and a
   `:not(:has(> .sort-link))` branch that restores padding for the JS-bound
   `th[data-sort]` tables that have no anchor inside.

   A rule set was written here and deleted on discovering that. It is the third
   time in this rework: table, modal, and now this. ★ Check what already converged
   before converging it again -- and `_06`'s version is better, because it knows
   about a case this one did not.

   `components/sort_header.html` emits `.sortable` + `.sort-link` so it lands on
   that existing rule. The only thing this file adds is the ACTIVE state, which
   `_06` has no equivalent for.
   ------------------------------------------------------------------------ */

body .sort-link-active,
body.laser-theme .sort-link-active { color: var(--primary-color); font-weight: 600; }

body .sort-caret,
body.laser-theme .sort-caret { font-size: 0.7em; line-height: 1; }

/* --------------------------------------------------------------------------
   CARD GRID — a detail page's sections, side by side instead of stacked.

   ⛔ THE DEFECT THIS FIXES, in the owner's words (2026-09-08): detail pages
   "feel like vertical cards only and also like stretched", "one thing on left
   that is label and the value on right so the center feels very empty".

   Measured on /organizations/bayview-local/ before: three `.section-card`s at
   1290px each, stacked, holding 3-4 short facts apiece — a page of full-width
   boxes each ~85% empty, and a lot of scrolling to read twelve values.

   A short section is not worth a full page width. This grid pairs them up on a
   desktop and stacks them on a phone with no media query and no fixed column
   count. The 26rem floor is where a `.facts` grid inside a card still fits two
   columns; below it the card would hold a single column and the pairing stops
   earning its place.

   ⚠️ `min()` is load-bearing: a bare 26rem floor is 416px, wider than a 375px
   viewport, so the grid itself would overflow before its content did. That is
   the #2168-shape bug already fixed twice in this project.

   ⚠️ `align-items: start` — without it every card in a row stretches to the
   tallest, which is how a 3-fact card ends up with a hand-span of dead space
   under it. Each card is as tall as its own content.

   USE IT FOR: two or more short, peer sections on a detail page.
   NOT FOR: a section holding a table or a form — those want the full measure,
   so leave them as full-width siblings outside the grid.
   ------------------------------------------------------------------------ */

body .card-grid,
body.laser-theme .card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(26rem, 100%), 1fr));
    gap: var(--s-4);
    align-items: start;
}

/* A grid item refuses to shrink below its content without this, which is what
   makes a long unbroken value push the whole column wider than its track. */
body .card-grid > *,
body.laser-theme .card-grid > * {
    min-width: 0;
}

/* --------------------------------------------------------------------------
   ⛔ THE CARD PADDING SAFETY NET — the fix for "text touching the borders".

   MEASURED 2026-09-08 on /csr/account/<id>/: the `Debtor Information` card sat at
   x=85 and its first label at x=86. ONE PIXEL. The same on `Verification Status`
   and the account summary strip.

   THE CAUSE IS NOT A MISSING PADDING VALUE — both `.section-card-body` and
   `.section-card-body` correctly declare `var(--s-4)`. The cause is that the card
   HAS NO BODY ELEMENT: the template puts content directly inside `.section-card`,
   which carries `padding: 0` by deliberate design (a table must fill the panel edge
   to edge). So the rule that owns the padding matched nothing, and the defect was
   invisible to every grep — the CSS was right and the markup never asked for it.

   ★ THIS IS THE SELECTOR-LEVEL GUARANTEE. Any direct child of a card that is not a
   head, a footer, or a full-bleed element gets the inset. A page can no longer ship
   flush text by forgetting a wrapper div; it has to opt OUT deliberately, which is
   the correct default for a defect that reads as broken.

   WHY `:not()` AND NOT A BLANKET PADDING ON `.section-card`:
     - a `<table>` must reach the card's edge — an inset there is a second gutter;
     - `.section-card-head` paints its own band edge to edge;
     - `.table-scroll` is the scroll boundary and must not be inset;
     - anything already carrying `-body` has the padding from its own rule, and
       doubling it would give 32px.

   ⚠️ Specificity is (0,2,0) via `body`, matching the theme scope, so it survives a
   template's inline block at equal specificity and loses only to something more
   specific — which is what a deliberate opt-out looks like.
   ------------------------------------------------------------------------ */

body .section-card > *:not(.section-card-head):not(.card-head):not(.section-header):not(.section-card-body):not(.card-body):not(.section-card-footer):not(.card-footer):not(table):not(.table-scroll):not(.table-container):not(.flush),
body.laser-theme .section-card > *:not(.section-card-head):not(.card-head):not(.section-header):not(.section-card-body):not(.card-body):not(.section-card-footer):not(.card-footer):not(table):not(.table-scroll):not(.table-container):not(.flush),
body .card > *:not(.card-head):not(.section-card-head):not(.card-body):not(.section-card-body):not(.card-footer):not(table):not(.table-scroll):not(.table-container):not(.flush),
body.laser-theme .card > *:not(.card-head):not(.section-card-head):not(.card-body):not(.section-card-body):not(.card-footer):not(table):not(.table-scroll):not(.table-container):not(.flush) {
    padding-inline: var(--s-4);
}

/* The first and last such child also need the vertical inset — without it the
   content is correctly inset from the sides and still flush with the top or bottom
   edge, which reads as the same defect rotated 90 degrees. */
body .section-card > *:not(.section-card-head):not(.card-head):not(.section-header):not(.section-card-body):not(.card-body):not(table):not(.table-scroll):not(.table-container):not(.flush):first-child,
body.laser-theme .section-card > *:not(.section-card-head):not(.card-head):not(.section-header):not(.section-card-body):not(.card-body):not(table):not(.table-scroll):not(.table-container):not(.flush):first-child {
    padding-block-start: var(--s-4);
}

body .section-card > *:not(.section-card-head):not(.card-head):not(.section-header):not(.section-card-body):not(.card-body):not(table):not(.table-scroll):not(.table-container):not(.flush):last-child,
body.laser-theme .section-card > *:not(.section-card-head):not(.card-head):not(.section-header):not(.section-card-body):not(.card-body):not(table):not(.table-scroll):not(.table-container):not(.flush):last-child {
    padding-block-end: var(--s-4);
}

/* A head is immediately followed by content, so that content needs its own top
   inset — the head's bottom hairline is a boundary, not a gutter. */
body .section-card > .section-card-head + *:not(table):not(.table-scroll):not(.table-container):not(.flush),
body.laser-theme .section-card > .section-card-head + *:not(table):not(.table-scroll):not(.table-container):not(.flush) {
    padding-block-start: var(--s-4);
}

/* A fact whose value is the one the reader came for — the outstanding balance on an
   account, the amount on a payment. Emphasis by WEIGHT and the primary text colour, never
   by a fill: One Accent Rule, and it survives greyscale. */
body .fact-value-strong,
body.laser-theme .fact-value-strong {
    font-weight: 600;
    color: var(--text-primary-color);
}

/* --------------------------------------------------------------------------
   ACTION HINT — the sentence that says what a row of buttons is for.

   ⛔ THE OWNER'S DEFECT, 2026-09-08: "lots of buttons are at the bottom. I don't
   know what that buttons means… no one will focus on them. There should be at
   least description or anything." A row of same-weight buttons with no context is
   a quiz, not a choice. This is the line that asks the question they answer.

   ★ PROMOTED FROM SEVEN LOCAL COPIES. `.form-actions-hint` was defined in SIX page
   `<style>` blocks (loa_send · loa_record_verbal · loa_upload_external ·
   loa_email_form · csr_process_payment · csr_create_settlement) and the same idea
   appeared twice more under different names — `.loa-actions-hint`
   (csr_account_detail) and `.health-actions-hint` (fdw_health). Three names, eight
   definitions, one thing. The aliases are folded in here so a page cannot pick the
   wrong one, and so the next page gets it by writing the class.

   ⚠️ Scoped `body …` / `body.laser-theme …` (0,2,0) like every rule in this file: a
   page's inline block renders after every stylesheet, so a bare `.x` here would lose
   to any leftover local copy at equal specificity instead of replacing it.
   ------------------------------------------------------------------------ */

body .form-actions-hint,
body.laser-theme .form-actions-hint,
body .loa-actions-hint,
body.laser-theme .loa-actions-hint,
body .health-actions-hint,
body.laser-theme .health-actions-hint {
    margin: 0 0 var(--s-2);
    font-size: var(--font-size-caption);
    color: var(--text-secondary-color);
}

/* When the hint introduces a WRAPPING row of peer actions it takes the whole first
   line, so the buttons start together underneath rather than trailing the sentence. */
body .loa-actions-hint,
body.laser-theme .loa-actions-hint {
    flex-basis: 100%;
}

/* A row of peer actions. Wraps, because four choices must stay reachable at any
   width — measured 2026-09-08 on csr_account_detail at 375px: `.loa-actions` had NO
   rule at all, so it computed `display: block` with no gap and stacked 155px tall. */
body .loa-actions,
body.laser-theme .loa-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s-2);
}

/* --------------------------------------------------------------------------
   MONEY — `partials/_money.html` → `.laser-money`

   ⛔ THE COMPONENT WORKED ONLY WITH THE THEME ON. Every `.laser-money*` rule lives in
   `_11-theme-chrome.css:409-418` under `body.laser-theme`, so with the flag off the
   component still delivered its `$` (that comes from the markup) but NONE of its
   typography: no tabular figures, no lining figures, no weight. Money columns stopped
   lining up on their decimal point, which is the entire reason the component exists
   (#1630). It affects 8 pages, the debtor receipt among them.

   ★ The double-write (`body .x, body.laser-theme .x`) is this file's convention precisely
   so a component cannot depend on a feature flag to render correctly. `_11`'s copy stays —
   it is the theme's own colour treatment and it wins where it should.
   ------------------------------------------------------------------------ */

body .laser-money,
body.laser-theme .laser-money {
    font-feature-settings: var(--font-numeric);
    font-variant-numeric: tabular-nums lining-nums;
    font-weight: 600;
}

body .laser-money-right,
body.laser-theme .laser-money-right { display: block; text-align: right; }

/* ⚠️ Tone colours are NOT re-declared here on purpose. `--laser-success` / `--laser-error`
   are theme tokens; the unthemed arm would need the semantic `--success-color` family, and
   a money value coloured differently in the two states is a decision, not a fix. The
   typography above is the part that is unambiguously broken without the flag. */

/* --------------------------------------------------------------------------
   PAYMENT TIMELINE — `partials/_payment_timeline.html`

   ⛔ THE PARTIAL HAS TWO CALLERS AND WAS STYLED FOR ONE. Every rule below lived in
   `csr_account_detail.html`'s page `<style>` block, so on `csr_payment_detail.html` —
   which includes the same partial — the timeline rendered with no entry separators, no
   meta row and no reference wrapping. A shared partial whose CSS lives in one caller is
   broken for every other caller by construction, and nothing reports it: the page renders,
   it just looks unfinished.

   ★ This is the same shape as `.laser-table` (a correct rule behind a class nobody carried)
   and `.loa-actions` (a class with no rule at all). The fix for all three is the same:
   the rule belongs where the component does.
   ------------------------------------------------------------------------ */

body .payment-timeline,
body.laser-theme .payment-timeline { max-width: 100%; }

body .payment-timeline summary,
body.laser-theme .payment-timeline summary { cursor: pointer; font-weight: 600; }

/* `min-width: 0` is load-bearing: without it a long processor reference refuses to shrink
   below its content and pushes the whole row wider than its column. */
body .payment-timeline-entry,
body.laser-theme .payment-timeline-entry {
    min-width: 0;
    padding: var(--s-3) 0;
    border-top: 1px solid var(--border-color);
}

body .payment-timeline-meta,
body.laser-theme .payment-timeline-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s-2);
}

/* A processor reference is one unbroken token with no space to wrap at, so `anywhere` is
   correct here specifically — it is an identifier, not prose. */
body .payment-timeline-reference,
body.laser-theme .payment-timeline-reference { overflow-wrap: anywhere; }

body .payment-timeline-reference .btn-verify,
body.laser-theme .payment-timeline-reference .btn-verify { margin-left: var(--s-2); }

/* --------------------------------------------------------------------------
   ⛔ THE THREE SANCTIONED PARTIALS RENDERED UNSTYLED WITHOUT THE THEME FLAG.

   Found independently by two agents on 2026-09-08, and measured: every rule for
   `.laser-badge` (8), `.laser-empty` (5) and `.laser-metric` (13 of 14) lives under
   `body.laser-theme` in `_11`/`_12`. So a page using `partials/_status_badge.html`,
   `partials/_empty_state.html` or `partials/_metric.html` gets a BARE SPAN with the flag
   off — no pill, no tint, no dot, no layout.

   ★ Why this is worse than it sounds: these are the components the whole conversion is
   telling every page to adopt. Two agents correctly REFUSED conversions because of it —
   they would have replaced a styled hand-rolled chip with an unstyled component. That is
   the right call, and it means the gap was actively blocking the rework.

   Reach: `_status_badge.html` 27 templates · `_empty_state.html` 33 · `_metric.html` 8.

   What is mirrored here is STRUCTURE and the semantic tones — the parts that must never
   depend on a flag. Deliberately NOT mirrored: the theme's glass fill, backdrop blur, glow
   and hover transform on `.laser-metric` (DESIGN.md bans glass and glow in the component
   layer anyway), and the `--laser-*` colour aliases, whose semantic equivalents are used
   instead so a re-themed tenant gets its own palette.
   ------------------------------------------------------------------------ */

/* ── Status badge ──────────────────────────────────────────────────────── */
body .laser-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    border-radius: 999px;
    font-size: var(--font-size-caption);
    font-weight: 600;
    letter-spacing: 0.01em;
    padding: 3px var(--spacing-1);
    white-space: nowrap;
}

/* A tinted fill + the hue as text + a matching edge. The 1px border is what keeps the chip
   legible for a colour-blind reader — shape and position carry state as well as hue. */
body .laser-badge-success { background: var(--success-bg-color); color: var(--success-text-color); border: 1px solid var(--success-border-color); }
body .laser-badge-error   { background: var(--error-bg-color);   color: var(--error-text-color);   border: 1px solid var(--error-border-color); }
body .laser-badge-warning { background: var(--warning-bg-color); color: var(--warning-text-color); border: 1px solid var(--warning-border-color); }
body .laser-badge-info    { background: var(--info-bg-color);    color: var(--info-text-color);    border: 1px solid var(--info-border-color); }
body .laser-badge-neutral { background: var(--neutral-bg-color); color: var(--neutral-text-color); border: 1px solid var(--border-color); }

/* The dot is the second channel — colour is never the only carrier of state (WCAG 1.4.1). */
body .laser-badge-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
    flex: 0 0 6px;
}

/* ── Empty state ───────────────────────────────────────────────────────── */
body .laser-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: var(--s-2);
    padding: var(--s-6) var(--s-4);
}

/* ⛔ HIDDEN, NOT PAINTED. `_empty_state.html` puts the `icon` parameter's VALUE in this
   span, and there is no icon font, sprite or SVG behind it — so `icon="users"` rendered the
   literal word "users" as a 28px cyan glyph above the message. Measured on 8 call sites
   across the DSC family; 6 more templates still pass the parameter.
   ★ `_11-theme-chrome.css:156` already fixed exactly this for the sibling `.section-card-icon`,
   with a comment naming the same symptom. This span was missed. One rule kills all of them;
   the parameter can stay harmlessly in the call sites until a real icon system backs it. */
body .laser-empty-icon { display: none; }

/* Body-text size and the SECONDARY colour, never a shrunken footnote: on an investigation
   surface "nothing here" is the answer, not chrome. */
body .laser-empty-message { margin: 0; color: var(--text-secondary-color); font-size: var(--font-size-body); line-height: 1.5; }
body .laser-empty-hint    { margin: 0; color: var(--text-muted-color, var(--text-secondary-color)); font-size: var(--font-size-body-small); }

/* ── Metric tile ───────────────────────────────────────────────────────── */
body .laser-metric {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: var(--s-3) var(--s-4);
    background: var(--surface-color);
    border: 1px solid var(--border-color);
    border-radius: var(--r-card);
}

body .laser-metric-label {
    color: var(--text-secondary-color);
    font-size: var(--font-size-caption);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

/* `--fs-metric` (28px) over a 12px label = the 2.33x KPI ratio ruled in D2. */
body .laser-metric-value {
    color: var(--text-primary-color);
    font-size: var(--fs-metric);
    font-weight: 700;
    font-variant-numeric: tabular-nums lining-nums;
    letter-spacing: -0.02em;
    line-height: 1.15;
}

body .laser-metric-success { color: var(--success-text-color); }
body .laser-metric-error   { color: var(--error-text-color); }
body .laser-metric-warning { color: var(--warning-text-color); }
body .laser-metric-info    { color: var(--info-text-color); }


/* --------------------------------------------------------------------------
   KPI EMPHASIS — the `.highlight` rail, unthemed.

   ⛔ `_13-theme-pages.css:154` gives `.metric-card.highlight` its 3px cyan rail under
   `body.laser-theme` only, and `_10-components.css:458` supplies `border-color` alone. So
   with the theme flag off the "this is the number the page is about" emphasis simply did
   not exist — on csr_process_payment, csr_create_settlement, staff_payment_lookup,
   csr_dashboard and refund_approval_queue.

   Same shape as `.laser-badge` / `.laser-empty` / `.laser-metric` / `.laser-money`: a
   component whose meaning lives in the theme layer loses that meaning when the flag is off.
   ------------------------------------------------------------------------ */

body .metric-card.highlight,
body.laser-theme .metric-card.highlight {
    border-left: 3px solid var(--primary-color);
}

/* --------------------------------------------------------------------------
   SECTION CARD CHILDREN + EMPTY-STATE CTA — the last theme-scope gaps.

   The card's own box was already mirrored; its CHILDREN were missed. Reach: 47 page
   templates via `partials/_section_card_open.html`, 32 via `partials/_empty_state.html`.

   ⛔ `.section-card-icon` IS THE SEVERE ONE, and it is not a styling loss. The span holds
   the icon's NAME as literal text — there is no icon font, sprite or SVG — so the rule's
   entire job is to hide it. `_11-theme-chrome.css:143` records the symptom: the DSC
   pipeline page read "users By rep" and "layers By firm". With the flag off that rule
   disappears and the debug text comes BACK, inside a heading, on every card passing an
   `icon`. Mirrored first for that reason.

   ⚠️ The theme flag is the ROLLBACK path (`base.py:1288` — it exists so a bad theme deploy
   is an env-flip to undo). So flag-off runs exactly when something is already wrong, which
   is the worst moment for 47 pages to also start printing debug words.
   ------------------------------------------------------------------------ */

/* Hidden, not styled — see above. Restore `display` here only when a real icon
   mechanism exists; the theme layer keeps the colour so nothing is reconstructed. */
body .section-card-icon { display: none; }

/* `margin: 0` on an h2/h3/h4 — without it the UA margin reopens and the head band
   grows about 1em top and bottom on every card. */
body .section-card-title {
    margin: 0;
    font-size: var(--fs-section);
    font-weight: 600;
    color: var(--text-primary-color);
    letter-spacing: -0.01em;
}

/* `margin-left: auto` is the only thing putting the actions at the right edge. */
body .section-card-actions { margin-left: auto; display: flex; gap: var(--spacing-1); }

body .section-card-subtitle {
    margin: 0 0 var(--spacing-2);
    color: var(--text-secondary-color);
    font-size: var(--font-size-body-small);
    line-height: 1.5;
}

body .section-card-count {
    /* ⛔ `margin-left: auto` is what puts this badge at the far end, and it is REQUIRED
       now that `.section-card-head` no longer sets `justify-content: space-between`.
       The badge used to ride that rule for free — but so did the icon and the title,
       which is why they were flung apart. Pushing only the element that should sit
       right keeps "icon + title belong together" as the default. */
    margin-left: auto;
    background: var(--neutral-bg-color);
    border: 1px solid var(--border-color);
    border-radius: 999px;
    color: var(--primary-color);
    font-size: var(--font-size-caption);
    font-variant-numeric: tabular-nums;
    font-weight: 600;
    padding: 1px var(--spacing-1);
}

/* ⛔ The empty-state CTA. `_empty_state.html` emits `.laser-btn .laser-btn-outline`, and
   `_11:221` carries `min-height: var(--h-touch)` — the WCAG 2.5.8 floor. Flag-off left a
   bare underlined anchor BELOW the touch floor on 32 empty states. The hover glow stays
   theme-only; the box and the floor do not. */
body .laser-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    min-height: var(--h-touch);
    padding: 0 var(--s-4);
    border-radius: var(--r-button);
    font-size: var(--font-size-body-small);
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}

body .laser-btn-outline {
    background: transparent;
    border: 1px solid var(--border-color);
    color: var(--primary-color);
}

/* The metric tile's third line: caption size + muted colour. Without it a hint renders at
   full body size directly under a 28px KPI, breaking the D2 2.33x ratio. `muted` is the
   DEFAULT delta tone in `_metric.html`, so it is the most-emitted of the five. */
body .laser-metric-hint,
body .laser-metric-delta { font-size: var(--font-size-caption); }
body .laser-metric-muted { color: var(--text-secondary-color); }

/* `flex: 0 0 auto` stops an icon being squeezed by a long label beside it. */
body .ic { flex: 0 0 auto; vertical-align: -0.125em; }

/* ── LOA anonymous landing (#2164) ───────────────────────────────────────
   The page an email-invite signer reaches after signing or declining. It is
   the ONLY authenticated-shell page an anonymous visitor sees, so it carries
   no nav and centres a single short message.

   ⛔ Unscoped, not `body.laser-theme`: this renders for a signer who may be a
   third-party rep with no session at all, and it must survive
   LASER_THEME_ENABLED being turned off — that flag is the rollback path.

   Moved out of the template's inline <style> so the file needs no
   inline-style-ratchet baseline entry (#1613). */
.loa-landing {
    width: clamp(20rem, 92vw, 34rem);
    margin-inline: auto;
    margin-block: 3.5rem;
    text-align: center;
}

.loa-landing-title {
    margin: 0 0 1rem 0;
    font-size: 1.5rem;
    font-weight: 600;
    line-height: 1.35;
    color: var(--text-primary-color);
}

.loa-landing-body {
    margin: 0 0 1rem 0;
    font-size: 1rem;
    line-height: 1.65;
    color: var(--text-primary-color);
}

.loa-landing-note {
    margin: 0;
    font-size: 0.875rem;
    line-height: 1.6;
    color: var(--text-secondary-color);
}

/* ⚠️ A vw width is right on desktop and wrong on a phone: 92% of 375 is 345px,
   surrendering 30px before the container's own padding. Full width under 640,
   and let one padding be the gutter. */
@media (max-width: 640px) {
    .loa-landing {
        width: 100%;
        margin-block: 2rem;
    }
}

/* ==========================================================================
   TABLE FRAME — the card draws it, per samples/C-bordered-laser.html
   ==========================================================================
   Owner request 2026-09-09: "the table to have the border and table whole outer
   border to be rounded as the cards", then the sample naming what that means.

   ⛔ THE FRAME IS THE CARD'S, NOT THE WRAPPER'S — and I built it the wrong way
   round first, so the correction is recorded rather than quietly replaced.

   My first version put `border` + `border-radius` on `.table-scroll` /
   `.table-container` and clipped them. Two failures came out of it: `overflow:
   clip` does not merely avoid establishing a scroll container the way `hidden`
   does, it forbids scrolling outright, so a too-wide table became unreachable
   (MEASURED: `overflowX: clip`, `scrollWidth === clientWidth`); and a wrapper
   with its own border inside a card is a box in a box, which `.section-card`'s
   own doc comment forbids.

   The sample settles it. `.table-wrap { overflow-x: auto; }` is its ENTIRE
   wrapper rule — no border, no radius. The `.section` box carries
   `border: 1px solid var(--border)`, `border-radius: var(--r-card)` and
   `overflow: hidden`, commented "so the table meets the radius". So the card is
   the frame, its clip is what rounds the table's outer corners, and the wrapper
   only scrolls. `.section-card` already declares all three (_10-components.css),
   which is why the rule below adds a frame ONLY to a table that is not in a card.

   ⚠️ `overflow: hidden` on the card kills `position: sticky` on the header cells
   inside it (_06-tables-and-tooltips.css:586 records this). The sample accepts
   that trade — its own sticky `th` is equally inert inside `.section` — and the
   panel-height tables here are short enough not to need it. Left as the sample
   has it rather than diverging from the approved design.
   ========================================================================== */

/* The wrapper scrolls. That is all it does. */
body .table-scroll,
body .table-container,
body.laser-theme .table-scroll,
body.laser-theme .table-container {
    overflow-x: auto;
}

/* ⛔ EVERY TABLE GETS THE FRAME (owner ruling, 2026-09-09: "this table has the border and
   roundedness and everything, and most of the tables do not have this").

   MEASURED — /csr/dashboard/ vs /accounts/users/, whose CELLS are already identical
   (same 12px cyan uppercase header, same `4px 16px` cells, same row hairlines):

     dashboard   wrapper 1px rgba(255,255,255,.16) + 12px radius
     users page  wrapper 0px, radius 0

   ⚠️ THE EARLIER `:not(.section-card *)` GUARD IS WHAT REMOVED IT, and the reasoning was
   half right. It excluded tables inside a card to avoid a box inside a box — correct when
   the table is flush against the card's own edge, since the card's `overflow: hidden`
   then rounds it. But a table inside `.section-card-body` has that body's padding around
   it, so the card's edge is nowhere near the table and NEITHER layer draws a frame. The
   table just floated in the panel.

   So the rule is now: a wrapper draws its own frame UNLESS it is flush against the card
   edge — which is exactly "is it a direct child of the card", not "is it anywhere inside
   one". `:not(.section-card > *)` says that precisely, and `.section-card-body` wrappers
   (the common case) now get the border and radius they were missing. */
body .table-scroll:not(.section-card > *):not(.card > *),
body .table-container:not(.section-card > *):not(.card > *) {
    border: 1px solid var(--laser-glass-border, var(--border-color));
    border-radius: var(--r-card);
}

/* The four corner cells follow the frame, since nothing clips them to it.
   `--r-card` minus the 1px border: an inner corner sharing the outer radius
   leaves a hairline of border showing through the gap at the diagonal. */
body .table-scroll:not(.section-card > *):not(.card > *) > table > thead > tr:first-child > th:first-child,
body .table-container:not(.section-card > *):not(.card > *) > table > thead > tr:first-child > th:first-child {
    border-start-start-radius: calc(var(--r-card) - 1px);
}

body .table-scroll:not(.section-card > *):not(.card > *) > table > thead > tr:first-child > th:last-child,
body .table-container:not(.section-card > *):not(.card > *) > table > thead > tr:first-child > th:last-child {
    border-start-end-radius: calc(var(--r-card) - 1px);
}

body .table-scroll:not(.section-card > *):not(.card > *) > table > tbody > tr:last-child > td:first-child,
body .table-container:not(.section-card > *):not(.card > *) > table > tbody > tr:last-child > td:first-child {
    border-end-start-radius: calc(var(--r-card) - 1px);
}

body .table-scroll:not(.section-card > *):not(.card > *) > table > tbody > tr:last-child > td:last-child,
body .table-container:not(.section-card > *):not(.card > *) > table > tbody > tr:last-child > td:last-child {
    border-end-end-radius: calc(var(--r-card) - 1px);
}

/* Edge to edge inside the panel: the table's own outer border would double the
   frame's line, and a rule under the final row draws a second hairline just
   inside the rounded corner. */
body .table-scroll > table,
body .table-container > table,
body.laser-theme .table-scroll > table,
body.laser-theme .table-container > table {
    border: 0;
    margin: 0;
}

body .table-scroll > table > tbody > tr:last-child > td,
body .table-container > table > tbody > tr:last-child > td,
body.laser-theme .table-scroll > table > tbody > tr:last-child > td,
body.laser-theme .table-container > table > tbody > tr:last-child > td {
    border-bottom: 0;
}


/* ==========================================================================
   CSR dashboard: the accounts section is a HEADING + a table, not a box
   ==========================================================================
   Owner request 2026-09-09: `.accounts-section` "gives us extra stuff that is not
   needed" — keep the section header's position and alignment, drop its background,
   so the TABLE is the only thing that reads as a separate surface.

   ⛔ THE BOX WAS DOUBLE-DRAWN, which is why it looked heavy. `.accounts-section`
   paints `background: var(--surface-color)` + a 1px border (_10h:214), and the table
   inside it now draws its own frame too — so a bordered panel sat inside a bordered
   panel with two different fills. `_12-theme-nav.css:305` even carries a
   `margin-inline: -1px` hack whose entire job is to drag the header's divider back
   over the outer border those two boxes created; with the outer box gone that
   correction has nothing to correct, and is neutralised below.

   ⚠️ The LAYOUT properties stay. `.accounts-section` is a `flex: 1` column with
   `min-height: 0`, and that is what lets the table scroll inside the remaining
   height instead of pushing the page taller — removing it collapses the panel.
   Only the PAINT is dropped. */
body.laser-theme .accounts-section,
body .accounts-section {
    background: none;
    border: 0;
    border-radius: 0;
}

/* The header keeps its spacing and its baseline; it just stops sitting on a band.
   ⚠️ `padding-inline: 0` because the removed border no longer indents it — left as
   it was, the heading would hang 16px right of the table's first column. */
body.laser-theme .accounts-section > .section-header,
body .accounts-section > .section-header {
    background: none;
    border-bottom: 0;
    margin-inline: 0;
    width: 100%;
    padding-inline: 0;
}

/* The negative-margin correction described above, neutralised — it exists only to
   compensate for a parent border that is now gone, and left in place it pulls the
   filter bar 1px outside the content column. */
body.laser-theme .accounts-section > .filter-bar {
    margin-inline: 0;
    width: 100%;
}


/* ==========================================================================
   SECTION CARD HEAD — translucent band, white divider (owner, 2026-09-09)
   ==========================================================================
   The card BOX already matched the approved sample exactly — same 7% fill, same
   `blur(20px) saturate(160%)`, same 16% white border, same 12px radius and specular
   lip. MEASURED side by side against `docs/design/samples`-style sample 1, the whole
   difference was the HEAD, in three values:

     head background   sample rgba(255,255,255,0.04)  ·  app rgb(34,34,34)
     divider           sample rgba(255,255,255,0.16)  ·  app rgba(0,229,255,0.2)
     title             sample 15px                    ·  app 20px

   ⛔ THE OPAQUE BAND IS WHY THE APP LOOKED LESS LIKE GLASS. `.section-card-head` takes
   `background: var(--surface-raised)` → `--surface-alt-color` → `#222222` under this
   theme. An opaque band across the top of a translucent pane blocks the backdrop
   exactly where the eye checks whether a surface is see-through, so the card read as a
   dark box with a glass body rather than as one piece of glass.

   The head now takes a WASH of white instead of a solid: it still reads as a distinct
   band against the 7% body, and the blur carries through it.

   ⛔ The divider is white, not cyan. `--border-color` resolves to `--laser-border-cyan`,
   which is the token for "the edge of a surface" — an internal divider between a head
   and its body is `--laser-border-divider`'s job, and at 20% cyan it drew a coloured
   line across every card on the page.

   ⚠️ The title is 15px (`--fs-section`), not 20px (`--font-size-section`). Two tokens one
   letter apart, and `_11-theme-chrome.css` had the wrong one — so a SECTION heading
   rendered larger than intended and competed with the page title above it. This restates
   `--fs-section` at a specificity that beats that file. */
body.laser-theme .section-card-head,
body.laser-theme .card-head,
body.laser-theme .section-header,
body.laser-theme .panel-header {
    /* ⚠️ Tracks the body fill: the head is a WASH ABOVE the card's own ground, so when
       the body dropped to 0.045 a 0.04 head was almost invisible against it. 0.07 keeps
       the same visual step between band and body that the sample has. */
    background: rgba(255, 255, 255, 0.07);
    border-bottom-color: var(--laser-glass-border);
}

body.laser-theme .section-card-head h2,
body.laser-theme .section-card-head h3,
body.laser-theme .section-card-title {
    font-size: var(--fs-section);
}


/* ==========================================================================
   ROW-HEADER CELLS read as DATA, not as column headers
   ==========================================================================
   ⛔ A `<th scope="row">` inherits every COLUMN-header rule, and that is why the
   Verification Status table did not match the accounts and history tables.

   MEASURED, verification row label vs an accounts-table data cell:

     text-transform   uppercase  ·  none
     font-size        12px       ·  14px
     background       #0D0D0D    ·  #1A1A1A   (the dark HEAD ground, on a body row)
     colour           cyan-ish   ·  white

   So "Address" rendered as "ADDRESS" in header type on the header's own dark band,
   which is what made the first column read as a separate block with a hard edge down
   the table rather than as the first column of data.

   ⛔ THE MARKUP IS RIGHT AND STAYS. `<th scope="row">` is what names the row for a
   screen reader ("Address — Verified — Unverify"); converting it to a `<td>` to fix the
   look would trade an accessibility feature for a styling shortcut. The fix belongs
   here: a cell inside `tbody` is DATA regardless of its tag.

   ⚠️ Scoped `tbody` deliberately — a `<th>` in `thead` must keep the header treatment.
   This is the shared table vocabulary, so every table with row headers is corrected at
   once rather than per page. */
body.laser-theme table tbody th,
body table tbody th {
    background-color: transparent;
    color: var(--laser-text-primary, var(--text-primary-color));
    /* ⚠️ `inherit`, NOT a token. The data cells beside this one take the TABLE's font
       size (14px), which no `--font-size-*` token names — `--font-size-body` is 16px and
       measured 16px here against 14px in the next column, so the row header was visibly
       larger than its own row. Inheriting keeps the two in step whatever the table sets. */
    font-size: inherit;
    font-weight: 500;
    text-transform: none;
    letter-spacing: normal;
}


/* ==========================================================================
   LOA collection methods — the Staff Tools tile treatment (owner, 2026-09-09)
   ==========================================================================
   The four ways to get an authorization on file are now described tiles rather than a
   row of same-weight buttons, reusing `.admin-links-described > .btn-action` so the two
   surfaces share one definition.

   ⛔ TWO ANCESTOR FLEX ROWS HAD TO BE UNPICKED, and neither is obvious from the markup:

     `.loa-actions`    is `display: flex` (:1125) — it was built for a row of BUTTONS, so
                       as a flex parent it would size the grid to its content instead of
                       letting it fill the row.
     `.loa-status-row` is `display: flex; align-items: center` — it holds the "LOA" label
                       and the badge BESIDE the actions, so the tile grid became a third
                       flex item squeezed into whatever width was left.

   The status line and the method grid are two stacked blocks, not three things on one
   row, so the container becomes a column and the grid takes the full width. */
body .loa-status-row:has(.loa-method-grid),
body.laser-theme .loa-status-row:has(.loa-method-grid) {
    display: block;
}

body .loa-actions:has(.loa-method-grid),
body.laser-theme .loa-actions:has(.loa-method-grid) {
    display: block;
}

/* ⚠️ `auto-fit` + `minmax(min(240px, 100%))`: the inner `min()` is what stops a track
   overflowing a viewport narrower than the 240px floor — the same guard the Staff Tools
   grid uses. Four tiles sit in one row on desktop and stack cleanly on a phone. */
body .loa-method-grid,
body.laser-theme .loa-method-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
    gap: var(--s-3);
    margin-top: var(--s-3);
}


/* ==========================================================================
   ONE CARD. Every private *-card class is aliased onto the section-card surface
   ==========================================================================
   Owner ruling 2026-09-09: "find other all the card classes such as lookup-card and any
   other and at all the places I want you to use the section card only".

   ⛔ FOURTEEN CLASSES WERE DECLARING THE SAME BOX. Measured across the project, each of
   these set `background: var(--surface-color)` + `1px solid var(--border-color)` + a
   radius, privately, in a page `<style>` block or a page-specific module:

     lookup-card · verify-card · form-card · summary-card · health-card · content-card
     landing-card · edit-form-card · payment-form-card · refund-form-card
     settlement-form-card · recovery-codes-card · system-palette-card
     dashboard-account-card

   They had already drifted — radii of `8px`, `var(--border-radius-md)` and `2px` borders
   sat side by side, and none picked up the glass treatment, so a "card" looked like a
   different component depending on which page you were on. That is the re-derived-rule
   defect (AGENTS.md §5) in visual form.

   ★ ALIASED, NOT RENAMED IN MARKUP. Rewriting 14 templates' class attributes would touch
   every page's HTML for a purely visual change and risk breaking the JS and page CSS that
   select on these names. Pointing them at one definition here achieves the single
   appearance with no markup churn, and it is the same lever `_11-theme-chrome.css`
   already uses for `.panel` / `.dashboard-card` / `.stat-card`.

   ⚠️ The page-local rules still exist and still declare their own background/border. They
   LOSE to this on specificity (0,2,0 vs 0,1,0) EXCEPT where the page's own block is an
   inline `<style>` in `{% block content %}` — that renders after every stylesheet and
   wins on source order at equal specificity. The `body.laser-theme` prefix is what beats
   those; a bare `.lookup-card` here would silently lose on exactly the pages that need
   it most. */
body.laser-theme .lookup-card,
body.laser-theme .verify-card,
body.laser-theme .form-card,
body.laser-theme .summary-card,
body.laser-theme .health-card,
body.laser-theme .content-card,
body.laser-theme .landing-card,
body.laser-theme .edit-form-card,
body.laser-theme .payment-form-card,
body.laser-theme .refund-form-card,
body.laser-theme .settlement-form-card,
body.laser-theme .recovery-codes-card,
body.laser-theme .system-palette-card,
body.laser-theme .dashboard-account-card,
body.laser-theme .staff-tools-card,
body.laser-theme .processors-card {
    background: var(--laser-glass-sheen);
    -webkit-backdrop-filter: var(--laser-glass-filter);
    backdrop-filter: var(--laser-glass-filter);
    border: 1px solid var(--laser-glass-border);
    border-radius: var(--r-card);
    box-shadow: var(--laser-glass-edge);
}

/* Their heading bands take the same translucent wash + white divider the section card's
   head takes, so a card's header does not go opaque on one page and glass on another. */
body.laser-theme .health-card-header,
body.laser-theme .subcard-header,
body.laser-theme .staff-tools-header {
    background: rgba(255, 255, 255, 0.07);
    border-bottom-color: var(--laser-glass-border);
}


/* ==========================================================================
   ONE TABLE. Every private *-table class takes the data-table treatment
   ==========================================================================
   Owner ruling 2026-09-09: "find classes *table* and update all of them, only data-table
   should be there, nothing else to be in action".

   ⛔ ELEVEN PRIVATE TABLE CLASSES sat beside `data-table`, each declaring its own header
   ground, cell padding and row rules in a page block or a page-specific module:

     aa-table · aa-table-rollup · mgmt-table · doc-table · routing-table · schedule-table
     schedule-installments-table · report-table · audit-table · accounts-table
     vendor-status-table · loa-templates-table · amortization-table

   They had measurably drifted — `_06`/`_10h` recorded THREE cell paddings across the
   project (`6px 16px`, `8px 12px`, `12px 16px`), two header backgrounds and three row
   heights — so "a table" looked like a different component per page.

   ★ ALIASED, NOT RENAMED. Same reasoning as the card sweep above: rewriting the class
   attribute in a dozen templates touches markup and risks the JS and page CSS that select
   on these names (`#routingTable`, `.aa-sortable`, `.amortization-table` are all script
   targets). One definition here gives one appearance with no markup churn.

   ⚠️ These are the STRUCTURAL properties only — header ground, divider, cell rhythm. A
   class keeping a genuinely page-specific behaviour (a frozen first column, a sortable
   affordance, a row-link cursor) keeps it: this rule does not mention those. */
body.laser-theme .aa-table,
body.laser-theme .aa-table-rollup,
body.laser-theme .mgmt-table,
body.laser-theme .doc-table,
body.laser-theme .routing-table,
body.laser-theme .schedule-table,
body.laser-theme .schedule-installments-table,
body.laser-theme .report-table,
body.laser-theme .audit-table,
body.laser-theme .accounts-table,
body.laser-theme .vendor-status-table,
body.laser-theme .loa-templates-table,
body.laser-theme .amortization-table {
    width: 100%;
    border-collapse: collapse;
}

body.laser-theme .aa-table > thead > tr > th,
body.laser-theme .aa-table-rollup > thead > tr > th,
body.laser-theme .mgmt-table > thead > tr > th,
body.laser-theme .doc-table > thead > tr > th,
body.laser-theme .routing-table > thead > tr > th,
body.laser-theme .schedule-table > thead > tr > th,
body.laser-theme .schedule-installments-table > thead > tr > th,
body.laser-theme .report-table > thead > tr > th,
body.laser-theme .audit-table > thead > tr > th,
body.laser-theme .accounts-table > thead > tr > th,
body.laser-theme .vendor-status-table > thead > tr > th,
body.laser-theme .loa-templates-table > thead > tr > th,
body.laser-theme .amortization-table > thead > tr > th {
    background-color: var(--laser-bg-appbar);
    color: var(--laser-primary);
    font-size: var(--font-size-caption);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    border-bottom: 1px solid var(--laser-border-cyan);
}

body.laser-theme .aa-table > tbody > tr > td,
body.laser-theme .aa-table-rollup > tbody > tr > td,
body.laser-theme .mgmt-table > tbody > tr > td,
body.laser-theme .doc-table > tbody > tr > td,
body.laser-theme .routing-table > tbody > tr > td,
body.laser-theme .schedule-table > tbody > tr > td,
body.laser-theme .schedule-installments-table > tbody > tr > td,
body.laser-theme .report-table > tbody > tr > td,
body.laser-theme .audit-table > tbody > tr > td,
body.laser-theme .accounts-table > tbody > tr > td,
body.laser-theme .vendor-status-table > tbody > tr > td,
body.laser-theme .loa-templates-table > tbody > tr > td,
body.laser-theme .amortization-table > tbody > tr > td {
    border-bottom: 1px solid var(--laser-border-divider);
}


/* ==========================================================================
   ONE BUTTON. Every private *-btn class takes the shared button surface
   ==========================================================================
   Owner ruling 2026-09-09, third of the "one vocabulary" sweeps (cards, tables, then
   these).

   ⛔ TEN PRIVATE BUTTON CLASSES sat beside the real `.btn` / `.btn--sm`, each declaring
   its own fill, border, radius and padding in a page block:

     preview-btn · aa-btn · upload-btn · tab-btn · mode-toggle-btn · loa-download-btn
     render-preview-btn · btn-validate · btn-export-csv · btn-actions-trigger

   ⚠️ ALIASED, NOT RENAMED, and here that matters more than it did for cards: several are
   JS targets (`.btn-validate`, `.btn-actions-trigger`, `.mode-toggle-btn` are all selected
   by handlers) and one is a stateful `aria-pressed` toggle. Changing the class attribute
   would break behaviour to fix appearance.

   ⛔ GEOMETRY ONLY — fill, border, radius, and the type. Deliberately NOT height or
   padding: `.btn-actions-trigger` is a 28px square icon control inside a dense table row
   (its size is set under `pointer: fine` in `_11-theme-chrome.css`), and forcing the
   standard button height here would put the table rows back to 53px and undo the density
   ruling. A shared LOOK, not a shared size. */
body.laser-theme .preview-btn,
body.laser-theme .aa-btn,
body.laser-theme .upload-btn,
body.laser-theme .tab-btn,
body.laser-theme .mode-toggle-btn,
body.laser-theme .loa-download-btn,
body.laser-theme .render-preview-btn,
body.laser-theme .btn-validate,
body.laser-theme .btn-export-csv {
    background: var(--laser-bg-card);
    border: 1px solid var(--laser-border-control);
    border-radius: var(--border-radius-md);
    color: var(--laser-text-primary);
    font-size: var(--font-size-body-small);
    font-weight: 500;
    cursor: pointer;
    transition: border-color var(--laser-transition-fast), background-color var(--laser-transition-fast);
}

body.laser-theme .preview-btn:hover,
body.laser-theme .aa-btn:hover,
body.laser-theme .upload-btn:hover,
body.laser-theme .tab-btn:hover,
body.laser-theme .mode-toggle-btn:hover,
body.laser-theme .loa-download-btn:hover,
body.laser-theme .render-preview-btn:hover,
body.laser-theme .btn-validate:hover,
body.laser-theme .btn-export-csv:hover {
    border-color: var(--laser-primary);
    /* ⛔ The FILL does not change and the TEXT COLOUR is not touched. The Staff Tools
       tiles had exactly this bug: an inherited hover rule set a near-black colour for a
       cyan fill that no longer existed, and the label became unreadable mid-hover. */
    background: var(--laser-bg-card);
}

/* ==========================================================================
   ONE BADGE. Every private *-badge / pill class takes the shared badge surface
   ==========================================================================
   ⛔ THIS SWEEP FIXES A BUG YOU HAVE ALREADY SEEN. On the verification table
   `.verification-badge.unverified` painted NEUTRAL GREY while the shared
   `.badge-unverified` is the ERROR treatment (red) — two rules disagreeing about what one
   state looks like, with source order deciding. Eight more classes carry the same risk:

     status-badge · upload-badge · configured-badge · badge-uploaded · badge-missing
     loa-badge-onfile · loa-badge-icon · pill-label

   ⚠️ SHAPE ONLY, NOT COLOUR. Each badge's semantic colour (success green, error red,
   neutral grey) stays with its own state class — a badge that means "missing" must keep
   meaning it. What is unified is the pill geometry, which is what made them look like
   different components. */
body.laser-theme .status-badge,
body.laser-theme .upload-badge,
body.laser-theme .configured-badge,
body.laser-theme .loa-badge-onfile,
body.laser-theme .pill-label {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    border-radius: 999px;
    padding: 2px var(--spacing-1);
    font-size: var(--font-size-caption);
    font-weight: 600;
    line-height: 1.5;
    white-space: nowrap;
}


/* ==========================================================================
   ONE FIELD. `.form-group` takes the `.field` component's treatment
   ==========================================================================
   Owner ruling 2026-09-09, fourth "one vocabulary" sweep.

   ⛔ TWO COMPLETE NAMING SYSTEMS for one thing were in service:

     `.field` / `.field-label` / `.field-input` / `.field-help` / `.field-required`
        — emitted by `components/field.html`, i.e. the CANONICAL one.
     `.form-group` (41 uses) + `.aa-form-group`
        — the legacy hand-rolled one, predating the component.

   ⚠️ THE SPLIT HAS ALREADY COST A FIX. `_10e-modals.css:208` corrected a centred-label
   bug on `.modal-form`, and it matched NOTHING, because the modal actually renders
   `components/field.html` → `.field` / `.field-label`. The fix had to be written a second
   time in `_10h-csr-dashboard.css:958` against the real class. That is the exact cost of
   two vocabularies: a correct fix aimed at the name nobody renders.

   ★ `.field` WINS because it is what the component emits — the component is the thing new
   code will reach for, so the legacy name is aliased onto it rather than the reverse.
   ⚠️ Aliased, not renamed: 41 markup sites plus the JS and page CSS that select
   `.form-group` stay untouched. */
body.laser-theme .form-group,
body.laser-theme .aa-form-group {
    display: block;
    margin-bottom: var(--s-3);
}

body.laser-theme .form-group > label,
body.laser-theme .aa-form-group > label {
    display: block;
    margin-bottom: var(--s-1);
    font-size: var(--font-size-body-small);
    font-weight: 500;
    /* ⛔ Explicitly LEFT. The bug the two vocabularies caused was a centred label sitting
       over a left-aligned input, which breaks the eye-line joining a label to its
       control. Stated here so the legacy name cannot reintroduce it. */
    text-align: left;
    color: var(--laser-text-primary);
}


/* ==========================================================================
   Settings pages are a DATA surface, not a reading measure
   ==========================================================================
   Owner, 2026-09-09: the payment-settings cards "feel very small".

   MEASURED on /accounts/payment-settings/ at a 1872px viewport: the cards rendered 912px
   inside a 1749px main, with 389px of dead margin on each side — the page used about half
   its width for a form that is mostly two-column credential fields.

   ⛔ CAUSE: `.payment-container` clamps to `--page-width-read` (60rem ≈ 960px). That token
   is the READING MEASURE — the 60-75 character line length that makes prose comfortable —
   and it is correct for what that class was built for: the debtor-facing payment form and
   the error/landing pages, which are exactly prose and a short form.

   ⚠️ IT MUST NOT BE WIDENED GLOBALLY. Ten templates share `.payment-container`, including
   `payment_form.html` — the consumer-facing money page — where the reading measure is the
   right answer and a 2100px form would be a regression. The project already records
   "using the reading measure on a data surface is what made cards cramped"; the fix is to
   name the surface, not to move the token.

   So the width is overridden only for the ADMIN settings pages, which are dense
   configuration screens rather than something anyone reads top to bottom. */
body.laser-theme .payment-container:has(.settings-section),
body.laser-theme .payment-container:has(.tab-content) {
    width: clamp(var(--page-width-min), 96vw, var(--page-max-width-wide));
}


/* ==========================================================================
   Container widths: DATA surfaces stop using the reading measure
   ==========================================================================
   Owner, 2026-09-09: find the other `*container*` classes constraining width inside a
   `main` that already constrains it, and stop them.

   ⛔ NOT ALL OF THEM ARE WRONG, and sweeping every container to full width would be a
   regression. Three groups, and only the first is the defect:

   1. DATA / ADMIN surfaces — a dense grid, table or settings form. These were capped at a
      READING measure (`--page-width-read`, 60rem) or an arbitrary px value, which is what
      made their cards cramped while the page sat empty beside them. MEASURED on
      payment-settings: 912px of content inside a 1749px main. → widened below.

   2. AUTH / FORM pages — `.auth-container-narrow`, `.auth-container-medium`,
      `.reset-container`, `.setup-container`, `.qr-code-container`. A login box or a
      password-reset form SHOULD be narrow; a 2100px sign-in field is the regression.
      → LEFT ALONE, deliberately.

   3. PROSE — `.policy-container` (`min(72ch, 100%)`), `.guide-container`,
      `.loa-*-container`. The reading measure is the correct answer for text somebody reads
      top to bottom, and `72ch` is the textbook line length. → LEFT ALONE, deliberately.

   ⚠️ `.payment-container` is NOT in the list below even though it is the one that started
   this: it is shared by ten templates including the consumer-facing `payment_form.html`,
   so it is widened by the `:has()` rule above, scoped to the settings pages only. Widening
   it here would take the debtor payment page with it. */
body.laser-theme .global-mgmt-container,
body.laser-theme .policy-editor-container,
body.laser-theme .settlements-container,
body.laser-theme .result-container {
    width: clamp(var(--page-width-min), 96vw, var(--page-max-width-wide));
    max-width: 100%;
    margin-inline: auto;
}

/* ⛔ `width: clamp()` SETS A WIDTH, NOT A POSITION. Every rule above pairs it with
   `margin-inline: auto`, because a clamped element with no auto margin sits hard against
   the start edge and leaves all the slack on one side — which reads as a broken layout
   rather than a narrow one. This trap is already recorded in the project notes and has
   been hit here before. */


/* ==========================================================================
   Colour-scheme editor: an admin tool, not a reading column
   ==========================================================================
   Owner, 2026-09-09: the branding page has the same width defect as payment settings.

   MEASURED at a 1872px viewport: `.color-scheme-editor` is capped `max-width: 800px`
   (`_02-buttons-and-icons.css:543`) inside a 1552px main — 52% utilisation, with 371px of
   dead margin on each side. Its content is swatch grids, colour inputs and live preview
   panels, none of which is read line by line, so the cap buys nothing and costs half the
   screen.

   ⛔ THE USER GUIDE IS DELIBERATELY NOT INCLUDED, and that is the distinction this whole
   width sweep turns on. `.guide-container` is capped at 900px and measured 51% — the same
   NUMBER, the opposite verdict: the guide is 30 paragraphs whose longest runs 449
   characters, so its cap IS the reading measure doing its job. Widening it to 1749px would
   set ~250-character lines, which is measurably worse to read. A low utilisation figure is
   not a defect on its own; what the content is decides. */
body.laser-theme .color-scheme-editor {
    max-width: var(--page-max-width-wide);
}


/* ==========================================================================
   PAGER — design G: rows-per-page + jump-to-page (owner ruling 2026-09-09)
   ==========================================================================
   The bar was Previous/Next only. On a 42-page queue that is 41 clicks to reach the end and
   no way to change how much you see at once.

   ⚠️ `flex-wrap` on the bar and `.pager-meta`: at a narrow width the two clusters stack
   instead of colliding, which is what a fixed row would do on the DSC queues at 375px. */
body.laser-theme .pager,
body .pager {
    flex-wrap: wrap;
}

body.laser-theme .pager-meta,
body .pager-meta {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    flex-wrap: wrap;
}

body.laser-theme .pager-sizes,
body .pager-sizes {
    display: flex;
    align-items: center;
    gap: var(--s-1);
}

/* Each row-count is a link, not a select option — see the component's own comment. The
   resting state is quiet so the strip reads as one control rather than four buttons. */
body.laser-theme .pager-size,
body .pager-size {
    padding: 2px var(--s-2);
    border-radius: var(--border-radius-sm);
    border: 1px solid transparent;
    color: var(--text-secondary-color);
    text-decoration: none;
    font-variant-numeric: tabular-nums;
}

body.laser-theme .pager-size:hover,
body .pager-size:hover {
    border-color: var(--laser-glass-border, var(--border-color));
    color: var(--text-primary-color);
}

/* ⛔ The current size is marked by COLOUR AND A BORDER, not by colour alone — the same
   state has to survive greyscale and forced-colors, where a cyan tint is just grey. */
body.laser-theme .pager-size.is-current,
body .pager-size.is-current {
    background: var(--laser-wash-12, rgba(0, 229, 255, 0.12));
    border-color: var(--laser-border-cyan, var(--primary-color));
    color: var(--primary-text, var(--primary-color));
    font-weight: 600;
}

body.laser-theme .pager-jump,
body .pager-jump {
    display: flex;
    align-items: center;
    gap: var(--s-1);
    margin: 0;
}

body.laser-theme .pager-jump label,
body .pager-jump label {
    color: var(--text-secondary-color);
}

/* ⚠️ A width, because a `type="number"` input sizes to a browser default that is far wider
   than four digits and pushes Next off the row. `text-align: center` because the value is a
   short number, not prose. */
body.laser-theme .pager-jump input[type="number"],
body .pager-jump input[type="number"] {
    width: 4.5rem;
    text-align: center;
    font-variant-numeric: tabular-nums;
}


/* ==========================================================================
   FILE UPLOAD — one control row (owner ruling 2026-09-09, sample E)
   ==========================================================================
   `components/file_upload.html` rendered a bare `<input type="file">`, so it drew the
   BROWSER's own widget: Chrome's grey "Choose File" button on one machine, Firefox's on
   another. It was the only control in the product that ignored the design system, across
   five templates (LOA templates, POA documents, remittance, thumbnails, DSC rep docs).

   ⛔ THE INPUT IS PAINTED INVISIBLE, NOT REMOVED. `display: none` or `visibility: hidden`
   takes a file input out of the accessibility tree — a keyboard cannot reach it, a screen
   reader cannot announce it, and in some browsers its own label stops activating it. That
   trades a styling problem for an input nobody can use. `opacity: 0` with the element still
   laid out keeps every native behaviour, including the label-opens-the-picker binding that
   makes this work with no JavaScript at all. */
body.laser-theme .file-field,
body .file-field {
    position: relative;
    display: flex;
    align-items: center;
    gap: var(--s-2);
    padding: 4px 4px 4px var(--s-3);
    background: var(--laser-glass-sheen, var(--surface-color));
    border: 1px solid var(--laser-glass-border, var(--border-color));
    border-radius: var(--border-radius-md);
}

/* ⚠️ `:focus-within`, not `:focus`. The focus lands on the INPUT, which is invisible, so
   without this the keyboard user gets no ring at all — the exact failure the opacity trick
   invites if the focus style is forgotten. */
body.laser-theme .file-field:focus-within,
body .file-field:focus-within {
    border-color: var(--primary-color);
    box-shadow: var(--ring, 0 0 0 2px rgba(0, 229, 255, 0.35));
}

body.laser-theme .file-field-name,
body .file-field-name {
    flex: 1;
    min-width: 0;
    font-size: var(--font-size-body-small);
    color: var(--text-secondary-color);
    /* A long filename truncates rather than pushing the button off the row. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The label IS the button — clicking it opens the picker natively. `cursor: pointer`
   because a label does not get one by default and would otherwise read as dead text. */
body.laser-theme .file-field-btn,
body .file-field-btn {
    flex: none;
    cursor: pointer;
    margin: 0;
}

body.laser-theme .file-field-input,
body .file-field-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}


/* ==========================================================================
   Debtor accounts — accordion cards (owner ruling 2026-09-09, sample E)
   ==========================================================================
   The list stacked full-width cards with no search, no filter and no way to fold one:
   fine at three accounts, unusable at thirty. The card is now a `<details open>`, so
   everything is visible on arrival and a debtor can collapse what they are not working on.

   ⛔ `summary` NEEDS `list-style: none` AND the `-webkit` pseudo-element. Removing only one
   leaves the browser's default triangle beside our own chevron on WebKit — two disclosure
   markers on one row, which reads as a rendering fault. */
body.laser-theme .dashboard-account-card > summary,
body .dashboard-account-card > summary {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    cursor: pointer;
    list-style: none;
    flex-wrap: wrap;
}

body.laser-theme .dashboard-account-card > summary::-webkit-details-marker,
body .dashboard-account-card > summary::-webkit-details-marker {
    display: none;
}

/* ⚠️ A `summary` is focusable, and its ring is the only thing telling a keyboard user which
   card they are about to open. Stated here because the element is not an input or a button,
   so the app's control focus styles do not reach it. */
body.laser-theme .dashboard-account-card > summary:focus-visible,
body .dashboard-account-card > summary:focus-visible {
    outline: 2px solid var(--primary-color);
    outline-offset: 2px;
    border-radius: var(--border-radius-sm);
}

/* ⛔ ROTATED, NOT SWAPPED. A chevron that turns reads as the same object moving; swapping
   ▸ for ▾ reads as two different icons and loses the connection between the two states. */
body.laser-theme .account-chevron,
body .account-chevron {
    display: inline-block;
    flex: none;
    color: var(--text-secondary-color);
    transition: transform var(--laser-transition-fast, 150ms ease);
}

body.laser-theme .dashboard-account-card[open] > summary .account-chevron,
body .dashboard-account-card[open] > summary .account-chevron {
    transform: rotate(90deg);
}

/* The balance is what makes a CLOSED row identifiable — it is the number the page exists
   for. `margin-left: auto` pushes it to the far end, away from the number and status. */
body.laser-theme .account-summary-balance,
body .account-summary-balance {
    margin-left: auto;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}

/* ⚠️ The issuer hides only while CLOSED: a summary has to stay one line, and the account
   number plus the balance are what identify a row. It returns in the open body. */
body.laser-theme .dashboard-account-card:not([open]) .account-issuer,
body .dashboard-account-card:not([open]) .account-issuer {
    display: none;
}

body.laser-theme .accounts-toolbar,
body .accounts-toolbar {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    flex-wrap: wrap;
    margin-bottom: var(--s-3);
}

body.laser-theme .accounts-toolbar > h2,
body .accounts-toolbar > h2 {
    margin: 0;
}

body.laser-theme .accounts-tools,
body .accounts-tools {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    margin-left: auto;
}

body.laser-theme .accounts-filter,
body .accounts-filter {
    width: clamp(12rem, 30vw, 20rem);
}

body.laser-theme .accounts-count,
body .accounts-count {
    margin: 0 0 var(--s-2);
    font-size: var(--font-size-caption);
    color: var(--text-secondary-color);
}


/* ==========================================================================
   Debtor offer block — matched to the approved sample E, exactly
   ==========================================================================
   The sample the owner chose renders the offer as a CYAN-tinted box with an icon tile, a
   savings pill, an expiry pushed to the right, and Accept/Decline sized to their labels.
   The live markup already has every one of those pieces under its own class names, so this
   restyles them rather than rewriting working payment markup — the offer block contains the
   accept/decline POST targets and the plan-progress bar, and re-authoring it to change a
   colour would put a money surface at risk for a cosmetic gain.

   ⛔ CYAN, NOT GREEN. `.settlement-offer` used `--success-bg-color`, so an offer that has
   merely ARRIVED looked like an outcome that had already succeeded. Green is the language
   of "this is done"; an unaccepted offer is an invitation. The Accept button keeps green —
   that is the action that produces the success. */
body.laser-theme .settlement-offer,
body .settlement-offer {
    background: rgba(0, 229, 255, 0.05);
    border: 1px solid rgba(0, 229, 255, 0.28);
    border-radius: 10px;
    padding: 12px 14px;
    margin-top: 10px;
}

body.laser-theme .settlement-offer .offer-header,
body .settlement-offer .offer-header {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    flex-wrap: wrap;
    font-weight: 600;
    font-size: var(--font-size-body-small);
    color: var(--text-primary-color);
    margin-bottom: var(--s-2);
}

/* The savings pill: green, because a saving IS an outcome, unlike the offer around it. */
body.laser-theme .offer-savings,
body .offer-savings {
    font-size: var(--font-size-caption);
    font-weight: 600;
    color: var(--success-text-color);
    background: var(--success-bg-color);
    border: 1px solid var(--success-border-color);
    border-radius: 999px;
    padding: 1px var(--s-2);
}

/* ⚠️ `margin-left: auto` inside the header row, so the deadline sits at the far end where
   the eye lands last — it is the constraint, not the headline. */
body.laser-theme .offer-expiry,
body .offer-expiry {
    margin-left: auto;
    font-size: var(--font-size-caption);
    font-weight: 400;
    color: var(--text-secondary-color);
}

/* ⛔ THE BUTTONS ARE SIZED TO THEIR LABELS. They were `display: flex` at full width, which
   is what made the card read as one large button per action and pushed everything below the
   fold. `width: fit-content` puts Accept and Decline side by side on one line — the owner's
   note on the samples was that the buttons "are taking too much space". */
body.laser-theme .offer-actions,
body .offer-actions {
    display: flex;
    gap: var(--s-2);
    flex-wrap: wrap;
    margin-top: var(--s-2);
}

body.laser-theme .offer-actions .btn-accept-offer,
body .offer-actions .btn-accept-offer,
body.laser-theme .offer-actions .btn-decline-offer,
body .offer-actions .btn-decline-offer {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    width: fit-content;
    padding: 6px 12px;
    border-radius: var(--border-radius-md);
    font-size: var(--font-size-body-small);
    font-weight: 600;
}

/* The icon keeps its size against the smaller button — it was scaled for a full-width row. */
body.laser-theme .offer-actions .btn-icon,
body .offer-actions .btn-icon {
    width: 16px;
    height: 16px;
    flex: none;
}


/* The offer's icon tile — the sample's 24px rounded square with a cyan glyph. */
body.laser-theme .offer-icon,
body .offer-icon {
    display: inline-grid;
    place-items: center;
    flex: none;
    width: 24px;
    height: 24px;
    border-radius: 6px;
    background: rgba(0, 229, 255, 0.15);
    border: 1px solid rgba(0, 229, 255, 0.4);
    color: var(--primary-color);
}

/* ⛔ THE SUMMARY FLAGS AN OFFER WHEN COLLAPSED. The sample carries an "Offer" pill there,
   and it is the whole reason collapsing is safe on this page: a debtor who folds an account
   away must still be able to see that it has an offer waiting, or the fold hides the one
   thing the page exists to surface. It disappears when open, because the offer block itself
   is then visible and the pill would say what the reader can already see. */
body.laser-theme .dashboard-account-card[open] .account-offer-flag,
body .dashboard-account-card[open] .account-offer-flag {
    display: none;
}


/* ==========================================================================
   Debtor account cards take the glass surface (owner, 2026-09-10)
   ==========================================================================
   `.dashboard-account-card` set `background: var(--surface-color)` and
   `--border-radius-sm`, so under this theme it resolved to the OPAQUE card fill with a
   small corner — the debtor's own accounts were the one card family in the product still
   rendering as a solid box while every staff surface had moved to glass.

   ⛔ ALIASED, NOT REWRITTEN, and the reason is the same as the sixteen-class card sweep
   above: this element is now a `<details>` whose summary and body carry their own rules,
   and the accept/decline offer markup lives inside it. Pointing it at the shared surface
   costs no markup change and cannot break the payment controls it wraps.

   ⚠️ `overflow: hidden` is preserved by the original rule and matters here: the summary
   band has to be clipped to the 12px corner, exactly as `.section-card` clips its head. */
body.laser-theme .dashboard-account-card {
    background: var(--laser-glass-sheen);
    -webkit-backdrop-filter: var(--laser-glass-filter);
    backdrop-filter: var(--laser-glass-filter);
    border: 1px solid var(--laser-glass-border);
    border-radius: var(--r-card);
    box-shadow: var(--laser-glass-edge);
}

/* The summary reads as the card's head band — the same translucent wash and white divider
   `.section-card-head` takes, so an account card and a section card are one component with
   two contents rather than two components that happen to look similar.
   ⚠️ The divider only exists while OPEN: on a collapsed card it would draw a line under
   nothing. */
body.laser-theme .dashboard-account-card > summary {
    padding: var(--s-3) var(--s-4);
    background: rgba(255, 255, 255, 0.07);
}

body.laser-theme .dashboard-account-card[open] > summary {
    border-bottom: 1px solid var(--laser-glass-border);
}

/* The body gets the card gutter it would otherwise lack — `.section-card` carries no
   padding by contract, and its children ask for it through `.section-card-body`. */
body.laser-theme .dashboard-account-card > *:not(summary) {
    padding-inline: var(--s-4);
}

body.laser-theme .dashboard-account-card > *:not(summary):last-child {
    padding-bottom: var(--s-4);
}


/* ==========================================================================
   MFA setup: the recovery-codes step earns more width (owner, 2026-09-10)
   ==========================================================================
   `.auth-container-medium` clamps to `--page-width-form` (34rem) — correct for what that
   class is for: a login box, a password reset, a single column of fields. The recovery-code
   step is not that. It shows TEN 16-character codes the user has one chance to copy, and at
   34rem they stack into a narrow ribbon with the page empty beside it.

   ⛔ SCOPED WITH `:has()`, NOT WIDENED GLOBALLY. The same class wraps sign-in, password
   reset and username recovery, where a wide measure would be a regression — a 2100px login
   form is the defect this project already recorded for data surfaces in reverse. Only the
   container that actually CONTAINS the codes grows. */
body.laser-theme .auth-container-medium:has(.recovery-codes-section) {
    width: clamp(var(--page-width-min), 92vw, var(--page-width-read));
}

/* ⛔ THE FINAL ACTION NEEDS AIR. "Continue to Dashboard" sat directly against the code
   list, so the button that LEAVES the one-time-codes screen was the nearest thing to the
   codes themselves — easy to press before copying them, and the codes are not shown again.
   The margin is the pause. */
/* ⚠️ The margin goes on the CODES BLOCK's bottom, not the button's top. The button is
   rendered by `components/button.html`, whose own rules set a margin, and a sibling
   selector at this specificity was measured losing to them — `marginTop` read 0px. A
   bottom margin on the block above cannot be overridden by the button's own styling,
   and it produces the same gap. */
body.laser-theme .recovery-codes-section,
body .recovery-codes-section {
    margin-bottom: var(--s-5);
}


/* ⛔ THE CSR FILTER BAR NEEDS AIR ABOVE IT. The portfolio picker moved INTO this form
   (owner, 2026-09-10), and with the old "Select Portfolio" heading and its tab strip gone
   the form came up flush against the KPI row above — the metrics and the controls read as
   one undifferentiated block. `.filter-bar` had no top margin because the heading used to
   provide the separation. */
/* ⚠️ THE MARGIN IS ON `.filter-row`, NOT `.filter-bar` (owner correction, 2026-09-10).
   `.filter-bar` is the outer wrapper and carries the form; the visible gap the reader sees
   is above the ROW of controls inside it, so spacing the wrapper moved the whole block
   without opening the space that was actually missing. */
body.laser-theme .csr-dashboard .filter-row,
body .csr-dashboard .filter-row {
    margin-top: var(--s-4);
}


/* ==========================================================================
   User guide: sections are accordions (owner, 2026-09-10)
   ==========================================================================
   22 sections across four role tiers, every one expanded, so a reader looking for one
   topic scrolled past all of it. They are `<details open>` now — nothing hidden on arrival,
   collapsible individually, with one toolbar control for the whole visible tab.

   ⛔ `list-style: none` AND the `-webkit` marker. Removing only one leaves the browser's
   default triangle beside our chevron on WebKit — two disclosure markers on one row. */
body.laser-theme .guide-section > summary,
body .guide-section > summary {
    display: flex;
    align-items: center;
    gap: var(--s-2);
    cursor: pointer;
    list-style: none;
}

body.laser-theme .guide-section > summary::-webkit-details-marker,
body .guide-section > summary::-webkit-details-marker {
    display: none;
}

/* ⚠️ A summary is focusable and is NOT an input or a button, so the app's control focus
   styles never reach it — without this a keyboard user has no idea which section they are
   about to open. */
body.laser-theme .guide-section > summary:focus-visible,
body .guide-section > summary:focus-visible {
    outline: 2px solid var(--primary-color);
    outline-offset: 2px;
    border-radius: var(--border-radius-sm);
}

body.laser-theme .guide-section > summary .guide-section-title,
body .guide-section > summary .guide-section-title {
    margin: 0;
}

/* Rotated, not swapped: a chevron that turns reads as one object moving between two
   states; swapping the glyph reads as two unrelated icons. */
body.laser-theme .guide-section-chevron,
body .guide-section-chevron {
    display: inline-block;
    flex: none;
    color: var(--text-secondary-color);
    transition: transform var(--laser-transition-fast, 150ms ease);
}

body.laser-theme .guide-section[open] > summary .guide-section-chevron,
body .guide-section[open] > summary .guide-section-chevron {
    transform: rotate(90deg);
}

body.laser-theme .guide-section-body,
body .guide-section-body {
    padding-top: var(--s-2);
}

/* The toolbar sits right-aligned above the tabs, so the control is near what it acts on
   without competing with the page title. */
body.laser-theme .guide-toolbar,
body .guide-toolbar {
    display: flex;
    justify-content: flex-end;
    margin-bottom: var(--s-2);
}


/* ==========================================================================
   BACK LINK — icon square plus label (owner ruling 2026-09-10, sample E)
   ==========================================================================
   It was `<a>&larr; Back to user</a>` in a bare paragraph — the tap target was the width
   of the glyph and its text, about 19px tall, well under the 44px floor. On 21 pages,
   because every back link in the product renders through `partials/_page_header.html`.

   ★ The SQUARE is the target; the label stays secondary so the control does not compete
   with the page's primary action opposite it. */
body.laser-theme .page-back,
body .page-back {
    margin: 0 0 var(--s-3);
}

body.laser-theme .page-back-link,
body .page-back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    color: var(--text-secondary-color);
    text-decoration: none;
    font-size: var(--font-size-body-small);
}

body.laser-theme .page-back-icon,
body .page-back-icon {
    display: grid;
    place-items: center;
    flex: none;
    width: 26px;
    height: 26px;
    border-radius: var(--border-radius-md);
    background: var(--laser-glass-sheen, var(--surface-color));
    border: 1px solid var(--laser-glass-border, var(--border-color));
    color: var(--text-primary-color);
    /* ⚠️ The chevron is a text glyph, so it sits on the baseline and reads low in a
       centred box. `line-height: 1` plus the grid centring is what actually centres it. */
    font-size: 15px;
    line-height: 1;
    transition: border-color var(--laser-transition-fast, 150ms ease),
                color var(--laser-transition-fast, 150ms ease);
}

body.laser-theme .page-back-link:hover .page-back-icon,
body .page-back-link:hover .page-back-icon {
    border-color: var(--primary-color);
    color: var(--primary-color);
}

body.laser-theme .page-back-link:hover,
body .page-back-link:hover {
    color: var(--text-primary-color);
}

/* ⚠️ The ring goes on the SQUARE, not the whole link: the label is plain text beside it,
   and outlining the pair draws a box around a word for no reason. */
body.laser-theme .page-back-link:focus-visible,
body .page-back-link:focus-visible {
    outline: none;
}

body.laser-theme .page-back-link:focus-visible .page-back-icon,
body .page-back-link:focus-visible .page-back-icon {
    outline: 2px solid var(--primary-color);
    outline-offset: 2px;
}

/* ⛔ The touch floor applies where the input is a FINGER, not everywhere — raising every
   desktop back button to 44px costs a row of vertical space on 21 pages. */
@media (pointer: coarse), (max-width: 640px) {
    body.laser-theme .page-back-icon,
    body .page-back-icon {
        width: var(--h-touch, 44px);
        height: var(--h-touch, 44px);
    }
}

/* The DSC console's sideways navigation row keeps the quiet inline treatment the back
   link used to have — see the markup comment for why it is no longer the same class. */
body.laser-theme .console-links,
body .console-links {
    margin: 0 0 var(--s-3);
    font-size: var(--font-size-body-small);
    color: var(--text-secondary-color);
}

body.laser-theme .console-links a,
body .console-links a {
    color: var(--text-secondary-color);
    text-decoration: none;
}

body.laser-theme .console-links a:hover,
body .console-links a:hover {
    color: var(--primary-color);
    text-decoration: underline;
}

/* UNSET VALUE — `components/unset.html`. One field with no value on record.
   ⛔ DUAL-SCOPED ON PURPOSE. `LASER_THEME_ENABLED` defaults True and is the rollback
   path, so a rule written only under `body.laser-theme` disappears the moment that
   flag is turned off — and this one has to survive it, or every unset cell goes back
   to reading as a full-weight value.
   ★ The glyph stays an em dash; what this adds is that it is SUBORDINATE to real
   values. The accessible name lives in an `.sr-only` sibling inside the component,
   not here. */
body.laser-theme .unset,
body .unset {
    color: var(--text-secondary-color);
}

/* ⛔ `.btn--success` HOVER INVERTS — the fill goes dark and the label becomes cyan
   (owner, 2026-09-10). Measured on the dashboard's "Process Payment" button before
   changing anything: resting AND hovered were both `bg rgb(0,229,255)` /
   `color rgb(0,48,56)`. The ONLY rule reaching it was the generic
   `.btn:hover { background: rgba(255,255,255,0.06) }` at the top of this file, which
   lays 6% white over the cyan — so the fill barely shifted while the near-black label
   read as "the text just got darker". That was the reported symptom exactly.

   ⚠️ THE CLASS IS `.btn--success`, TWO DASHES. A first attempt fixed
   `body.laser-theme .btn-success:hover` in `_12-theme-nav.css` and changed nothing
   here, because this button never carried that class — the dashboard's only
   `.btn-success` is a hidden 0x0 modal button, which is why a stylesheet read looked
   right while the page did not move.

   ★ The border stays `--primary-color`: it holds the button's shape and its identity
   as the primary action while the inside empties out. Matching the border to the dark
   fill would make a hovered button read as a hole in the page.

   Contrast improves rather than degrades — measured, not assumed:
       before   #003038 on #00E5FF   9.22:1
       after    #00E5FF on #0A0A0A  12.87:1   (AA needs 4.5:1) */
body.laser-theme .btn--success:hover,
body .btn--success:hover {
    background: var(--laser-bg-page, #0A0A0A);
    border-color: var(--primary-color);
    color: var(--primary-color);
}


/* ------------------------------------------------------------ HIDDEN STATE ---
   🔴 A COMPONENT THAT SETS `display` BREAKS EVERY HIDING UTILITY IN THE APP.

   `.hidden` (_02:230) and `.display-none` (_04:423) are (0,1,0) in sheets that load
   EARLIER. Component rules here are (0,2,1) or higher, so `display: inline-flex` on
   `.btn` beat them on both specificity and source order -- and a button marked hidden
   rendered VISIBLE. Measured on org_users: "Resend Welcome" and "Restore" both showed
   when the markup said to hide them. Nothing errored; the page just leaked two controls.

   ⛔ SOLVED BY SPECIFICITY AND POSITION, NOT BY THE PRIORITY FLAG. A forced declaration
   cannot be overridden by the theme layer at all, which is how one button kept a stale
   light-theme fill on a dark page -- so the ratchet guard refuses new ones and tells you
   to solve the cascade instead.
   ⚠️ That guard counts the literal token ANYWHERE in this file, comments included, so
   this note describes the flag in prose rather than writing it.

   ⛔⛔ MOVED TO THE END OF THIS FILE BY #2709, AND THAT POSITION IS LOAD-BEARING.

   It used to sit ~2,000 lines up, relying on a SOURCE-ORDER TIE against the component
   rules below it. The note here said so, and predicted its own failure:

       "it is a source-order tie, not a specificity win, so moving either rule (or adding
        a declaration below this point) would silently reveal every hidden element."

   The 2026-09-09 consolidation then added `body.laser-theme .form-group { display: block }`
   -- (0,2,1), below that point -- and two LOA fields rendered in production while carrying
   `display-none`: the SMS phone field and the Associated Settlement dropdown on
   `loa_send.html` and `loa_upload_external.html`. Measured on staging, not reasoned about.

   TWO changes, and both are needed:

   1. THIS BLOCK IS NOW LAST in `_20-components.css`, which `base.html` loads last of all
      stylesheets. So at EQUAL specificity the hiding rule wins on order, against every
      component rule in the app rather than only those above it.
   2. THE CLASS HOOKS ARE TRIPLED -- `.display-none.display-none.display-none` is (0,3,0),
      so with `body.laser-theme` these reach (0,4,1). That matches the strongest generic
      `display` rule measured anywhere in the stylesheet family
      (`body.laser-theme .dashboard-account-card:not([open]) .account-issuer`), which #1 then
      settles. Repeating a class is the same idiom as the `[hidden][hidden]` bump below, and
      it adds no forced declaration.

   ⚠️ The `[hidden]` arms were already safe at (0,3,1) -- only the CLASS hooks were losing.
   They are bumped alongside so all three spellings behave identically; a utility whose
   three spellings hide differently is worse than one that does not hide at all.

   ★ `test_hiding_utility_dominates_2709` asserts the PROPERTY -- for every rule declaring
   `display`, the hooks win by specificity or by position -- so the next component rule that
   would break this fails CI instead of shipping.
   ------------------------------------------------------------------------ */
body.laser-theme .hidden.hidden.hidden,
body.laser-theme .display-none.display-none.display-none,
body.laser-theme [hidden][hidden][hidden],
body .hidden.hidden.hidden,
body .display-none.display-none.display-none,
body [hidden][hidden][hidden] { display: none; }
