/* =============================================================================
   KANINE — Blazor design-system foundation (Step-0, UI-FIRST gate)
   -----------------------------------------------------------------------------
   THE PORT. This is the hand-written `kl-*` / `kf-*` realization of the prototype's
   design system (`ProposedUI/design-system/kanine.css` + `kanine.config.js`).
   The prototype uses Tailwind utilities for component LOOK; the Blazor build does
   NOT ship Tailwind — its pages carry semantic `kl-<page>-*` / `kf-*` class names
   that, until now, had NO backing CSS (screens rendered unstyled). This file is
   that backing: the --k-* brand token mirror, the light/dark/outdoor flip, and a
   base treatment for the shared component vocabulary, keyed to the SUFFIX of the
   page-prefixed families (kl-ce-card, kl-uof-field, kl-medit-input, …) so every
   screen gets coherent, on-brand structure for free. Per-page R11 then REFINES
   each screen with page-scoped CSS; it never reinvents the system.

   Loaded BEFORE app.css (which keeps the a11y target-size/reflow overrides that
   must win) so app.css's narrower fixes take precedence on cascade ties.

   Source of truth for hexes/tokens: brand/BRAND-AND-DESIGN.md + DESIGN-INTEGRATION
   -BRIEF §5.1 + ProposedUI/design-system/kanine.css. Do not introduce off-token
   colour here — the R11 token-conformance scan reds on it.

   SCOPING RULE (load-bearing): the broad suffix-family selectors are scoped under
   `.kl-shell-main` (the page content region). The app shell chrome (the ShellNav
   `.kl-shellnav-*` rail + top bar) lives OUTSIDE `<main>`, so a family like
   [class*="-header"] can never bleed onto the chrome. Named primitives (kf-*,
   kl-btn, kl-dialog) are global so top-layer <dialog> + cross-cutting buttons
   style everywhere.
   ========================================================================== */

/* ---- 0. Self-hosted web fonts (offline-first; no CDN) --------------------
   The four families the tokens below name are SELF-HOSTED from wwwroot/fonts
   because Kanine runs offline in the field — a Google Fonts CDN link would
   vanish the moment the device loses signal, and the icon font failing open
   renders ligature NAMES as literal words (the pg35 finding this fixes).
   Regenerate with tools/fonts/build-fonts.py. Material Symbols is subset to the
   68 icons in tools/fonts/icon-names.txt (3.95 MB full -> ~50 KB); the text
   families are Google's 'latin' slice. Licenses in wwwroot/fonts/LICENSE.md
   (Material Symbols Apache-2.0; the rest OFL-1.1). The text faces use
   font-display:optional + a <head> preload of each woff2 (App.razor): the faces
   are LOCAL files, so with the preload they are in the font cache at first
   paint and always render — but if a paint ever races the load, the browser
   keeps the fallback for that page view instead of RE-WRAPPING painted text
   (font-display:swap re-flowed content-sized bands site-wide — the record-view
   CLS 0.24 root cause, final-draft sweep 2026-07-03). The ICON face uses
   font-display:block so the load window shows a blank box, never a flash of
   the literal ligature word. */
@font-face {
  font-family:"Hanken Grotesk";
  font-style:normal; font-weight:400 800; font-display:optional;
  src:url("../fonts/hanken-grotesk-latin.woff2") format("woff2");
}
@font-face {
  font-family:"Inter";
  font-style:normal; font-weight:400 600; font-display:optional;
  src:url("../fonts/inter-latin.woff2") format("woff2");
}
@font-face {
  font-family:"JetBrains Mono";
  font-style:normal; font-weight:400 600; font-display:optional;
  src:url("../fonts/jetbrains-mono-latin.woff2") format("woff2");
}
@font-face {
  font-family:"Material Symbols Outlined";
  font-style:normal; font-weight:100 700; font-display:block;
  src:url("../fonts/material-symbols-outlined.woff2") format("woff2");
}

/* The icon primitive. Pages render icons as LIGATURE NAMES, e.g.
   <span class="material-symbols-outlined">verified</span>; the 'liga' feature
   maps the name to its glyph. Default axes mirror the prototype (opsz 24 /
   wght 400 / FILL 0 / GRAD 0); a page may override font-variation-settings
   (e.g. 'FILL' 1 for a filled state). */
.material-symbols-outlined {
  font-family:"Material Symbols Outlined";
  font-weight:normal; font-style:normal;
  font-size:24px; line-height:1; letter-spacing:normal; text-transform:none;
  display:inline-block; white-space:nowrap; word-wrap:normal; direction:ltr;
  /* pg40 CLS hardening: reserve EXACTLY the glyph's 1em advance + clip the pre-load ligature
     NAME. font-display:block (above) only makes the FOUT text INVISIBLE — it still occupies the
     full ligature-name width ("radio_button_unchecked" ≈ 400px) and then collapses to a ~1em glyph
     when the icon font applies, a reflow that produced a deterministic ~0.32 CLS on /suspects'
     icon-bearing filter chips. The LOADED-state width is unchanged (Material Symbols glyphs advance
     1em), so this is purely a loading-state guard that cannot move any already-painted icon — a
     strict, app-wide improvement. text-align:center keeps the glyph optically centred in the box. */
  inline-size:1em; overflow:hidden; text-align:center;
  -webkit-font-feature-settings:"liga"; font-feature-settings:"liga";
  -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
  font-variation-settings:"FILL" 0,"wght" 400,"GRAD" 0,"opsz" 24;
  vertical-align:middle;
}

/* ---- 1. Token mirror (--k-* ; mirrors kanine.css §1) ---------------------- */
:root {
  --k-ink-900:#0A1A2B; --k-ink-800:#0E2236; --k-ink-700:#13304A; --k-ink-500:#2B5A8C;
  --k-teal-700:#0A7D77; --k-teal-500:#10B5A8; --k-teal-400:#2DD4C4;
  --k-paper:#F5F7FA; --k-gray-100:#E6EBF2; --k-gray-300:#C2CCD9;
  --k-gray-500:#7A8699; --k-gray-700:#3E4A5C; --k-gray-900:#161C26;
  --k-seal-brass:#C9A227;
  --k-success:#1E9E6A; --k-caution:#E08A00; --k-critical:#D7402B; --k-info:#2B7FFF;
  /* The attention COUNT bubble (shell "My feedback" badge). Same red hue as before (#d64545), one step deeper:
     white text on it measures 4.93:1 (was 4.38, under the 4.5 floor) and its HSL saturation is 0.55 (was 0.64, over
     the UI-GUAR-027 0.62 restraint cap). One value for every mode: the bubble sits on the navy rail and hub. */
  --k-attention-badge-bg:#C84040;

  /* flipping semantic channels — RGB is the source of truth (swap with mode) */
  --k-bg-rgb: 245 247 250;        /* paper #F5F7FA */
  --k-surface-rgb: 255 255 255;   /* white card */
  --k-surface-2-rgb: 238 242 247; /* #EEF2F7 subtle fill */
  --k-surface-3-rgb: 230 235 242; /* #E6EBF2 */
  --k-text-rgb: 14 34 54;         /* ink-800 body text */
  --k-text-muted-rgb: 62 74 92;   /* gray-700 muted */
  --k-border-rgb: 194 204 217;    /* gray-300 */
  --k-outline-rgb: 122 134 153;   /* gray-500 */

  --k-bg:rgb(var(--k-bg-rgb));
  --k-surface:rgb(var(--k-surface-rgb));
  --k-surface-2:rgb(var(--k-surface-2-rgb));
  --k-surface-3:rgb(var(--k-surface-3-rgb));
  --k-text:rgb(var(--k-text-rgb));
  --k-text-muted:rgb(var(--k-text-muted-rgb));
  --k-border:rgb(var(--k-border-rgb));
  --k-outline:rgb(var(--k-outline-rgb));
  --k-accent:var(--k-teal-700);        /* teal as TEXT/foreground accent (AA on light) */
  --k-accent-bright:var(--k-teal-500);
  --k-on-accent:#FFFFFF;
  --k-header-bg:var(--k-ink-800);
  --k-header-text:#FFFFFF;
  --k-focus:var(--k-teal-400);

  /* STATUS TINTS — the fill and the ink of a tinted status chip, as tokens (2026-09-16). The values are the
     `kl-chip--critical/--caution/--info` palette in §5f, unchanged. They exist so that a SCOPED component chip
     (the entity history panel, the retention ledger) takes the house tone by name. Before this, those sheets
     named `--k-danger-bg`, `--k-warn-fg` and similar tokens that were defined nowhere, and every mode painted the
     light-mode hex fallback. The fill mixes into --k-surface, so it follows the canvas of each mode. Each mode
     block below restates the pair, because `:root[data-outdoor]` (0,2,0) outranks `html.dark` (0,1,1). */
  --k-critical-fg:#7a1b12; --k-critical-bg:color-mix(in srgb,var(--k-critical) 12%,var(--k-surface));
  --k-caution-fg:#7a4a00;  --k-caution-bg:color-mix(in srgb,var(--k-caution) 14%,var(--k-surface));
  --k-info-fg:#0b4f8a;     --k-info-bg:color-mix(in srgb,var(--k-info) 10%,var(--k-surface));
  /* SUCCESS ink for TEXT on a success tint or on a canvas. `--k-success` is a fill colour: #1E9E6A on a light
     tint measures 2.99:1. Dark and outdoor modes restate it below, like the three pairs above. */
  --k-success-fg:#0b5c3c;
  /* HOVER / PRESSED wash for a row on a panel. It mixes the TEXT ink into nothing, so it darkens a light panel and
     lightens a dark one. A fixed `rgb(0 0 0 / .06)` is invisible on a dark panel. */
  --k-hover-wash:color-mix(in srgb,var(--k-text) 9%,transparent);

  --k-font-display:"Hanken Grotesk",system-ui,sans-serif;
  --k-font-ui:"Inter","IBM Plex Sans",system-ui,sans-serif;
  --k-font-mono:"JetBrains Mono","IBM Plex Mono",ui-monospace,monospace;

  --k-space-1:4px; --k-space-2:8px; --k-space-3:12px; --k-space-4:16px;
  --k-space-6:24px; --k-space-8:32px; --k-space-12:48px; --k-space-16:64px;
  --k-radius-sm:4px; --k-radius-md:8px; --k-radius-lg:16px; --k-radius-xl:28px; --k-radius-full:9999px;
  --k-elev-1:0 1px 2px rgb(0 0 0 / .08);
  --k-elev-2:0 2px 6px rgb(0 0 0 / .10);
  --k-elev-3:0 6px 16px rgb(0 0 0 / .12);
  --k-touch:48px;
  --k-content-max:1120px;

  /* §A10 phone-card title line (§8b-m). Both are DERIVED, not chosen: the line floor is the touch target minus
     the card's block padding, which is the tallest line the 48px pinned kebab can sit on while bleeding
     symmetrically into that padding instead of reaching the next line; the reserve is the pinned slot's own width
     plus one gutter. They are tokens so the pin inset, the line height and the horizontal reserve cannot drift
     apart in three separate rules — the guard test asserts they are still derived from --k-touch. */
  --k-tcard-line:calc(var(--k-touch) - var(--k-space-3));
  --k-tcard-reserve:calc(var(--k-touch) + var(--k-space-2));

  /* motion — restrained-premium register (R11). Calm, precise; signature flows override. */
  --k-ease:cubic-bezier(.2,.6,.2,1);
  --k-dur-1:.12s; --k-dur-2:.2s; --k-dur-3:.32s;
}

/* ---- 2. Modes (mirrors kanine.css §2) ------------------------------------- */
/* Dark: never pure black (halation). Set by the shell theme toggle (html.dark). */
html.dark {
  --k-bg-rgb: 15 22 32;
  --k-surface-rgb: 22 30 43;
  --k-surface-2-rgb: 30 40 56;
  --k-surface-3-rgb: 40 53 73;
  --k-text-rgb: 230 235 242;
  --k-text-muted-rgb: 159 176 194;
  /* R11 punch-list TOP-5: dark card borders were near-invisible (#28394C on #161E2B). Raise the edge so a card
     reads AS a card in the dark field POV (still on the ink ramp; bg/surface tokens untouched so the R11 canvas
     gate holds). */
  --k-border-rgb: 58 80 107;
  --k-outline-rgb: 122 140 162;
  --k-accent:var(--k-teal-400);
  --k-on-accent:var(--k-ink-900);
  --k-header-bg:#161E2B;
  /* status tints: the §5f dark chip remap (light ink on a stronger fill) */
  --k-critical-fg:#f6ab9d; --k-critical-bg:color-mix(in srgb,var(--k-critical) 30%,var(--k-surface));
  --k-caution-fg:#f4c884;  --k-caution-bg:color-mix(in srgb,var(--k-caution) 32%,var(--k-surface));
  --k-info-fg:#aacdff;     --k-info-bg:color-mix(in srgb,var(--k-info) 30%,var(--k-surface));
  --k-success-fg:#9ce6c1;
}
/* Outdoor / high-contrast: glove-grade, ≥7:1. Outdoor alone = light high-contrast. */
:root[data-outdoor="true"], html[data-outdoor="true"] {
  --k-bg-rgb: 255 255 255;
  --k-surface-rgb: 255 255 255;
  --k-surface-2-rgb: 237 242 247;
  --k-surface-3-rgb: 222 229 238;
  --k-text-rgb: 6 18 31;
  --k-text-muted-rgb: 35 53 74;
  --k-border-rgb: 43 90 140;
  --k-outline-rgb: 43 90 140;
  --k-accent:#066159;
  /* status tints: the light pair (outdoor alone is LIGHT high-contrast) */
  --k-critical-fg:#7a1b12; --k-critical-bg:color-mix(in srgb,var(--k-critical) 12%,var(--k-surface));
  --k-caution-fg:#7a4a00;  --k-caution-bg:color-mix(in srgb,var(--k-caution) 14%,var(--k-surface));
  --k-info-fg:#0b4f8a;     --k-info-bg:color-mix(in srgb,var(--k-info) 10%,var(--k-surface));
  --k-success-fg:#0b5c3c;
}
/* Dark + outdoor = DARK high-contrast (night field). Specificity beats both singles. */
html.dark[data-outdoor="true"] {
  --k-bg-rgb: 6 11 18;
  --k-surface-rgb: 19 27 39;
  --k-surface-2-rgb: 30 41 56;
  --k-surface-3-rgb: 44 59 80;
  --k-text-rgb: 255 255 255;
  --k-text-muted-rgb: 201 214 229;
  --k-border-rgb: 86 116 148;
  --k-outline-rgb: 120 150 180;
  --k-accent: var(--k-teal-400);
  --k-on-accent: var(--k-ink-900);
  --k-header-bg: #0C141F;
  /* status tints: the dark pair again, or the outdoor block above would hand night field the light pair */
  --k-critical-fg:#f6ab9d; --k-critical-bg:color-mix(in srgb,var(--k-critical) 30%,var(--k-surface));
  --k-caution-fg:#f4c884;  --k-caution-bg:color-mix(in srgb,var(--k-caution) 32%,var(--k-surface));
  --k-info-fg:#aacdff;     --k-info-bg:color-mix(in srgb,var(--k-info) 30%,var(--k-surface));
  --k-success-fg:#9ce6c1;
}
html[data-outdoor="true"] body { font-size:18px; font-weight:500; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
}

/* ---- 3. Base reset + typography ------------------------------------------- */
html { -webkit-text-size-adjust:100%; }
body {
  margin:0;
  background:var(--k-bg);
  color:var(--k-text);
  font-family:var(--k-font-ui);
  font-size:16px;                /* 16px floor (glare/low-vision) */
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}
.k-mono { font-family:var(--k-font-mono); font-variant-ligatures:none; }
.k-display { font-family:var(--k-font-display); }

:where(a,button,input,select,textarea,[tabindex],[role="button"],[role="tab"],[role="switch"],[role="treeitem"]):focus-visible {
  outline:3px solid var(--k-focus);
  outline-offset:2px;
  border-radius:var(--k-radius-sm);
}
::selection { background:color-mix(in srgb, var(--k-teal-500) 30%, transparent); }

/* ---- 4. Page content region ----------------------------------------------- */
/* MainLayout's <main class="kl-shell-main"> is the content surface. Give the
   page a comfortable measure + rhythm; headings get the display face. */
.kl-shell-main {
  background:var(--k-bg);
  color:var(--k-text);
}
.kl-shell-main > * { box-sizing:border-box; }
.kl-shell-main h1 {
  font-family:var(--k-font-display);
  font-size:clamp(22px, 2.2vw, 28px);
  line-height:1.2; font-weight:800; letter-spacing:-0.01em;
  margin:0 0 var(--k-space-4);
  color:var(--k-text);
}
.kl-shell-main h2 { font-family:var(--k-font-display); font-size:18px; font-weight:700; margin:var(--k-space-2) 0; }
/* Buttons styled as links (a.kl-btn) must keep their button text colour, not the teal link colour (which, at its
   higher specificity, was washing the navy "New record" anchor to low-contrast teal-on-navy). Exclude any -btn class. */
.kl-shell-main a:not([class*="-btn"]) { color:var(--k-accent); text-underline-offset:2px; }
.kl-shell-main p { color:var(--k-text); }

/* =============================================================================
   5. THE COMPONENT FOUNDATION — suffix-keyed families, scoped to page content.
   Each rule targets the shared SUFFIX of the page-prefixed family classes, so
   kl-ce-card / kl-uof-card / kl-medit-card all inherit one card treatment, etc.
   `:where([class^="kl-"],[class*=" kl-"])[class$="-x"]` (ends-with) disambiguates -card from -card-title cleanly.
   ========================================================================== */
.kl-shell-main { --kf-field-gap: var(--k-space-2); }

/* ---- 5a. Cards / sections ------------------------------------------------- */
/* `.kl-card` — the GENERIC card, and the one member of this family that had no rule at all (found 2026-07-24).
   The suffix selector matches the whole class ATTRIBUTE, so `class="kl-card"` alone happened to end in "-card"
   and picked the chrome up by luck, while every MULTI-class use — `kl-card kl-gate-profile`, `kl-card kl-onb-fields`,
   `kl-card kl-hub-group`, `kl-card kl-sign-pad`, … — ended in some other suffix and got NOTHING. On the device
   that shipped the /signin remembered-profile tile as a raw UA <button> (grey outset border, square corners).
   Same escape hatch, same declarations, as the .kl-medit-card / .kl-ce-card / .kl-ee-card lines below. */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-card"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-section"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-panel"],
.kl-shell-main .kl-card,
.kl-shell-main .kl-medit-card,
.kl-shell-main .kl-ce-card,
.kl-shell-main .kl-ee-card {
  display:block;
  background:var(--k-surface);
  border:1px solid var(--k-border);
  border-radius:var(--k-radius-lg);
  box-shadow:var(--k-elev-1);
  padding:var(--k-space-6);
  margin-block-end:var(--k-space-4);
}
/* THE HEAD TYPE. 18px — one step above body text, and level with `.kl-shell-main h2`, which every other
   second-level heading in the product already reads at.

   OWNER, live on /certifications/{id}/edit, 2026-09-15: "check the css of the headers of the cards on this
   page. there is no spaceing between the words and the icon of the header and they are normal text size."
   Measured on that card at 1400x900 before the change: the head read 16px, the subject VALUE inside the same
   card read 17px, body text read 16px and the head's own glyph read 20px. A head that is smaller than a data
   value in its own card, equal to body text, and smaller than its own glyph is not reading as a head. The
   owner chose the shared fix over a page one, so the size lives HERE and the page sheets that restated 16px
   were emptied of that line rather than bumped to a second copy of 18px. */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-card-title"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-section-title"] {
  font-family:var(--k-font-display);
  font-size:18px; font-weight:700; line-height:1.3;
  margin:0 0 var(--k-space-3);
  color:var(--k-text);
}
/* A card head that LEADS WITH A GLYPH puts the glyph and the words on one centre line.
   OWNER, live 2026-09-14, on /users-roles/{id}/edit: "for Identity and Operational defaults they are not
   vertically centred with their icon."

   THE CAUSE. The rule above sets type and margin and deliberately sets no `display`, so the head stays a
   BLOCK and the glyph is laid out as an inline-level box on the text baseline. Where the glyph sits then
   depends on its SIZE, and nothing governs that: measured across 72 live heads at 1400x900, 1280x800 and
   393x852, the block heads read -5.4px (a 20px glyph with overflow:hidden, which takes its bottom margin
   edge as its baseline — the two heads the owner named), -2.4px (a 20px glyph), -1.4px (16px), +0.7px
   (14.72px) and +1.39px (16.56px). Two block heads land inside half a pixel, by glyph size and not by
   rule. Every one of the 26 heads that was already `display:flex; align-items:center` read -0.01px, at
   all three widths, across four glyph sizes and three type sizes. That invariance is why this belongs
   here and not on 20 pages.

   NO `gap` HERE, AND THAT IS DELIBERATE. The gutter between glyph and words lives once, on the GLYPH, in
   the rule below: `margin-inline-end:var(--k-space-2)`, the value `.kl-detail-h-icon` (§5r) already carries
   on 163 elements. A `gap` on THIS rule would ADD to that margin and open every one of those heads to 16px.
   That defect was live on one page before 2026-09-14 — /people/{id} declared both and measured a 16px gutter
   against the product's 8px. A margin on the glyph sets the SAME property as the glyph's own, so it replaces
   rather than composes. CardHeadGlyphCentreLineGuardTests holds this rule to no `gap`.

   `:has(> :where(.ki:first-child))` — a LEADING glyph only. 145 heads in this family carry no glyph and
   must keep their block layout; none carries a glyph anywhere but first, so a trailing-glyph head cannot
   be caught by accident. The `:where()` inside `:has()` is not decoration: it holds this rule at (0,2,0),
   the same rank every other family rule in §5a has, so a page sheet still overrides it on order the way
   PacketBuilder's deliberate `inline-flex` does. Bare `:has(> .ki:first-child)` would rank (0,4,0) and
   silently outrank every page sheet in the product. */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-card-title"]:has(> :where(.ki:first-child)),
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-section-title"]:has(> :where(.ki:first-child)) {
  display:flex;
  align-items:center;
}

/* THE LEADING GLYPH IS SUBORDINATE TO ITS HEAD, and it carries the gutter. Both numbers are the ones
   `.kl-detail-h-icon` (§5r) has always used — `font-size:0.92em` and `margin-inline-end:var(--k-space-2)` —
   said once here so they reach EVERY glyph-led head in these two families, including the heads that never
   wrote that class.

   WHAT WAS WRONG WITHOUT IT. Nine page sheets each sized their own head glyph, and six of them chose 20px —
   so /certifications/{id}/edit, /vehicles/{id}/edit, /users-roles/{id}/edit, /medical/{id}/edit,
   /master-names/{id} and the activity finds entry each put a 20px glyph beside a 16px word: the icon read
   LOUDER than the heading it labelled. The other three read 14px and 16px, which is a third and a fourth
   answer to one question. Seven page sheets hand-rolled the gutter as a `gap` on the head, and that was the
   only reason those pages had a gutter at all — the `display:flex` above deletes the collapsed inline space a
   block head relies on, so any NEW iconned head welded its glyph to its words. Every one of those page copies
   is deleted. This is the one place the relationship lives.

   `em`, NOT px. The glyph tracks whatever size its head ends at, so a page that deliberately deviates —
   PacketBuilder's 18px inline-flex head, RecordBuildSurface's 18px/800 step head, Home's 15px tiles — keeps a
   proportionate glyph without restating anything.

   `flex-shrink:0` is for ONE glyph, and there it is real. An icon-font span is atomic, so its automatic
   minimum size IS its glyph width and a flex row cannot squeeze it: the declaration is inert on every head
   but `.kl-ure-card-icon`, which sets `overflow:hidden`. That zeroes the automatic minimum, and a long
   heading CAN then squeeze that glyph out of square. It was declared on that one page sheet; it is declared
   once here instead, because the page that next copies `overflow:hidden` will not think to copy this too. */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-card-title"] > :where(.ki:first-child),
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-section-title"] > :where(.ki:first-child) {
  font-size:0.92em;
  margin-inline-end:var(--k-space-2);
  flex-shrink:0;
}

/* ---- 5b. Form kit (kf-* explicit + page -field/-label/-input families) ---- */
.kf-field,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field-wide"] {
  display:flex; flex-direction:column; gap:var(--kf-field-gap);
  margin-block-end:var(--k-space-4); min-inline-size:0;
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field-wide"] { grid-column:1 / -1; }

.kf-label,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-label"] {
  font-size:13px; font-weight:600; line-height:1.3;
  color:var(--k-text-muted); letter-spacing:.01em;
}
/* Field helper text (`<p class="kf-hint">` under a control). Until 2026-09-10 the kit shipped NO rule for it, so a
   hint rendered as a bare paragraph: 16px, full text colour, and the UA's 16px block margins on both sides — larger and
   darker than the 13px muted label above the control it explains. Measured on /tenant-settings: 55 hints, 3,649px of a
   12,085px page. Three surfaces had each hand-rolled a copy (DailyLog 0.76rem, TenantSettingsLicense colour-only,
   TenantSettingsRetention margin-only) — the A1 drift this sheet exists to stop. One rule, both heads: the same 13px /
   muted register as .kf-label and the -note family, so label, control and hint read as one field. `.kl-shell-main` is
   required, not decoration: `.kl-shell-main p` sets color at (0,1,1) and a bare `.kf-hint` would lose on every <p>.
   margin:0 — the field's own gap (--kf-field-gap) is the rhythm; the UA margins were doubling it. */
.kl-shell-main .kf-hint {
  margin:0;
  font-size:13px; line-height:1.45;
  color:var(--k-text-muted);
}

/* The OPTIONAL marker inside a field label — the shared convention (owner report 2026-08-09).
   `<label class="kf-label">Email <span class="kl-optional">Optional</span></label>` rendered as the single
   run-on phrase "Email Optional": the label rule above styles the whole label, the inline <span> inherited it
   exactly, and a plain space between two same-weight same-size same-colour runs is not a separator a reader
   can see. The same defect was reported on /dogs/new ("Microchip ID Optional") and the record wizard
   ("Ref. #Optional") — so the fix is ONE class here in the shared RCL sheet (§A1), not three inline styles.
   It reads as a distinct token: its own gutter, a hairline pill, lighter weight and smaller type, so the word
   is plainly ABOUT the label rather than part of it. Deliberately NOT named `*-badge`/`*-note`/`*-label` —
   those are live suffix families (trap T7) and would sweep this span into a family it does not belong to.
   Adoptable verbatim by any label that carries an optionality marker; the markup contract is the span with
   this class INSIDE the <label>, so the accessible name still reads "Email Optional" to a screen reader. */
/* The DEFAULTED marker — owner ruling 2026-08-09, the same shape one step further.
   `<label>Date received <span class="kl-defaulted">Default</span></label>`.

   Two create surfaces pre-fill a date with today — /training-aids/new's "Date received" (where a controlled
   aid's CHAIN OF CUSTODY starts) and /medical/new's "Service date" (which also drives the next-due
   calculation) — and nothing on screen distinguished a value the user CHOSE from one the form FILLED IN. An
   aid received last Tuesday and entered today silently started its custody chain on the wrong day. The owner's
   ruling: keep the pre-fill, and add a today affordance — the value stays, but it must READ as a default.

   It shares `.kl-optional`'s declarations verbatim rather than inventing a second treatment: both say the same
   kind of thing (a token that is ABOUT the value, not part of it), and one visual vocabulary is the point of
   §A1. It is a MARKER, not a warning — no --k-critical, no icon, no colour alarm. A permanent warning on the
   overwhelmingly common correct case (the entry IS being made today) is how operators learn to ignore
   warnings, which is the same reasoning that kept the veterinarian near-match advisory narrow.

   It is present ONLY while the value is still the untouched form-supplied default, and clears the moment the
   user takes ownership — edits the date, or presses the paired `Today` control, which asserts the same thing
   deliberately. Nothing about what is STORED changes; this is a UI honesty change.

   Not named `*-badge` / `*-note` / `*-pill` for trap T7's reason (those are live suffix families).           */
.kl-optional,
.kl-defaulted {
  display:inline-block; vertical-align:baseline;
  margin-inline-start:var(--k-space-2);
  padding-inline:var(--k-space-1);
  border:1px solid var(--k-border); border-radius:var(--k-radius-sm);
  font-size:11px; font-weight:500; line-height:1.5; letter-spacing:.02em;
  color:var(--k-text-muted);
}

/* The date field + its one-tap `Today` control, sharing ONE row (§B2 — related things share a row; §B1 — a
   second row for a 72px button is vertical space this product does not spend). The wrapper exists so the
   affordance never becomes a line of its own, and so both create surfaces get the identical arrangement from
   one place rather than two page-scoped copies (§A1).

   `flex-wrap: wrap` is the safety valve, not the plan: measured at 320/375/393/430 the pair fits on one line,
   but a longer localised label must wrap rather than force horizontal overflow (hard constraint 2).
   `min-inline-size:0` on the control is required — a flex item's auto minimum refuses to shrink below an
   <input type=date>'s intrinsic width, which is what would push the button off the edge.                     */
.kl-datefill {
  display:flex; align-items:center; flex-wrap:wrap;
  gap:var(--k-space-2);
}
.kl-datefill > :where(input, select) { flex:1 1 9rem; min-inline-size:0; }
.kl-datefill > :where(button) { flex:0 0 auto; }

.kf-input, .kf-select, .kf-textarea,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-input"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-select"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-textarea"],
.kl-shell-main input[type="text"], .kl-shell-main input[type="date"],
.kl-shell-main input[type="datetime-local"],
.kl-shell-main input[type="time"], .kl-shell-main input[type="number"],
.kl-shell-main input[type="email"], .kl-shell-main input[type="search"],
/* password was MISSING from this list, and nothing anywhere else styled it — grep the whole tree for
   input[type="password"] and this is the only hit. So on every auth surface in the product the password box fell
   outside the design system while the field directly above it sat inside it: a different height (no --k-touch
   floor), a different font size, a different border colour, browser default padding. It is visible on /login,
   /account/first-login, /account/forgot-password, /account/reset-password, the invite accept, account security,
   the Apple/Google link pages and /k96/login-page.
   Found by an owner watching the K96 film (2026-07-30) — "the K96 password entry does not look the same as the
   other two entry boxes" — where the mismatch is starkest because that page also painted its own border colour.
   It is not a K96 defect; that page is just where it was noticed.
   Safe to add precisely BECAUSE nothing else styles password inputs: there is no component rule for this
   selector (0,2,1) to out-specify, which is the hazard §5c's kf-combobox reset exists to undo. */
.kl-shell-main input[type="password"],
.kl-shell-main input:not([type]),   /* a typeless <input> IS type=text but [type="text"] would miss it */
.kl-shell-main select, .kl-shell-main textarea {
  appearance:none;
  inline-size:100%; max-inline-size:100%; box-sizing:border-box;
  min-block-size:var(--k-touch);
  padding:0 var(--k-space-3);
  font:inherit; font-size:15px; color:var(--k-text);
  background:var(--k-surface);
  border:1px solid var(--k-outline);
  border-radius:var(--k-radius-sm);
  transition:border-color var(--k-dur-1) var(--k-ease), box-shadow var(--k-dur-1) var(--k-ease);
}
.kl-shell-main textarea, .kf-textarea, .kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-textarea"] {
  min-block-size:5rem; padding-block:var(--k-space-2); line-height:1.5; resize:vertical;
}
.kl-shell-main input:focus, .kl-shell-main select:focus, .kl-shell-main textarea:focus,
.kf-input:focus, .kf-select:focus, .kf-textarea:focus {
  outline:none; border-color:var(--k-accent);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--k-accent) 22%, transparent);
}
.kl-shell-main input:disabled, .kl-shell-main select:disabled, .kl-shell-main textarea:disabled {
  background:var(--k-surface-2); color:var(--k-text-muted); cursor:not-allowed;
}

/* ---- 5b-empty. ONE INK FOR "THIS FIELD IS EMPTY" -------------------------------------------------------------
   Owner, 2026-08-10, on /medical/new's Event type beside its neighbours: "there is also a difference in font
   weight or color or something on that box vs the others." Font-weight (400), font-family (Inter), font-size
   (15px) and `color` were measured IDENTICAL on all four controls. The difference is the EMPTY state's ink, and
   measured live it was a THREE-way split, not a two-way one:

     combobox ::placeholder            rgb(62, 74, 92)    = var(--k-text-muted)  ← the only one on-token
     native <input> ::placeholder      rgb(117,117,117)   = the USER-AGENT default, no rule anywhere in the repo
     native <select> placeholder row   rgb(14, 34, 54)    = var(--k-text), i.e. full ink

   Before this rule the ONLY `::placeholder` declaration in the entire shared surface was the combobox's, so every
   native text input in the product was painting an untokened browser grey that matches neither the design system
   nor the combobox. This sets the house placeholder ink once. `opacity:1` is required: Gecko applies a default
   placeholder opacity, which would otherwise re-introduce a second, dimmer grey on one engine only. */
.kl-shell-main input::placeholder,
.kl-shell-main textarea::placeholder,
.kf-input::placeholder, .kf-textarea::placeholder {
  color:var(--k-text-muted); opacity:1;
}

/* The <select> half of the same idea, and it is OPT-IN ON PURPOSE.
   A select's empty row is not always a placeholder. Of the 141 `<option value="">` rows across 53 files, a large
   share are MEANINGFUL chosen states — "(any)" (×9), "All units", "All types", "All record types", "Any type",
   "— none —", "— inherit —", "— use my default —". Those fields are FILLED, with the "no restriction" choice, and
   grey-ing them would make a deliberate filter read as unfilled: the exact inverse of the defect being fixed. CSS
   cannot tell "— select a K9 —" from "(any)" by itself, and `:required:invalid` cannot stand in for the
   distinction either — only 5 selects in the product carry `required`.

   So the placeholder-ness is declared on the OPTION, as a data attribute (never a modifier class: T7 — a
   `[class$="…"]` family keys on where the attribute ENDS, so a trailing modifier silently drops a member). The
   rule then reads the SELECTED option, so the box greys only while the placeholder is what is showing.
   `select option { color: var(--k-text) }` keeps the opened dropdown list itself at full ink — otherwise the muted
   colour inherits into every row of the popup and the whole list looks disabled. */
.kl-shell-main select:has(option[data-placeholder]:checked),
.kf-select:has(option[data-placeholder]:checked) {
  color:var(--k-text-muted);
}
.kl-shell-main select option, .kf-select option { color:var(--k-text); }
/* Native <select> dropdown affordance: appearance:none (above) strips the UA arrow and nothing redrew one, so a
   select was indistinguishable from a text input (the record-entry "which of these is pickable?" finding). Draw an
   on-brand chevron (encoded inline SVG, ink-800 stroke — a data URI cannot read custom properties, so html.dark
   swaps a light-tint variant below; an SVG image is NOT a gradient, so the R11 canvas tooth is untouched) in the
   end-padding slot. The :not([multiple]):not([size]) does DOUBLE duty: it scopes to REAL closed selects (open list
   boxes take no closed-box arrow) AND lifts specificity to (0,3,1) so this outranks the form-kit family's
   `background` / `padding` SHORTHANDS on `.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-select"]` (0,2,0) — which otherwise silently
   reset the image away (shot-verified). The LookupComboBox's native mirror is display:none + draws its own
   .kf-combobox-chevron — never double-chevroned. */
.kl-shell-main select:not([multiple]):not([size]) {
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%230E2236' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;
  background-position:right var(--k-space-3) center;
  background-size:12px 8px;
  padding-inline-end:var(--k-space-8);
}
html.dark .kl-shell-main select:not([multiple]):not([size]) {
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none'%3E%3Cpath d='M1 1.5 6 6.5 11 1.5' stroke='%23E6EBF2' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
/* THE COMPACT SELECT (2026-09-16). `kf-select--sm` was declared only in Scheduler.razor.css, a SCOPED sheet, so it
   reached the scheduler roster and nothing else. The on-behalf RSVP select on /events/{id}/edit carries the same
   class for the same job, and there it matched no rule: the select rendered at the full 15px form size beside two
   `kl-btn--sm` buttons. Declared once here. It trims the type and the block padding ONLY. The --k-touch floor stays
   (A6), and the chevron slot stays, because the (0,3,1) rule above still owns padding-inline-end.
   (0,2,0) is required: the bare `.kf-select--sm` (0,1,0) loses to `.kl-shell-main select` (0,1,1). */
.kl-shell-main .kf-select--sm {
  padding-block:var(--k-space-1); padding-inline-start:var(--k-space-2);
  font-size:12px;
}

/* Bool form field — a REAL brand-accented checkbox at its natural size. The generic FieldWidget gives a checkbox
   the kf-checkbox class (never kf-input, whose appearance:none + 100% inline-size + box border would strip the
   native tick and render the field as an empty full-width box — the record-entry "K9 alert reads as an empty box"
   finding). Keeps the native control (no appearance:none) so the tick + focus ring are real and accessible. */
.kf-checkbox {
  inline-size:1.15rem; block-size:1.15rem; min-block-size:auto;
  margin-block:var(--k-space-1); accent-color:var(--k-teal-500); cursor:pointer;
}
.kf-checkbox:disabled { cursor:not-allowed; opacity:.55; }

/* THE CHECKBOX ROW'S TARGET (A6 wave 3, 2026-09-13). `<label class="kf-afe-check">` wraps a `.kf-checkbox` and
   its words, so the LABEL is what a press activates — the same anatomy 5b-sw below relies on, and the same
   answer: the target is bought from the row's touch floor, never by inflating the 1.15rem control.

   MEASURED at 393x852 on /records/{id}/build's wizard FIND step: `wiz-find-field-test` box 18.4 / hit 28,
   `wiz-find-hidden-compartment` box 18.4 / hit 26.

   A BAND WAS IMPOSSIBLE HERE, and the sample says so plainly: `wiz-find-cash-value` ends 16px above the first
   checkbox and the second checkbox begins 20px below it. A `--k-touch` band (48px) on an 18.4px box overhangs
   14.8px each
   way, which reaches into a text input above and the neighbouring checkbox below. The label has to grow.

   Declared once, here, rather than in the four scoped sheets that lay this label out — RecordFindFieldsSection,
   RecordActivityFieldsSection, RecordBuildSurface and ActivityFindsEntry all spell `.kf-afe-check` and all load
   this sheet. Additive: `.kl-afe`'s copy already adds 8px of padding and keeps it, and a label whose words
   already run past one touch unit is unaffected. */
.kl-shell-main .kf-afe-check { min-block-size:var(--k-touch, 44px); }

/* ---- 5b-sw. THE SWITCH ROW — one checkbox, its words, and ONE affordance --------------------------------------
   `<label class="kf-field kf-field--switch">` wraps a checkbox and a `.kf-label` span. The label activates the
   checkbox, so the label IS the hit target. The foundation lays every `.kf-field` out as a COLUMN, which is wrong
   for this shape twice over: it stacks the box above its words, and it stretches the box to the field width.

   WHY THIS RULE MOVED HERE (A1, one shared CSS surface). The 50811bd0 standardization retired the per-page
   "Show deleted" pill and deleted the `.kl-switch` / `.kf-field--switch` rules from MasterNames.razor.css. It did
   NOT give the class a shared home, and it did not touch the pages that spell the class this way. Three pages then
   hand-rolled the same row for themselves and one page hand-rolled nothing:
     - CustomFields.razor.css declared `.kf-field--switch` (18px box, --k-space-3 gap).
     - Lookups.razor.css declared `.kl-lookups-add-dismiss` (18px box, --k-space-2 gap).
     - Lookups.razor.css declared `.kl-lookups-add-scope-kind` (16px box, 2px block padding) for its checkbox LIST.
     - Lookups' two attachment switches carried NO rule, so each box rendered 224x13 — stretched flat.
   The first three are Blazor-scoped, so no page can inherit another page's copy. This is the drift the shared
   surface exists to end. The two page copies of the SWITCH row are deleted with this promotion. The scope-kind
   CHECKBOX LIST stays page-scoped and keeps its own tighter rhythm, because it is a list and not a field.

   §A6 — THE FLOOR. Measured live on 2026-09-12 before this rule, at 320/375/393/430/768/1400: the activating label
   of `cf-is-pii-checkbox` and `cf-is-required-checkbox` was 18px tall at every width above 320. The label was the
   same size as the 18px box, so there was no enlarged hit area at all. `lookups-add-dismiss` read 18px to 33.8px
   and the two attachment switches read 39.9px. `min-block-size: var(--k-touch)` is the house floor and is never
   traded for space. `align-items: center` keeps the box and the words where they already sat, so the control keeps
   its visual weight and only the row's own box grows.

   KEYED TO `label` ON PURPOSE. /medical and /users-roles spell the same class on a `<div>` that holds a `.kf-label`
   caption ABOVE a `role="switch"` button (filter-date, show-deactivated-btn). There the button carries the floor
   already, both measured 48px at every width, and a row layout would restack the caption beside the button. An
   element selector keeps those two rows exactly as they are. ---- */
label.kf-field--switch {
  flex-direction:row; align-items:center; gap:var(--k-space-3);
  min-block-size:var(--k-touch); cursor:pointer;
}
/* `flex:0 0 auto` is load-bearing, not tidiness: beside a long label a shrinkable box is squeezed off its own
   size. The same squeeze was measured on /tenant-settings at 393px and is fixed the same way there. */
label.kf-field--switch > input[type="checkbox"] {
  flex:0 0 auto;
  inline-size:18px; block-size:18px; margin:0;
  accent-color:var(--k-teal-700,var(--k-accent)); cursor:pointer;
}
/* The words of a switch read as the control's own text, not as a field caption above a control. */
label.kf-field--switch > .kf-label { color:var(--k-text); font-weight:600; cursor:pointer; }

/* ---- 5b-sw2. THE SIBLING-LABEL CHECKBOX ROW — the same shape, where the label does NOT wrap the input ----------
   OWNER, on /dogs/{id}/edit 2026-09-12: "on custom variables the checkbox control does not line up with its label
   vertically."

   THE DEFECT, MEASURED LIVE before the fix: the custom-field boolean rendered 991x13 at 1400 and 304x13 at 393 —
   stretched to the FULL FIELD WIDTH and collapsed to the 13px UA height — with its label stranded 22-30px above it.
   TWO causes, and neither is the one it looks like:

   1. THE CLASS ON THE INPUT WAS DEAD. The markup writes `.kf-check` (CustomFieldsPanel) and `.kl-dde-check`
      (CustomFieldEditor). Neither string appears in ANY stylesheet in the repo — checked. The live class is
      `.kf-checkbox`, which every other checkbox in the product uses. So these two inputs matched no rule at all and
      inherited the field's layout wholesale.
   2. THE FIELD IS A COLUMN, AND ONE OF THESE TWO NEVER ASKED FOR THAT. `.kf-field` at :287 is
      `display:flex; flex-direction:column`, and the selector beside it sweeps the `[class$="-field"]` FAMILY — which
      is how `.kl-dde-field` acquires a column layout it never declares. That is the class-suffix family trap for the
      EIGHTH time in this campaign.

   WHY THIS IS A SECOND BLOCK AND NOT A WIDENING OF 5b-sw ABOVE. That rule is deliberately scoped to
   `label.kf-field--switch` because the class carries two shapes, and widening it was rejected on the record. These
   two sites have a different ANATOMY: a `<div class="kf-field">` holding a sibling `<label class="kf-label" for=id>`
   and a bare `<input>`. The label is not the wrapper, so 5b-sw cannot reach them however it is written. Restructuring
   both components to the wrapping shape would move `data-testid` and the label/for relationship on two SHARED
   components used across many pages — a markup change with real blast radius, for a layout defect. This fixes the
   layout where the layout is wrong.

   BLAST RADIUS IS EXACTLY TWO SITES, by construction: the selectors key on the two DEAD class names, which appear at
   `CustomFieldsPanel.razor:137` and `CustomFieldEditor.razor:73` and nowhere else. Every other checkbox in the
   product carries `.kf-checkbox` and is untouched. `:has()` is already used in 45 sheets here, so it is established.

   `order:-1` puts the box before its words, matching 5b-sw, so the two checkbox shapes read identically even though
   their markup differs. The box keeps the 18px of 5b-sw — the target is bought from the row's touch floor, never by
   inflating the control.

   2026-09-14, THIRD SITE — /users-roles/{id}/edit. OWNER, live: "for this page the show in user lists checkjbox
   is below not next to teh label." Measured before, at 1400x900, 1280x800 and 393x852: the box rendered
   224.5x13 on the desk and 304x13 on the phone, its label 24px tall and 35px ABOVE it, vertical overlap 0, and
   the continuous hit span down the centre line was 14px against the 44px floor. Same page, same two causes,
   same fix.

   `.kf-check` KEYS THE THIRD SITE. The class was written into this rule in the paragraph above and then matched
   no element at all — the two components it was named for spell `.kf-checkbox` and `.kl-dde-check`. It is now a
   LIVE, deliberate house name for one anatomy: a bare checkbox that is a SIBLING of its label inside a field.
   `UsersRolesEdit.razor` carries it. The `-field` family arm is added beside the `.kf-field` arm because this
   page's wrapper is `.kl-pedit-field`, a family member and not the literal class.

   BLAST RADIUS IS ONE ELEMENT, and it is checkable: `.kf-check` appears on exactly one input in the tree.

   2026-09-14, THE FOURTH SITE — AND THE REASON THIS BLOCK READ AS COVERAGE AND DELIVERED NONE. Until the third
   site above, `.kf-check` matched NO element anywhere in the tree, so every selector here was inert from the day
   it was written. The two components the block was authored for spell the class `.kf-checkbox` and
   `.kl-dde-check`. Putting the house class on `UsersRolesEdit` took the count from zero elements to one. It left
   the site the block was NAMED for still broken.

   MEASURED LIVE at 1400x900, 1280x800 and 393x852 on /dogs/{id}/edit, custom field `cf-input-x_kenneled_on_site`:
   box 18.4x18.4, checkbox/label vertical overlap 0, and an 18px continuous hit span walked down the activating
   label's centre line against the 44px floor. Same anatomy, same two causes, same fix.

   THIS ARM KEYS THE ANATOMY, NOT THE CLASS ALONE. `.kf-checkbox` is the product's universal checkbox class: 27
   authoring sites carry it, and a bare arm would reach every one. This arm reaches an input only where the input
   is a DIRECT CHILD of its field — the sibling-label shape. A survey of all 109 checkbox inputs in the markup
   found exactly two shapes in that position, and no third:
     - CustomFieldsPanel's boolean value field, the defect above;
     - FieldWidget's checkbox field, which FormSectionView always renders inside a `.kf-checkgroup`.
   Every other checkbox in the product sits INSIDE its own label (`.kf-afe-check`, `.kl-toggle-row`,
   `.kl-toggle-label`, `.kl-showdeleted-label`, `.kl-account-notif-cell`, `.kl-sip-control`, `.kl-rb-col-label`
   and the rest). A wrapping label is not a child of a field, so this arm cannot reach one. All 77 of them were
   measured at the three widths before the change: the smallest activating target was 45px, and none moved.

   THE CHECKGROUP IS EXCLUDED ON PURPOSE. §5b-cg below lays a GROUPED checkbox out as a compact `row-reverse`
   line, per the owner preference of 2026-06-24, so the box already leads its words there. `order:-1` in a
   reversed row would move the box AFTER them, and a per-member touch floor would stretch a set that is
   deliberately tight. `:where()` contributes zero specificity, so `:not(:where(.kf-checkgroup > *))` filters and
   never re-ranks the rule. */
.kf-field:has(> .kf-check),
.kf-field:not(:where(.kf-checkgroup > *)):has(> .kf-checkbox),
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kf-check),
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kl-dde-check) {
  flex-direction:row; flex-wrap:wrap; align-items:center; gap:var(--k-space-3);
  min-block-size:var(--k-touch);
}
/* `flex:0 0 auto` is load-bearing for the same reason it is in 5b-sw: beside a long label a shrinkable box is
   squeezed off its own size. */
.kf-check,
.kf-field:not(:where(.kf-checkgroup > *)):has(> .kf-checkbox) > .kf-checkbox,
.kl-dde-check {
  order:-1; flex:0 0 auto;
  inline-size:18px; block-size:18px; margin:0;
  accent-color:var(--k-teal-700,var(--k-accent)); cursor:pointer;
}
/* The words of a checkbox read as the control's own text, not as a caption above a control — same as 5b-sw.

   2026-09-13, A6 CORRECTION TO THIS BLOCK. The row above buys a `var(--k-touch)` floor, but the floor was
   spent on the ROW and not on the thing a finger presses. Measured live at 393x852 on /dogs/{id}/edit with an
   `elementFromPoint` walk down the centre line: the row reported a 72px continuous hit span, the `label[for]`
   that ACTIVATES the checkbox reported 36px, and the 18px box reported 20px. A reader aiming at the words
   "Kenneled on site" therefore had a 36px target inside a row sized for 48px.

   `align-self:stretch` makes the label fill the row's cross size, so the label IS the row's height. The words
   stay vertically centred through the inner flex box. This costs zero layout: the row already had that height,
   and the label only grows into space the row already occupied. It cannot overlay a neighbour, because the
   label is a flex ITEM in the row and grows only along the cross axis of its own column.

   Measured after, same probe, same page: label 72px. At 1400x900 the row is 48px and the label is 48px.

   TWO CORRECTIONS TO THE SELECTOR LIST, both found while measuring the above.

   1. THE SECOND SITE'S LABEL HAS NO CLASS. CustomFieldEditor writes `<label for="…">` with no class attribute at
      all, so neither `> .kf-label` nor `> [class$="-label"]` reached it and the colour rule above was inert there.
      A bare `> label` child selector reaches it. Inside a `-field` that holds a `.kl-dde-check`, the only child
      label IS the field's caption, so the element selector is precise. (CustomFieldEditor has no call site in the
      tree today, so this branch renders nowhere; the rule is correct for the day it does.)
   2. `> [class$="-label"]` WAS AN UNGUARDED SUFFIX SELECTOR — the exact shape D14 exists to stop, and it was the
      single red that guard reported. A bare suffix matches by spelling, including the generated icon pack. It is
      now namespace-guarded the way every other family here is. Nothing loses the rule: no element in either site
      carries a non-`kl-` class ending in `-label`.

   3. THE FLOOR BELONGS ON THE LABEL, NOT ONLY ON THE ROW. `align-self:stretch` buys the target from the row's
      height, and that works only while the row is ONE line. The moment a help string wraps the row (see the
      block below), the label's flex LINE is 18px tall and stretch buys nothing: measured live at 393x852 the
      label went back to 18px inside a 102px row. `min-block-size` puts the floor on the thing a finger actually
      presses, so it holds whether the row wraps or not. The two are kept together on purpose — stretch still
      fills a taller single-line row, and the floor still holds a wrapped one. */
.kf-field:has(> .kf-check) > .kf-label,
.kf-field:not(:where(.kf-checkgroup > *)):has(> .kf-checkbox) > .kf-label,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kf-check) > .kf-label,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kf-check) > label,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kf-check) > :where([class^="kl-"],[class*=" kl-"])[class$="-label"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kl-dde-check) > .kf-label,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kl-dde-check) > label,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kl-dde-check) > :where([class^="kl-"],[class*=" kl-"])[class$="-label"] {
  color:var(--k-text); font-weight:600; cursor:pointer; margin:0;
  align-self:stretch; display:flex; align-items:center;
  min-block-size:var(--k-touch);
}
/* THE HELP STRING IS A THIRD CHILD, AND IT IS WHAT MADE THE FIRST ATTEMPT AT THIS ARM WRONG. A `.kf-field` holds
   its label, its control AND an optional `<span class="kf-help">`. As a COLUMN that span sat under the control
   and read correctly. Turned into a ROW it became a third item on the same line: measured live at 393x852 on
   /dogs/{id}/edit, the help took 193.8px, the words "Kenneled on site" were squeezed to a 60.2px ribbon and wrapped
   over three lines, and the row grew to 96px. The row must therefore WRAP, and the help must claim a whole line of
   its own. §5b-cg answers the identical shape for a grouped checkbox the identical way, and the indent aligns the
   help under the WORDS it explains rather than under the box. The 18px term is the box size this block declares
   above, so the two stay in step. */
.kf-field:has(> .kf-check) > .kf-help,
.kf-field:not(:where(.kf-checkgroup > *)):has(> .kf-checkbox) > .kf-help,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kf-check) > .kf-help,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-field"]:has(> .kl-dde-check) > .kf-help {
  flex-basis:100%; margin-inline-start:calc(18px + var(--k-space-3));
}

/* UI-GUAR-036 (R2-25): a native <input type="file"> renders ~21px tall — below the WCAG 2.5.5/2.5.8 floor (the live
   sweep flagged the GPX picker at 253×21). Give every file input in the app surface a ≥44 CSS-px touch height so its
   native "Choose file" button sits in a full-size tap target. Cursor + vertical centring keep it usable. */
.kl-shell-main input[type="file"] {
  min-block-size:var(--k-touch); display:inline-flex; align-items:center; cursor:pointer;
}

/* UI-GUAR-036 (R2-25): a breadcrumb is NAVIGATION (not inline prose), so its link is a ≥44 CSS-px touch target. The
   EventCreate breadcrumb link (.kl-ee-crumb) has no scoped sheet, so it was a bare ~20px link — give it a full-height
   tap area (authored globally so any breadcrumb-style link of this class qualifies).

   2026-07-28: the parenthetical above was the INTENT but not the CODE — the selector named .kl-ee-crumb alone, so
   every OTHER crumb link stayed ~20px, and each new one silently inherited the same miss (the vault's .kl-ev-crumb
   was one). Widened to the -crumb link FAMILY, matching the sibling selector in §5e that already styles crumbs this
   way. Anchors only: a touch target is something you TAP, and the family also contains non-interactive spans
   (-crumb-current, -crumb-sep) that must not become inline-flex boxes.

   2026-09-13: the family was STILL incomplete, and for the third time it is the class-suffix trap.
   `[class$="-crumb"]` keys on the END of the class attribute. A crumb anchor named `-breadcrumb` therefore
   never matched. `kl-mastername-breadcrumb` measured a 16px continuous hit span on /master-names/{id} and on
   /master-names/dedup-queue. Every other crumb anchor in the product measures 48px.

   The crumb TYPOGRAPHY pair below (`-breadcrumb`, `-crumb`) already names both suffixes. Only this TOUCH rule
   was out of step, so the two rules covered different sets. Both suffixes are listed here now. A box floor is
   correct for a crumb and a transparent ::after expander is not: crumb anchors sit in a horizontal row of
   siblings, and an expander with a 48px `min-inline-size` would reach sideways over the separator and the next
   segment. A box floor cannot overlay a neighbour. Anchors only, for the reason above. */
.kl-shell-main a:where([class^="kl-"],[class*=" kl-"])[class$="-crumb"],
.kl-shell-main a:where([class^="kl-"],[class*=" kl-"])[class$="-breadcrumb"] {
  display:inline-flex; align-items:center; min-block-size:var(--k-touch);
}

/* UI-GUAR-036 (R2-25): the "More filters" ghost buttons (.kl-filters-more on Approvals — renamed 2026-07-02 from
   .kl-more-filters, whose "-filters" SUFFIX made the :where([class^="kl-"],[class*=" kl-"])[class$="-filters"] CONTAINER family style the BUTTON itself
   (align-items:flex-end → the bottom-hugged label of the owner's Round-2 complaint; live-probed); .kl-drafts-more
   on the Drafts panel) sit in a flex filter row whose cross-sizing shrank them below the foundation 48px floor
   (the live sweep measured 93×30). Authored GLOBALLY (scoped .razor.css would not reach the other component) —
   re-assert the full touch height and stop them from shrinking. */
.kl-shell-main .kl-filters-more,
.kl-shell-main .kl-drafts-more {
  min-block-size:var(--k-touch); flex:0 0 auto;
}

/* Segmented radio-group affordance (label box IS the tap target; the native radio is the accessible control,
   visually collapsed). Authored ONCE here in the GLOBAL sheet so EVERY page that uses it gets a ≥44 CSS-px touch
   target (UI-GUAR-036 / R2-25). It was previously defined ONLY in Onboarding's SCOPED .razor.css, so DogCreate's
   identical markup rendered as bare ~20px native radios (the live sweep flagged dogcreate-sex-* at NN×20). Onboarding
   keeps its own scoped copy (identical sizing) — no conflict. */
.kl-onb-radiogroup { display:flex; gap:var(--k-space-2); flex-wrap:wrap; }
.kl-onb-radio {
  flex:1 1 0; display:flex; align-items:center; justify-content:center;
  min-block-size:var(--k-touch); padding:0 var(--k-space-2);
  border:1px solid var(--k-border); border-radius:var(--k-radius-sm);
  background:var(--k-surface); color:var(--k-text); cursor:pointer; user-select:none;
  transition:background-color 120ms ease, border-color 120ms ease, color 120ms ease;
}
.kl-onb-radio input[type="radio"] { position:absolute; opacity:0; inline-size:1px; block-size:1px; }
.kl-onb-radio.kl-is-checked { border-color:var(--k-teal-500); background:color-mix(in srgb,var(--k-teal-500) 12%,var(--k-surface)); color:var(--k-text); font-weight:600; }
.kl-onb-radio:focus-within { outline:3px solid var(--k-teal-400); outline-offset:2px; }

/* Vertical, grouped checkbox set (owner pref 2026-06-24). The generic FormSectionView coalesces a RUN of consecutive
   boolean(checkbox) placements into ONE <fieldset class="kf-checkgroup">. The fieldset SPANS the full section grid
   (grid-column:1/-1) so the group is one stacked column — not one tiny checkbox stranded per minmax(220px,1fr) cell
   with its label floating above (the "huge blank space" finding). Each member is still a generic FieldWidget .kf-field
   carrying its data-field-key (the structural test surface is unchanged); inside the group it lays out as a compact
   horizontal row — checkbox FIRST, then its label — mirroring ActivityFindsEntry's kf-afe-perf-metrics rows. */
.kf-checkgroup {
  grid-column:1 / -1;
  display:flex; flex-direction:column; gap:var(--k-space-1);
  margin:0; padding:0; border:0; min-inline-size:0;
}
/* A grouped checkbox field: a single inline row (box → label), not the default label-above-input column. The
   FieldWidget emits <label> then <input>; row-reverse + flex-end puts the box first while keeping the label the
   accessible <label for>. The row collapses to its content (no full-width box, no stacked margins). */
.kf-checkgroup .kf-field[data-widget="Checkbox"] {
  flex-flow:row-reverse wrap; justify-content:flex-end; align-items:center;
  gap:var(--k-space-1) var(--k-space-3); margin-block-end:0; padding-block:var(--k-space-1);
}
.kf-checkgroup .kf-field[data-widget="Checkbox"] .kf-label {
  margin:0; color:var(--k-text); font-weight:500; cursor:pointer; line-height:1.3;
}
.kf-checkgroup .kf-field[data-widget="Checkbox"] .kf-checkbox { flex:0 0 auto; }
/* The optional help string for a grouped checkbox drops to its own muted line under the row (full width). */
.kf-checkgroup .kf-field[data-widget="Checkbox"] .kf-help {
  flex-basis:100%; margin-inline-start:calc(1.15rem + var(--k-space-3));
  color:var(--k-text-muted); font-size:13px;
}

/* Native date / time / datetime-local pickers render their INTERNAL chrome (the value text, spinners, and the
   calendar-picker indicator) per the element's used color-scheme — not the element background. Without this they
   default to a light scheme, so on the dark canvas the widget reads as a jarring white-box-on-dark (R11 bar-4/6).
   Pin the scheme to the active brand mode so the native widget matches the themed box. (Light + outdoor are both
   light schemes; only dark — incl dark-outdoor, which keeps the .dark class — flips.)
   The BARE .kf-input/.kf-select/.kf-textarea classes are included (not only the .kl-shell-main-scoped element
   selectors) so the GENERIC FieldWidget controls (record-entry's startAt/endAt datetime-local + every other
   FormRenderer page) carry the scheme even if a future host renders the form outside .kl-shell-main — this was
   the record-entry "raw white date input on dark/outdoor" finding. Harmless on text inputs (color-scheme only
   touches native UA chrome: number spinners, the <select> popup, scrollbars), and correct for consistency. */
.kf-input, .kf-select, .kf-textarea, .kf-checkbox,
.kl-shell-main input[type="date"],
.kl-shell-main input[type="datetime-local"],
.kl-shell-main input[type="time"],
.kl-shell-main input[type="checkbox"] { color-scheme: light; }
html.dark .kf-input, html.dark .kf-select, html.dark .kf-textarea, html.dark .kf-checkbox,
html.dark .kl-shell-main input[type="date"],
html.dark .kl-shell-main input[type="datetime-local"],
html.dark .kl-shell-main input[type="time"],
html.dark .kl-shell-main input[type="checkbox"] { color-scheme: dark; }

/* ---- 5b-combo. Lookup COMBOBOX brand skin (structure is in form-renderer.css) ----
   The pick-or-write control that replaces the native <select> Dropdown. The visible BOX is the wrapper
   (.kf-combobox-control); the inner <input> is a borderless reset (it would otherwise be boxed by the generic
   input rule above — hence the specificity-winning override). Colours/surfaces compose from --k-* so the control
   auto-flips light/dark/outdoor with no per-mode rule needed; the chevron uses the muted text token. */
.kf-combobox-control {
  background:var(--k-surface);
  border:1px solid var(--k-outline);
  border-radius:var(--k-radius-sm);
  color:var(--k-text);
  transition:border-color var(--k-dur-1) var(--k-ease), box-shadow var(--k-dur-1) var(--k-ease);
}
.kf-combobox-control:focus-within {
  border-color:var(--k-accent);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--k-accent) 22%, transparent);
}
.kf-combobox--disabled .kf-combobox-control {
  background:var(--k-surface-2); color:var(--k-text-muted); cursor:not-allowed;
}
/* Reset the inner input: defeat the generic .kl-shell-main input[type="text"] box (0,2,1) so the wrapper is the box. */
/* ⚠ This rule is (0,3,1) and form-renderer.css's `.kf-combobox-input` is (0,1,0), so THIS is the min-block-size the
   browser actually uses. Editing only the structure sheet changes nothing — that is why the height fix on
   2026-08-10 had to touch both files, and why the two values are written from the same token rather than as two
   copies of a number that can drift apart silently. */
.kl-shell-main .kf-combobox-control input.kf-combobox-input,
.kf-combobox-control input.kf-combobox-input {
  appearance:none; border:0; background:transparent; box-shadow:none;
  min-block-size:calc(var(--k-touch) - 2px); padding:0; color:var(--k-text); font-size:15px;
}
.kl-shell-main .kf-combobox-control input.kf-combobox-input:focus,
.kf-combobox-control input.kf-combobox-input:focus {
  outline:none; border:0; box-shadow:none;
}
.kf-combobox-input::placeholder { color:var(--k-text-muted); }
.kf-combobox-chevron { color:var(--k-text-muted); }

/* selected-value chips (multi) — a quiet accent pill that flips with the tokens. */
.kf-chip {
  background:color-mix(in srgb, var(--k-accent) 14%, var(--k-surface));
  border:1px solid color-mix(in srgb, var(--k-accent) 30%, var(--k-border));
  color:var(--k-text);
}
.kf-chip-remove { color:var(--k-text-muted); }
.kf-chip-remove:hover { color:var(--k-text); }

/* the popover listbox + options. */
.kf-combobox-listbox {
  background:var(--k-surface);
  border:1px solid var(--k-border);
  box-shadow:var(--k-elev-3);
  color:var(--k-text);
}
.kf-combobox-option:hover,
.kf-combobox-option.is-active {
  background:color-mix(in srgb, var(--k-accent) 14%, var(--k-surface));
}
.kf-combobox-option.is-selected { font-weight:600; }
.kf-combobox-option-hint { color:var(--k-text-muted); }
.kf-combobox-empty { color:var(--k-text-muted); }

/* the blur add-confirm popup. */
.kf-combobox-addpopup {
  background:var(--k-surface);
  border:1px solid var(--k-border);
  box-shadow:var(--k-elev-3);
  color:var(--k-text);
}
.kf-combobox-addpopup-suggest-head { color:var(--k-text-muted); }
.kf-combobox-useexisting {
  background:var(--k-surface-2);
  color:var(--k-text);
  border:1px solid var(--k-border);
}
.kf-combobox-useexisting:hover { background:color-mix(in srgb, var(--k-accent) 14%, var(--k-surface)); }
.kf-combobox-useexisting-action { color:var(--k-accent); }
.kf-combobox-addpopup-advtoggle { color:var(--k-accent); }
.kf-combobox-addpopup-warn { color:var(--k-critical); }
.kf-combobox-proposed-note { color:var(--k-text-muted); }
/* D54 item 5 - the empty-vocabulary note reads as guidance, not as an error: an empty list is a configuration
   fact, and painting it in the critical ink would tell a reader the page is broken when it is not.
   The `.kl-shell-main` prefix is REQUIRED, not decoration - the same contest `.kl-muted` documents further down.
   The note is a <p>, and `.kl-shell-main p { color:var(--k-text) }` is (0,1,1), so a bare `.kf-vocab-emptynote` at
   (0,1,0) LOSES inside the shell, which is every page that renders this note. Measured live before it was written
   this way: the note computed to rgb(14,34,54) (full body ink) with the bare rule matching and losing. Both heads
   render <main class="kl-shell-main">, so the prefixed rule reaches the field head too; the bare pair stays for
   any surface outside the shell. The scope NAME keeps full ink - it is the one word the sentence turns on. */
.kf-vocab-emptynote { color:var(--k-text-muted); }
.kl-shell-main .kf-vocab-emptynote { color:var(--k-text-muted); }
.kf-vocab-emptynote-scope,
.kl-shell-main .kf-vocab-emptynote .kf-vocab-emptynote-scope { color:var(--k-text); }
.kf-combobox-addpopup-cancel {
  background:var(--k-surface); color:var(--k-text); border:1px solid var(--k-border); border-radius:var(--k-radius-sm);
  min-block-size:var(--k-touch); padding:0 var(--k-space-4); cursor:pointer; font:inherit; font-weight:600;
}
.kf-combobox-addpopup-confirm {
  background:var(--k-accent); color:var(--k-on-accent); border:1px solid var(--k-accent); border-radius:var(--k-radius-sm);
  min-block-size:var(--k-touch); padding:0 var(--k-space-4); cursor:pointer; font:inherit; font-weight:600;
}
.kf-combobox-addpopup-confirm:disabled { opacity:.55; cursor:not-allowed; }

/* read-only field value display (kl-*-readonly / -value) */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-readonly"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-value"] {
  display:inline-block; padding-block:var(--k-space-1);
  color:var(--k-text); font-size:15px;
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-readonly-note"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-note"] { font-size:13px; color:var(--k-text-muted); }

/* Owner ruling 2026-08-10 — the SHARED-CASE-NUMBER flag (Approvals + RecordList). It takes its whole visual
   register from the -note family directly above (13px, muted): a flag, never an error, because filing two records
   against one case is legitimate police work.
   These two declarations are the only thing it adds, and both were MEASURED, not guessed. At 393px the registry's
   case-number cell is a card row carrying white-space:nowrap, so an inline flag extended the line to 514px and gave
   the whole page a horizontal scrollbar (measured: scrollWidth 514 with the flag, 393 with it hidden). Block puts it
   on its own line under the number it is about; normal lets it wrap there. Desktop is unaffected - the cell is wider
   than the text at every table width. */
.kl-shell-main .kl-case-shared-note { display:block; white-space:normal; }

/* The muted-text helper. 98 usages across 39 files had NO rule anywhere in the shared surface — the five
   definitions that existed were all in Kanine.Web page-scoped .razor.css, so `kl-muted` was a dead class on BOTH
   heads: author-labelled muted text rendered at full body colour on /tenant-settings, /admin/settings-hub,
   /users-roles and /onboarding/first-k9, and in nine Kanine.Maui components. Found 2026-07-24 by the MAUI
   undefined-class guard (MauiUndefinedCssClassGuardTests) on its first run.
   The `.kl-shell-main` prefix is REQUIRED, not decoration: `.kl-shell-main p` above sets color:var(--k-text) at
   (0,1,1), so a bare `.kl-muted` at (0,1,0) LOSES on every <p> — which is most usages — and would have been
   completely inert. This matches the -note idiom directly above: same shape, same (0,2,0) specificity, same token.
   Colour only — the -note treatment also sets font-size, and applying that here would be a second, unmeasured
   shift. Both heads render <main class="kl-shell-main"> (web AuthLayout/MainLayout, MAUI MainLayout + GateLayout),
   so this reaches the field head too. */
.kl-shell-main .kl-muted { color:var(--k-text-muted); }

/* field rows that should grid two-up on wider screens */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-grid"] {
  display:grid; gap:var(--k-space-4);
  grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));
  margin-block-end:var(--k-space-4);
}

/* ---- 5c. Buttons ----------------------------------------------------------
   LAYOUT + touch-target on every button shape so each meets the 48px WCAG-2.5.8
   target: stem-at-start (kf-btn-attach, kf-btn-create, kl-btn-confirm) via
   [class*="kf-btn"] / [class*="kl-btn"], AND stem-at-end (kl-more-btn,
   kl-ee-add-btn, kl-dl-export-btn) via :where([class^="kl-"],[class*=" kl-"])[class$="-btn"]. APPEARANCE is set at LOW
   specificity (:where(), 0 weight) so the colour variants below — plain single-
   class selectors — always win by source order, no specificity war.

   UI-GUAR-012 (button hygiene): the BROAD substring/suffix selectors are ELEMENT-GATED to a
   real interactive element via a zero-weight `:where(button,a,input,label,summary,
   [role="button"])` prefix. A button is therefore styled ONLY when it is an actual
   button/anchor/file-label — never a wrapper <div>/<section> or an icon <span> that
   merely ends in a magic suffix (e.g. `ki-ms-add`, the `kl-delete` container, a
   `kl-*-confirm` panel). This kills the broad-substring/suffix over-capture that
   painted two concentric borders (a `.kl-delete` div around a `.kl-action-delete`
   button) and gave glyph spans button chrome. The gate uses `:where()` (0,0,0) so
   EVERY rule keeps its original specificity exactly — the gate only narrows WHICH
   elements match, never the colour-variant cascade order below.
   The opt-in `.kl-btn`/`.kf-btn` classes stay UN-scoped (global, as before) so a
   button rendered OUTSIDE `.kl-shell-main` (dev-login, the error page) keeps its
   layout; those are an explicit opt-in vocabulary placed only on real buttons, never
   a broad capture, so they need no element gate. */
.kl-btn, .kf-btn,
.kl-shell-main :where(button, a, input, label, summary, [role="button"]):where(
  [class*="kf-btn"], [class*="kl-btn"],
  :where([class^="kl-"],[class*=" kl-"])[class$="-save"], :where([class^="kl-"],[class*=" kl-"])[class$="-cancel"], :where([class^="kl-"],[class*=" kl-"])[class$="-add"], :where([class^="kl-"],[class*=" kl-"])[class$="-delete"],
  :where([class^="kl-"],[class*=" kl-"])[class$="-confirm"], :where([class^="kl-"],[class*=" kl-"])[class$="-btn"], :where([class^="kl-"],[class*=" kl-"])[class$="-use-template"]) {
  display:inline-flex; align-items:center; justify-content:center; gap:6px;
  min-block-size:var(--k-touch); min-inline-size:var(--k-touch);
  padding:0 var(--k-space-4);
  border-radius:var(--k-radius-md);
  font:inherit; font-size:15px; font-weight:600; line-height:1;
  cursor:pointer; text-decoration:none;
  transition:background var(--k-dur-1) var(--k-ease), color var(--k-dur-1) var(--k-ease),
             border-color var(--k-dur-1) var(--k-ease), transform var(--k-dur-1) var(--k-ease);
}
/* neutral DEFAULT appearance — :where keeps specificity at 0,1,0 so a themed
   variant (0,1,0, later in source) overrides it; an un-themed action button is
   still a clear, tappable, on-brand affordance rather than a UA-grey default.
   Element-gated by the leading :where(button,a,…) (0,0,0) so only real buttons get it. */
.kl-shell-main :where(button, a, input, label, summary, [role="button"]):where([class*="kf-btn"], [class*="kl-btn"], :where([class^="kl-"],[class*=" kl-"])[class$="-btn"], :where([class^="kl-"],[class*=" kl-"])[class$="-save"],
  :where([class^="kl-"],[class*=" kl-"])[class$="-cancel"], :where([class^="kl-"],[class*=" kl-"])[class$="-add"], :where([class^="kl-"],[class*=" kl-"])[class$="-delete"], :where([class^="kl-"],[class*=" kl-"])[class$="-confirm"], :where([class^="kl-"],[class*=" kl-"])[class$="-use-template"]) {
  background:var(--k-surface); color:var(--k-text); border:1px solid var(--k-border);
}
.kl-shell-main :where(button, a, input, label, summary, [role="button"]):where([class*="kf-btn"], [class*="kl-btn"], :where([class^="kl-"],[class*=" kl-"])[class$="-btn"]):hover { background:var(--k-surface-2); }
.kl-btn:active, .kf-btn:active,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-save"]:active,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-confirm"]:active { transform:translateY(1px); }

/* PRIMARY (navy-on-teal, never white text on teal) — 0,1,0 / 0,2,0, beats the
   :where default by source order. The scoped [class$=…] members are 0,2,0. */
.kl-btn--primary, .kf-btn--primary, .kl-btn-primary, .kl-btn-confirm,
.kf-btn-create, .kf-btn-create-from-track,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-save"],
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-confirm"],
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-use-template"],
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-add"] {
  background:var(--k-teal-500); color:var(--k-ink-900); border:1px solid transparent;
}
.kl-btn--primary:hover, .kf-btn--primary:hover, .kl-btn-confirm:hover,
.kf-btn-create:hover, .kf-btn-create-from-track:hover,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-save"]:hover,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-add"]:hover { background:var(--k-teal-400); }

/* GHOST / secondary (cancel) */
.kl-btn--ghost, .kf-btn--ghost, .kl-btn-cancel,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-cancel"] {
  background:transparent; color:var(--k-text); border:1px solid var(--k-border);
}
.kl-btn--ghost:hover, .kf-btn--ghost:hover, .kl-btn-cancel:hover,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-cancel"]:hover { background:var(--k-surface-2); }

/* SECONDARY (tonal) — a weighted-but-not-primary action that must sit ABOVE ghost in the hierarchy without
   competing with the single teal primary (e.g. record-entry's "Save (Draft)" beside the lone "Create record"
   primary). A quiet filled chip on surface-2 with a hairline; hover lifts to a faint accent wash. Brand-token
   only (no raw hex / gradient), so the R11 canvas/token teeth hold. Defined ABOVE the navy `.kl-btn:not([--])`
   default — and it carries `--`, so that default never matches it. */
.kl-btn--secondary, .kf-btn--secondary {
  background:var(--k-surface-2); color:var(--k-text); border:1px solid var(--k-outline);
}
.kl-btn--secondary:hover, .kf-btn--secondary:hover {
  background:color-mix(in srgb, var(--k-accent) 10%, var(--k-surface-2)); border-color:var(--k-accent);
}

/* plain kl-btn default = navy (dev-login personas, the shell sign-out). Scope to the UN-modified kl-btn (no `--`
   variant) so kl-btn--primary / --ghost / --danger / --seal — defined ABOVE in source — actually win instead of
   being overridden by this later, equal-specificity rule (the bug that flattened every button to navy + made the
   disabled ghost read as a grey slab). */
.kl-btn:not([class*="--"]) { background:var(--k-ink-700); color:#fff; border:1px solid transparent; }
.kl-btn:not([class*="--"]):hover { background:var(--k-ink-500); }

/* DANGER / delete */
.kl-btn--danger,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-delete"] {
  background:transparent; color:var(--k-critical);
  border:1px solid color-mix(in srgb, var(--k-critical) 40%, transparent);
}
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-delete"]:hover { background:color-mix(in srgb, var(--k-critical) 10%, transparent); }

/* SEAL (sign-to-seal signature flow) */
.kl-btn--seal, .kf-btn-seal, .kf-btn-seal-init {
  background:var(--k-ink-800); color:#fff; border:1px solid transparent;
}
/* …and its HOVER, which was the one filled variant that never declared one. Every other variant does
   (--primary → teal-400, --ghost/--cancel → surface-2, --secondary → accent wash, the un-modified .kl-btn →
   ink-500), so seal alone fell through to the generic `…[class*="kl-btn"]:hover { background:var(--k-surface-2) }`
   a few rules above — which repainted a NAVY button #EEF2F7 while `color:#fff` stayed, i.e. white text on
   near-white at about 1.1:1. The button did not highlight on hover; it vanished.

   FOUND BY FILMING (B5 owner review 2026-07-29: "the request scope override button becomes visible, it wasn't").
   The page had scrolled under a parked pointer, so /users-roles' full-width "Request scope override" landed under
   the cursor and blanked out for four seconds — and read on camera as a control popping into existence when the
   pointer finally moved off it. The harness half of that is fixed too (Story.ParkPointerAsync now releases the
   real hover whenever the drawn cursor is hidden), but the illegible hover was never a filming problem: it is
   what every user sees the moment they point at a seal button anywhere in the product.

   ink-700 keeps the fixed white foreground at >7:1 and reads as the same button, lit — which is what a hover is
   for. Equal specificity (0,2,0) with the generic rule, won on source order, exactly as --ghost's hover is. */
.kl-btn--seal:hover, .kf-btn-seal:hover, .kf-btn-seal-init:hover {
  background:var(--k-ink-700);
}

.kl-shell-main button:disabled,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-save"]:disabled,
.kl-shell-main :where(button, a, input, label, summary, [role="button"])[class$="-confirm"]:disabled { opacity:.55; cursor:not-allowed; }

/* Dark: a disabled PRIMARY at bare .55 opacity still reads as a saturated, enabled teal fill (the notifications
   "Mark all as read" finding) — wash the fill into the surface + mute the ink so the disabled state is legible in
   dark (E.20). Covers every primary member (class variants + the -save/-confirm/-add suffix magic) app-wide. */
html.dark .kl-btn--primary:disabled, html.dark .kf-btn--primary:disabled,
html.dark .kl-btn-primary:disabled, html.dark .kl-btn-confirm:disabled,
html.dark .kf-btn-create:disabled, html.dark .kf-btn-create-from-track:disabled,
html.dark .kl-shell-main :where(button, input, [role="button"])[class$="-save"]:disabled,
html.dark .kl-shell-main :where(button, input, [role="button"])[class$="-confirm"]:disabled,
html.dark .kl-shell-main :where(button, input, [role="button"])[class$="-add"]:disabled {
  background:color-mix(in srgb, var(--k-teal-500) 28%, var(--k-surface));
  color:var(--k-text-muted);
  opacity:1;
}

/* ---- 5c-k. THE ROW "MORE ACTIONS" KEBAB — one definition, for every page with rows of items -----------------

   OWNER DEFECT, 2026-08-06, watching film A20 at 1:25: "the more actions button on intro of the schedule
   definitions does not look like the more actions buttons the other pages with rows of items. It has a large box
   around it. Can we just make sure it looks like the rest of the app's more button."

   THE STANDARD. The row kebab is the trigger inside the shared RowContextMenu primitive (UI-GUAR-026), which
   fourteen registry pages mount. Its anatomy is two elements: an unstyled inline-flex WRAPPER, and a BORDERLESS
   icon target — transparent fill, a transparent 1px ring kept only so hover and focus can colour it without moving
   layout, a muted glyph, and a full --k-touch square. Nothing in it reads as a button box, because the kebab is an
   icon and the ROW is the object.

   WHAT /scheduled-reports DREW INSTEAD, AND THE MECHANISM. That page hand-rolled the control rather than mounting
   the primitive, and it named the wrapper `kl-sr-menu`. Section 5l's floating-popover family selector is
   `[class$="-menu"]`, which keys on the END OF THE CLASS ATTRIBUTE — so the WRAPPER matched it and was painted as
   a menu PANEL: min-inline-size 12rem, an opaque --k-surface fill, `border:1px solid var(--k-border)`,
   a radius and an --k-elev-3 shadow. A 192px bordered, shadowed card was therefore drawn around a 32px glyph, on
   every row, on one page in the product. That is the large box, and it also set the width of the actions column —
   an earlier pass measured that column at ~202px and capped the BUTTON, which could not reach it, because the
   width was never the button's.

   THE FIX IS TO USE THE STANDARD, NOT TO PATCH THE COPY. /scheduled-reports now names the same two classes the
   primitive names, so there is one look with one definition instead of a second look that must be kept in step.

   THESE RULES MOVED HERE OUT OF RowContextMenu.razor.css, and the move is what makes that possible. A scoped
   .razor.css only reaches elements carrying that component's [b-…] attribute, so a second page could not adopt the
   look by naming the class — it could only COPY the declarations, which forks a shared style into two sheets that
   then drift. Held here, in the single shared client sheet both heads load from _content/, the definition exists
   ONCE and every consumer resolves the same chrome. RowContextMenu keeps its own scoped sheet for the parts that
   are the component's own: the panel, the items and the reason block. */
.kl-rowmenu-wrap {
  display:inline-flex; align-items:center;
}

.kl-rowmenu-kebab {
  display:inline-flex; align-items:center; justify-content:center;
  min-block-size:var(--k-touch, 44px); min-inline-size:var(--k-touch, 44px);
  padding:0;
  background:transparent;
  border:1px solid transparent;
  border-radius:var(--k-radius-md, 8px);
  color:var(--k-text-muted, #64748b);
  font:inherit; font-size:18px; font-weight:400; line-height:1;
  cursor:pointer;
  transition:background var(--k-dur-1, 120ms) var(--k-ease, ease), color var(--k-dur-1, 120ms) var(--k-ease, ease);
}

.kl-rowmenu-kebab:hover {
  background:var(--k-surface-2, #f1f5f9);
  color:var(--k-text, #0f172a);
}

.kl-rowmenu-kebab:focus-visible {
  outline:2px solid var(--k-accent, #0d9488);
  outline-offset:2px;
}

/* ---- 5c-k2. THE KEBAB'S TARGET, WHEN A PAGE COMPACTS THE BUTTON (A6 wave 3, 2026-09-13) ---------------------

   MEASURED, all fifteen mount sites of RowContextMenu, at 1400x900 and at 393x852, as a continuous
   `document.elementFromPoint` hit span walked in 2px steps after `scrollIntoView({block:'center'})`:

     /records 19, /approvals 3, /audit 26, /dogs 9, /master-names 17, /lookups 1319, /certifications 10,
     /medical 12, /daily-log 20, /expenses 25, /users-roles 13   → box 48, hit 48-50. Compliant.
     /vehicles 7 and /training-aids 9                            → box 30, hit 30-32. NOT compliant.
     /feedback                                                   → 0 rows in this seed.

   So the kebab is ONE shared control with one standard, and exactly two pages step out of it. Both do it for
   DESK ROW DENSITY, and both record an owner request for that density: Vehicles.razor.css "Compact roster
   density (owner request 2026-07-01)" and TrainingAids.razor.css "Dense registry rows (owner §3)".

   WHY A BAND AND NOT A FLOOR. Raising the BOX reverses those two requests: measured live, forcing
   `min-block-size:var(--k-touch)` takes /training-aids from 494px of tbody to 620px (+14px per row) and /vehicles
   from 328 to 426. A band costs nothing and keeps the density. §8b-m already reached the same conclusion for
   the phone card, where the slot reserves `--k-touch` anyway.

   THE BAND WAS SAMPLED BEFORE IT WAS WRITTEN, on both pages, at both widths. `--k-touch` is 48px here, so on a
   30px button the band overhangs 9px above and 9px below. What that 9px lands in is the row's own
   `td.kl-tcard-actions` padding — at every sample point on /training-aids, whose rows measure 54.5px and give
   it 12.5px of clearance (28px at 393x852).

   ⚠ IT DID NOT FIT ON /vehicles, AND ARITHMETIC IS HOW THAT WAS ALMOST MISSED. Those rows are 47px, so the
   clearance is 8.5px against a 9px overhang, and an OVERLAP-AWARE probe found the band crossing 0.9px into the
   next row's `tr.kl-row-nav` on six of seven rows. A naive "nearest control above / nearest control below" scan
   reports both neighbours as clear, because it only considers controls whose rects do not overlap the band's
   own row — so it cannot see a 0.9px crossing at all. Vehicles.razor.css now floors that row's action cell at
   one touch unit, which makes the clearance exactly equal to the overhang and the containment structural
   rather than lucky. THE INVARIANT A FUTURE PAGE MUST HOLD: if you compact this kebab, your row must still be
   at least `--k-touch` tall.

   WHAT THE BAND DOES TAKE is the 9px of the row's OWN actions cell above and below the glyph, which used to
   fall through to `tr.kl-row-nav` (open the row). Inside an actions cell, "open this row's menu" is the better
   of those two answers, and it is the same trade §8b-m accepted on the card.

   IT IS ONE RULE FOR ALL FIFTEEN SITES ON PURPOSE. On the thirteen compliant pages the button is 48x48 and the
   band is 44x44, so the band is strictly INSIDE the control and changes nothing that can be measured — it
   cannot overlay anything, because it is smaller than the thing it sits on. A sixteenth registry that compacts
   its kebab tomorrow keeps its target without anyone noticing it needed one.

   `inset-inline` is centred, not 0: the button is alone in its column on both pages (11rem on /training-aids),
   so the 7px each side lands in empty cell, and a target that is 44 in one axis and 30 in the other is not the
   thing WCAG 2.5.5 asks for. `position:relative` on the button is the positioning context and moves nothing. */
.kl-rowmenu-kebab { position:relative; }
.kl-rowmenu-kebab::after {
  content:"";
  position:absolute;
  inset-block:calc((100% - var(--k-touch, 44px)) / 2);
  inset-inline:calc((100% - var(--k-touch, 44px)) / 2);
}

/* ---- 5c-k3. THE RECORD-BUILDER TEXT LINKS — one target for three deliberate copies (A6 wave 3) -------------

   `.kl-rb-link` is the "Set as default K9" / "Set as default supervisor" / "Link this record to a scheduled
   event" affordance. Its LOOK is declared three times on purpose, and that arrangement is documented in
   RecordEnvelopeFieldsSection.razor.css: Blazor CSS isolation stamps a per-component scope, so
   `.kl-rb-envelope .kl-rb-link`, `.kl-rb .kl-rb-link` and `.kl-dl-dialog .kl-rb-link` each reach only their own
   markup. Adding the TARGET to all three would be a fourth copy to keep in step, and this shared sheet is
   loaded by every one of those markups, so the target is declared here ONCE instead.

   MEASURED at 1400x900: box 19.5, hit 20, on all three build-surface sites.

   THE BAND HANGS DOWNWARD ONLY, and that is not a per-site convenience — it is what the shape guarantees. This
   link always sits UNDER the control it modifies (a "set this as your default" link under its own <select>),
   which is the cluster-F reasoning of wave 2 read the other way round: the direction that is occupied at every
   site is UP, so the direction that is free at every site is DOWN. Sampled: `meta-k9` ends 8px above
   `k9-set-default` and `meta-supervisor` ends 8px above `supervisor-set-default`, so a centred or upward band
   would reach 4px into a select on both. Below, the nearest control is 52.9px away on both, and 191.8px away
   on `link-scheduled-event`; a downward band reaches 24.5px, so it clears the next control by 28px at worst.

   `inset-block-start:0` pins the band's TOP to the link's own top edge, so no width and no copy length can ever
   let it climb into the select. Cost: zero — `.kl-rb-link` is `align-self:flex-start` in a flex column and the
   band is out of flow. */
.kl-shell-main .kl-rb-link { position:relative; }
.kl-shell-main .kl-rb-link::after {
  content:"";
  position:absolute;
  inset-block-start:0;
  inset-inline:0;
  block-size:var(--k-touch, 44px);
}

/* The weather field's "Enter weather manually" / "Re-check" buttons, same two-copy situation
   (RecordWeatherCaptureField.razor.css and RecordBuildSurface.razor.css both declare the look; the component
   mounts on /records/{id}/build and /records/quick-start). MEASURED box 24, hit 24 at 1400x900. Here the band
   is CENTRED, because both directions were sampled and both are clear: `rb-weather-clear` ends 34.4px above
   (a centred band reaches 10px above the link's top, so 24px short of it) and the next control is 224.9px
   below. */
.kl-shell-main .kl-weather-manual-link { position:relative; }
.kl-shell-main .kl-weather-manual-link::after {
  content:"";
  position:absolute;
  inset-block:calc((100% - var(--k-touch, 44px)) / 2);
  inset-inline:0;
}

/* ---- 5c-gate. THE DISABLED-CONTROL REASON (owner ruling 2026-08-10, ruled twice in one day) ------------------
   "A disabled control must state its own precondition, visibly — not in a `title`." Hover text does not exist on a
   phone and is invisible to touch, and a rotor user reaching the control gets a disabled button and nothing else.

   This is the ONE treatment for that sentence, app-wide, rendered by Components/ControlGateReason.razor from a
   Components/ControlGate value that is ALSO the source of the control's disabled state — so the note and the gate
   cannot drift. It lives in the SHARED sheet (never a page's .razor.css) precisely because it had already been
   solved five different ways on five surfaces, which is the shape §A1 exists to prevent.

   Quiet, not an alarm: this explains, it does not scold. It is `display:flex` so a long sentence wraps under itself
   rather than under the glyph. A page that needs a different BOX (the report builder's accent-tinted info panels)
   passes its own class; the higher specificity of a page-scoped rule wins, and only the layout differs. */
.kl-gate-reason {
  display:flex;
  align-items:flex-start;
  gap:var(--k-space-2);
  margin:0;
  color:var(--k-text-muted);
  font-size:13px;
  line-height:1.4;
}
.kl-gate-reason .kl-gate-reason-icon {
  flex:0 0 auto;
  line-height:1.4;
  color:var(--k-accent);
}
/* ⚠ THE <p> SPECIFICITY CONTEST, the same one §5b documents for `.kl-muted`. The note is a <p>, and
   `.kl-shell-main p { color: var(--k-text) }` is (0,1,1) — it BEATS a bare `.kl-gate-reason` at (0,1,0), so the
   muted ink above never lands inside the shell. Measured live 2026-08-10: computed rgb(14,34,54) where the rule
   says rgb(62,74,92). One shell-scoped line settles it, exactly as `.kl-shell-main .kl-muted` does. */
.kl-shell-main .kl-gate-reason { color:var(--k-text-muted); }

/* Outdoor mode drops muted inks to full contrast, exactly as the page-scoped guidance lines already do. */
html[data-outdoor="true"] .kl-gate-reason,
html[data-outdoor="true"] .kl-shell-main .kl-gate-reason { color:var(--k-text); }

/* ---- 5c-findcat. THE FIND-TYPE CATEGORY NARROWING NOTE (owner ruling 2026-08-10) -----------------------------
   Components/FindTypePicker.razor renders ONE line under the category control while — and only while — the find
   type list is actually narrowed: how many of the vocabulary survive, and that typing still searches all of them.

   IT LIVES HERE, NOT IN A PAGE'S `.razor.css`, for the reason §5c-gate's panel variant records one block below:
   the note is emitted by a SHARED component, so it never carries a host page's `b-xxxx` scope attribute and no
   page-scoped rule can reach it. It is also mounted by four surfaces across both heads (§A1) — one treatment.

   Not named `*-note` / `*-label` / `*-badge`: those are live `[class$="…"]` suffix families (trap T7) and would
   sweep this line into a treatment it does not want. `-count` matches no family in this sheet — checked. */
.kl-findcat-count {
  margin:var(--k-space-1) 0 0;
  color:var(--k-text-muted);
  font-size:12px;
  line-height:1.4;
}
/* The same <p> specificity contest as `.kl-gate-reason` above: `.kl-shell-main p` is (0,1,1) and beats a bare
   (0,1,0) class, so the muted ink would never land inside the shell. One shell-scoped line settles it. */
.kl-shell-main .kl-findcat-count { color:var(--k-text-muted); }
html[data-outdoor="true"] .kl-findcat-count,
html[data-outdoor="true"] .kl-shell-main .kl-findcat-count { color:var(--k-text); }

/* THE PANEL VARIANT — a calm accent-tinted info box rather than a bare line, for a surface where the sentence sits
   among dense controls and needs to read as a distinct answer (the report builder's step ④/⑤ guidance lines).

   ⚠ IT LIVES HERE, NOT IN A PAGE'S .razor.css, AND THAT IS THE WHOLE REASON THIS BLOCK EXISTS. Blazor stamps a
   component's scope attribute only on markup the component itself writes; the note is emitted by
   ControlGateReason, a DIFFERENT component, so it never carries the page's `b-xxxx` and NO page-scoped rule can
   reach it. Measured live on 2026-08-10: `.kl-rb-viz-hint` computed as the base flex line, with none of its
   padding, border or tint, because the selector simply did not match. Any treatment a shared note needs must be
   defined on the shared surface — a page class passed as `Class` is inert unless the class is defined here too.

   `display:block` (not the base flex) so the glyph, the sentence and a trailing jump link flow as ONE paragraph
   and wrap together, which is what the report builder's approved copy does. */
.kl-gate-reason--panel {
  display:block;
  margin:var(--k-space-2) 0 0;
  padding:var(--k-space-2) var(--k-space-3);
  border-radius:var(--k-radius-md, 8px);
  background:color-mix(in srgb, var(--k-accent) 10%, var(--k-surface));
  border:1px solid color-mix(in srgb, var(--k-accent) 30%, var(--k-surface));
  line-height:1.5;
}
.kl-gate-reason--panel .kl-gate-reason-icon { margin-inline-end:4px; vertical-align:-2px; }

/* A trailing jump link inside the sentence (ControlGateReason's ChildContent) — same reason as above: the page's
   own `.kl-linkbtn` cannot reach into a child component, so the shared note carries the treatment. */
.kl-gate-reason .kl-linkbtn {
  appearance:none;
  background:none;
  border:0;
  padding:0;
  font:inherit;
  color:var(--k-accent);
  font-weight:600;
  text-decoration:underline;
  cursor:pointer;
  /* A6 (measured 2026-09-13) — A BOX FLOOR, DELIBERATELY, AND NOT THE HOUSE EXPANDER.
     `intel-flag-gate-link` on /master-names/{id} measured a 36.4px box and a 38px continuous hit span at both
     393x852 and 1400x900. The control DIRECTLY above it is `intel-flag-btn`, a real 48px button, and the gap is
     ZERO: `div.kl-mastername-intel-actions` ends at y=604 and this note starts at y=604. An upward or centred
     `::after` band therefore lays an invisible surface over the very button the note explains. A gate note sits
     under the control it explains BY CONSTRUCTION, so that is true at every site of this shared component, not
     just this one — which is why the answer cannot be a band.
     A box floor cannot overlay anything: it grows the note and moves the following content down. The link is a
     blockified FLEX ITEM of `.kl-gate-reason` (align-items:flex-start), so the extra height is taken DOWNWARD and
     the underlined ink does not move off the sentence's first line.
     MEASURED COST. At 393x852 it is ZERO — the note is already 90.9px tall there and the link occupies only its
     top 36.4px, so the growth lands inside the note's own box. At 1400x900 the note grows 36.4 -> 48.
     THE BAND IT NOW OWNS was sampled before the change: directly below the link is the note's own empty column,
     and `intel-review-queue-toggle` (the next control) sits 16px clear below the note at 393 and moves down with
     it at 1400. Nothing is overlaid at any width. */
  min-block-size:var(--k-touch);
}
.kl-gate-reason .kl-linkbtn:hover { text-decoration:none; }

/* THE PANEL VARIANT NEEDS THE OTHER ANSWER, and this is why one class carries two idioms.
   `.kl-gate-reason--panel` is `display:block`, so its jump link is INLINE inside flowing prose (the report
   builder's "Pick a Group by in step 4" sits at the end of a wrapped sentence). `min-block-size` does nothing to
   an inline box, and forcing `display:inline-block` to make it bite would add 28.5px of dead space inside a
   tinted panel and stop the sentence wrapping through the link — which is exactly the shape §5c-gate's own
   comment says the approved copy wants.
   So the panel takes the house `::after` expander, and the band was sampled first. `rb-viz-hint-jump` measured a
   19.5px box and a 20px hit span; it is the LAST line inside a panel that runs from y=350.7 to y=460.8, with
   65.6px of panel above it and 25px below. A centred 48px band claims 402 to 450 and stays INSIDE the panel at
   both 393x852 and 1400x900. Nothing in that band is a control: above is the sentence's own last text line,
   below is the panel's padding. `inset-inline:0` keeps the band inside the link's own inline extent so it can
   never reach sideways into the sentence. */
.kl-gate-reason--panel .kl-linkbtn {
  position:relative;
  min-block-size:0;
}
.kl-gate-reason--panel .kl-linkbtn::after {
  content:"";
  position:absolute;
  inset-inline:0;
  inset-block-start:50%;
  block-size:var(--k-touch);
  transform:translateY(-50%);
}

/* ---- 5c-case. THE SHARED-CASE-NUMBER NOTE'S "Compare" LINK (A6, measured 2026-09-13) -------------------------
   `.kl-case-shared-note` is emitted by TWO pages — RecordList and Approvals — with IDENTICAL markup and no rule
   of its own (it rides the `[class$="-note"]` family for its type). A page-scoped `.razor.css` can only reach one
   of them, and copying the rule into both is the drift §A1 exists to stop, so the target is defined ONCE here.

   THE LINK IS INLINE PROSE, so the house `::after` expander is the right idiom and a box floor is not: flooring it
   would put a 48px line box in the middle of a wrapped two-line sentence and grow both registries' rows by ~30px
   per flagged row for no gain in reach.

   BEFORE: /records box 17 / hit 18 at 1400x900 and box 17 / hit 16 at 393x852; /approvals box 32 / hit 32.

   THE BAND, SAMPLED BEFORE THE RULE WAS WRITTEN, at the tightest site (/approvals 1400x900, where the note shares
   a cell with two other controls):
     · `approval-link`            t=432.6  b=464.6
     · `approval-record-summary`  t=466.6  b=484.6   (a <span>, not a control)
     · this link                  t=484.6  b=516.6   centre 500.6
     · the row ends at            b=528.6
   A CENTRED 48px band claims 476.6 to 524.6. Its top clears `approval-link` by 12px and its bottom stays 4px
   inside the row. It was checked upward-only as well (468.6 to 516.6, 4px of clearance) and centred has more
   room, so centred is what is written. On /records the band stays inside `td.kl-tcard-foot` at 1400 and inside
   the card's own trailing gap at 393; nothing in it is a control at either width, and a real press on
   `approval-link`, on the row-menu kebab and on the next card still lands on its own target.

   `inset-inline:0` pins the band to the link's own inline extent. The note is a horizontal sentence, so a band
   with an inline minimum would reach sideways over the words beside it — the same reason §5b-crumb floors the
   breadcrumb row by box instead. */
.kl-case-shared-note a {
  position:relative;
}
.kl-case-shared-note a::after {
  content:"";
  position:absolute;
  inset-inline:0;
  inset-block-start:50%;
  block-size:var(--k-touch);
  transform:translateY(-50%);
}

/* ---- 5d. Action bars (sticky top/bottom action surfaces) ------------------ */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-actionbar"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-bottombar"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-actions"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-actionbar"] {
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--k-space-3);
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-actionbar"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-bottombar"] {
  justify-content:space-between;
  padding:var(--k-space-3) 0;
  margin-block-end:var(--k-space-4);
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-actionbar"] { border-block-end:1px solid var(--k-border); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-bottombar"] { border-block-start:1px solid var(--k-border); margin-block:var(--k-space-6) 0; justify-content:flex-end; }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-actions"] { justify-content:flex-end; }

/* ---- 5d-w. The WIZARD FOOTER — owner report 2026-08-09 ("crowded at the left edge")
   `.kl-wizard-footer` had NO CSS RULE ANYWHERE IN THE REPO. Not a wrong rule — a MISSING one: the element was a bare
   block <footer>, so its buttons sat against the inline-start edge with no gap between them, on all four surfaces that
   use it. Why it slipped through is mechanical and worth naming: every neighbouring action surface above is matched by
   an attribute-SUFFIX family (`-actionbar`, `-bottombar`, `-actions`), and this class ends in `-footer`, which is not
   one of them. It LOOKED like it belonged to a styled family and belonged to none — trap T7 from the other direction
   (the usual bite is an element falling OUT of a family; this one never fell IN).

   Fixed HERE, once, in the shared RCL sheet rather than on the page the owner happened to be looking at: the defect is
   in the shared layer, and patching /dogs/new alone would manufacture exactly the per-page drift this review exists to
   remove. Written as an explicit class rather than by renaming the four call sites into the `-actions` family, because
   a suffix-keyed family silently drops an element the moment a modifier class is added — and one of the four
   (`kl-wizard-footer kl-ft-footer`) already carries a modifier.

   Deliberately minimal: flex + gap + inline-end anchoring, and nothing else. No border and no block padding, which
   `-actionbar`/`-bottombar` add — those would spend vertical space on four surfaces to answer a horizontal complaint
   (B1). `RecordFromTrack` already declares its own `.kl-ft-footer { display:flex; justify-content:flex-end }`; this
   AGREES with it, so that page renders identically either way and its page-scoped rule stays the specificity winner.

   The four surfaces: /dogs/new, /users-roles/new, /onboarding/regional (TWO footers — the identity sub-form's single
   Save, and the step's Save Progress & Exit + Save & Continue pair) and /records/new-from-track. /users-roles/new is
   left-crowded in exactly the same way and is fixed by the same rule.                                              */
.kl-shell-main .kl-wizard-footer {
  display:flex; flex-wrap:wrap; align-items:center;
  gap:var(--k-space-3);
  justify-content:flex-end;
}

/* ---- 5e. Identity / header / breadcrumb ----------------------------------- */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-identity"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-header"] {
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--k-space-2) var(--k-space-3);
  min-inline-size:0;
}

/* ---- 5e-a10.9. A COMPOSED ROW SHARES ONE CENTRE - and align-items cannot deliver that on its own.
   OWNER REPORT 2026-09-10, on /dogs/{id}/edit: "make the cancel and save changes button centered on
   editing:argo". MEASURED live at 1400: the h1 "Editing: Argo" centred at y=102, Cancel and Save at y=110.
   The row already declared display:flex and align-items:center above, and every declaration read correct.

   THE CAUSE IS NOT align-items. align-items centres MARGIN BOXES. The heading carried the global
   `.kl-shell-main h1 { margin:0 0 var(--k-space-4) }` trailing margin - 16px - so its border box sat exactly
   half of that, 8px, above the centre of its own margin box, while the buttons (margin 0) sat on it. 16 / 2 = 8,
   which is the reported number to the pixel. A trailing text margin inside a flex row is dead weight in EVERY
   axis: the row's own row-gap already owns the spacing if the line wraps, and nothing below the row can see it.

   This is the exact mechanism recorded in UI-REVIEW-DESIGN-PRINCIPLES.md section A10 clause 9, where three
   registries measured 32px out for the same reason, and it takes the same remedy: SUBTRACT the margin. Do not
   compensate with a second one. A nudge would have to be re-derived for every heading level and every font
   size, and it would be wrong the moment --k-space-4 changed.

   SCOPE. Only text elements that are DIRECT flex children of one of these two composed rows are touched. A card,
   a div, a button or a nested wrapper keeps every margin it had. Both families already declare align-items:center,
   so any block margin on a direct child was already unable to mean what it looked like it meant.

   SPECIFICITY, and it is load-bearing. `.kl-shell-main h1` is (0,1,1). The suffix compound is (0,2,0) but a
   :where() wrapper contributes ZERO, so the type selector must sit OUTSIDE any :where() or the h1 margin wins and
   this rule reads as if it did nothing. Written with :is(), which takes the specificity of its most specific
   argument, this lands at (0,2,1) and wins on both families.

   MEASURED after, live at 1400 on /dogs/{id}/edit: h1 110.0, Cancel 110.0, Save 110.0. */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-identity"] > :is(h1,h2,h3,h4,h5,h6,p),
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-header"] > :is(h1,h2,h3,h4,h5,h6,p) {
  margin-block: 0;
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-subject"] { font-family:var(--k-font-display); font-size:18px; font-weight:700; }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-breadcrumb"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-crumb"] { font-size:13px; color:var(--k-text-muted); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-crumb-sep"] { color:var(--k-outline); padding-inline:var(--k-space-1); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-back"] {
  display:inline-flex; align-items:center; gap:6px;
  color:var(--k-accent); text-decoration:none; font-size:14px; font-weight:600;
  min-block-size:var(--k-touch);
}
/* ID-token display (record GUIDs etc.) — exclude form controls so an *-id filter
   input (e.g. kl-audit-flt-entity-id) keeps the normal input treatment. */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-id"]:not(input):not(select):not(textarea) { font-family:var(--k-font-mono); font-size:13px; color:var(--k-text-muted); }

/* ---- 5f. Status chips (shape + text + colour, never colour alone) --------- */
/* UI-GUAR-027 (R2-16, ruling §H.1 #16) — CHIP-LEAF SCOPING. A status TINT must only ever paint a small
   badge/chip/pill LEAF, never bleed onto a structural surface (row/cell/card/panel/section). The page emits
   `data-status`/`data-cert-status` ON THE ROW (`<tr data-status="active">`, `<tr data-cert-status="CURRENT">`)
   purely as a hook for that row's OWN page-scoped descendant selector (e.g. Lookups `.kl-lookups-row[data-status]
   .kl-lookups-status-pill`); the row is NOT meant to carry the fill. A bare `.kl-shell-main [data-status="active"]`
   selector therefore HIJACKS the row and paints the whole `<tr>` green (the owner's exact Round-2 report). The
   fix: every status SHAPE + TINT selector is gated to a chip leaf — `.kl-chip` OR a class suffixed `-chip`/`-pill`/
   `-badge`/`-status` — AND structural container tags (tr/td/th/table/thead/tbody/p/div/section/article/ul/ol/li)
   are excluded so an over-matching suffix (e.g. a `<td class="kl-lookups-cell-status">` / `<p class="kl-dog-status">`)
   can never become a tinted pill. No page depends on the bare row-level token for its chip colour (every pill
   colours itself via `kl-chip--*` or a page-scoped `.razor.css`), so this tightening is non-breaking. */
/* `.kl-badge` IS a chip, and until 2026-07-25 it was not one anywhere but inside one component.
   THE TRAP, recorded because it is the general lesson: `.kl-badge` was declared ONLY in
   `Lifecycle/RecordStatusBadge.razor.css`, a Blazor-SCOPED sheet — its rules are rewritten with that
   component's own `[b-xxxx]` attribute, so they reach that component's markup and NOTHING else. The
   suffix families below could not cover for it either: they gate on a `-chip`/`-status` leaf, and the
   `-badge` alternative appears only inside a `:where(...)` GATE on the data-status TINTS, which colours
   a pill without ever making one. So the four other users — `SessionRevocationAdmin` (the client name),
   `SignInPolicyAdmin` (5 of them, incl. the live/blocked IdP status) and `CustomFields` (PII /
   Analyzable) — all rendered their badge as PLAIN TEXT. Aliasing the class onto the ONE chip primitive
   fixes all of them in one place; RecordStatusBadge is unaffected, because its scoped rule is (0,2,0)
   and still wins. */
.kl-chip, .kl-badge,
.kl-shell-main :is(:where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]):not(td):not(th):not(tr):not(table):not(thead):not(tbody):not(p):not(div):not(section):not(article):not(ul):not(ol):not(li) {
  display:inline-flex; align-items:center; gap:6px;
  padding:2px 10px; border-radius:var(--k-radius-full);
  font-size:12px; font-weight:600; line-height:1.5; border:1px solid;
  color:var(--k-gray-700); background:var(--k-gray-100); border-color:var(--k-gray-300);
}
/* UI-GUAR-027 (R2-16) — NEUTRAL-base dark remap. The NEUTRAL chip base above uses the FIXED --k-gray-100 fill /
   --k-gray-700 text (these do NOT re-point per mode), so a plain `.kl-chip` with no `--*` variant (e.g. the
   agency-dashboard / reliability metric-id pill `kl-chip kl-agency-tile-id`) keeps a LIGHT gray-100 fill in the dark
   field — an unreadable bright pill (caught by the live badge-contrast tooth). Re-map the neutral base to a dark
   NEUTRAL surface fill + light text (zero-saturation, restrained) so an un-variant chip is legible in dark +
   dark-outdoor, mirroring the `kl-chip--*` remaps below. Variant chips override this via their own dark remap. */
html.dark .kl-chip, html.dark .kl-badge,
html.dark .kl-shell-main :is(:where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]):not(td):not(th):not(tr):not(table):not(thead):not(tbody):not(p):not(div):not(section):not(article):not(ul):not(ol):not(li) {
  color:var(--k-text); background:var(--k-surface-3); border-color:var(--k-border);
}
/* The chip-leaf gate the derived data-* tints below reuse: the SAME element must also be a chip/badge/pill leaf
   (never a row/cell/card). `:where(...)` contributes ZERO specificity, so the tint keeps the same weight it had
   before (a single [data-*] attribute), but it can no longer match a structural surface. */
.kl-shell-main [data-cert-status="CURRENT"]:where(.kl-chip, :where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-pill"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]),
.kl-shell-main [data-status="active"]:where(.kl-chip, :where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-pill"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]),
.kl-chip--success {
  color:#0b5c3c; background:color-mix(in srgb,var(--k-success) 12%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-success) 35%,var(--k-surface));
}
.kl-shell-main [data-cert-status="PENDING"]:where(.kl-chip, :where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-pill"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]), .kl-chip--caution {
  color:#7a4a00; background:color-mix(in srgb,var(--k-caution) 14%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-caution) 38%,var(--k-surface));
}
.kl-shell-main [data-cert-status="EXPIRED"]:where(.kl-chip, :where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-pill"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]), .kl-chip--critical {
  color:#7a1b12; background:color-mix(in srgb,var(--k-critical) 12%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-critical) 35%,var(--k-surface));
}
.kl-chip--info { color:#0b4f8a; background:color-mix(in srgb,var(--k-info) 10%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-info) 32%,var(--k-surface)); }
/* The two `kl-badge--*` variants SignInPolicyAdmin emits (`Live` / the blocked reason) were declared
   NOWHERE, so they were dead beside a base that was itself dead. Aliased onto the chip variants above
   rather than re-declaring their colours — one palette, one place. */
.kl-badge--success { color:#0b5c3c; background:color-mix(in srgb,var(--k-success) 12%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-success) 35%,var(--k-surface)); }
html.dark .kl-badge--success { color:#9ce6c1; background:color-mix(in srgb,var(--k-success) 30%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-success) 55%,var(--k-surface)); }
.kl-badge--muted { color:var(--k-text-muted); background:var(--k-surface-2); border-color:var(--k-border); }

/* Dark-mode pill contrast (R11 design-skeptic BLOCKER): the fixed dark text colours above are near-invisible on the
   dark surface. Re-map to LIGHT text on a more-saturated fill so STATUS — load-bearing, evidentiary info — stays
   clearly legible in dark + dark-outdoor. (Light/outdoor keep the dark-on-tint above; high contrast on white.)
   The SAME chip-leaf gate as the light tints — a `<tr>`/cell is never painted in dark either. */
html.dark .kl-shell-main [data-cert-status="CURRENT"]:where(.kl-chip, :where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-pill"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]),
html.dark .kl-shell-main [data-status="active"]:where(.kl-chip, :where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-pill"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]),
html.dark .kl-chip--success {
  color:#9ce6c1; background:color-mix(in srgb,var(--k-success) 30%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-success) 55%,var(--k-surface));
}
html.dark .kl-shell-main [data-cert-status="PENDING"]:where(.kl-chip, :where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-pill"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]), html.dark .kl-chip--caution {
  color:#f4c884; background:color-mix(in srgb,var(--k-caution) 32%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-caution) 55%,var(--k-surface));
}
html.dark .kl-shell-main [data-cert-status="EXPIRED"]:where(.kl-chip, :where([class^="kl-"],[class*=" kl-"])[class$="-chip"], :where([class^="kl-"],[class*=" kl-"])[class$="-pill"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"], :where([class^="kl-"],[class*=" kl-"])[class$="-status"]), html.dark .kl-chip--critical {
  color:#f6ab9d; background:color-mix(in srgb,var(--k-critical) 30%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-critical) 55%,var(--k-surface));
}
html.dark .kl-chip--info {
  color:#aacdff; background:color-mix(in srgb,var(--k-info) 30%,var(--k-surface)); border-color:color-mix(in srgb,var(--k-info) 55%,var(--k-surface));
}

/* ---- 5g. Banners / notices ------------------------------------------------ */
/* `.kl-signin-policy-ok` is named here explicitly (2026-07-25). Its SIBLING, `kl-signin-policy-error`, ends
   in `-error` and so picks up the banner treatment from the suffix family below; the success half ends in
   `-ok`, matches no family, and was declared nowhere — so on /admin/sessions and /admin/sign-in-policy a
   FAILURE rendered as a bordered banner and the matching SUCCESS rendered as a bare line of body text. Found
   by `app/tools/page-review/unstyled-class-scan.mjs`; the tint comes from the `-valid`/success rule below. */
.kl-shell-main .kl-signin-policy-ok,
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-notice"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-banner"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-conflict"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-failure"] {
  display:flex; align-items:flex-start; gap:var(--k-space-2);
  padding:var(--k-space-3) var(--k-space-4); border-radius:var(--k-radius-md);
  border:1px solid var(--k-border); background:var(--k-surface-2);
  font-size:14px; line-height:1.5; margin-block-end:var(--k-space-4);
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-conflict"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-failure"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-error"] {
  color:#7a1b12; background:color-mix(in srgb,var(--k-critical) 10%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-critical) 38%,var(--k-surface));
}
/* Dark remap (mirrors the R11 critical-chip pattern at §5f): --k-critical does NOT flip, so the fixed dark-red
   #7a1b12 text above is invisible once --k-surface darkens the tinted box. Re-point to LIGHT critical text on a
   more-saturated fill so error/conflict/failure banners stay legible in dark + dark-outdoor. */
html.dark .kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-conflict"],
html.dark .kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-failure"],
html.dark .kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-error"] {
  color:#f6ab9d; background:color-mix(in srgb,var(--k-critical) 30%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-critical) 55%,var(--k-surface));
}

/* ---- 5g-1. OUT-OF-FLOW REFUSAL SLOTS on the record command surface (owner ruling 2026-07-27) ---------------
   Found by the owner watching the sign-and-seal film: when a signing attempt is refused, the step-up refusal
   banner appeared INSIDE the lifecycle action bar in normal flow. That grew the bar's intrinsic width (the
   banner's max-content is a full sentence) and added a flex line, so the shared command row re-wrapped and
   Delete + "Edit record" dropped onto a second line — the controls moved under the user's cursor at the exact
   moment they were told the action had failed. The same shape bit the sibling soft-delete refusal.

   THE GUARANTEE, and why it is this and not a reserved height: `position:absolute` removes the slot from flex
   layout entirely — it is not a flex item, so it contributes NOTHING to the row's intrinsic sizes, line count or
   gaps. The action row's geometry is therefore identical with and without a message unconditionally, rather than
   "as long as the message fits the reservation" (which a min-height reservation can only promise until someone
   ships a longer string or a narrower viewport). The slot is rendered UNCONDITIONALLY by
   LifecycleActionBar.razor / RecordDeleteAction.razor so the row's DOM is byte-identical in both states too, and
   so the role="alert" live region exists BEFORE its text arrives.

   `.kl-record` is the DEFAULT containing block: a host that has not chosen a home for the message still anchors
   it to the shared surface rather than to the viewport. The two record-review pages re-anchor it deliberately
   (web: the page header row, per the ruling; device: directly under the command bar) in their own .razor.css.
   `pointer-events` is dropped on the (usually empty) slot and restored on the banner, so an invisible box can
   never eat a click in whatever region the host parks it over.

   Plain descendant selectors — the :where() families above contribute ZERO specificity, so `[class$="-failure"]`
   lands at (0,2,0); `.kl-shell-main .kl-actionbar-message > .kl-failure` is (0,3,0) and reliably wins the
   margin reset regardless of source order. */
.kl-shell-main .kl-record { position: relative; }
.kl-shell-main .kl-actionbar-message,
.kl-shell-main .kl-deleteaction-message {
  position: absolute;
  inset-inline: auto 0;
  inset-block-end: calc(100% + var(--k-space-2));
  z-index: 4;
  inline-size: max-content;
  max-inline-size: min(46rem, 100%);
  pointer-events: none;
}
.kl-shell-main .kl-actionbar-message > .kl-failure,
.kl-shell-main .kl-deleteaction-message > .kl-failure {
  margin-block-end: 0;
  pointer-events: auto;
}

/* THE EXPLICIT .kl-banner FAMILY (final-draft foundation heal). ~10 pages compose
   `class="kl-banner kl-banner--success|--caution|--error"` (save-success readbacks, the scheduled-reports
   email-degrade band, seat-cap notices…). The suffix families above NEVER matched them: :where([class^="kl-"],[class*=" kl-"])[class$="…"] tests the
   FULL class-attribute suffix, and on those elements the attribute ends in "--success"/"--caution" — so the
   banners rendered completely UNSTYLED (no box, no tint). Explicit classes fix the whole family at once.
   Recipe = the canonical status tint (§5f): fill ≈12% into the flipping surface, border ≈40%, dark-status text;
   dark remap = light status text over 30%/55% fills. The text IS the meaning (never color alone). */
.kl-banner {
  display:flex; align-items:flex-start; gap:var(--k-space-2);
  padding:var(--k-space-3) var(--k-space-4); border-radius:var(--k-radius-md);
  border:1px solid var(--k-border); background:var(--k-surface-2);
  font-size:14px; line-height:1.5; margin-block-end:var(--k-space-4);
}
.kl-banner--success {
  color:#0b5c3c; background:color-mix(in srgb,var(--k-success) 12%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-success) 40%,var(--k-surface));
}
.kl-banner--caution {
  color:#7a4a00; background:color-mix(in srgb,var(--k-caution) 14%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-caution) 40%,var(--k-surface));
}
/* A BANNER THAT CARRIES A ROUTE TO THE FIX MUST BE ABLE TO WRAP (owner ruling 2026-09-09 — a surface that raises a
   problem carries the route to fixing it). `.kl-banner` above is a NOWRAP flex row, which is right for an icon plus a
   sentence and wrong the moment a var(--k-touch) control joins them: at 320px the sentence plus the button exceed the
   card and the page scrolls sideways, which is a hard constraint. Keyed on :has() so it reaches exactly the banners
   that gained an action and no others; :has() is already a dependency of this sheet (§8b-m, §5j-a). */
.kl-banner:has(> .kl-btn, > .kf-btn, > a[href]) { flex-wrap: wrap; }
.kl-banner--error, .kl-banner--critical {
  color:#7a1b12; background:color-mix(in srgb,var(--k-critical) 12%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-critical) 40%,var(--k-surface));
}
/* Canonical dark status palette (E.20): light status text, 30% fill / 55% border over the dark surface. */
html.dark .kl-banner--success {
  color:#9ce6c1; background:color-mix(in srgb,var(--k-success) 30%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-success) 55%,var(--k-surface));
}
html.dark .kl-banner--caution {
  color:#f4c884; background:color-mix(in srgb,var(--k-caution) 30%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-caution) 55%,var(--k-surface));
}
html.dark .kl-banner--error, html.dark .kl-banner--critical {
  color:#f6ab9d; background:color-mix(in srgb,var(--k-critical) 30%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-critical) 55%,var(--k-surface));
}
/* INFO member of the explicit family (the signup "check your email" band composes it; it previously
   fell back to the neutral base). Same recipe as .kl-chip--info at §5f. */
.kl-banner--info {
  color:#0b4f8a; background:color-mix(in srgb,var(--k-info) 10%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-info) 38%,var(--k-surface));
}
html.dark .kl-banner--info {
  color:#aacdff; background:color-mix(in srgb,var(--k-info) 30%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-info) 55%,var(--k-surface));
}

/* PENDING SIGNING REQUEST BANNER (owner test 2026-09-30). The whole banner is one link, so the tap target is the full
   row. It keeps the info tint of the banner family and drops the link underline. */
.kl-pendingsign-banner {
  align-items:center; text-decoration:none; min-block-size:var(--k-touch); cursor:pointer;
}
.kl-pendingsign-banner .kl-pendingsign-text { flex:1 1 auto; min-inline-size:0; }
.kl-pendingsign-banner .kl-pendingsign-open { flex:0 0 auto; font-weight:600; text-decoration:underline; }

/* ---- 5h. State kit (loading / empty / error / not-found / denied) --------- */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-state"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-empty"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-loading"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-denied"] {
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:var(--k-space-3); text-align:center;
  padding:var(--k-space-12) var(--k-space-6);
  color:var(--k-text-muted);
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-state-icon"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-state-message"] { color:var(--k-text-muted); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-state-icon"] { font-size:32px; opacity:.6; }
.kl-notfound, .kl-error {
  display:block; padding:var(--k-space-4);
  border-radius:var(--k-radius-md);
  /* Mix the critical tint into the FLIPPING surface token, not a literal #fff — otherwise the box stays a glaring
     near-white card on the dark canvas (the account-page "could not be loaded" eyesore). */
  color:#7a1b12; background:color-mix(in srgb,var(--k-critical) 10%,var(--k-surface));
  border:1px solid color-mix(in srgb,var(--k-critical) 35%,var(--k-surface));
}
/* Dark remap so the dark-red text doesn't vanish on the now-darkened tint (same R11 critical palette as above). */
html.dark .kl-notfound, html.dark .kl-error {
  color:#f6ab9d; background:color-mix(in srgb,var(--k-critical) 30%,var(--k-surface));
  border-color:color-mix(in srgb,var(--k-critical) 55%,var(--k-surface));
}
/* ---- 5h-a13. THE ROUTE A PROBLEM SCREEN CARRIES ---------------------------
   Owner ruling 2026-09-09 (§A13): a surface that raises a problem carries the route to fixing it. The row rendered
   by the shared <ProblemRoutes> component. ONE rule, defined ONCE, per §A1 — and deliberately NOT scoped to
   .kl-shell-main, because the same component renders on the signed-out /login, /signup and invitation screens,
   which are outside the shell. Start-aligned, because the control is the reader's way out and not a form's commit;
   wraps, because a var(--k-touch) control plus a long label must not overflow a 320px card (the same hard
   constraint the .kl-banner :has() rule above answers for a banner that gained an action).
   The class ends in "-routes", which no attribute-suffix family in this sheet matches — checked, not assumed
   (the [class$="-actions"] family at §5d would have right-aligned it, and only inside the shell). */
.kl-problem-routes {
  display:flex; flex-wrap:wrap; align-items:center; gap:var(--k-space-3);
  margin-block-start:var(--k-space-3);
}
/* A problem screen offers ONE route far more often than several. A lone button that stretches edge-to-edge on a
   phone reads as a form's submit; keep it intrinsic at every width. */
.kl-problem-routes > .kl-btn { flex:0 0 auto; }

/* ---- 5h-a13b. THE K96 PREVIEW REFUSAL'S TWO ROUTES ------------------------
   Owner ruling 2026-09-15: the route-ceiling refusal (MirrorRouteRefusal.razor, rendered by BOTH the
   /k96/not-in-preview page and the MirrorShellGate circuit guard) carries a second route, to the full product.

   WHY NOT .kl-problem-routes ABOVE. That row renders every route as a .kl-btn, ranked primary then ghost. Two
   buttons is exactly what the ruling does NOT want here: the way out must stay the only button on the screen, and
   the upgrade route must be available without competing for the eye with it. So the second route is a quiet text
   link, and this row is its own rule rather than a variant bolted onto a shape that means something else.

   W4 (a command bar never wraps) IS ENFORCED, NOT ASSERTED. flex-wrap is `nowrap` at every width, so these two
   can never break into a ragged second line the way a wrapping bar does. MEASURED on the review host, in the live
   DOM, at seven widths: at 393x852 the card's content box is 311px and the two on one row need 400px (a 189px
   button, the 16px row gap, a 195px link), so at phone width they genuinely do not fit. The degrade is therefore
   DELIBERATE and declared — an explicit column, start-aligned, the way out first — rather than a wrap that
   happens to look like one. 560 is the measured crossing point: the content box is 465px there, which takes the
   400px row with 65px to spare, and 540/559 still measure 447/464 and stay stacked.

   `-routes` and `-more` match no attribute-suffix family in this sheet — checked against every [class$=…] in the
   file, not assumed. ([class$="-actions"] at §5d would have right-aligned this row, and only inside the shell.)
   The LAYOUT is not scoped to .kl-shell-main: the refusal renders inside the shell on both layers today, but it
   is a terminal screen and the §5h-a13 row next to it already had to be unscoped for exactly that reason. */
.kl-refusal-routes {
  display:flex; flex-wrap:nowrap; flex-direction:column; align-items:flex-start;
  gap:var(--k-space-3); margin-block-start:var(--k-space-4);
}
@media (min-width: 560px) {
  .kl-refusal-routes { flex-direction:row; align-items:center; gap:var(--k-space-4); }
}
/* The way out keeps its intrinsic width at every breakpoint. A primary that stretches edge-to-edge on a phone
   reads as a form's submit — the same finding §5h-a13 records one rule above. */
.kl-refusal-routes > .kl-btn { flex:0 0 auto; }

/* The SECOND route: quiet by construction. No chrome, so it reads as a link and not as a rival button, while
   still holding the A6 var(--k-touch) floor because it is a real tap target. */
.kl-refusal-more {
  flex:0 0 auto;
  display:inline-flex; align-items:center; gap:6px;
  min-block-size:var(--k-touch);
  font-size:14px; font-weight:600; text-decoration:none;
}
/* THE INK IS STATED TWICE, AND NEITHER COPY IS A HACK. §1 line 245 paints every non-button link inside the shell
   `var(--k-accent)` at (0,2,1), which outranks a bare `.kl-refusal-more` (0,1,0) however late it appears — the
   link measured teal in the live DOM before this rule existed, which is the whole reason it does. A teal link
   beside the teal primary is the competition the owner ruling 2026-09-15 rules out, so the quiet ink is asserted
   at a specificity that can win honestly: the scoped copy reaches (0,3,1) and beats §1 outright rather than
   relying on source order, and the unscoped copy covers a render outside .kl-shell-main. No !important anywhere.

   THE :not([class*="-btn"]) IS THE SHEET'S STANDING RULE FOR AN ANCHOR COLOUR RULE, not decoration — an anchor
   colour that also claims button anchors is the AUTH-CTA-CONTRAST defect (accent text on an accent fill, 1:1),
   and AuthLinkRuleButtonExclusionGuardTests refuses any `.kl-shell-main a…{color}` rule without it. It is also
   the honest statement of intent here: if this link ever becomes a button, the button's own ink must win. */
a.kl-refusal-more:not([class*="-btn"]),
.kl-shell-main a.kl-refusal-more:not([class*="-btn"]) { color:var(--k-text-muted); }
a.kl-refusal-more:not([class*="-btn"]):hover,
.kl-shell-main a.kl-refusal-more:not([class*="-btn"]):hover { color:var(--k-accent); text-decoration:underline; }

.kl-valid { color:#0b5c3c; }
/* --k-success does not flip; the fixed dark-green is near-invisible on the dark dialog/card surface. */
html.dark .kl-valid { color:#9ce6c1; }

/* ---- 5i. Dialogs (the PANEL card inside the .kl-modal-overlay scrim) -------
   .kl-dialog is the modal PANEL, NOT the overlay: it is centered + scrimmed by
   its <KlOverlay> wrapper (.kl-modal-overlay above). It carries no positioning
   of its own — it is a plain card. The former `.kl-dialog::backdrop` rule is
   RETIRED (UI-GUAR-033): the app ships no native <dialog>/showModal, so a
   ::backdrop on the <div>/<form> these classes mark NEVER rendered — it gave a
   false sense of a scrim while leaving the modal scrimless. The real scrim is
   now the overlay element, always present, never a pseudo-element. */
.kl-dialog {
  border:0; padding:0; border-radius:var(--k-radius-lg);
  max-inline-size:30rem; inline-size:calc(100% - 2rem);
  background:var(--k-surface); color:var(--k-text); box-shadow:var(--k-elev-3);

  /* A tall dialog (the daily-log create form, with its ten fields, is taller than a phone) MUST scroll on touch.
     The overlay-scroll fix below relied on `align-items: safe center`, which does not reliably let a touch drag
     scroll the scrim in the Android WebView — so on a phone the dialog overflowed and could not be scrolled inside
     at all (reported 2026-07-24). The robust fix is to cap the DIALOG itself to the viewport and make IT the scroll
     container: this works on touch regardless of `safe` support. 100dvh (dynamic viewport height) accounts for the
     mobile browser/WebView chrome that 100vh ignores; the vh line is the fallback for engines without dvh.
     overscroll-behavior:contain stops the drag from chaining out to the scrim once the dialog hits its end. */
  max-block-size: calc(100vh - 2 * var(--k-space-4));
  max-block-size: calc(100dvh - 2 * var(--k-space-4));
  overflow-y: auto;
  overscroll-behavior: contain;

  /* THE CAP ABOVE MUST SIZE THE BORDER BOX, and this sheet ships no universal reset that makes it one (D48.1).
     Its only two box-sizing resets are `.kl-shell-main > *` and the form-control rule, and a modal panel is
     neither - so `max-block-size` capped the CONTENT box and every panel that adds its OWN padding-block or
     border rendered that much TALLER than the cap it declares. Measured 2026-09-13 across all 58 `.kl-dialog`
     sites at 1400x900, 393x852 and 780x360: 20 panels carried that debt, from 2px (a 1px border on
     ManifestReceipt and PacketBuilder) to 50px (`padding: var(--k-space-6)` plus a border on AuditView,
     OrgUnits, Reports and the reminders create-sheet), and the debt is a CONSTANT at every viewport because
     both terms are constants. Three panels had already bought the fix privately
     (ReliabilityDashboard/AdminOverview `.kl-drill-dialog`, Notifications `.kl-reminder-detail`); this makes it
     the rule instead of a per-page remedy.
     IT FIXES THE INLINE AXIS FOR THE SAME REASON. `max-inline-size:30rem` / `inline-size:calc(100% - 2rem)`
     were also content-box, so a padded panel rendered up to `padding-inline + border-inline` WIDER than the
     30rem it declares and ate into the 1rem the overlay reserves each side.
     A panel that wants its old CONTENT height keeps it by raising its own cap - never by going back to
     content-box, which reintroduces the overrun for every future panel. */
  box-sizing: border-box;
}
.kl-dialog > * { padding-inline:var(--k-space-6); }
.kl-dialog > :first-child { padding-block-start:var(--k-space-6); }
.kl-dialog-actions, .kl-reason-actions {
  display:flex; justify-content:flex-end; gap:var(--k-space-2);
  padding:var(--k-space-4) var(--k-space-6) var(--k-space-6);
}
.kl-reason { display:flex; flex-direction:column; gap:var(--k-space-2); padding:var(--k-space-4) var(--k-space-6); }

/* DEV-ONLY step-up autofill hint (SignatureStepUpDialog) — renders only when the development-gated IStepUpAutofill
   pre-fills the masked secret field, so a developer can see which persona phrase was filled. Never shown in
   production (no host supplies a PrefilledSecret there). A quiet dashed caution note, not a first-class control. */
.kl-stepup-devhint {
  display:flex; flex-wrap:wrap; align-items:baseline; gap:var(--k-space-2);
  margin:0 var(--k-space-6) 0; padding:var(--k-space-2) var(--k-space-3);
  border:1px dashed color-mix(in srgb,var(--k-caution) 55%,var(--k-surface)); border-radius:var(--k-radius-md);
  background:color-mix(in srgb,var(--k-caution) 8%,var(--k-surface)); font-size:0.85rem;
}
.kl-stepup-devhint-label { color:var(--k-text-muted); }
.kl-stepup-devhint-value { font-weight:600; user-select:all; }

/* ---- 5j. Menus / overflow (top-layer-safe popovers) ----------------------- */
/* The shared <PageActionsMenu> wrapper's DEFAULT class (owner ruling 2026-08-09). The panel is `position:absolute`
   (below), so SOMETHING must be its containing block; a page that adopts the component without naming its own
   wrapper gets this one. Declared here rather than left to each adopter because the failure mode is silent and ugly:
   with no positioned ancestor the dropdown escapes to the page corner. A page that ALREADY has a positioned actions
   row keeps anchoring the panel there and passes its own WrapClass instead — that is a per-page measurement, not a
   default (see the sweep note in PageActionsMenu.razor). */
.kl-page-overflow-wrap { position:relative; display:inline-flex; }
/* …and the panel's ANCHORING, for the same reason: the panel is the component's markup, so a page-scoped
   `inset-inline-end` never reaches it. Drop it under the trigger, right-aligned to the wrapper — the arrangement
   all fourteen hand-rolled copies independently wrote for themselves. `--k-touch` is the trigger's own height, so
   the offset tracks the touch floor instead of restating it as a magic number. */
.kl-page-overflow-wrap > [role="menu"] {
  inset-inline-end:0; inset-block-start:calc(var(--k-touch) + var(--k-space-1));
  /* UNIVERSAL CLAMP — no engine required. The panel can never be wider than the viewport's usable width, so a narrow
     phone cannot get a panel wider than its own screen. This alone does NOT fix the escape below; it bounds it. */
  max-inline-size:min(16rem, calc(100vw - (var(--k-space-4) * 2)));
}

/* ---- 5j-a. The off-screen dropdown (owner ruling 2026-08-09) ---------------
   THE DEFECT, measured before it was ruled: at 393px the master-name header's actions cluster wraps to the LEFT of
   its own line (wrapper inline-end at x=89), and a 202px panel right-aligned to it renders at `left: -113` — HALF
   OFF-SCREEN. `scrollWidth` does not grow, because inline-start overflow is clipped rather than scrollable in LTR,
   which is exactly why every horizontal-overflow sweep this repo has run reported the page clean. Pre-existing on
   all fourteen hand-rolled copies, not introduced by the component.

   WHY THERE IS NO PURE-CSS FIX WITHOUT AN ENGINE FEATURE, established by measurement rather than argued:
     * right-aligning is wrong where the cluster wraps LEFT   (master-name detail: panel would run to -113 at 393);
     * left-aligning is wrong where the cluster sits RIGHT    (person edit: wrapper inline-start at 329, so a 202px
       panel would run to 531 against a 393 viewport);
   so no single static side is correct, and choosing per page is fourteen constants — the drift this component
   exists to delete. Classic CSS cannot read where the anchor sits in the viewport. Two mechanisms can: JavaScript
   measurement, or CSS anchor positioning. Anchor positioning is chosen because it needs no script, no resize
   listener, and no per-instance wiring, and because it re-evaluates on scroll and resize for free.

   REACH. Anchor positioning shipped in Chromium 125. BOTH heads run evergreen Chromium — the MAUI head is WebView2
   on Windows and Android System WebView on Android — so the field head and the desk browser both get the flip. The
   page-review harness measures on Chromium 149. Where it is NOT supported (Safari and Firefox lag), the @supports
   block simply does not apply and the panel keeps the inset placement above: those users get exactly today's
   behaviour, never worse. That is the honest degrade, and it is stated here rather than implied — if the owner
   wants parity for those engines before they ship it, the alternatives are a JS clamp or a phone bottom-sheet, and
   both are bigger decisions than this rule. */
@supports (position-area: block-end span-inline-start) and (position-try-fallbacks: flip-inline) {
  /* `[data-kl-menu-anchor="page"]` is the OPT-IN for a menu that is not the shared component but has the same
     shape — a trigger with a right-aligned panel absolutely positioned in a relative wrapper. Added 2026-08-10
     with the owner's "all of the pages" ruling, because /lookups' hand-rolled Columns picker reproduced this
     section's original defect exactly: at 430px its cluster wraps left, the wrapper's inline-end lands at x=106,
     and its 202px panel rendered at `left: -96`. Opting a menu in is one attribute on the wrapper and one on the
     panel, which is a great deal less than a fifteenth private copy of a placement constant. */
  .kl-page-overflow-wrap, [data-kl-menu-anchor="page"] { anchor-name:--kl-page-actions; }
  /* A SECOND ANCHOR SCOPE, and it is not a nicety — it is a defect fix, measured 2026-09-13.
     `position-anchor: --name` resolves to the LAST element in tree order carrying that anchor-name, so one shared
     name across the whole document means the shell's own menu and the page's own menu compete for it, and the one
     that renders EARLIER loses. The shell account hub renders before <main>, so on every page that carries an
     enrolled menu of its own the hub took the PAGE's kebab as its anchor:

       /templates  1400x900  hub panel [983,262.9]   — beside the page kebab, 357px below its own trigger
       /templates   320x568  hub panel [0,573.9]     — a 568px-tall viewport: the whole panel below the fold

     It reads as a pass on any page WITHOUT an enrolled menu (the hub is then the only holder of the name), which is
     exactly how a shared-name bug hides: the shell is correct alone and wrong in company. A menu's anchor scope is
     therefore named after WHAT OWNS IT, and the shell owns this one. */
  [data-kl-menu-anchor="shell"] { anchor-name:--kl-shell-account; }
  /* The class is REPEATED to lift specificity to (0,3,0). The generic `[class$="-menu"]` popover rule further down
     this section is also (0,3,0) — `.kl-shell-main` plus the attribute — and being LATER in the file it won the tie
     and re-imposed `position:absolute`, silently. Measured, not assumed: the panel computed `position: absolute`
     while `position-area`, `position-anchor` and `position-try-fallbacks` all computed correctly, which is why the
     phone case looked fixed and the desktop case did not — with the wrapper as containing block the fallback has
     nothing to overflow, so it never fires. Repeating the class rather than adding `.kl-shell-main` keeps this
     working on the MAUI head, whose shell root class is not guaranteed to be the same. */
  .kl-page-overflow-wrap.kl-page-overflow-wrap > [role="menu"],
  [data-kl-menu-panel="anchor"][data-kl-menu-panel][data-kl-menu-panel],
  [data-kl-menu-panel="shell"][data-kl-menu-panel][data-kl-menu-panel] {
    /* FIXED, not absolute — and this is the load-bearing line, established by measurement after the first attempt
       failed. `position-try-fallbacks` evaluates overflow against the element's containing block. Left as
       `absolute`, that block is the `position:relative` WRAPPER, so a panel hanging far outside it is not
       "overflowing" anything the algorithm looks at and the flip never fires: measured `left 1271 / right 1473`
       against a 1400 viewport — the desktop case actively made WORSE than the inset rule it replaced, while the
       phone case looked fixed. `fixed` makes the containing block the viewport, which is the thing we actually
       need it to fit inside. Anchor positioning keeps the panel glued to the trigger through scroll and resize,
       so `fixed` costs nothing here; it also frees the panel from any `overflow:hidden` ancestor. */
    position:fixed;
    position-anchor:--kl-page-actions;
    /* Below the trigger, extending toward inline-start — the same look all fourteen copies hand-wrote. */
    position-area:block-end span-inline-start;
    /* …and when THAT would leave the viewport, flip to extend the other way instead. This is the whole fix: the
       side is decided by what fits, per instance, per width, instead of by a constant nobody can get right for
       every page.

       ALL FOUR EDGES, not one (owner ruling 2026-08-10 — "it stays fully on screen"). `flip-inline` alone only
       ever answers the inline axis, so a trigger low in the viewport still dropped its panel through the BOTTOM
       edge: measured 58px past it on /vehicles' last row and 10px on /users-roles', both at 1400x900, and a row
       menu on the last row of a table is the ordinary case, not an edge case. The three tactics are tried in
       order, so the ordinary placement is unchanged wherever it already fits — this strictly widens what is
       corrected and narrows nothing. */
    position-try-fallbacks:flip-inline, --kl-menu-flip-block, --kl-menu-flip-block-inline;
    /* position-area owns placement now; the inset pair above would fight it. */
    inset:auto;
    margin-block-start:var(--k-space-1);
    /* THE BOTTOM BAR IS PART OF "DOES IT FIT" (measured 2026-09-13, closing the gap §5j-b.1 opened).
       The fallbacks above answer "does the panel overflow?", and the engine answers it against the VIEWPORT.
       `--kl-menu-safe-bottom` was not part of that answer, so a panel ending just inside `innerHeight` and just
       INSIDE the fixed bottom bar was judged to fit and no fallback fired. MEASURED on /templates with the
       trigger scrolled to the foot of the viewport, AFTER correct enrolment: 2.9px of the panel under the bar at
       320x568 and 2.0px at 375x812. Both read as CONTAINED to a `rect vs innerHeight` check, and both put menu
       items under an opaque bar, so the press lands on the bar. It survived a correct enrolment, so it could not
       be fixed on any single page — it is a defect in this contract.

       Declared as a MARGIN because position-try computes overflow on the MARGIN box: naming the bar as margin
       makes the engine see the obstruction and flip for it, rather than being handed a number that hides it.

       WHY THE TWO BLOCK FALLBACKS ARE NAMED RULES AND NO LONGER `flip-block` — measured, not preferred. The
       `flip-block` TACTIC swaps the two block margins along with the insets, so it carried this bottom margin up
       to the TOP of the flipped candidate. At 320x568 there is no 56px of room above the trigger, so the flipped
       candidate then overflowed too, every fallback was rejected, and the engine fell back to the FIRST position
       — which overflows the bottom. /training-aids went from contained to 21.8px past the bottom edge and 78.8px
       behind the bar: a bottom-bar fix that made the bottom edge WORSE. A custom `@position-try` sets the
       properties it names outright, so the flipped placement below states its own margins and is never poisoned
       by this one. */
    margin-block-end:var(--kl-menu-safe-bottom);
  }
  /* The flipped-above placements. They exist as named rules only so the margins do not travel with the flip
     (see directly above); the geometry is exactly what `flip-block` and `flip-block flip-inline` produced. The
     safe inset is NOT repeated here: above the trigger, the bottom bar is not the edge this panel can reach. */
  /* Same placement, same flip, different anchor — the whole of the shell opt-in. */
  [data-kl-menu-panel="shell"][data-kl-menu-panel][data-kl-menu-panel] { position-anchor:--kl-shell-account; }
  @position-try --kl-menu-flip-block {
    position-area:block-start span-inline-start;
    margin-block-start:0; margin-block-end:var(--k-space-1);
  }
  @position-try --kl-menu-flip-block-inline {
    position-area:block-start span-inline-end;
    margin-block-start:0; margin-block-end:var(--k-space-1);
  }
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-overflow"] {
  display:inline-flex; align-items:center; justify-content:center;
  min-block-size:var(--k-touch); min-inline-size:var(--k-touch);
  border:1px solid transparent; border-radius:var(--k-radius-md);
  background:transparent; color:var(--k-text-muted); cursor:pointer; font-size:18px;
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-overflow"]:hover { background:var(--k-surface-2); color:var(--k-text); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-menu"] {
  position:absolute; z-index:50; min-inline-size:12rem;
  background:var(--k-surface); border:1px solid var(--k-border);
  border-radius:var(--k-radius-md); box-shadow:var(--k-elev-3);
  padding:var(--k-space-1); margin:0;
}

/* ---- 5j-b. VIEWPORT CONTAINMENT — the POINTER-anchored half of the menu contract ---------------------------
   OWNER RULING 2026-08-10, reported on /vehicles: "that drop down is kind of going off the screen. So take out
   of service. I can't see the last four letters of service … we need to make sure that it stays fully on screen
   for ALL of the more actions buttons across ALL of the pages."

   MEASURED BEFORE THE FIX (live, Chromium, /vehicles, desk width — not a phone):
     row 1   panel left 1184  right 1442  258x250   viewport 1400x900  ->  42px past the RIGHT edge
     row 7   panel top   708  bottom  958                              ->  58px past the BOTTOM edge
   and the reader saw "Take ZZ-TEST out of serv" with no ellipsis, no scrollbar and no hint that anything was
   missing — because the text was cut by the VIEWPORT, not by the item. Every other registry measured 202px wide
   and fitted only because its labels happened to be shorter; /vehicles is not special, it is just first.

   TWO CAUSES, and both are a constant standing in for a measurement:

   1. `RowContextMenu` pinned its panel with `left: clamp(8px, {tapX}px, calc(100vw - 13.5rem))` and
      `top: clamp(8px, {tapY}px, calc(100vh - 12rem))`. 13.5rem (216px) and 12rem (192px) are GUESSES at how big
      the panel would turn out to be. It is a shrink-to-fit box, so its real size is whatever its longest label
      needs — 258x250 here. 258 - 216 = the 42px overhang, exactly.
   2. §8b-o gives `td.kl-tcard-actions` `white-space: nowrap` above 640px, so a slot holding a control PLUS a
      kebab cannot wrap the kebab onto a second line. `white-space` INHERITS, and this panel renders INSIDE that
      cell — so every menu LABEL became unbreakable too, the panel's MIN-CONTENT width grew to the longest label,
      and a shrink-to-fit box can never be narrower than its min-content. That is why the same menu measures
      216px at 393px (where §8b-o does not apply, labels can wrap, the box can shrink) and 258px at 1400px. The
      popover is not the cell, so the inheritance is undone below.

   THE MECHANISM — A SELF-REFERENTIAL CLAMP. A percentage in `translate` resolves against the element's OWN
   border box, so the `100%` below IS the panel's rendered width/height. Nothing here guesses a size, which is
   precisely what both constants got wrong and precisely what one longer label defeats. It needs no engine
   feature, so it also covers the engines §5j-a's anchor positioning cannot reach (Safari, Firefox, any pre-125
   WebView), and it bounds all four edges rather than one.

   WHY THIS IS NOT CONVERGED ONTO §5j-a's ANCHOR POSITIONING — decided against the measured geometry, not
   assumed. §5j-a flips a panel to the other side of ITS ANCHOR ELEMENT. This panel has no anchor element:
   UI-GUAR-026 opens it at the RIGHT-CLICK POINT, so its anchor is a coordinate pair the page captured, and
   `flip-inline` has nothing to flip about. (It is also why the overflow here is on the anchor's own side, which
   a flip could not have corrected.) Synthesising a zero-size element at the pointer purely to feed the anchor
   engine would be the same second mechanism wearing the first one's clothes, and it would still leave every
   non-Chromium engine on the broken path. §5j-a and §5j-b are therefore the ELEMENT-anchored and POINTER-
   anchored halves of ONE contract — they key on different values of the same `data-kl-menu-panel` attribute, so
   they can never both apply, and ONE guard — `Ui.Web.E2E/Ui_MenuViewportContainment_Sweep` — asks the same
   question of both, and of every other menu in the product: is the panel's rect inside the viewport. It has to be
   a LIVE guard rather than a source scan, because the thing that was wrong was a rendered rectangle, and it finds
   its population by BEHAVIOUR (click an `[aria-haspopup="menu"]`, measure whatever `[role="menu"]` appears) so a
   new menu is covered the day it is written, with nothing to register.

   MARKED BY ATTRIBUTE, NEVER BY CLASS SUFFIX — Trap T7. `[class$="-menu"]` above keys on where the class
   ATTRIBUTE ends, so `kl-lh-rowmenu`, `kl-sr-menu-list` and `kl-rv-more-panel` are all silently outside it. A
   `data-` attribute cannot be dropped out of the family by a trailing modifier class. */

/* ---- 5j-b.1 THE BOTTOM SAFE INSET — the FIXED BOTTOM NAV is part of "off screen" -----------------------------
   OWNER REPORT 2026-08-11, on /dogs: "the more actions dropdown on the last item in the row is cutoff by the
   bottom of the screen and the bottom toolbar on tablet."

   The clamp below already kept every POINTER-anchored panel inside `100dvh`. It was not enough, because
   `.kl-shellnav-bottom` is `position:fixed; inset-block-end:0; z-index:55` at <=1280px — ABOVE the panel's
   z-index 50 — so the last 56px of the viewport is not viewable area, it is an opaque bar that paints over the
   menu. MEASURED live on the last row's kebab, before this rule:

     /dogs   @1280x800   panel [1080,590 192x202]  bottom 792, nav top 743  ->  49px of the menu behind the bar
     /dogs   @1024x768   panel  [824,558 192x202]  bottom 760, nav top 711  ->  49px
     /dogs    @393x660   panel  [193,447 192x202]  bottom 649, nav top 603  ->  46px
     /vehicles@1280x800  panel [1014,542 258x250]  bottom 792, nav top 743  ->  49px

   Every one of those reads as CONTAINED to a `rect vs innerHeight` check, which is why the sweep that exists for
   this class never said a word: 100dvh is the window, not the free space in it. The token below is the free
   space, and BOTH halves of the contract are written against it.

   It tracks the bar's own conditions rather than restating them: the bar appears at <=1280px (the drawer band)
   and is hidden again for a PINNED rail inside 1025-1280. env(safe-area-inset-bottom) rides along because the
   bar carries it as padding, so an iOS home indicator grows the obstruction. */
:root { --kl-menu-safe-bottom:var(--k-space-2); }
@media (max-width: 1280px) {
  :root { --kl-menu-safe-bottom:calc(var(--k-space-2) + var(--k-bottomnav-h) + env(safe-area-inset-bottom, 0px)); }
}
@media (min-width: 1025px) and (max-width: 1280px) {
  html[data-shellrail="pinned"] { --kl-menu-safe-bottom:var(--k-space-2); }
}

/* Shared by EVERY panel under the contract, whichever half anchors it. Three attribute selectors = (0,3,0), the
   same specificity as the `[class$="-menu"]` family above and later in the file, so it wins that tie the way
   §5j-a had to repeat its class for the same reason. */
[data-kl-menu-panel][data-kl-menu-panel][data-kl-menu-panel] {
  /* box-sizing is LOAD-BEARING, and this is the FOURTH appearance of one mechanism in this file (see the
     `[class$="-menu-item"]` family below, and `.kl-shellnav-accountmenu-item`). There is no global reset in this
     product — the sheet's only ones are `.kl-shell-main > *` and the form-control rule, and a popover is neither —
     so this panel took the UA default, `content-box`, and every `max-inline-size` below was being applied to its
     CONTENT box. Its own `padding: --k-space-1` (4px a side) plus a 1px border then put the BORDER box 10px wider
     than the cap. Caught by this change's own guard, not by reading: `/audit` at 320px measured 314px against a
     304px cap and escaped the right edge by 2. A 2px escape is not the owner's 42px, but it is the same defect
     with a smaller number, and a cap that silently means "plus whatever chrome the box carries" is not a cap. */
  box-sizing:border-box;
  /* Undo cause 2. A label the panel genuinely cannot fit now WRAPS inside it. `text-overflow: ellipsis` was
     considered and rejected: an ellipsis still deletes the end of "Take ZZ-TEST out of service", and a menu item
     is a command to be read in full, not a preview. What must never survive is the silent cut, and a wrapped
     item is visibly complete. Items floor at --k-touch and grow, so wrapping costs height, never a hit area. */
  white-space:normal; overflow-wrap:anywhere;
  /* A panel with more items than the viewport is tall scrolls inside itself rather than running off the bottom.
     dvh so mobile browser chrome is accounted for; the vh line is the fallback for engines without dvh. The cap
     is the gutter at the TOP plus the safe inset at the BOTTOM (§5j-b.1) — not two gutters — so a tall panel
     cannot grow into the fixed bottom bar. */
  max-block-size:calc(100vh - var(--k-space-2) - var(--kl-menu-safe-bottom));
  max-block-size:calc(100dvh - var(--k-space-2) - var(--kl-menu-safe-bottom));
  overflow-y:auto; overscroll-behavior:contain;
}

/* The POINTER-anchored panel: the page captured (x, y) on right-click or on the kebab click, and hands them over
   as custom properties. The component supplies NO positioning of its own — an inline style would out-specify
   everything here, which is how the broken clamp came to live in a .razor file in the first place. */
[data-kl-menu-panel="pointer"][data-kl-menu-panel][data-kl-menu-panel] {
  /* fixed, not absolute: the panel renders inside a table cell, and `fixed` is what keeps it clip-clean of every
     scrolling ancestor (the posture the clip-audit gate enforces). It also makes the containing block the
     viewport, which is the box the clamp below is written against. */
  position:fixed; z-index:50;
  inset-block-start:var(--kl-menu-y); inset-inline-start:var(--kl-menu-x);
  /* A DEFINITE size, not shrink-to-fit — and this line is load-bearing. Shrink-to-fit resolves against the space
     remaining to the right of the panel's own `left`, so clamping `left` and letting the width follow is
     circular: pull the panel inboard and it gets WIDER, which pushes it back out. Worse, at a kebab near the
     right edge the available space collapses and the box falls to its min-content — a 70px-wide, very tall
     column of broken words. `max-content` + a cap breaks the loop: the box is sized first, then moved. */
  inline-size:max-content; min-inline-size:12rem;
  max-inline-size:min(20rem, calc(100vw - (var(--k-space-2) * 2)));
  /* THE CLAMP. Per axis: shift by 0 when the panel already fits; shift LEFT/UP by exactly the overhang when it
     runs past the far edge (the `min(0px, …)` term, negative only when it overflows); and never past the near
     edge (the `max(…)` term wins when the anchor itself is inside the gutter). `100%` is this panel's own
     border box — the measurement the two constants were standing in for. */
  translate:
    max(calc(var(--k-space-2) - var(--kl-menu-x)),
        min(0px, calc(100vw - var(--k-space-2) - var(--kl-menu-x) - 100%)))
    max(calc(var(--k-space-2) - var(--kl-menu-y)),
        min(0px, calc(100dvh - var(--kl-menu-safe-bottom) - var(--kl-menu-y) - 100%)));
}

/* The same panel opened with NO pointer coordinates — a keyboard Enter/Space on the kebab reports clientX/Y 0,
   and so does a bUnit host.

   ⚠ THIS HALF WAS THE OWNER'S 2026-08-11 DEFECT, and the comment that used to sit here — "at its own static block
   position beside the kebab, which is on-screen by construction" — was simply false. It set no block inset at all,
   so `inset-block-start` stayed `auto` and a POSITION:FIXED box with an auto block inset is painted at its STATIC
   position: where it would have sat in flow, inside that row's <td>, measured from the viewport. For the LAST row
   of a list that is at or past the fold, and nothing clamped it. MEASURED on /dogs' last-row kebab, opened with
   Enter (the keyboard path UI-GUAR-026 exists to provide), before this rule:

     @1400x900  panel [1200,884 192x202]  ->  186px past the viewport bottom
     @1280x800  panel [1080,881 192x202]  ->  283px past it, 340px past the bottom bar; the panel's TOP EDGE is
                                             81px BELOW the fold, so all four items are entirely off screen
      @393x660  panel  [193,1580 192x202] ->  1122px past it

   The scrim is drawn either way, so the reader clicks the kebab, the page greys out, and no menu appears anywhere:
   it reads as a frozen page rather than as a clipped menu. The pointer half was fine at all three widths — this is
   why "it works when I click it" and "it is completely broken" were both true reports of the same control.

   THE FIX IS A REAL BLOCK INSET, and it is anchored to the VIEWPORT rather than to the trigger. CSS cannot read
   where the kebab sits (that is exactly what §5j-a's anchor positioning is for, and it needs Chromium 125+), and
   the pointer half's clamp cannot help because it clamps a coordinate this path does not have. Pinning the panel a
   safe inset up from the trailing-bottom corner is on-screen BY CONSTRUCTION at every width, on every engine, with
   no coordinate and no feature query — which is what the old comment claimed and did not do. The cost is honest
   and stated: a keyboard-opened menu appears in the bottom-trailing corner rather than beside its kebab. Focus,
   Escape and the scrim are unchanged, and a menu the reader can see beats one placed politely off screen. */
[data-kl-menu-panel="static"][data-kl-menu-panel][data-kl-menu-panel] {
  position:fixed; z-index:50;
  inset-inline-end:var(--k-space-2);
  inset-block-start:auto;
  inset-block-end:var(--kl-menu-safe-bottom);
  inline-size:max-content; min-inline-size:12rem;
  max-inline-size:min(20rem, calc(100vw - (var(--k-space-2) * 2)));
}

/* Owner UI review (item 2, swept project-wide): the reusable click-away backdrop for any floating [class$="-menu"]
   popover. A transparent full-viewport catcher sitting BELOW the menu's z-index:50, so a click anywhere outside the
   menu dismisses it while the menu itself stays interactive above the scrim. Render it as a SIBLING of the menu panel
   inside the @if(open) block, with an @onclick that clears the open-state. */
.kl-menu-scrim { position:fixed; inset:0; z-index:45; background:transparent; border:0; }
/* Owner UI review (item 2, swept): the click-away backdrop for the aria-modal SLIDE-OVER panels (.kl-slideover /
   .kl-cert-slideover, z-index:40 / 1000). This class was referenced by Dogs / Medical / Certifications / TrainingAids
   slide-overs but had NO rule, so click-away was non-functional in a real browser. It mirrors the .kl-dialog modal
   backdrop dim and sits BELOW the slide-over panel (z-index:39) so an outside click hits it (the sibling scrim's
   @onclick closes the panel) while the panel stays interactive above it. */
.kl-slideover-scrim { position:fixed; inset:0; z-index:39; background:rgb(10 26 43 / .55); backdrop-filter:blur(2px); border:0; } /* UI-GUAR-033: bumped .45→.55 so even edge slide-overs lay a ≥0.5 dim scrim. */

/* THE SHARED SLIDE-OVER PANEL FAMILY (final-draft foundation heal). Dogs + Medical mount `aside.kl-slideover`
   (+ -head/-title/-actions/-facts/-section/-foot) but the panel rules lived ONLY in TrainingAids' Blazor-SCOPED
   sheet — so opening quick-view on those pages dimmed the screen and dropped an UNPOSITIONED block at the end of
   the document flow. Promoted here (the scrim above was already shared); a page's scoped kl-slideover-* rules
   still win ties (the [b-hash] attribute out-specifies these plain classes), so TrainingAids is unchanged. */
.kl-slideover {
  position:fixed; inset-block:0; inset-inline-end:0; z-index:40;
  inline-size:min(27rem, 100vw);
  display:flex; flex-direction:column;
  background:var(--k-surface); border-inline-start:1px solid var(--k-border);
  box-shadow:var(--k-elev-3);
  overflow-y:auto; overscroll-behavior:contain;
}
.kl-slideover-head {
  display:flex; align-items:flex-start; justify-content:space-between; gap:var(--k-space-3);
  padding:var(--k-space-6); border-block-end:1px solid var(--k-border);
}
.kl-slideover-head h2, .kl-slideover-title {
  margin:0; font-family:var(--k-font-display); font-size:18px; font-weight:700; color:var(--k-text);
  overflow-wrap:anywhere;
}
.kl-slideover-actions {
  display:flex; align-items:center; gap:var(--k-space-2); flex-wrap:wrap;
  padding:var(--k-space-3) var(--k-space-6); border-block-end:1px solid var(--k-border);
}
dl.kl-slideover-facts, .kl-slideover-facts {
  display:grid; grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));
  gap:var(--k-space-3) var(--k-space-4);
  margin:0; padding:var(--k-space-4) var(--k-space-6);
}
.kl-slideover-facts > div { min-inline-size:0; }
.kl-slideover-facts dt {
  font-size:12px; font-weight:600; letter-spacing:.04em; text-transform:uppercase;
  color:var(--k-text-muted);
}
.kl-slideover-facts dd { margin:0; font-weight:600; color:var(--k-text); overflow-wrap:anywhere; }
.kl-slideover-section {
  padding:var(--k-space-4) var(--k-space-6); border-block-start:1px solid var(--k-border);
}
.kl-slideover-foot {
  margin-block-start:auto;
  display:flex; align-items:center; justify-content:flex-end; gap:var(--k-space-2); flex-wrap:wrap;
  padding:var(--k-space-4) var(--k-space-6); border-block-start:1px solid var(--k-border);
}
/* Dark: a crisp left edge + heavier shadow so the panel reads AS a raised surface against the ink canvas. */
html.dark .kl-slideover {
  border-inline-start-color:color-mix(in srgb,var(--k-border) 72%,var(--k-text-muted));
  box-shadow:-8px 0 24px rgb(0 0 0 / .45);
}
/* Outdoor (glove-grade): heavier separators so the panel holds up in direct sun. */
html[data-outdoor="true"] .kl-slideover { border-inline-start-width:2px; }
html[data-outdoor="true"] .kl-slideover-head,
html[data-outdoor="true"] .kl-slideover-actions,
html[data-outdoor="true"] .kl-slideover-section,
html[data-outdoor="true"] .kl-slideover-foot { border-color:var(--k-outline); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-menu-item"] {
  display:flex; align-items:center; gap:var(--k-space-2);
  /* box-sizing is LOAD-BEARING, exactly as it is for .kl-shellnav-accountmenu-item below — this is the THIRD
     appearance of one mechanism, so it is fixed on the family rather than a fourth time on a page.

     `inline-size:100%` + `padding:0 var(--k-space-3)` only adds up to 100% while the element is BORDER-BOX. There is
     no global reset in this product: the sheet's only ones are `.kl-shell-main > *` (DIRECT children of the shell,
     which a menu item nested in a popover is not) and the form-control rule. So every item took the UA default —
     `<button>` is border-box and looked correct, `<a>` inherits `content-box` and ran 100% + 12 + 12 = panel + 24.
     The family LOOKED right for years because the first fourteen consumers were all buttons.

     Measured live 2026-08-09, item border-box width vs its panel's content width, before this line:
       /dogs/{id}      "Edit profile"  <a>  264 in 240  (+24)   <- overran the popover's rounded edge
       /medical/{id}/edit, /traffic-stops/{id}, /training-aids/{id}, /training-aids/{id}/edit,
       /use-of-force/{id}, /vehicles/{id}/edit — the same <a> shape, 8 anchors over 7 surfaces
       every <button> consumer  240 in 240 / 192 in 192 (0)     <- unchanged by this line, it is a no-op for them
     `/master-names` was already fixed page-scoped (MasterNames.razor.css) and that copy is now redundant.

     Why not "make the anchors buttons": they navigate, so they must be links for middle-click, focus order and the
     status bar. Why not `calc(100% - 24px)`: that re-encodes the padding as a magic number in a second place. */
  box-sizing:border-box;
  /* UI-GUAR-036 (R2-25): row/context menu items are ≥44 CSS-px touch targets (was 40). */
  inline-size:100%; min-block-size:var(--k-touch); padding:0 var(--k-space-3);
  background:transparent; border:0; border-radius:var(--k-radius-sm);
  font:inherit; font-size:14px; color:var(--k-text); text-align:start; cursor:pointer;
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-menu-item"]:hover { background:var(--k-surface-2); }
.kl-menu-pinned { opacity:1 !important; pointer-events:auto !important; }

/* ---- 5k. Tabs (active driven by [aria-selected]) -------------------------- */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-tablist"] {
  display:flex; gap:var(--k-space-6); align-items:center;
  /* overflow-x:auto with no overflow-y set is an invalid axis pairing per the CSS Overflow spec: the browser
     silently computes the unset "visible" axis to "auto" too, which paints a spurious vertical scrollbar on a
     strip that only ever needs to scroll horizontally (owner ruling, dog detail tabs). overflow-y:clip is the
     fix, not overflow-y:hidden: a tab's focus-visible ring (outline 3px + offset 2px, :where(...) rule above)
     needs to bleed a few px past the tab's border box, and hidden would clip it flush; overflow-clip-margin
     restores that room without turning the y-axis back into a scroll container. */
  overflow-x:auto; overflow-y:clip; overflow-clip-margin:var(--k-space-2);
  border-block-end:1px solid var(--k-border);
  margin-block-end:var(--k-space-4);
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-tab"] {
  appearance:none; background:none; border:0; cursor:pointer; white-space:nowrap;
  padding:var(--k-space-2) 0; min-block-size:var(--k-touch);
  border-block-end:2px solid transparent; color:var(--k-text-muted);
  font:inherit; font-size:14px; font-weight:600;
  transition:color var(--k-dur-1) var(--k-ease), border-color var(--k-dur-1) var(--k-ease);
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-tab"]:hover { color:var(--k-accent); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-tab"][aria-selected="true"] { color:var(--k-text); border-block-end-color:var(--k-teal-700); }
html.dark .kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-tab"][aria-selected="true"] { border-block-end-color:var(--k-teal-400); }
.kl-shell-main [role="tabpanel"][hidden] { display:none; }

/* ---- 5k-a. The shared tab-strip component (Components/KlTabList.razor) -----
   Owner ruling 2026-08-10 (/scheduler). KlTabList deliberately ships NO stylesheet: its class names END in
   `-tablist` / `-tab` so it inherits §5k above wholesale, and its selected state is expressed ONLY through
   [aria-selected], which §5k already styles. What §5k does not cover is a tab carrying a leading QUALIFIER
   (a time, a count) beside its label — needed because two tabs can share a label or share a time, and only the
   PAIR tells them apart. Three declarations, once, here rather than page-scoped (§A1).

   Specificity note: §5k's `.kl-shell-main :where(…)[class$="-tab"]` is (0,2,0) because :where() contributes
   nothing, so `.kl-shell-main .kl-tabstrip-tab` ties at (0,2,0) and wins on sheet order. A bare
   `.kl-tabstrip-tab` would be (0,1,0) and would silently lose — which is exactly how a rule that reads correct
   in source turns out not to apply.

   The label is deliberately NOT truncated. §5k already sets white-space:nowrap and overflow-x:auto on the strip,
   so a strip too wide to fit SCROLLS; clipping the label instead would delete the one thing distinguishing two
   tabs, which is the defect this component exists to fix. */
.kl-shell-main .kl-tabstrip-tab { display:inline-flex; align-items:center; gap:var(--k-space-2); }
.kl-shell-main .kl-tabstrip-tab > .kl-tab-meta { color:var(--k-text-muted); font-variant-numeric:tabular-nums; font-weight:500; }
.kl-shell-main .kl-tabstrip-tab[aria-selected="true"] > .kl-tab-meta { color:var(--k-text); }

/* ---- 5k-b. NO TAB MAY BE OFF THE STRIP (owner, 2026-09-11, /scheduler) -----
   Owner, verbatim: "days that have more than one event. Can we finalize accessibility to those titles? It seems
   like on some page sizes, you can scroll by clicking and dragging with your mouse, and others, you can't ... I
   just wanna make sure that everybody can access any of the events in that day even if there's three."

   MEASURED, on the live host, on a day carrying three events. The strip's content box was 763px against a
   visible box of 288px at 320, 361 at 393, 412 at 1400 and 439 at 1920. Two of the three tabs sat OUTSIDE the
   strip at 320/375/393/430/1024/1400 and one of the three at 768/1920. So at EVERY width measured, at least one
   event was drawn where nobody could see it, and the only cue that it existed was a hover scrollbar.

   §5k's answer was `overflow-x:auto` plus `white-space:nowrap` on the tab, and the 5k-a note argues correctly
   that TRUNCATING a tab label deletes the one thing that tells two tabs apart. Both halves of that reasoning
   survive here. What does not survive is the third option nobody wrote down: the strip can WRAP. A wrapped strip
   hides nothing at any width, needs no scroll affordance to discover, and behaves identically for a mouse, a
   finger, an arrow key and a screen reader — which is exactly the four-way access the owner asked for.

   THE LABEL WRAPS TOO, and it has to. One tab measured 318.4px against a 288px strip at 320, so wrapping the
   STRIP alone would still have left that tab overflowing on its own. `white-space:normal` plus
   `overflow-wrap:anywhere` plus a zero min size on the label lets a long title take two lines inside the strip
   instead of one line outside it. Nothing is truncated: the label is complete, only re-flowed.

   `overflow-x:auto` from §5k STAYS as the last resort. Wrapping makes it inert in every measured case, and an
   unbreakable 400px token in some future title must still be reachable rather than clipped.

   SCOPE. These four rules key on `.kl-tabstrip-*`, i.e. Components/KlTabList.razor and nothing else — NOT the
   `[class$="-tablist"]` family, whose thirteen hand-rolled strips are unchanged. */
.kl-shell-main .kl-tabstrip-tablist { flex-wrap:wrap; column-gap:var(--k-space-6); row-gap:var(--k-space-1); }
.kl-shell-main .kl-tabstrip-tab { flex-wrap:wrap; max-inline-size:100%; white-space:normal; text-align:start; }
.kl-shell-main .kl-tabstrip-tab > .kl-tab-label { min-inline-size:0; overflow-wrap:anywhere; }

/* ---- 5l. Lists / rows / facts --------------------------------------------- */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-list"] { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:var(--k-space-2); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-row"] {
  display:flex; align-items:center; gap:var(--k-space-3);
  padding:var(--k-space-3) var(--k-space-4);
  background:var(--k-surface); border:1px solid var(--k-border); border-radius:var(--k-radius-md);
}
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-facts"] {
  display:grid; gap:var(--k-space-2) var(--k-space-6);
  grid-template-columns:repeat(auto-fit, minmax(180px, 1fr));
}
/* A grid item's min-inline-size defaults to auto, which refuses to shrink below its content's min-content width —
   so a long unbroken token (an email has no spaces) forces its own track wide and overflows into the next column
   the moment the grid has more tracks than the content wants (found on /people, .kl-person-facts: aokafor@rcso.
   example.gov painted into "Certification level"). Every -facts item gets a real floor, and every dd gets a wrap
   opportunity — additive only: a value that already fits is unaffected, this only fires when one would overflow. */
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-facts"] > * { min-inline-size:0; }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-facts"] dd { overflow-wrap:anywhere; }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-filters"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-filterdrawer"] {
  display:flex; flex-wrap:wrap; gap:var(--k-space-3); align-items:flex-end;
  margin-block-end:var(--k-space-4);
}

/* ---- 5l-a. `.kl-factgrid` — THE fact row: one definition, every surface that shows a label over a value ----
   OWNER RULING 2026-08-11, reviewing /dogs/{id}: "I would want them to look as close to exactly like the other
   values as possible just witht the added tooltip." The tenant-authored custom fields on that page were a form
   (13px/600 sentence-case labels, 15px values carrying a UA <p> margin, one full-width field per row) sitting
   directly under the built-in facts (11.52px/400 uppercase labels over 16px values in a four-track grid). A
   reader could tell which fields the agency had authored purely by looking, which is the defect.

   WHY THIS LIVES HERE AND NOT IN DogDetail.razor.css — the trap that would have made the fix INERT.
   The dt/dd treatment used to be page-scoped to DogDetail, so its elements carried `b-txqy1liigh`. Blazor stamps
   a scope attribute only on markup the component itself writes, so CustomFieldsPanel — a different component, in
   a different RCL — could have emitted the very same `.kl-dog-facts` markup and rendered COMPLETELY UNSTYLED
   while looking correct in source. The §A1 answer is one shared family both surfaces opt into, so the built-in
   facts and the tenant's own fields cannot drift apart: if they were styled by two rules they WOULD drift, which
   is exactly the state this ruling is correcting.

   The declarations are DogDetail's former ones, moved verbatim and MEASURED before and after (built-in `dt`
   11.52px/400/uppercase/0.4608px tracking, `dd` 16px with a 4px `margin-top`, container `grid` at
   `12px 16px` with four 241.5px tracks at 1400x900 — identical either side of the move).

   Deliberately NOT named `*-facts`: §5l's `[class$="-facts"]` family is an 8px/24px 180px grid that eighteen
   pages already lean on with their own page-scoped pair rules on top. Sweeping a dt/dd treatment into THAT
   selector would restyle all of them. `.kl-factgrid` is opt-in, and it is self-sufficient (it carries §5l's
   min-inline-size floor and dd wrap opportunity itself) precisely so a surface adopting it needs nothing else. */
/* auto-FILL, not auto-fit — and the difference is the whole point of the ruling. `auto-fit` COLLAPSES the tracks it
   cannot fill, so a grid holding three custom fields resolved three 327px columns while the eight built-in facts
   beside it resolved four 241.5px ones (measured). Same rule, same width, visibly different pitch: the two blocks did
   not line up, which is exactly the "these are not the same kind of thing" tell the owner is asking us to remove.
   `auto-fill` keeps the empty track, so both grids resolve the same tracks at the same width regardless of how many
   fields the tenant happens to have authored. It is a no-op for the built-in facts, which have more items than tracks
   at every realistic width (measured identical either side of the change). */
.kl-shell-main .kl-factgrid {
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(220px, 1fr));
  gap:var(--k-space-3) var(--k-space-4);
  margin:0;
}
.kl-shell-main .kl-factgrid > div { min-inline-size:0; }
/* No line-height here on purpose: the dt inherits the shell's unitless 1.5, which is what produces the measured
   17.28px. Pinning an absolute value would silently change the built-in facts this rule was promoted from. */
.kl-shell-main .kl-factgrid dt {
  margin:0;
  font-size:0.72rem; font-weight:400;
  text-transform:uppercase; letter-spacing:0.04em;
  color:var(--k-text-muted);
}
.kl-shell-main .kl-factgrid dd {
  margin:var(--k-space-1) 0 0;
  color:var(--k-text);
  overflow-wrap:anywhere;
}
/* The dd rule above is (0,2,1); the shared `.kl-muted` helper is (0,2,0) and would silently LOSE, painting a
   "you may not see this value" restriction sentence at full body colour. Restated at matching weight. */
.kl-shell-main .kl-factgrid dd.kl-muted { color:var(--k-text-muted); }

/* The custom-fields block is a SECTION following the built-in facts, and it started at a MEASURED 0px from them —
   its "Custom fields" heading sat directly on the last value's baseline. That is the same defect the owner ruled on
   for this page on 2026-08-10 ("there needs to be a larger gap between the last row of the info on profile tab and
   the start of the related records card"), which is why the related-records card below already spends --k-space-6.
   Now that the two blocks share one visual register, the boundary is the ONLY thing separating them, so it has to
   be real. Same token, same page, same reasoning. */
.kl-shell-main .kl-customfields { margin-block-start:var(--k-space-6); }

/* THE HINT — the owner asked for the field's descriptor "hidden behind a hint tool tip type thing".
   It is NOT a title attribute. The same owner ruled twice on 2026-08-10 that hover text does not exist on a phone
   and is invisible to a screen-reader rotor user; that ruling is enforced product-wide (ControlGate, the
   Ui_DisabledControlReason_Sweep guard), and a hover-only tooltip here would re-introduce exactly what was just
   removed everywhere else. So the affordance is a real <button>: focusable, operable by Enter/Space natively, and
   operable by a finger.

   §A6 (never sacrifice a touch target) is honoured WITHOUT a new exception. The INK is sized to the 11.52px label
   so the fact row keeps its rhythm; the TARGET is a transparent ::after carrying the full var(--k-touch) square,
   which costs zero layout because it is absolutely positioned. Reducing space between elements is always fine;
   reducing a hit area is not, and this reduces neither. */
.kl-shell-main .kl-facthint {
  position:relative;
  display:inline-flex; align-items:center; justify-content:center;
  inline-size:1.25em; block-size:1.25em; padding:0;
  margin-inline-start:var(--k-space-1);
  border:1px solid var(--k-border); border-radius:var(--k-radius-full);
  background:var(--k-surface-2); color:var(--k-text-muted);
  font:inherit; font-size:inherit; line-height:1; font-weight:700;
  cursor:pointer;
}
.kl-shell-main .kl-facthint::after {
  content:""; position:absolute; top:50%; left:50%;
  inline-size:var(--k-touch); block-size:var(--k-touch);
  transform:translate(-50%, -50%);
}
.kl-shell-main .kl-facthint:hover,
.kl-shell-main .kl-facthint[aria-expanded="true"] { color:var(--k-accent); border-color:var(--k-accent); }

/* The descriptor itself. COLLAPSED IS VISUAL ONLY — it stays in the accessibility tree so the `aria-describedby`
   on the value resolves whether or not a sighted reader has opened the hint. `hidden`/`display:none` would break
   that association silently, which is the same class of defect as the hover text this replaces.
   Open, it sits at the LABEL's size (0.72rem), never larger — the owner's third complaint was a 16px descriptor
   dominating its own 13px label. It flows inline in the dd rather than floating, so it cannot be clipped by an
   ancestor's overflow and needs no positioning context inside the grid. */
.kl-shell-main .kl-facthint-text[data-open="false"] {
  position:absolute; inline-size:1px; block-size:1px;
  margin:-1px; padding:0; border:0; overflow:hidden;
  clip-path:inset(50%); white-space:nowrap;
  /* It is a real text node inside the value's <dd>, so without this a reader dragging across "K-12" to copy it
     would silently also copy the collapsed descriptor. Scoped to the CLOSED state — the open text stays
     selectable — and invisible to assistive tech, which does not consult user-select. */
  user-select:none; -webkit-user-select:none;
}
.kl-shell-main .kl-facthint-text[data-open="true"] {
  display:block; margin-block-start:var(--k-space-1);
  font-size:0.72rem; line-height:1.4; color:var(--k-text-muted);
}

/* ---- 5m. Toggles / switches / reveals ------------------------------------- */
.kl-shell-main [role="switch"],
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-toggle"] {
  display:inline-flex; align-items:center; gap:var(--k-space-2);
  min-block-size:var(--k-touch); padding:0 var(--k-space-3);
  background:var(--k-surface-2); border:1px solid var(--k-border); border-radius:var(--k-radius-full);
  font:inherit; font-size:14px; color:var(--k-text); cursor:pointer;
}
.kl-shell-main [role="switch"][aria-checked="true"] { background:color-mix(in srgb,var(--k-teal-500) 22%,var(--k-surface)); border-color:var(--k-teal-700); }
.kl-shell-main :where([class^="kl-"],[class*=" kl-"])[class$="-reveal"] { color:var(--k-accent); cursor:pointer; font-weight:600; }

/* ---- 5n. Table-structural guard -------------------------------------------
   The suffix families above (-row / -status / -list / -grid / -field / -header …)
   must NOT hijack native <table> layout on the table-based pages (approvals,
   conflicts, lookups, record-list, report-analytics): a :where([class^="kl-"],[class*=" kl-"])[class$="-row"] on a <tr>
   would set display:flex and collapse the columns. Re-assert native table display
   — the !important beats the 0,2,0 attribute-family rules — and clear the pill/card
   border-radius that would otherwise round table rows/cells. Per-table colour,
   width, borders, and spacing stay with each page's own kl-*-table CSS + R11. */
.kl-shell-main table { display:table !important; }
.kl-shell-main thead { display:table-header-group !important; }
.kl-shell-main tbody { display:table-row-group !important; }
.kl-shell-main tfoot { display:table-footer-group !important; }
.kl-shell-main tr { display:table-row !important; border-radius:0; }
.kl-shell-main th, .kl-shell-main td { display:table-cell !important; border-radius:0; }

/* ---- 6. Theme toggle control (shell chrome — global, outside main) -------- */
.kl-theme-toggle { display:inline-flex; gap:var(--k-space-1); align-items:center; }
.kl-theme-btn {
  display:inline-flex; align-items:center; justify-content:center;
  min-block-size:36px; min-inline-size:36px; padding:0 var(--k-space-2);
  background:rgb(255 255 255 / .08); color:#fff;
  border:1px solid rgb(255 255 255 / .18); border-radius:var(--k-radius-md);
  font:inherit; font-size:13px; cursor:pointer;
  transition:background var(--k-dur-1) var(--k-ease);
}
.kl-theme-btn:hover { background:rgb(255 255 255 / .16); }
.kl-theme-btn[aria-pressed="true"] { background:var(--k-teal-500); color:var(--k-ink-900); border-color:transparent; }

/* ---- 7. Application SHELL chrome (ShellNav — global, outside main) ----------
   The permission-gated left rail + top bar + mobile bottom nav + DEV persona
   switcher (UI-GUAR-006 / IAM-006). On-brand via the --k-* token mirror so it
   flips correctly in light / dark / outdoor (the ink ramp is the chrome canvas
   in every mode). Lives OUTSIDE .kl-shell-main so the broad suffix families
   never bleed onto it. Additive: it mounts ABOVE the existing legacy header. */
.kl-shellnav-rail {
  display:flex; flex-direction:column; gap:var(--k-space-1);
  background:var(--k-ink-800); color:#fff;
  inline-size:240px; padding:var(--k-space-2);
  border-inline-end:1px solid rgb(255 255 255 / .08);
  /* The rail is a fixed-height flex column at every width (full-height fixed column on desktop, a full-height drawer
     when open on narrow). It never scrolls as a whole — the brand stays pinned and the inner .kl-shellnav-list is the
     SOLE scroll region — so a long persona's items (incl. Expenses) are reachable instead of clipped below the fold. */
  overflow:hidden;
}
.kl-shellnav-brand {
  display:flex; align-items:center; gap:var(--k-space-2);
  padding:var(--k-space-2) var(--k-space-3); min-block-size:var(--k-touch);
  color:#fff; text-decoration:none; font-family:var(--k-font-display);
  font-weight:800; font-size:18px; letter-spacing:-.01em;
}
.kl-shellnav-mark {
  display:inline-flex; align-items:center; justify-content:center;
  inline-size:28px; block-size:28px; border-radius:var(--k-radius-md);
  background:var(--k-teal-500); color:var(--k-ink-900); font-weight:800;
}
/* Brand lockup image in the (always-navy) rail — the white-text 'dark' variant. */
.kl-shellnav-logo { display:block; block-size:54px; inline-size:auto; }
/* Onboarding wizard-rail brand mark — follows the theme bg, so light/dark swap. */
.kl-wizard-rail__logo { display:block; block-size:40px; inline-size:auto; margin-block-end:var(--k-space-2); }
.kl-wizard-rail__logo--dark { display:none; }
html.dark .kl-wizard-rail__logo--light { display:none; }
html.dark .kl-wizard-rail__logo--dark { display:block; }
/* -- Onboarding wizard STEP RAIL (shared by the onboarding routes) -- a restrained vertical stepper. The brand block + the
   logo swap are above; these rules give the rail a card surface + the steps a done / active / deferred state (the
   markup carries .kl-wizard-step with .kl-is-done / .kl-wizard-step--active / .kl-is-deferred). Authored but never
   styled before pg48 — the steps rendered as a browser-default bullet list. All on-token, so it flips light/dark/outdoor. */
.kl-wizard-rail {
  align-self:start;
  display:flex; flex-direction:column; gap:var(--k-space-4);
  padding:var(--k-space-4); min-inline-size:14rem;
  background:var(--k-surface); border:1px solid var(--k-border);
  border-radius:var(--k-radius-lg); box-shadow:0 1px 3px rgb(0 0 0 / .06);
}
.kl-wizard-rail__brand { display:flex; flex-direction:column; }
.kl-wizard-rail__title { font-family:var(--k-font-display); font-weight:800; font-size:16px; color:var(--k-text); }
.kl-wizard-rail__sub { font-size:12px; color:var(--k-text-muted); }
.kl-wizard-rail ul, .kl-wizard-rail ol { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:2px; }
.kl-wizard-step {
  display:flex; align-items:center; gap:var(--k-space-2);
  padding:var(--k-space-2) var(--k-space-3); min-block-size:var(--k-touch);
  border-radius:var(--k-radius-md); font-size:14px; font-weight:600;
  color:var(--k-text-muted); border-inline-start:2px solid transparent;
}
.kl-wizard-step::before {
  content:""; inline-size:9px; block-size:9px; border-radius:var(--k-radius-full); flex:0 0 auto;
  background:transparent; border:2px solid var(--k-outline);
}
.kl-wizard-step > a { color:inherit; text-decoration:none; font:inherit; }
.kl-wizard-step > a:hover { text-decoration:underline; }
/* A navigable step rendered as a <button> (WizardRail when OnStepSelected is bound) — reset to read like the > a label. */
.kl-wizard-step__btn {
  appearance:none; background:transparent; border:0; margin:0; padding:0;
  font:inherit; color:inherit; text-align:start; cursor:pointer;
  display:flex; align-items:center; gap:var(--k-space-2); inline-size:100%; min-block-size:var(--k-touch);
}
.kl-wizard-step__btn:hover { text-decoration:underline; }
.kl-wizard-step__btn:focus-visible { outline:2px solid var(--k-accent); outline-offset:2px; }
/* DONE — a filled accent dot; the label reads at full body ink. */
.kl-wizard-step.kl-is-done { color:var(--k-text); }
.kl-wizard-step.kl-is-done::before { background:var(--k-accent); border-color:var(--k-accent); }
/* ACTIVE (current step) — teal lead-rail + emphasis + a ringed filled dot. */
.kl-wizard-step.kl-wizard-step--active {
  color:var(--k-text); font-weight:800;
  background:color-mix(in srgb, var(--k-accent) 12%, transparent);
  border-inline-start-color:var(--k-accent);
}
.kl-wizard-step.kl-wizard-step--active::before {
  background:var(--k-accent); border-color:var(--k-accent);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--k-accent) 25%, transparent);
}
/* DEFERRED (unbuilt step) — dimmed + a dashed marker; clearly not-yet-reachable, never a dead link. */
.kl-wizard-step.kl-is-deferred { color:var(--k-text-muted); opacity:.6; }
.kl-wizard-step.kl-is-deferred::before { border-style:dashed; border-color:var(--k-outline); }

/* -- HORIZONTAL wizard TRACKER (Task-30) — the WizardRail horizontal variant: a compact top progress band for the
   narrow / field record-build surface. A connected progress line of numbered dots (a checkmark once done), the ACTIVE
   step named in a caption, completed dots tappable to jump back. On-token so it flips light/dark/outdoor. The
   record-build surface owns the show/hide (below): hidden on wide (the vertical .kl-wizard-rail is the desktop
   stepper), shown ≤640. Onboarding never renders the horizontal variant, so its rail is untouched. */
.kl-wizard-track {
  background:var(--k-surface); border:1px solid var(--k-border);
  border-radius:var(--k-radius-lg); box-shadow:0 1px 3px rgb(0 0 0 / .06);
  padding:var(--k-space-3) var(--k-space-2) var(--k-space-2);
}
.kl-wizard-track__list { list-style:none; margin:0; padding:0; display:flex; align-items:flex-start; }
.kl-wizard-track__step { position:relative; flex:1 1 0; min-inline-size:0; display:flex; justify-content:center; }
/* The connector line runs from the previous dot's centre to this one's (drawn on each step but the first). */
.kl-wizard-track__step::before {
  content:""; position:absolute;
  /* Centre the connector on the dot's vertical middle (the dot is centred in the touch-tall button,
     so its centre sits at touch/2 — not at the dot radius), and inset both ends so the line stops
     just short of each circle: clean segments BETWEEN the dots, never drawn behind them. */
  top:calc(var(--k-touch, 44px) / 2); transform:translateY(-50%);
  inset-inline-start:calc(-50% + 18px); inline-size:calc(100% - 36px); block-size:2px;
  background:var(--k-outline); z-index:0;
}
.kl-wizard-track__step:first-child::before { display:none; }
/* A done / active step fills its lead-in connector with accent = a progressing "how far along" line. */
.kl-wizard-track__step.kl-is-done::before,
.kl-wizard-track__step.kl-wizard-step--active::before { background:var(--k-accent); }
.kl-wizard-track__btn {
  appearance:none; background:transparent; border:0; margin:0; padding:0;
  font:inherit; cursor:pointer; z-index:1;
  display:inline-flex; align-items:center; justify-content:center;
  min-block-size:var(--k-touch); min-inline-size:var(--k-touch);
}
.kl-wizard-track__btn:focus-visible { outline:2px solid var(--k-accent); outline-offset:2px; border-radius:var(--k-radius-full); }
.kl-wizard-track__dot {
  inline-size:28px; block-size:28px; flex:0 0 auto; border-radius:var(--k-radius-full);
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--k-surface); border:2px solid var(--k-outline);
  font-size:13px; font-weight:700; line-height:1; color:var(--k-text-muted);
  transition:background var(--k-dur-1) var(--k-ease), border-color var(--k-dur-1) var(--k-ease), color var(--k-dur-1) var(--k-ease);
}
/* DONE — filled accent dot with a check. */
.kl-wizard-track__step.kl-is-done .kl-wizard-track__dot { background:var(--k-accent); border-color:var(--k-accent); color:var(--k-ink-900); }
/* ACTIVE — ringed accent dot, emphasised. */
.kl-wizard-track__step.kl-wizard-step--active .kl-wizard-track__dot {
  border-color:var(--k-accent); color:var(--k-text); font-weight:800;
  box-shadow:0 0 0 3px color-mix(in srgb, var(--k-accent) 25%, transparent);
}
/* UPCOMING — dimmed, plainly not-yet-reached. */
.kl-wizard-track__step:not(.kl-is-done):not(.kl-wizard-step--active) .kl-wizard-track__dot { opacity:.7; }
.kl-wizard-track__step.kl-is-deferred .kl-wizard-track__dot { border-style:dashed; opacity:.55; }
.kl-wizard-track__caption {
  margin:var(--k-space-2) 0 0; text-align:center;
  font-size:13px; font-weight:700; color:var(--k-text);
}
/* The nav-link region is the rail's SINGLE scroll container: flex:1 + min-block-size:0 lets it shrink inside the
   fixed-height column so a long signed-in persona (Supervisor has 15+ items) scrolls its items into reach instead of
   clipping below the fold. A VISIBLE on-rail-navy scrollbar is the affordance — without it the list silently cut off
   at "Vehicles" and Expenses/Medical/Certifications/etc. looked unreachable (the items rendered, but the overlay
   scrollbar gave no cue there was more). overscroll-behavior:contain keeps a rail flick from scrolling the page. */
.kl-shellnav-list {
  display:flex; flex-direction:column; gap:2px;
  flex:1 1 auto; min-block-size:0; overflow-y:auto; overscroll-behavior:contain;
  padding-block-end:var(--k-space-4);
  scrollbar-width:thin; scrollbar-color:rgb(255 255 255 / .28) transparent;
}
/* On a desktop browser, styling the webkit pseudo-elements promotes the list to a CLASSIC always-present scrollbar
   (it stops being a hover-only overlay), so a long persona always shows the groove + thumb = a persistent "there is
   more below" affordance. A faint track groove keeps the cue visible even when the thumb is at rest. */
.kl-shellnav-list::-webkit-scrollbar { inline-size:10px; }
.kl-shellnav-list::-webkit-scrollbar-track { background:rgb(255 255 255 / .06); border-radius:var(--k-radius-full); }
.kl-shellnav-list::-webkit-scrollbar-thumb {
  background:rgb(255 255 255 / .26); border-radius:var(--k-radius-full);
  border:2px solid transparent; background-clip:padding-box;
}
.kl-shellnav-list:hover::-webkit-scrollbar-thumb,
.kl-shellnav-list::-webkit-scrollbar-thumb:hover { background:rgb(255 255 255 / .44); background-clip:padding-box; }
.kl-shellnav-group {
  margin:var(--k-space-3) var(--k-space-3) var(--k-space-1);
  font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.06em;
  /* >= AA (4.5:1) for this 11px label on the --k-ink-800 rail; .45 measured ~4.2:1 and axe-failed color-contrast. */
  color:rgb(255 255 255 / .62);
}
.kl-shellnav-link {
  display:flex; align-items:center; gap:var(--k-space-2);
  padding:var(--k-space-2) var(--k-space-3); min-block-size:40px;
  color:rgb(255 255 255 / .82); text-decoration:none;
  border-radius:var(--k-radius-md); font-size:14px;
  transition:background var(--k-dur-1) var(--k-ease), color var(--k-dur-1) var(--k-ease);
}
.kl-shellnav-link:hover { background:rgb(255 255 255 / .08); color:#fff; }
.kl-shellnav-link[aria-current="page"] { background:var(--k-teal-700); color:#fff; }
/* Reporter reply round-trip — the "My feedback" attention badge (NEEDS_INFO + awaiting-confirmation count). */
.kl-shellnav-linklabel { flex:1 1 auto; }
.kl-shellnav-badge {
  flex:0 0 auto; min-inline-size:18px; block-size:18px; padding:0 6px;
  display:inline-flex; align-items:center; justify-content:center;
  border-radius:999px; background:var(--k-attention-badge-bg); color:#fff;
  font-size:11px; font-weight:700; line-height:1;
}
.kl-shellnav-operatornote {
  margin:var(--k-space-3); padding:var(--k-space-3); border-radius:var(--k-radius-lg);
  background:rgb(255 255 255 / .06); color:rgb(255 255 255 / .82); font-size:13px;
}
.kl-shellnav-operatornote strong { display:block; margin-block-end:var(--k-space-1); color:#fff; }

/* Top bar (secondary chrome row). */
.kl-shellnav-topbar {
  display:flex; align-items:center; gap:var(--k-space-1);
  background:var(--k-header-bg); color:#fff;
  padding:var(--k-space-1) var(--k-space-3); min-block-size:48px;
  border-block-end:1px solid rgb(255 255 255 / .08);
}
.kl-shellnav-spacer { flex:1 1 auto; }
.kl-shellnav-iconbtn {
  display:inline-flex; align-items:center; justify-content:center;
  /* UI-GUAR-036 (R2-25): a ≥44² touch target on every phone width. These sit in a flex topbar, so a fixed
     inline/block-size was SHRUNK below 44 by flex when several buttons + the persona ellipsis competed for room
     (the live sweep measured 20–35×40). min-* + flex-shrink:0 makes the WCAG 2.5.5/2.5.8 floor non-negotiable. */
  inline-size:var(--k-touch); block-size:var(--k-touch);
  min-inline-size:var(--k-touch); min-block-size:var(--k-touch); flex:0 0 auto;
  border-radius:var(--k-radius-full);
  background:transparent; color:rgb(255 255 255 / .82);
  border:0; font:inherit; font-size:18px; cursor:pointer; text-decoration:none;
  transition:background var(--k-dur-1) var(--k-ease);
}
.kl-shellnav-iconbtn:hover { background:rgb(255 255 255 / .12); color:#fff; }
.kl-shellnav-iconbtn[aria-pressed="true"] { background:var(--k-teal-500); color:var(--k-ink-900); }
/* UI-GUAR-036 (R2-25): the account avatar is a ≥44 CSS-px touch target (was an explicit 36×36 that overrode the
   .kl-shellnav-iconbtn floor; the axe target-size pass flagged it on every route). */
.kl-shellnav-account { background:var(--k-ink-700); color:var(--k-teal-400); inline-size:var(--k-touch); block-size:var(--k-touch); font-size:13px; font-weight:700; }

/* The top bar's account cluster — the shell's SINGLE auth region (the redundant legacy header was retired). MainLayout
   fills this via ShellNav's AccountSlot: the signed-in persona + a working sign-out, or a dev-sign-in link. White ink
   on the dark bar (AA on the navy header canvas in every mode). */
.kl-shellnav-account-cluster { display:flex; align-items:center; gap:var(--k-space-2); min-inline-size:0; }
.kl-shellnav-account-cluster .kl-shellnav-signout-form { margin:0; }
.kl-shellnav-persona {
  font-size:13px; font-weight:600; color:#fff; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; max-inline-size:16ch;
}
.kl-shellnav-signout {
  font:inherit; font-size:13px; cursor:pointer; white-space:nowrap;
  color:#fff; background:rgb(255 255 255 / .12);
  border:1px solid rgb(255 255 255 / .28); border-radius:var(--k-radius-md);
  /* UI-GUAR-036 (R2-25): ≥44 CSS-px touch height (was 32; the live sweep flagged 77×32). */
  padding-block:var(--k-space-1); padding-inline:var(--k-space-3);
  min-block-size:var(--k-touch); display:inline-flex; align-items:center;
  transition:background var(--k-dur-1) var(--k-ease);
}
.kl-shellnav-signout:hover { background:rgb(255 255 255 / .22); }
.kl-shellnav-signin { color:#fff; font-size:13px; text-decoration:underline; white-space:nowrap; }

/* AUTH go-live — the top-bar account MENU (k-account). A native <details> disclosure (opens with no circuit on the
   static-SSR shell). The <summary> is the avatar trigger; the panel drops below, right-aligned to the bar edge. */
.kl-shellnav-accountmenu { position:relative; display:inline-flex; }
.kl-shellnav-accountmenu > summary { list-style:none; cursor:pointer; }
.kl-shellnav-accountmenu > summary::-webkit-details-marker { display:none; }
.kl-shellnav-accountmenu-panel {
  position:absolute; inset-block-start:calc(100% + var(--k-space-1)); inset-inline-end:0; z-index:50;
  min-inline-size:12rem; display:flex; flex-direction:column; padding-block:var(--k-space-1);
  background:var(--k-surface, #fff); color:var(--k-text);
  border:1px solid var(--k-border, rgb(0 0 0 / .12)); border-radius:var(--k-radius-md);
  box-shadow:0 8px 24px rgb(0 0 0 / .18);
}
/* box-sizing is LOAD-BEARING here (feedback 72cac066): the shell chrome renders outside <main>, so it inherits no
   box-sizing reset (the sheet's only ones are `.kl-shell-main > *` above and the form-control rule). Content-box +
   inline-size:100% + 12px padding-inline made each row 216px inside the 192px right-anchored panel, and the 24px
   surplus escaped past the viewport edge — measured at 1280x800, opening the menu took document scrollWidth
   1280 -> 1291 and gave the whole page a horizontal scrollbar. */
.kl-shellnav-accountmenu-item {
  display:flex; align-items:center; gap:var(--k-space-2); box-sizing:border-box;
  font:inherit; font-size:14px; text-align:start; text-decoration:none; white-space:nowrap; cursor:pointer;
  color:inherit; background:transparent; border:0; inline-size:100%;
  padding-block:var(--k-space-2); padding-inline:var(--k-space-3); min-block-size:var(--k-touch);
}
.kl-shellnav-accountmenu-item:hover,
.kl-shellnav-accountmenu-item:focus-visible { background:var(--k-hover-wash); }
/* The ring is INSET: the hub panel scrolls (overflow-y:auto), so an outset ring on a full-width row is clipped at
   the panel edge. It names the accent token (3:1 on the panel in light and dark). (0,3,0) beats the topbar focus
   rule `.kl-shellnav-topbar button:focus-visible` (0,2,1), which paints teal-400 (1.9:1 on a white panel). */
.kl-shellnav-accountmenu .kl-shellnav-accountmenu-item:focus-visible {
  outline:2px solid var(--k-accent); outline-offset:-2px;
}
.kl-shellnav-accountmenu .kl-shellnav-signout-form { margin:0; }

/* ---- ACCOUNT HUB (owner-directed 2026-07-23) --------------------------------------------------------------
   The topbar account dropdown is the single account-level surface on both heads: identity header (face, full
   name, agency), the copyable user id, the head-supplied change-photo control, then every profile destination.
   It is also the ONLY account surface at phone widths, so it is width-capped and scrolls INSIDE itself rather
   than growing past the viewport (the same box-sizing lesson as the menu items above).
   KEEP IN SYNC with the other copy of this file (web serves Kanine.Web/wwwroot, MAUI serves _content/). */
.kl-account-hub { min-inline-size:17rem; max-inline-size:min(21rem, calc(100vw - var(--k-space-4)));
  overflow-y:auto; overscroll-behavior:contain; }
/* THE HEIGHT CAP, and why it is written three times. This panel is the ONE menu in the product that hangs off the
   TOP BAR rather than off a control inside the page, so "how tall may it be" is the viewport MINUS that bar, and
   minus the bottom bar the panel can otherwise run behind (§5j-b.1). The old value was `calc(100vh - 5rem)`, which
   is neither: 5rem is 80px, the topbar is 57px, and nothing in it named the bottom bar at all. MEASURED at 320x568
   before this line: the panel ended 43px past the top edge of the bottom bar, with the last two rows unpressable.

   Repeated to (0,3,0) because the shared `[data-kl-menu-panel]` rule in §5j-b is exactly that specific and would
   otherwise impose the whole-viewport cap on this panel — the same tie §5j-a and §5j-b.1 each had to break, for the
   same reason. This rule is LATER in the file, so equal specificity resolves here. */
.kl-account-hub.kl-account-hub.kl-account-hub {
  max-block-size:min(34rem, calc(100vh - var(--k-topbar-h) - var(--k-space-2) - var(--kl-menu-safe-bottom)));
  max-block-size:min(34rem, calc(100dvh - var(--k-topbar-h) - var(--k-space-2) - var(--kl-menu-safe-bottom)));
}
/* Defect 35 (2026-10-02): the Sign out row stays at the bottom edge of the hub while the rows above it scroll. On a
   phone the hub is taller than the free space, and the last row was below the fold. The footer takes the panel
   surface, so the rows that scroll under it do not show through. */
.kl-account-hub-footer {
  position:sticky; inset-block-end:0; z-index:1; display:flex; flex-direction:column;
  margin-block-end:calc(-1 * var(--k-space-1)); padding-block-end:var(--k-space-1);
  background:var(--k-surface, #fff);
}
.kl-account-hub-identity {
  display:flex; align-items:center; gap:var(--k-space-3); box-sizing:border-box;
  inline-size:100%; padding:var(--k-space-3);
}
/* The hub's own big face. Shares the circle treatment with the topbar avatar; an <img> fills it edge to edge. */
.kl-account-hub-face {
  flex:0 0 auto; inline-size:44px; block-size:44px; border-radius:50%; overflow:hidden;
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--k-teal-500); color:var(--k-ink-900); font-weight:800; font-size:15px;
}
.kl-account-hub-face-img { inline-size:100%; block-size:100%; object-fit:cover; display:block; }
.kl-account-hub-who { display:flex; flex-direction:column; min-inline-size:0; }
/* Names and agencies are arbitrary user data: they wrap-or-ellipsis instead of widening the panel. */
.kl-account-hub-name { font-size:14px; font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.kl-account-hub-agency { font-size:12px; color:var(--k-text-muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.kl-account-hub-uid {
  display:flex; align-items:center; gap:var(--k-space-2); box-sizing:border-box;
  inline-size:100%; padding:0 var(--k-space-3) var(--k-space-2);
}
.kl-account-hub-uid-label { font-size:11px; text-transform:uppercase; letter-spacing:.04em; color:var(--k-text-muted); flex:0 0 auto; }
/* The id is a machine token — monospaced, and it TRUNCATES rather than forcing the panel wider. */
.kl-account-hub-uid-value {
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size:11px;
  flex:1 1 auto; min-inline-size:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* §A6 (2026-09-11): 32x32 before, which is under the var(--k-touch) floor, and this button is in the SHARED shell —
   it is on every page of the web head and on the MAUI field head, where 32px is a thumb target. The id beside it
   truncates (min-inline-size:0 + ellipsis), so the extra 16px of button width costs characters of a machine token
   nobody reads off the screen, never layout: measured at 320/375/393/430 the row holds one line and the panel does
   not widen. `.kl-account-hub-photobtn` two rules down already stands at var(--k-touch); this now matches it. */
.kl-account-hub-copy {
  flex:0 0 auto; appearance:none; border:1px solid var(--k-border, rgb(0 0 0 / .12)); background:transparent;
  box-sizing:border-box; display:inline-flex; align-items:center; justify-content:center;
  color:inherit; cursor:pointer; font:inherit; font-size:13px; line-height:1;
  border-radius:var(--k-radius-md); padding:6px 8px;
  min-inline-size:var(--k-touch); min-block-size:var(--k-touch);
}
.kl-account-hub-copy:hover, .kl-account-hub-copy:focus-visible { background:var(--k-hover-wash); }
.kl-account-hub .kl-account-hub-copy:focus-visible { outline:2px solid var(--k-accent); outline-offset:-2px; }
/* Copy feedback is a real state change the JS stamps — never a silent no-op button. */
.kl-account-hub-copy.is-copied { background:var(--k-teal-400); border-color:transparent; color:var(--k-ink-900); }
.kl-account-hub-photo { box-sizing:border-box; inline-size:100%; padding:0 var(--k-space-3) var(--k-space-2); }
.kl-account-hub-photoform { display:flex; align-items:center; gap:var(--k-space-2); margin:0; }
.kl-account-hub-photobtn {
  display:inline-flex; align-items:center; gap:var(--k-space-2); cursor:pointer;
  font-size:13px; font-weight:600; padding:8px 10px; min-block-size:var(--k-touch); box-sizing:border-box;
  border:1px solid var(--k-border, rgb(0 0 0 / .12)); border-radius:var(--k-radius-md);
  /* A <button> does not inherit `color` and paints UA buttonface. Take the panel's themed ink and a clear fill, so
     the two native photo buttons follow the theme like every other row of the hub. */
  background:transparent; color:inherit;
}
.kl-account-hub-photobtn:hover { background:var(--k-hover-wash); }
/* The submit button is the NO-JS fallback for the auto-submitting file input: hidden from pointer users (who never
   need it) but kept in the accessibility tree and revealed on keyboard focus, so it is never an unreachable control. */
.kl-account-hub-photofallback {
  appearance:none; background:transparent; border:0; font:inherit; font-size:12px; cursor:pointer;
  color:inherit; opacity:0; inline-size:1px; block-size:1px; padding:0; overflow:hidden;
}
.kl-account-hub-photofallback:focus-visible {
  opacity:1; inline-size:auto; block-size:auto; padding:6px 8px;
  border:1px solid var(--k-border, rgb(0 0 0 / .12)); border-radius:var(--k-radius-md);
}
.kl-account-hub-sep { block-size:1px; background:var(--k-border, rgb(0 0 0 / .12)); margin-block:var(--k-space-1); }
/* The my-feedback badge sits at the row's end rather than beside the label. */
.kl-account-hub .kl-shellnav-accountmenu-item .kl-shellnav-badge { margin-inline-start:auto; }
/* The topbar circle itself, once a real photo is set. */
.kl-shellnav-avatar-img {
  inline-size:100%; block-size:100%; border-radius:50%; object-fit:cover; display:block;
}


.kl-shellnav-topbar a:focus-visible,
.kl-shellnav-topbar summary:focus-visible,
.kl-shellnav-topbar button:focus-visible { outline:3px solid var(--k-teal-400); outline-offset:2px; border-radius:2px; }

/* Mobile bottom nav (thumb reach). Hidden on wide; the rail is the desktop nav.
   ONE height token shared by the bar and the content clearance so the two can never drift apart (the page
   must scroll clear of the bar). */
:root { --k-bottomnav-h: 56px; }

/* ---- P4: the STICKY-OFFSET contract for in-page sticky elements (2026-07-24) ------------------------------
   The shell top bar is sticky at z-index:70 wherever it is pinned. An in-page element that also sticks — the
   record-build wizard tracker, a settings section rail — must therefore start BELOW it, or once stuck it slides
   under the bar and its controls become unclickable while still being visible and enabled. That is a genuine
   unreachability defect, not a paint-order nit: the C4 ladder settles WHO PAINTS ON TOP, but nothing declared the
   OFFSET, so every new sticky element rediscovered the same bug (the wizard tracker's steps 1-2 were unclickable
   once stuck; /tenant-settings had the identical shape and was patched page-side).

   --k-topbar-h  the bar's own box: min-block-size 48 + 2x--k-space-1 padding + 1px border = 57px. A documented
                 constant, exactly like --k-bottomnav-h above (the bar can only grow if its content wraps, which
                 the >=44px icon row prevents).
   --k-sticky-top  what an in-page sticky element should use for `top`. ZERO by default, because the bar is NOT
                 sticky everywhere: at desktop widths with the rail pinned it scrolls away normally, and an offset
                 there would leave a 57px gap above the stuck element. It resolves to the bar height only where the
                 bar is actually pinned — the collapsed-rail state below, and the <=1280 band (declared in the web
                 head's app.css, right beside the rule that makes the bar sticky, so the two cannot drift). */
:root { --k-topbar-h: 57px; --k-sticky-top: 0px; }
html[data-shellrail="collapsed"] { --k-sticky-top: var(--k-topbar-h); }
.kl-shellnav-bottom { display:none; }
.kl-shellnav-bottomitem {
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
  flex:1 1 0; min-block-size:var(--k-touch);
  background:transparent; border:0; font:inherit; font-size:11px;
  color:rgb(255 255 255 / .65); text-decoration:none; cursor:pointer;
}
.kl-shellnav-bottomitem--primary { color:var(--k-teal-400); font-weight:700; }
/* SELECTED tab (owner-directed 2026-07-23): the tab for the page you are already on reads as current — full-strength
   teal label + glyph and a teal cap on the bar's top edge, so the state is carried by MORE than colour alone. The
   ::before is positioned against the item (relative) and hugs its top so the cap lines up with the bar border. */
.kl-shellnav-bottomitem { position:relative; transition:color 120ms ease; }
.kl-shellnav-bottomitem.is-active { color:var(--k-teal-400); font-weight:700; }
.kl-shellnav-bottomitem.is-active::before {
  content:""; position:absolute; inset-block-start:0; inset-inline:22%;
  block-size:3px; border-end-start-radius:2px; border-end-end-radius:2px;
  background:var(--k-teal-400);
}

/* ---- CAPTURE FAB + RADIAL MENU (owner-directed 2026-07-22) ------------------------------------------------
   The centre of the bar is a raised CIRCULAR button, larger than the flanking tabs, that rises roughly half out
   of the bar. The "half-moon cut" is the ring of bar-coloured box-shadow around the disc: it paints OVER the
   bar's top border so the disc reads as punched through the bar rather than floating on top of it — one shadow,
   no SVG mask, no second stacking context. .kl-shellnav-bottomspacer is the flex column it occupies so the two
   flanking tabs never slide underneath.
   Both the FAB and the radial are positioned against .kl-shellnav-bottom (position:fixed at <=1024px), so the
   whole cluster travels with the docked bar and inherits its safe-area padding. */
.kl-shellnav-bottomspacer { flex:1 1 0; min-inline-size:72px; pointer-events:none; }
.kl-shellnav-fab {
  position:absolute; inset-inline-start:50%; inset-block-start:8px;
  transform:translate(-50%, -50%);
  inline-size:64px; block-size:64px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  border:0; cursor:pointer; font:inherit; z-index:2;
  color:var(--k-ink-900);
  background:var(--k-teal-500);
  /* The bar-coloured ring IS the notch; the outer drop shadow lifts the disc off the page. */
  box-shadow:0 0 0 5px var(--k-ink-800), 0 6px 16px rgb(0 0 0 / .45);
  transition:transform 140ms ease, background-color 140ms ease;
}
.kl-shellnav-fab:hover { background:var(--k-teal-400); }
.kl-shellnav-fab:focus-visible { outline:3px solid #fff; outline-offset:3px; }
.kl-shellnav-fab-glyph { font-size:30px; line-height:1; transition:transform 160ms ease; }
/* Open state: the + becomes an x, so the FAB is its own close affordance (no second control to find). */
.kl-shellnav-fab[aria-expanded="true"] .kl-shellnav-fab-glyph { transform:rotate(45deg); }

/* The radial itself is a ZERO-SIZE origin pinned to the FAB's centre; each spoke is translated out to its own
   (--k-spoke-x, --k-spoke-y) point on the arc, supplied inline by the markup. Closed = not rendered for the
   pointer (visibility:hidden + zero scale), so it can never intercept taps on the bar beneath it. */
.kl-capture-radial {
  position:absolute; inset-inline-start:50%; inset-block-start:8px;
  inline-size:0; block-size:0; z-index:1;
  visibility:hidden; pointer-events:none;
}
.kl-capture-radial.is-open { visibility:visible; pointer-events:auto; }
.kl-capture-spoke {
  position:absolute; inset-inline-start:0; inset-block-start:0;
  display:flex; flex-direction:column; align-items:center; gap:4px;
  /* The 48px DOT is what sits ON the arc point, so the column is shifted -50% of its OWN width (centred on the
     point) and lifted by the dot's radius (the point lands on the dot's centre, not the column's top). The label
     stays on ONE line so the column's height is predictable — that height is what keeps the low outer spokes and
     their labels clear of the bar. */
  inline-size:max-content; max-inline-size:112px; white-space:nowrap;
  text-decoration:none; color:#fff;
  transform:translate(-50%, -22px) scale(.4); transform-origin:top center; opacity:0;
  transition:transform 180ms cubic-bezier(.2,.8,.3,1.2), opacity 140ms ease;
}
.kl-capture-radial.is-open .kl-capture-spoke {
  transform:translate(calc(var(--k-spoke-x) - 50%), calc(var(--k-spoke-y) - 22px)) scale(1);
  opacity:1;
}
.kl-capture-spoke-dot {
  display:flex; align-items:center; justify-content:center;
  inline-size:44px; block-size:44px; border-radius:50%;
  background:var(--k-ink-700); color:#fff; font-size:21px;
  border:1px solid rgb(255 255 255 / .16); box-shadow:0 4px 12px rgb(0 0 0 / .4);
}
.kl-capture-spoke--primary .kl-capture-spoke-dot {
  background:var(--k-teal-400); color:var(--k-ink-900); border-color:transparent;
}
.kl-capture-spoke-label {
  font-size:10px; font-weight:600; line-height:1.3; text-align:center; letter-spacing:.01em;
  padding:1px 6px; border-radius:999px;
  background:rgb(10 26 43 / .92); color:#fff;
}
.kl-capture-spoke:focus-visible .kl-capture-spoke-dot { outline:3px solid var(--k-teal-400); outline-offset:2px; }
/* The capture scrim dims the page so the spokes read against arbitrary content underneath. */
.kl-menu-scrim--capture { background:rgb(0 0 0 / .45); }
/* The floating FEEDBACK bubble lives at z-index 10000 (it must clear Blazor's reconnect overlay), so nothing in the
   shell can paint over it — it would sit ON TOP of the right-hand spokes. It stands down while the radial is open.
   HEAD-AGNOSTIC ON PURPOSE: keyed off the radial's own is-open state via :has(), NOT off a class the JS stamps.
   The two heads open this menu by different paths — the web shell is static SSR so kanine-shell-nav.js carries it,
   while on the Blazor-Hybrid (MAUI) head the shell renders POST-boot with no enhancedload, so the JS never wires and
   the C# ToggleCapture is what fires. A JS-stamped body class was therefore correct on web and dead on device (seen
   red on a Pixel 7a: the bubble sat on top of the Video spoke). The state itself is the trigger, so both paths work.
   :has() is already a dependency here — kanine-feedback.css uses it to hide this same bubble on the build surface. */
body:has(.kl-capture-radial.is-open) .kl-feedback-fab { opacity:0; pointer-events:none; }
/* …and an OPEN MENU stands it down for the same reason, measured 2026-09-13. The FAB is `z-index:10000`
   deliberately (above Blazor's reconnect overlay); every menu panel in this product is `z-index:50`. So the FAB
   paints over any panel it overlaps, and `elementFromPoint` across the row returns the FAB, not the item.

   FOUND BY FIXING SOMETHING ELSE, which is the part worth keeping. Before the account hub was contained, its last
   rows ran BEHIND the bottom bar and never reached the FAB's band at all. Containing the panel moved those rows up
   into it, and at 320x568 the FAB then owned the right-hand end of SIGN OUT — a destructive control whose far edge
   silently opened the feedback sheet instead. MEASURED across the row at 320x568, left to right, after the fix and
   before this rule: `111111111F` — nine tenths live, the last tenth the FAB.

   The row is not the point. Any panel that reaches the bottom-trailing corner meets the FAB, and every menu in the
   product is now clamped INTO that corner by §5j-b, so the contract created the overlap for all of them at once.
   A press aimed at a menu belongs to the menu. Scoped to a menu that is genuinely OPEN: no `[data-kl-menu-panel]`
   panel exists in the DOM while closed (verified on twelve routes), and the account hub's panel DOES persist inside
   its closed <details>, so that one is keyed on `[open]` and never on the panel. */
body:has([data-kl-menu-panel="anchor"], [data-kl-menu-panel="pointer"], [data-kl-menu-panel="static"],
         details.kl-shellnav-accountmenu[open]) .kl-feedback-fab { opacity:0; pointer-events:none; }
/* Honour reduced-motion: the spokes still appear/disappear, they just do not fly or scale. */
@media (prefers-reduced-motion: reduce) {
  .kl-capture-spoke,
  .kl-capture-radial.is-open .kl-capture-spoke {
    transition:opacity 100ms ease;
    transform:translate(calc(var(--k-spoke-x) - 50%), calc(var(--k-spoke-y) - 22px));
  }
  .kl-shellnav-fab, .kl-shellnav-fab-glyph { transition:none; }
}
/* A sheet row that must be a <button> (the sign-out POST) reads exactly like the <a> rows. */
.kl-shellnav-link--btn {
  appearance:none; background:transparent; border:0; margin:0;
  inline-size:100%; text-align:start; cursor:pointer;
}

/* ---- SHELL BACK ARROW: hidden ONLY when pressing it would do nothing (owner ruling 2026-07-24) --------------
   "The back arrow can be hidden if there is no history and it would not go anywhere but back to where we are. But
   if I just came from record listing and I'm home now, I could press the back arrow and go back."

   BEHAVIOURAL, NEVER ROUTE-BASED. The rule is an AND of two independently-sourced signals, and the arrow hides only
   when BOTH actively assert a no-op:
     html[data-shellhistory="none"]  — kanine-shell-history.js: this TAB has no in-app history behind it
     .kl-shellnav-back[data-back-noop="route"] — ShellBackNoOp (C#): this route's TASK_ROOTS parent IS this route

   So "/" reached from /records keeps a working arrow (history says otherwise), and a cold deep link to /records/new
   keeps one too (its parent /records genuinely goes somewhere). Only the intersection — a cold arrival on a page
   whose parent is itself — is a dead control, and that is what disappears.

   FAIL VISIBLE BY CONSTRUCTION: this is the ONLY rule that hides the arrow, and it needs both attributes present.
   A missing, errored or never-ran signal (storage denied, JS blocked, an unknown route) omits one of them and the
   arrow stays on screen. A present-but-occasionally-useless arrow beats a missing one on a page that needs it.

   display:none rather than visibility/opacity so the dead control also leaves the accessibility tree and the tab
   order — a control that cannot do anything should not be reachable by keyboard or screen reader either. The
   element itself stays in the DOM, so data-testid="shell#topbar.k-back" is never removed from the markup.

   NO LAYOUT SHIFT: .kl-shellnav-spacer (flex:1 1 auto) sits between the left controls and the right cluster and
   absorbs the freed width, so the right-hand cluster stays right-aligned and the left controls precede the arrow —
   nothing moves. The stamp is applied pre-paint, so the arrow is absent from the FIRST frame rather than painted
   and then removed. */
html[data-shellhistory="none"] .kl-shellnav-back[data-back-noop="route"] { display:none; }

/* ---- SHELL RAIL SHOW/HIDE (owner ruling 2026-07-24, extended same day) --------------------------------------
   The topbar control that shows or hides the left rail, on any screen size at or above 1025px, remembered per
   device. ONE preference with THREE values — `pinned`, `collapsed`, or ABSENT (= whatever this width does by
   default) — carried by one localStorage key + one cookie flag + one <html data-shellrail> attribute. It is
   deliberately an ABSOLUTE state and not an "invert the width default" flag: an inverting flag would mean that
   collapsing at 1440 and then narrowing to 1280 brought the rail BACK, which is nonsense. Absolute means the
   answer to "is the sidebar showing?" only ever changes when the user presses the button (the <=1024 phone band
   is the one exception — see below).

     >=1281  default PINNED    · press → collapsed
     1025-1280 default HIDDEN  · press → pinned (240px rail + the content beside it, bottom nav stands down)
     <=1024  always the drawer; the toggle is HIDDEN so it can never be a no-op control (the same UI-GUAR-015
             §H.1 #4 rule that hides the hamburger where the rail already shows). A stored preference is kept,
             not cleared, so it takes effect again as soon as the window is wide enough to honour it.

   Two glyphs, one shown per EFFECTIVE state, so the icon reports the state without a label swap: a chevron
   pushing the rail away when it is showing, a hamburger offering it back when it is hidden. `.kl-shellnav-iconbtn`
   gives it the >=44px WCAG touch floor. */
.kl-shellnav-railtoggle { display:none; }
/* Default (no stored preference): >=1281 the rail shows, so the button offers "hide". */
.kl-shellnav-railtoggle .kl-railtoggle-collapsed { display:none; }
/* Default in the 1025-1280 band: the rail is hidden, so the button offers "show". */
@media (max-width: 1280px) {
  html:not([data-shellrail="pinned"]) .kl-shellnav-railtoggle .kl-railtoggle-pinned { display:none; }
  html:not([data-shellrail="pinned"]) .kl-shellnav-railtoggle .kl-railtoggle-collapsed { display:inline; }
  html:not([data-shellrail="pinned"]) .kl-shellnav-railtoggle { background:var(--k-teal-500); color:var(--k-ink-900); }
}
/* An EXPLICIT preference overrides the width default in both directions. Declared after the media block because
   these carry the same specificity — source order is what settles the tie. */
html[data-shellrail="collapsed"] .kl-shellnav-railtoggle .kl-railtoggle-pinned { display:none; }
html[data-shellrail="collapsed"] .kl-shellnav-railtoggle .kl-railtoggle-collapsed { display:inline; }
html[data-shellrail="pinned"] .kl-shellnav-railtoggle .kl-railtoggle-pinned { display:inline; }
html[data-shellrail="pinned"] .kl-shellnav-railtoggle .kl-railtoggle-collapsed { display:none; }
/* The state cue is CSS-driven off <html>, NOT off the aria-pressed the JS stamps: on the Blazor-Hybrid head the
   shell renders POST-boot, long after the applicator's DOMContentLoaded sync, so an aria-pressed-only cue would be
   stale there. aria-pressed still carries the state for assistive tech (and the shared iconbtn rule paints it). */
html[data-shellrail="collapsed"] .kl-shellnav-railtoggle { background:var(--k-teal-500); color:var(--k-ink-900); }
html[data-shellrail="pinned"] .kl-shellnav-railtoggle { background:transparent; color:rgb(255 255 255 / .82); }

/* Desktop (>=1281px): the rail is a FIXED full-height left column; the rest of the
   shell — the top bar, the legacy header and the page content — flows to the RIGHT of
   it via a matching inline-start offset on .kl-shell. THIS is the rule that was
   missing: without it the rail rendered as a 240px block stacked ABOVE the header and
   content (a left bar with blank space to its right) instead of beside them. Below
   1281px the rail collapses to a drawer (the max-width:1280px block) and the offset is
   dropped so the content reclaims the full width. The parent's box-sizing:border-box +
   max-inline-size:100% keep the 240px offset INSIDE the viewport, so the §6.2
   zero-horizontal-overflow guarantee still holds at 1281 / 1440 / 1920.

   BREAKPOINT RAISED 1025 -> 1281 (owner ruling 2026-07-24): a landscape tablet is ~1280 CSS px wide, and there a
   240px fixed rail left the content visibly cramped. The owner's call was that the tablet should HIDE the rail and
   give the content the width, so 1280-and-below now gets the drawer + bottom nav. Three sibling rules move with it
   or the band breaks: the drawer/bottom-nav block below, the <main> bottom-bar clearance in MainLayout.razor.css,
   and the feedback FAB lift in kanine-feedback.css. The CRAMPED-TOPBAR hides (search / notifications / persona /
   account cluster) deliberately did NOT move — they were measured and justified at 320px, and a 1280 landscape
   tablet has ample room for all four (see the max-width:1024px block below).

   Every rule here is gated on html:not([data-shellrail="collapsed"]) so the user's own collapse preference wins at
   desktop widths; when collapsed, the rail block below takes over at every width. */
@media (min-width: 1281px) {
  html:not([data-shellrail="collapsed"]) .kl-shellnav-rail {
    position:fixed; inset-block:0; inset-inline-start:0;
    inline-size:240px; z-index:50;
    /* overflow:hidden is inherited from the base rule — the rail never scrolls as a whole; the brand stays pinned and
       the inner .kl-shellnav-list is the SOLE scroll region, so every persona's items (incl. Expenses) are reachable. */
  }
  html:not([data-shellrail="collapsed"]) .kl-shell { padding-inline-start:240px; }
  /* UI-GUAR-015 (owner ruling §H.1 #4): the hamburger is HIDDEN where the rail already shows (the fixed desktop
     column above), so it is never a no-op toggle. When the user has COLLAPSED the rail the hamburger stays hidden
     too — the rail toggle is the one control that governs the rail at these widths, so there is never a second,
     competing nav opener. */
  .kl-shellnav-menu { display:none; }
}

/* The show/hide toggle renders from 1025 up — at 1024 and below the rail is unconditionally a drawer, so the
   control would be a no-op there. Owner UX (2026-06-29): the FIRST top-bar control sat flush against the rail's
   right edge and its round hover highlight read as "cut off" by the left nav; that first control is now the rail
   toggle (the hamburger is hidden wherever the rail shows, and Back follows the toggle), so the nudge moved with
   it and the arrow no longer carries its own. */
@media (min-width: 1025px) {
  .kl-shellnav-railtoggle { display:inline-flex; margin-inline-start:var(--k-space-2); }
}

/* ---- EXPLICITLY PINNED IN THE 1025-1280 BAND (owner ruling 2026-07-24) --------------------------------------
   The band's DEFAULT is the drawer + bottom nav (a landscape tablet reclaiming the rail's 240px). A user on a
   1280-wide LAPTOP wants the opposite, so pressing the toggle here restores the fixed rail column — the same
   layout >=1281 gets. The bottom nav and its <main> clearance stand down with it: the rail IS the navigation
   when it is pinned, so keeping a bottom bar would duplicate every destination and spend 56px of vertical space
   on it (principle B1). Scoped to >=1025 so a phone can never land in this state. */
@media (min-width: 1025px) and (max-width: 1280px) {
  html[data-shellrail="pinned"] .kl-shellnav-rail {
    position:fixed; inset-block:0; inset-inline-start:0;
    inline-size:240px; z-index:50; display:flex; box-shadow:none;
  }
  html[data-shellrail="pinned"] .kl-shell { padding-inline-start:240px; }
  /* The rail is the nav again ⇒ the hamburger would be a no-op, and the bottom bar a duplicate. */
  html[data-shellrail="pinned"] .kl-shellnav-menu { display:none; }
  html[data-shellrail="pinned"] .kl-shellnav-bottom { display:none; }
  /* The feedback FAB's bottom-bar lift (kanine-feedback.css:38-45) stands down with the bar it was clearing.
     Physical `bottom` on purpose — it is the exact property that rule sets, and this selector out-specifies it. */
  html[data-shellrail="pinned"] .kl-feedback-fab { bottom:20px; }
}

/* Narrow widths: the rail collapses to a drawer (k-menu toggles is-open); the bottom nav appears. */
@media (max-width: 1280px) {
  .kl-shellnav-rail { position:fixed; inset-block:0; inset-inline-start:0; z-index:60; display:none; box-shadow:var(--k-elev-3); }
  .kl-shellnav-rail.is-open { display:flex; }
  /* DOCKED for real (owner mobile-nav pass 2026-07-03): the old position:sticky was a NO-OP — ShellNav renders
     near the TOP of the document flow, and sticky bottom:0 never moves an element below its flow position, so the
     "bottom" bar rendered as a second row under the topbar. position:fixed docks it to the viewport bottom (§7
     thumb reach); z-index 55 sits under the drawer/sheet (60) and the modal overlay (80). env(safe-area-inset-bottom)
     keeps the row of targets above an iOS home indicator. */
  .kl-shellnav-bottom {
    position:fixed; inset-inline:0; inset-block-end:0; z-index:55;
    display:flex; align-items:stretch;
    min-block-size:var(--k-bottomnav-h);
    padding-block-end:env(safe-area-inset-bottom, 0px);
    background:var(--k-ink-800); border-block-start:1px solid rgb(255 255 255 / .08);
  }
  /* The fixed bar is out of flow, so the routed content must clear it: the bar-height clearance on the shell's
     <main> lives in MainLayout.razor.css (its scoped padding-block rule OWNS main's padding and would out-order an
     equal-specificity override here) — see the max-width:1280px block there. */
}

/* CRAMPED TOPBAR — a SEPARATE, NARROWER query on purpose (owner ruling 2026-07-24 kept these at <=1024 when the
   drawer band moved up to <=1280). Everything below is a PHONE-width space concession measured at 320px; a 1280
   landscape tablet has ample topbar room, so folding these into the drawer query would silently strip four
   affordances (search, notifications, the persona label and the whole account cluster) off every tablet. */
@media (max-width: 1024px) {
  /* UI-GUAR-036 (R2-25): the topbar holds hamburger + back + the theme toggles + account at phone widths. With every
     icon button now a non-shrinkable ≥44² target (the WCAG floor), six of them + the persona + sign-out overflowed
     320px (the live sweep measured documentElement scrollWidth 413 > 320). The two NAV icon buttons (search,
     notifications) are fully reachable on mobile via the rail drawer (hamburger) AND the bottom-nav, so they are
     hidden from the cramped topbar here — no function is lost. The theme/outdoor toggles are KEPT (they live ONLY on
     the topbar, so they must stay reachable on mobile), and the long persona label collapses so the row fits with
     full-size touch targets and zero horizontal overflow. */
  .kl-shellnav-iconbtn[href="/records/search"],
  .kl-shellnav-iconbtn[href="/notifications"] {
    display:none;
  }
  .kl-shellnav-persona { display:none; }
  /* The sign-out + persona cluster CROWDS the account avatar at phone widths — the topbar ran out of room and the
     cluster overlapped the 48² avatar (axe: only ~7.5px of unobscured clickable space). Hide the in-topbar cluster
     on mobile; account / sign-out / sign-in all live in the topbar account HUB dropdown at these widths (the
     avatar the user taps), so the avatar gets its full, unobscured ≥44 touch target back. */
  .kl-shellnav-account-cluster { display:none; }
}

/* ---- COLLAPSED RAIL, ANY WIDTH (owner ruling 2026-07-24) ---------------------------------------------------
   The user's per-device preference, persisted by kanine-theme.js and stamped on <html> BEFORE first paint (and
   re-emitted by the server during prerender from the same kanine_theme cookie), so there is no flash of the wrong
   shell and no dependence on a circuit. HEAD-AGNOSTIC: the trigger is an ATTRIBUTE ON <html>, never a class a
   JS wiring pass stamps on the shell — the web shell is static SSR while the Blazor-Hybrid head renders it
   POST-boot with no enhancedload, so a shell-stamped class is correct on web and dead on device (kanine-design-
   system.css:1494 records the Pixel 7a proof of exactly that failure).

   Specificity note: `html[data-shellrail="collapsed"] .kl-shellnav-rail` is (0,2,1), so it beats the plain
   `.kl-shellnav-rail` rules in the media blocks above regardless of source order; the .is-open companion is
   (0,3,1) and still wins over it. The rail is given the full DRAWER treatment rather than a bare display:none so
   a drawer opened at <=1280 stays correct if the window is then widened past the desktop breakpoint. */
html[data-shellrail="collapsed"] .kl-shellnav-rail {
  position:fixed; inset-block:0; inset-inline-start:0; z-index:60; display:none; box-shadow:var(--k-elev-3);
}
html[data-shellrail="collapsed"] .kl-shellnav-rail.is-open { display:flex; inset-block-start:56px; }
/* Keep the topbar (and with it the rail toggle) ABOVE an open drawer, so the control that governs the rail is
   never covered by the rail. The web head carries the same pairing for the <=1280 band in app.css; declaring it
   here as well is what makes the collapsed state correct on the native head too. Topbar box =
   min-block-size:48px + 2x--k-space-1 padding + 1px border ≈ 56px, matching the inset above. */
html[data-shellrail="collapsed"] .kl-shellnav-topbar { position:sticky; inset-block-start:0; z-index:70; }

/* =============================================================================
   8. R11 CROSS-CUTTING PRIMITIVES (opt-in classes; additive — a page must carry
   the class, so the ~35 already-wired-but-unpolished screens are NOT disturbed).
   These are the highest-leverage punch-list fixes (R11-AESTHETIC-PUNCHLIST.md
   TOP §2–§4): a real data-table treatment, a segmented/chip selected-state, and
   a content-measure wrapper that kills the "grey void" on sparse pages. Built
   here ONCE so every page-major sign-off EXTENDS them instead of reinventing.
   No gradients, no body-bg change → the R11 token/canvas gate holds.
   ========================================================================== */

/* ---- 8a. Content measure (kill the empty-canvas void; punch-list TOP-4) -----
   A page wraps its content in <div class="kl-page"> to get a centered, readable
   measure instead of stretching edge-to-edge as a sparse grey field. -wide opts
   a single page out to the full content max (dense tables/dashboards). */
.kl-shell-main .kl-page {
  inline-size: 100%;
  max-inline-size: 64rem;          /* ~1024px readable measure for forms/lists */
  margin-inline: auto;
  display: flex; flex-direction: column; gap: var(--k-space-4);
}
/* Phone-width rhythm (owner ruling 2026-07-24, D11): tighten the inter-card gap from 16px to 12px on
   narrow viewports — vertical space is scarce on a phone (principle B1). Shared here, for ALL screens, so
   the tightening cannot drift the way it did when it lived as a per-page patch on /records. The MAUI field
   head is always in this range and inherits it. */
@media (max-width: 640px) {
  .kl-shell-main .kl-page { gap: var(--k-space-3); }
}
.kl-shell-main .kl-page--wide { max-inline-size: var(--k-content-max); } /* 1120px for dense screens */
.kl-shell-main .kl-page--narrow { max-inline-size: 40rem; }             /* centered single-card (login, one-draft) */
/* Operator mission-control boards (feedback kanban, leads pipeline) are full-bleed dashboards, NOT reading
   surfaces: opt out of the measure cap so their columns fill the whole shell width instead of scrolling inside a
   1120px box on a wide monitor. Declared AFTER --wide so it wins when both classes are present. */
.kl-shell-main .kl-page--board { max-inline-size: none; }
/* A vertically-centered hero surface for genuinely sparse pages (login persona card): presence, not abandonment. */
.kl-shell-main .kl-hero {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--k-space-4); min-block-size: 60vh; text-align: center;
}

/* ---- 8b. Data table (punch-list TOP-3 — the strongest "unstyled" evidence) --
   <div class="kl-tablewrap"><table class="kl-data-table"> … </table></div>
   Elevated card wrapper (rounded, clips the header, scrolls a wide table INSIDE
   itself so a narrow viewport never gets a page-level horizontal scrollbar), a
   styled sticky header with sort carets, row dividers + hover. The §5n guard
   already re-asserts native <table> display; this only adds skin, never layout. */
.kl-shell-main .kl-tablewrap {
  background: var(--k-surface);
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-lg);
  box-shadow: var(--k-elev-1);
  overflow-x: auto;                /* wide table scrolls inside the card — see §8b-s for why auto alone is not enough */
  overflow-y: hidden;
  max-inline-size: 100%;
}

/* ---- 8b-s. SCROLL CUE — the shared affordance for "this region continues off-screen" -------------
   Owner ask 2026-09-14, raised on /feedback in a 1280x800 landscape-tablet window:
   "on this page on the tablet view, there isnt a click and drag scroll bar you are assumed to have
   touch. can we check for this pattern across grids similar to this one and fix across the app?"

   WHAT WAS ACTUALLY WRONG, measured live at 1280x800 (headed Chromium — see the note below on why
   headless cannot answer this). The bar on that page was NOT suppressed: scrollbar-width computed to
   auto and a classic 15px horizontal bar WAS drawn. It sat at the container's bottom edge, y=1166,
   in an 800px viewport — 366px BELOW THE FOLD. A real pointer drag on the track at that position
   moved scrollLeft by 0, because the track is not in the viewport to press. The comment above used
   to read "auto = reachable"; auto is reachable only by a swipe, a trackpad two-finger scroll, or a
   shift-wheel. A mouse user sees a table cut at the right edge with no bar and no cue, which is
   exactly the report.

   THE FIX IS A CUE, NOT A BAR. The bar already exists and the keyboard path already works (measured:
   5 tabs reach a cell control, then ArrowRight scrolls the container to its maximum). What was
   missing is the one thing a pointer user needs and never had — a signal, inside the viewport, that
   the content continues. This paints one.

   SELF-CANCELLING, WITH NO JAVASCRIPT AND NO LAYOUT COST. Four background layers: two opaque
   "covers" pinned to the CONTENT box (background-attachment: local, so they travel with the content)
   and two shadows pinned to the PADDING box (attachment: scroll, so they stay put). At the start of
   the scroll the left cover sits over the left shadow and hides it; scroll right and the cover
   travels away, revealing the shadow that says "there is content back that way". The right pair
   behaves the same in reverse. A region that does not overflow shows neither shadow, so this can
   never paint a false cue on a table that fits.

   MEASURED on /feedback at 1280x800, luma of the container's edge against its middle:
     before, scrollLeft=0    right cue  0   (nothing — the defect)
     after,  scrollLeft=0    right cue 26
     after,  scrollLeft=max  left cue  44, right cue 0   (correctly cancelled at the end)

   COST: none. Backgrounds do not take layout space, so no control moves, nothing is squeezed below
   its hit area, and there is no layout shift. That is why this is a background and not a scrollbar
   or an arrow button — see §A6.

   NOT GATED ON POINTER TYPE, DELIBERATELY. The obvious lever is a hover/pointer media query, and it
   is the wrong one here. `(hover: hover) and (pointer: fine)` reports on the PRIMARY input, so a
   touch tablet with a trackpad attached — the exact machine in the report — answers "coarse" and
   would be EXCLUDED from the fix by that query. `any-pointer: fine` would include it, but a cue that
   costs nothing and reads as "there is more this way" helps a touch user just as much as a mouse
   user, so there is nothing to gate. Touch behaviour is unchanged: no bar is promoted, no space is
   taken, no hit area moves.

   WHY THE TABLE GOES TRANSPARENT. `.kl-data-table` carries its own `background: var(--k-surface)`,
   the SAME token the wrapper paints, so it is redundant where the wrapper shows — but being opaque
   it would paint over these gradients and hide them. Dropping it is visually inert and is scoped to
   tables INSIDE a cue container, so a `.kl-data-table` used without the wrapper keeps its own
   background. The sticky `thead` keeps its `--k-surface-2` and simply sits above the cue.

   THE COVER COLOUR IS A TOKEN. The two cover layers must match the colour BEHIND the region, or they
   read as two slabs instead of disappearing. `--k-scrollcue-surface` defaults to --k-surface, which is
   right for a card-backed grid. A region sitting on another canvas sets the token; it never re-declares
   the gradients.

   MEASUREMENT NOTE FOR WHOEVER CHECKS THIS. A HEADLESS Chromium draws OVERLAY scrollbars and reports
   a 0px gutter for every scroll container in the app, suppressed or not, and no launch flag changes
   it. Every "the scrollbar is missing" reading taken headless is worthless. Measure this headed.
   ------------------------------------------------------------------------------------------------ */
.kl-shell-main .kl-scrollcue {
  background-image:
    linear-gradient(to right, var(--k-scrollcue-surface, var(--k-surface)) 30%, rgb(0 0 0 / 0)),
    linear-gradient(to left,  var(--k-scrollcue-surface, var(--k-surface)) 30%, rgb(0 0 0 / 0)),
    radial-gradient(farthest-side at 0    50%, rgb(0 0 0 / .22), rgb(0 0 0 / 0)),
    radial-gradient(farthest-side at 100% 50%, rgb(0 0 0 / .22), rgb(0 0 0 / 0));
  background-repeat: no-repeat;
  background-size: 44px 100%, 44px 100%, 16px 100%, 16px 100%;
  background-position: left center, right center, left center, right center;
  background-attachment: local, local, scroll, scroll;
}
/* The block-axis twin, for a region that scrolls DOWN rather than across (a capped list, a dialog
   body). Same four-layer idiom rotated a quarter turn. */
.kl-shell-main .kl-scrollcue--block {
  background-image:
    linear-gradient(to bottom, var(--k-scrollcue-surface, var(--k-surface)) 30%, rgb(0 0 0 / 0)),
    linear-gradient(to top,    var(--k-scrollcue-surface, var(--k-surface)) 30%, rgb(0 0 0 / 0)),
    radial-gradient(farthest-side at 50% 0,    rgb(0 0 0 / .22), rgb(0 0 0 / 0)),
    radial-gradient(farthest-side at 50% 100%, rgb(0 0 0 / .22), rgb(0 0 0 / 0));
  background-repeat: no-repeat;
  background-size: 100% 44px, 100% 44px, 100% 16px, 100% 16px;
  background-position: center top, center bottom, center top, center bottom;
  background-attachment: local, local, scroll, scroll;
}
/* THE MASK TWIN, for a region whose CHILDREN are opaque. A container paints its background BEHIND its
   children, so the shadow idiom above is invisible wherever the content carries its own surface — measured
   on the operator Kanban, whose lanes are --k-surface-2 slabs: cue delta 0 with the shadows applied. A mask
   composites over the children instead, so it always shows. Cost: it is not self-cancelling, so a container
   that adopts it must reserve `padding-inline-end` at least as deep as the fade, and then at full scroll the
   fade sits over empty space rather than over the last lane. That is the same trade RecordList's quick-filter
   rail already documents; this is that idiom, named and shared rather than copied a third time.
   Trailing edge only: "there is more this way" is the claim, and a leading fade at scrollLeft 0 would dim the
   first item for no reason. */
.kl-shell-main .kl-scrollcue--fade {
  -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 28px), transparent 100%);
  mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 28px), transparent 100%);
}

/* An opaque table would paint over the cue. Scoped to a cue container so an unwrapped table keeps
   its own background (0,3,0 beats the 0,2,0 that sets it). */
.kl-shell-main .kl-scrollcue > .kl-data-table { background: transparent; }

/* THE SHARED GRID WRAPPER GETS THE CUE BY DEFAULT. Every registry in the product renders its table
   into `.kl-tablewrap`, so putting the cue here fixes the whole family from one place rather than
   page by page. A page-scoped scroller that is not a data grid opts in with `.kl-scrollcue`. */
.kl-shell-main .kl-tablewrap {
  background-image:
    linear-gradient(to right, var(--k-scrollcue-surface, var(--k-surface)) 30%, rgb(0 0 0 / 0)),
    linear-gradient(to left,  var(--k-scrollcue-surface, var(--k-surface)) 30%, rgb(0 0 0 / 0)),
    radial-gradient(farthest-side at 0    50%, rgb(0 0 0 / .22), rgb(0 0 0 / 0)),
    radial-gradient(farthest-side at 100% 50%, rgb(0 0 0 / .22), rgb(0 0 0 / 0));
  background-repeat: no-repeat;
  background-size: 44px 100%, 44px 100%, 16px 100%, 16px 100%;
  background-position: left center, right center, left center, right center;
  background-attachment: local, local, scroll, scroll;
}
.kl-shell-main .kl-tablewrap > .kl-data-table { background: transparent; }

/* §8b-t — TOUCH-REACHABLE INLINE LINK. One shared answer to A6 for a link that lives inside prose.
   Added 2026-09-14 under the owner's R1 ruling (touch targets are handled page by page as each page comes
   up for review, not in one sweep). It exists because the same hand-rolled pattern had already been copied
   into two pages and the copies did not agree.

   THE PROBLEM. An inline link's ink is ~16-20px tall, well under the 44px touch floor, and simply padding
   it apart would change the paragraph's line rhythm on every page that carries one. The house answer is a
   transparent, absolutely-positioned ::after that costs zero layout: the ink stays text-sized and the
   pressable band is 48px. The anchor's own getBoundingClientRect() therefore still reports ~16-20px BY
   DESIGN. Prove this with document.elementFromPoint down the control's centre line after
   scrollIntoView({block:'center'}), never with a box measurement.

   THE TRAP THIS CLASS EXISTS TO CLOSE, measured on /admin/weather-key 2026-09-14. As a plain inline, an
   anchor FRAGMENTS when it wraps across two line boxes. An absolutely-positioned ::after on a fragmented
   inline resolves its containing block from ONE fragment, so the band collapses and the real hit span
   falls to 1px. Worse, getBoundingClientRect() then returns the union of BOTH fragments and reports a
   LARGER number than before — so a box reading does not merely miss the defect, it disguises it. That
   link measured a healthy 49px on a phone (one line) and 1px on desktop (two lines) from the same CSS.
   `display: inline-block` cannot fragment, which is why it is load-bearing here and not cosmetic. The
   cost is that the link's words wrap as a unit.

   BEFORE USING IT, CHECK WHAT THE BAND COVERS. It hangs ~16px above and below the ink. Over prose that is
   free. Over a neighbouring CONTROL it steals that control's press — which is why /notifications'
   obligation-for was deliberately left as plain text rather than given a band. Read the inset together
   with any transform: an inset that looks downward-only is centred once a translate is applied.

   NOT FOR A CARD TITLE AT <= 640px. §8b-m clips the pseudo there; the computed box looks right and the
   hit span still reads ~20. Use a block-level control instead. */
.kl-touchlink {
  position: relative;
  display: inline-block;
  border-radius: var(--k-radius-sm);
}

.kl-touchlink::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  inset-block-start: 50%;
  block-size: var(--k-touch);
  min-inline-size: var(--k-touch);
  transform: translateY(-50%);
}


.kl-shell-main .kl-data-table {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: 14px;
  background: var(--k-surface);
}
.kl-shell-main .kl-data-table caption {
  text-align: start;
  padding: var(--k-space-3) var(--k-space-4) 0;
  font-size: 13px; color: var(--k-text-muted);
}
.kl-shell-main .kl-data-table thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--k-surface-2);
  color: var(--k-text-muted);
  font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  text-align: start; white-space: nowrap;
  padding: var(--k-space-3) var(--k-space-4);
  border-block-end: 1px solid var(--k-border);
}
.kl-shell-main .kl-data-table tbody td {
  padding: var(--k-space-3) var(--k-space-4);
  border-block-end: 1px solid color-mix(in srgb, var(--k-border) 55%, transparent);
  color: var(--k-text); vertical-align: middle;
}
.kl-shell-main .kl-data-table tbody tr:last-child td { border-block-end: 0; }
.kl-shell-main .kl-data-table tbody tr { transition: background var(--k-dur-1) var(--k-ease); }
.kl-shell-main .kl-data-table tbody tr:hover { background: var(--k-surface-2); }
.kl-shell-main .kl-data-table tbody tr[role="button"], .kl-shell-main .kl-data-table tbody tr.kl-row-nav { cursor: pointer; }
/* Header sort button: looks like the header label, carries an aria-sort-driven caret (no layout shift on flip). */
.kl-shell-main .kl-data-table th .th-sort {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  font: inherit; font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
  color: var(--k-text-muted);
  /* A6 (measured 2026-09-13). `min-block-size:0` was here to defeat the §5c button family's 48px floor, and it
     did: the four /records column-header buttons measured an 18px box and an 18px CONTINUOUS hit span at
     1400x900. The header cell around them is 42.5px, so filling the cell would still land 1.5px short — the
     floor has to be the token.

     BLOCK AXIS ONLY, and `min-inline-size` STAYS AT ZERO. Column headers are horizontally adjacent, exactly like
     the breadcrumb row §5b-crumb floored by box rather than by expander: a 48px INLINE minimum on "Dog" would
     reach over the column boundary and lay a surface on "Handler". A block-axis box floor cannot overlay a
     neighbour in either axis — it grows the header row and nothing else. Measured after: header row 42.5 -> 48,
     each button hit 48, and each header still sorts its OWN column (proven with a real click on all four).

     `.th-sort` is used by ONE page today (RecordList) but the rule lives here, in the shared table vocabulary, so
     the next registry that adopts the idiom inherits the target instead of re-losing it. */
  min-block-size: var(--k-touch); min-inline-size: 0;
}
.kl-shell-main .kl-data-table th .th-sort:hover { color: var(--k-accent); }
.kl-shell-main .kl-data-table th .th-sort::after { content: "↕"; opacity: .35; font-size: 11px; }
.kl-shell-main .kl-data-table th[aria-sort="ascending"] .th-sort { color: var(--k-text); }
.kl-shell-main .kl-data-table th[aria-sort="ascending"] .th-sort::after { content: "▲"; opacity: 1; color: var(--k-accent); }
.kl-shell-main .kl-data-table th[aria-sort="descending"] .th-sort { color: var(--k-text); }
.kl-shell-main .kl-data-table th[aria-sort="descending"] .th-sort::after { content: "▼"; opacity: 1; color: var(--k-accent); }

/* ---- 8b-r. Two-line row identity (the cross-cutting cell vocabulary the pages already emit): a
   <span class="kl-row-title"> lead over a quiet <span class="kl-row-sub"> secondary line (e-mail, key, id).
   Both are plain inline spans, so with no display they weld into ONE string on ONE line — feedback 3ed53a15
   read "Smoke Probesmoke-admin@kaninesoftware.com" on the users-roles directory (measured: title right edge
   === sub left edge, same y). Block-level splits the stack; the sub-line stays muted + smaller so the name
   still leads the row. Lives HERE, not in a page's scoped sheet, because the pages that emit it are shared
   across heads via the Kanine.Presentation.Pages RCL. ---- */
.kl-shell-main .kl-row-title { display: block; font-weight: 600; color: var(--k-text); }
.kl-shell-main .kl-row-sub {
  display: block; margin-block-start: 2px;
  font-size: 12px; color: var(--k-text-muted);
}

/* ---- 8b-m. MOBILE CARD COLLAPSE for data tables (2026-07-16 mobile sweep) ----
   At phone widths a wide .kl-data-table inside .kl-tablewrap "works" only via an undiscoverable horizontal
   scroll — on the live sweep a wide registry showed only its first 3 columns and HID the rest entirely,
   and fixed-width tables (training aids) overlapped their own cells. OPT-IN class `kl-table-cards` on the
   <table> reflows each row into a stacked card at ≤640px: header row hidden, each <tr> becomes a bordered
   card, each <td> a labeled field (label from its data-label attribute). Desktop is untouched (media-scoped),
   markup unchanged except the opt-in class + data-label/kl-tcard-* annotations — same components, same teeth.
   Helpers: .kl-tcard-title (full-width lead cell), .kl-tcard-full (full-width field),
   .kl-tcard-hide (desk-only column dropped from the card), .kl-tcard-actions (top-right corner slot). */
@media (max-width: 640px) {
  .kl-shell-main .kl-tablewrap:has(> .kl-table-cards) {
    background: transparent; border: 0; border-radius: 0; box-shadow: none;
    overflow: visible;
    /* The §8b-s cue must stand down with the card, not just fade with it. Its two cover layers are
       painted in --k-surface to hide the shadows at the ends of the scroll, so over a TRANSPARENT
       wrapper they would read as two opaque slabs on the page canvas. The cards each carry their own
       background and this row does not scroll across, so there is nothing left for a cue to say. */
    background-image: none;
  }
  /* display overrides carry !important to beat the §5n table-structural guard (also !important, earlier). */
  .kl-shell-main .kl-table-cards { display: block !important; inline-size: 100%; }
  .kl-shell-main .kl-table-cards thead { display: none !important; }
  .kl-shell-main .kl-table-cards tbody { display: flex !important; flex-direction: column; gap: var(--k-space-3); }
  .kl-shell-main .kl-table-cards tbody tr {
    position: relative;
    display: flex !important; flex-wrap: wrap; align-items: baseline;
    column-gap: var(--k-space-5); row-gap: var(--k-space-2);
    background: var(--k-surface); border: 1px solid var(--k-border);
    border-radius: var(--k-radius-lg); box-shadow: var(--k-elev-1);
    padding: var(--k-space-3) var(--k-space-4);
  }
  .kl-shell-main .kl-table-cards tbody tr:hover { background: var(--k-surface); }
  .kl-shell-main .kl-table-cards tbody td {
    display: block !important; padding: 0; border: 0; min-inline-size: 0;
    overflow-wrap: anywhere;
  }
  .kl-shell-main .kl-table-cards td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-size: 10.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
    color: var(--k-text-muted);
  }
  /* ---- THE CARD'S TITLE LINE (§A10, owner ruling 2026-07-25) ------------------------------------------------
     ONE line carries the card's identity: [ title ..................... status badge ] [ ⋮ ]. A label alone on a
     row, a badge alone on a row and a kebab alone on a row are three instances of the same waste (§A9/§B1/§B2),
     and the owner reported all three on the same day. The lead cell is `.kl-tcard-title`; the row's status chip
     opts onto the line with `.kl-tcard-badge`; the overflow slot `.kl-tcard-actions` is PINNED to the corner.

     VERTICAL CENTRING is the point, not a detail. The three have different intrinsic heights — a 24px text line,
     a 24px padded pill and a 48px touch target — so any of `baseline`/`stretch`/`flex-start`, and any stray
     margin/padding/border on one of them, splits them onto three different centres (measured 25 / 32 / 35.5 on
     /records before this block existed). The line therefore declares a SINGLE height floor
     (`--k-tcard-line`) and everything on it is centred against that floor:
       · the title cell is `display:flex; align-items:center`, so its text — even a bare text node, which becomes
         an anonymous flex item — is centred rather than sitting on the cell's top edge;
       · the badge cell is centred with `align-self`;
       · the pinned slot is inset by exactly HALF the difference between the touch target and the line floor,
         which puts its centre on the line's centre and lets its A6 48px hit area bleed symmetrically into the
         card's own block padding. Nothing shrinks; only the empty space around the glyph leaves the flow. */
  .kl-shell-main .kl-table-cards .kl-tcard-title {
    order: -6;
    flex-basis: 100%;
    display: flex !important; align-items: center;
    min-block-size: var(--k-tcard-line);
    font-size: 16px; font-weight: 700;
    /* Space kept clear for the pinned corner slot. The slot is out of flow, so nothing else knows it is there —
       this margin is what stops a long value sliding under it (the 2026-07-24 overlap, closed by reserving the
       line rather than by giving the slot a whole row). It is one token-derived value, not a per-page constant:
       the slot is `--k-touch` wide at `--k-space-2` from the card edge, and the card's own inline padding is
       `--k-space-4`, so `--k-touch + --k-space-2` over-reserves by exactly one `--k-space-2` gutter. */
    margin-inline-end: var(--k-tcard-reserve);
    /* BELT AND BRACES, and it earns its keep. The margin above reserves the corner while the title is the last
       in-flow item on the line; when a badge rides the line the reserve moves to the badge — and if that badge is
       too wide to fit (the longest status labels at 320-393px) the badge wraps to a line of its own and the title
       is suddenly last again, with no reserve, running straight under the pinned slot (measured: the title cell
       reaching 40px into the slot on /records and /medical, 113px on /certifications). A max on the cell holds the
       clearance in EVERY one of those states without costing anything in the normal one. */
    max-inline-size: calc(100% - var(--k-tcard-reserve));
  }
  /* ---- THE FIRST-LINE BAND, and why the pin needed one (owner review 2026-09-09) ---------------------------
     THE DEFECT. Clause 5 above declares one centre for the three items, and the pin holds it with a FIXED
     `inset-block-start` sized for a one-line title. That inset is not wrong — it is the definition of the line:
     half the difference between the touch target and the line floor puts the slot's centre exactly
     `--k-space-3 + --k-tcard-line / 2` below the card's top edge. What was missing is that NOTHING held the
     title and the badge to that same band once the title cell grew past the floor: `align-items: center` centres
     the cell's CONTENT in whatever height the cell ends up with, so a two-line title moves the text (and, through
     the flex line's cross size, the badge) DOWN while the pinned slot stays on the line. Measured on
     /legal-hold at 320px, on the two released rows: title / badge / kebab centres 1426 / 1426 / 1420 — the
     kebab reading 6px high because the OTHER TWO had left the line. At 375/393/430 the title fits one line and
     all three agreed exactly, which is why sampling four phone widths never showed it.
     TWO CAUSES, and they are different. (1) On /legal-hold and /vehicles the cell is a flex ROW that also holds
     the `data-label` eyebrow every data cell gets, so "HELD RECORD" renders BESIDE the record number, taking
     62.7px of a 112.8px cell and wrapping to three 15.75px lines — the eyebrow, not the title, is what makes the
     cell 47.3px tall. §8b-m already deletes that eyebrow from the badge cell for the same reason ("on the title
     line the chip IS the label"); on the title line the TITLE is the label, and the caption is the §A9/§B1 waste
     this section exists to reclaim, here rendered sideways. (2) On /lookups (and any page that deliberately puts
     a subordinate line under the name) the cell is genuinely taller than the floor.
     ONE BOX ANSWERS BOTH. `content: ""` beats `attr(data-label)` — the class is repeated to out-specify the
     `td[data-label]` rule above rather than to depend on source order — and the same box is then a zero-WIDTH
     strut of exactly `--k-tcard-line`, so the cell's FIRST flex line is the band the pin is on however tall the
     rest of the cell becomes. Zero width, so it costs the title not one pixel of the line it is holding open. */
  .kl-shell-main .kl-table-cards .kl-tcard-title.kl-tcard-title::before {
    content: "";
    flex: 0 0 0;
    inline-size: 0;
    block-size: var(--k-tcard-line);
  }
  /* An element child ellipsizes rather than pushing the badge off the line — see the "what gives" note below. */
  .kl-shell-main .kl-table-cards .kl-tcard-title > * {
    min-inline-size: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* WITH a badge on the line the title stops being the full-width lead cell and becomes the elastic one.
     `flex-basis: 0`, NOT `auto`, and that is the whole trick: a wrapping flex container picks its LINES from base
     sizes BEFORE it shrinks anything, so a title with `basis:auto` bases at its full text width and pushes the
     badge onto a second row — making the card TALLER on exactly the long-title case that motivated the change.
     A zero basis asks for nothing and then GROWS into whatever the badge leaves.
     WHAT GIVES WHEN SPACE IS TIGHT: the TITLE ellipsizes, the badge stays whole. A truncated status
     ("Needs Appro…") is not merely unhelpful in a records product, it is misreadable; a truncated title is
     recoverable from context and from opening the row. The reserve moves to the badge, which now ends the line.
     The selector reaches the title THROUGH THE ROW rather than as `~ .kl-tcard-badge` from the title, because the
     badge is not always the later cell: /expenses renders Credit/Debit and /lookups renders its status pill BEFORE
     the title in DOM order (the table's desktop column order), and a sibling combinator silently misses those two —
     leaving the title full-width and the badge stranded on a line of its own, which is the defect verbatim. */
  .kl-shell-main .kl-table-cards tbody tr:has(> .kl-tcard-badge) > .kl-tcard-title {
    flex: 1 1 0;
    flex-basis: 0;
    min-inline-size: 4rem;
    margin-inline-end: 0;
  }
  /* THE BADGE IS CENTRED ON THE FIRST LINE, NOT ON THE FLEX LINE'S CROSS CENTRE. `align-self: center` centres the
     chip against whatever the TALLEST item on the row's first flex line turns out to be — which is the title
     cell, so a title cell taller than the floor carried the badge down with it and off the band the pin holds
     (measured /lookups at 393px: badge 885.3 against a pin at 855.1, 30px apart). `stretch` + a max of the line
     floor is the pair that fixes it without costing anything anywhere else:
       · when the badge SHARES the title line, stretch takes the line's cross size and the max clamps it back to
         `--k-tcard-line`, and a clamped stretch is placed at the line's cross-START — so the cell IS the band and
         the chip, centred inside it, lands on exactly the pin's centre whatever the title does;
       · when the badge is too wide for the line and WRAPS to a line of its own (the state the "belt and braces"
         note below describes, reproducible on /records + /certifications + /medical at 320px), that line's cross
         size is the chip's own height, the max never binds, and the card is unchanged to the pixel.
     `display: flex` carries !important for the same reason `.kl-tcard-actions` does — `tbody td` sets
     `display: block !important` and importance is settled before specificity. */
  .kl-shell-main .kl-table-cards .kl-tcard-badge {
    order: -5;
    flex: 0 0 auto;
    align-self: stretch;
    max-block-size: var(--k-tcard-line);
    display: flex !important; align-items: center;
    margin-inline-end: var(--k-tcard-reserve);
  }
  /* The eyebrow every data cell gets would read as a caption stacked above the chip — on the title line the chip
     IS the label. (The desktop table still has its column header; nothing is lost.) */
  .kl-shell-main .kl-table-cards .kl-tcard-badge[data-label]::before { content: none; }
  /* THE LINE BREAK, and why it has to be a generated box. Flexbox chooses line breaks by summing outer
     hypothetical sizes in order, so there is NO declaration you can put on the title, the badge or the slot that
     ends the line after them — anything big enough to force the break also pushes the badge itself onto the next
     line. The container's own ::after IS a flex item, so a zero-height 100%-basis box ordered between the badge
     and the body cells ends the line deterministically, at every width and for every badge label. Without it a
     short badge simply leaves room and the first body field joins the title line (measured: "Breed / German
     Shepherd" fits beside "Active" at 393px). The negative start margin pays back the extra row-gap the empty
     line would otherwise cost, so the break is free. */
  .kl-shell-main .kl-table-cards tbody tr:has(> .kl-tcard-badge)::after {
    content: "";
    order: -3;
    flex: 0 0 100%;
    block-size: 0;
    margin-block-start: calc(-1 * var(--k-space-2));
  }
  .kl-shell-main .kl-table-cards .kl-tcard-full { flex-basis: 100%; }
  /* A field the owner wants on a line of its own, at the BOTTOM of the card, wherever it sits in the table's
     column order (the case number on /records: "i just wish it was its own row at the bottom"). */
  .kl-shell-main .kl-table-cards .kl-tcard-foot { order: 9; flex-basis: 100%; }
  /* …and ABSENT, not blank, when it has no value: an empty labeled cell still paints its eyebrow, its box and a
     row-gap, which is the same wasted line by another name. This is an OPT-IN class the page sets from its own
     null check, deliberately NOT a generic "empty cell" rule — the action slots on records/dogs/vehicles/medical/
     training-aids all report empty textContent (a kebab's glyph is not text), so a generic rule would delete
     every row menu on every registry. */
  .kl-shell-main .kl-table-cards .kl-tcard-empty { display: none !important; }
  .kl-shell-main .kl-table-cards .kl-tcard-hide { display: none !important; }
  /* CARD ACTIONS — PINNED to the card's top-inline-end corner (owner ruling 2026-07-25).
     This REPLACES the full-width footer band approved on 2026-07-24. The owner's clarification is that the band
     "pertains to when showing in the list" — the desktop TABLE rendering — and not to the phone CARD, where a
     lone kebab on a row of its own cost a measured 57px per card on dogs / medical / training-aids /
     certifications / people and 49px on vehicles / records, for one control.

     THE 2026-07-24 OBJECTION IS ANSWERED, NOT IGNORED. The corner was abandoned because `position:absolute` takes
     the slot OUT OF FLOW, so no sibling knew the space was occupied and a long value ran underneath it. The fix
     is the reserve on the title line above (`--k-tcard-reserve`) plus the line floor (`--k-tcard-line`), which
     bound the slot in BOTH axes: horizontally nothing on the title line may enter the reserve, and vertically the
     slot cannot reach the next line because the floor is `--k-touch` minus the padding it bleeds into. The
     objection that a reserve "is a magic number that silently becomes wrong" is met by deriving it from the same
     tokens that size and inset the slot, and by the guard test that fails if they drift apart.

     A SLOT HOLDING MORE THAN ONE CONTROL KEEPS THE BAND — `.kl-tcard-actions--band` below. Lookups' proposed rows
     carry four actions (Approve / Merge / Fix code / Reject); those do not fit a corner at any width worth
     reserving on a 320px card, and that half of the 2026-07-24 reasoning still holds. The rule is therefore
     "ONE overflow control pins; a SET of inline actions bands", which is the same judgement §W4/B3 already makes
     about command bars.

     `display` CARRIES !important, and it has to. The `tbody td` rule above sets `display:block !important` (itself
     needed to beat the §5n table-structural guard), and importance is settled BEFORE specificity in the cascade —
     so no selector, however specific, can win against it. */
  /* THE SLOT IS ORDERED AFTER THE TITLE-LINE BREAK, not before it (owner review 2026-09-09). `order` does nothing
     at all for the PINNED form — an absolutely-positioned flex child is out of flow — so this number only ever
     described where the BANDED form sits, and -4 put it BEFORE the `::after` break box at -3. That was invisible
     while the band claimed `flex-basis: 100%`, because a 100% basis starts its own line whatever it is ordered
     against. It stopped being invisible the moment a page re-pointed that basis: `Lookups.razor.css` re-pointed the
     band to `flex: 1 1 auto` so the Order field could share the action row (owner request 2026-07-24), and the band
     then had nothing between it and the title — so the whole Edit-plus-kebab cell joined the TITLE'S first flex line
     and competed with the title at equal `flex-grow`, squeezing the title cell to 66px at 393px and leaving
     /lookups cards at 201px against a sibling band of 115-160px. (That page has since collapsed its actions into
     the row menu — owner ruling 2026-09-09 — so it no longer bands at all. The ordering stays, because the defect
     was in the SHARED contract: any page that re-points the basis reproduces it, and the guard now pins the two
     order values against each other so nobody has to re-derive this.)
     Clause 8's argument applies one line down: nothing you can declare on the band ends the line before it, so
     the break has to be a generated box — and there already IS one, at order -3. Ordering the slot after it makes
     "share the action row" expressible THROUGH the shared mechanism instead of against it: a page re-points the
     basis and gives its companion cell the same order, and the break still holds the title line. It costs no new
     box, no new class and no row-gap, and it changes nothing for a band that keeps its 100% basis. */
  .kl-shell-main .kl-table-cards .kl-tcard-actions {
    order: -2;
    position: absolute;
    inset-block-start: calc((var(--k-touch) - var(--k-tcard-line)) / 2);
    inset-inline-end: var(--k-space-2);
    inline-size: auto;
    display: flex !important; align-items: center; justify-content: flex-end; gap: var(--k-space-2);
    min-block-size: var(--k-touch);
    min-inline-size: var(--k-touch);
    margin: 0;
    padding: 0;
    border: 0;
  }
  /* A6 ON THE CARD. Two registries compact their row kebab to 30px for DESK density (`kl-data-table--compact` on
     vehicles, the dense-registry compaction on training aids). That is a desktop choice with a mouse behind it;
     on the phone card the same control is a thumb target, and 30px is below the WCAG 2.5.8 floor this repo holds.
     The slot already reserves `--k-touch`, so restoring the button to fill it costs no layout at all. Media-scoped
     — desktop density is untouched. */
  .kl-shell-main .kl-table-cards .kl-tcard-actions .kl-rowmenu-kebab {
    min-block-size: var(--k-touch);
    min-inline-size: var(--k-touch);
  }
  /* The multi-action opt-out: the pre-2026-07-25 footer band, for slots that hold a SET of controls. */
  .kl-shell-main .kl-table-cards .kl-tcard-actions--band {
    position: static;
    flex-basis: 100%;
    flex-wrap: wrap;
    justify-content: flex-start;
    inline-size: auto;
    margin-block-start: var(--k-space-1);
    padding-block-start: var(--k-space-2);
    border-block-start: 1px solid color-mix(in srgb, var(--k-border) 55%, transparent);
  }
  /* …BUT THE BAND IS NOT ALL-OR-NOTHING (owner ruling 2026-07-25, /approvals). A slot can legitimately hold both
     kinds of control at once, and they belong in different places:

       · PRIMARY DECISIONS keep the band. Approve / Reject on the approval queue are supervisor decisions, not
         overflow; burying either behind an extra tap would be felt on every row of a queue-clearing session.
       · The OVERFLOW control still corners, exactly as it does on a single-control slot. A kebab is the same
         affordance whatever it happens to share a cell with, and §A10's whole point is that it never owns a row.

     Expressing it on the CONTROL rather than on the cell is what makes it general: the row menu is always the same
     component, so this rule needs no per-page markup, no second opt-out class and no split <td> (which would
     change the DESKTOP table's column count for a phone-only concern). A slot with no row menu — Lookups' four
     value actions, the pending-invitation link + Revoke — matches nothing here and is untouched, which is the
     honest way for a rule to "not apply" rather than being excluded by name.

     The corner it lands in is already reserved: the title line's `--k-tcard-reserve` is unconditional, so nothing
     slides under this kebab any more than under a pinned slot's. */
  .kl-shell-main .kl-table-cards .kl-tcard-actions--band > .kl-rowmenu-wrap {
    position: absolute;
    inset-block-start: calc((var(--k-touch) - var(--k-tcard-line)) / 2);
    inset-inline-end: var(--k-space-2);
    min-block-size: var(--k-touch);
    min-inline-size: var(--k-touch);
    justify-content: flex-end;
  }
  /* The label pseudo-element that every data cell gets would read as a stray caption above the buttons. */
  .kl-shell-main .kl-table-cards .kl-tcard-actions[data-label]::before { content: none; }
}

/* ---- 8b-o. THE SAME SLOT, ON A REAL TABLE (>640px) — owner review 2026-07-31 ----------------------------------
   §8b-m pins `.kl-tcard-actions` to a CARD's corner, and that block is `max-width: 640px`. Above it the registry is
   a real <table> again and the class styled nothing at all, so a trailing actions cell was an ordinary <td>: its
   controls sat at the START of the column, which reads as "three pixels to the right of the last word of the last
   column" rather than as a slot at the row's edge. Reported on /training-aids; it was true of every registry.

   THE RULE ALREADY EXISTED — PRIVATELY, ONCE. `Approvals.razor.css` carried `.kl-queue .kl-col-action
   { text-align: end; white-space: nowrap; }` under its own class name, and its ≤640px block then had to undo it.
   That is verbatim the shape §A10 was written about: /records held the card title line privately and eight sibling
   registries could not inherit it. So this is a PROMOTION, not a new idea — same declarations, keyed on the slot
   class the whole product already marks, so every registry inherits it and Approvals' copy could be deleted.

   Keyed on `td.kl-tcard-actions` rather than on a page or a second opt-out class, and media-scoped to the exact
   complement of §8b-m so the two can never both apply: the card form positions the slot absolutely and owns its
   own alignment, and a stray `text-align` reaching it would fight the corner it has already been pinned to.

   `white-space: nowrap` is the other half and is not cosmetic: a slot holding a control PLUS a kebab wraps the
   kebab onto a second line the moment the column is narrow, which is the banded-footer look §A10 removed. */
@media (min-width: 641px) {
  .kl-shell-main table.kl-table-cards td.kl-tcard-actions {
    text-align: end;
    white-space: nowrap;
  }
}

/* ---- 8b-n. THE SAME TITLE LINE, OFF-TABLE (§A10 made universal, owner ruling 2026-07-25) ----
   The owner ruled ONE card anatomy for the whole product — title, then its badge, then the overflow, on line 1,
   with NO exemption for notification or triage surfaces. The argument he ruled against was that on a reminders /
   feedback board the badge is the scanning key and earns line 1; the argument he ruled FOR is teachability and
   guardability: one rule has no boundary to argue about and nothing to drift.

   THIS IS THE SAME RULE, NOT A SECOND COPY. It reuses the identical class vocabulary — `.kl-tcard-title`,
   `.kl-tcard-badge`, `.kl-tcard-actions` — so a reader learns three names once and the guard asks one question of
   every card in the product. What differs is only what the SURFACE supplies: §8b-m reflows a <table> whose cells
   are siblings and therefore needs a generated line break and a corner reserve; a purpose-built card already has a
   header element, so the line is just that element wearing `.kl-cardline`.

   THREE DELIBERATE DIFFERENCES from the table form, each measured rather than assumed:
     1. NOT media-scoped. A registry card only exists below 640px; a reminder card is a card at every width, so its
        anatomy is not a phone reflow and pretending otherwise would leave the inverted layout live on desktop.
     2. THE TITLE WRAPS — no ellipsis. A table card's title is a short type or call-name in a fixed-height row; an
        off-table title is a complaint line or a subject sentence (`/feedback/mine` line-clamps its snippet to two
        lines by design). Truncating those to one line would delete content the card exists to show.
     3. NO generated line break. §8b-m needs one because a table's body cells are the title's SIBLINGS and would
        otherwise join its line. A card's header element already bounds the line — and here the next item on it is
        usually something that SHOULD share the row (a relative timestamp, a category label), so forcing a break
        would spend the row §A10 just reclaimed. */
.kl-shell-main .kl-cardline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--k-space-2) var(--k-space-3);
}

/* The line only takes the touch-target floor when it actually carries a control — a badge and a title do not need
   48px of room, and forcing it on every card would hand back the height this ruling reclaimed. */
.kl-shell-main .kl-cardline:has(> .kl-tcard-actions) {
  min-block-size: var(--k-tcard-line);
}

.kl-shell-main .kl-cardline > .kl-tcard-title {
  order: -6;
  flex: 1 1 0;
  flex-basis: 0;
  /* `min-inline-size: 0` is not cosmetic: a flex item's automatic minimum is its CONTENT size, so without this a
     long subject refuses to shrink, pushes the badge off the line, and re-creates the exact defect. */
  min-inline-size: 0;
  margin: 0;
}

.kl-shell-main .kl-cardline > .kl-tcard-badge {
  order: -5;
  flex: 0 0 auto;
  align-self: center;
}

.kl-shell-main .kl-cardline > .kl-tcard-actions {
  order: -4;
  flex: 0 0 auto;
  /* Trailing edge without a magic reserve: the title grows, so the overflow is already last on the line. In flow,
     not pinned — an off-table card has no `position: relative` row to pin against and needs none, and staying in
     flow means no sibling can ever run underneath it. */
  margin-inline-start: auto;
  display: flex;
  align-items: center;
  gap: var(--k-space-2);
  min-block-size: var(--k-touch);
  min-inline-size: var(--k-touch);
  justify-content: flex-end;
}

/* ---- 8b-n2. TWO MORE ROLES ON THE SAME LINE: a LEADING control, and the SUBTITLE ---------------------------
   Owner asks 2026-09-14, on /tenant-settings/retention:
     "lets turn the agency setting plain button at the top into something more indicative of what that does …
      maybe pin it right on the same row as 'How long records are kept' title?"
     "Not sealed badge should not take up its own row either"

   §8b-n gave the line three roles — title, badge, overflow — and every one of them follows the title. A page head
   also carries a route OUT (a back affordance), and that role has to LEAD. `.kl-tcard-lead` is that slot: order
   -7, one step ahead of `.kl-tcard-title` at -6, and inflexible, so the title still takes every remaining pixel.

   MEASURED, and this is why the slot is a shared rule rather than a page's own. The retention head put its back
   control on a row of its own (top 85, the full 1033px measure) and its title on the next (top 157), which is the
   two-row head §A10 exists to close. Nothing about that is specific to retention: any detail head that leads with
   a back link has the same shape.

   `.kl-tcard-lead-where` is the part of the label a phone cannot afford. "Back to agency settings" measures 209px
   inside a ghost button. At 320 the content box is 288px, so the control, the title and a badge exceed the line
   and flex line-breaking sends the badge to a second row — the defect again, one width lower. Hiding the tail
   leaves "Back" at 90px and all three share the line at 1400, 393 and 320. The tail is a SPAN inside the control,
   so the accessible name comes from the control's own `aria-label` and loses nothing; `display: none` also keeps
   the hidden words out of the a11y tree, which is what §8i requires of every width-hidden control.

   640px is the product's phone breakpoint (§8e-s, §8j, `.kl-desk-only`), and it is also where the measurement
   turns: at 641 the content box is 609px and the full label, the title and the badge measure 315px together.

   NOTHING IS ADDED HERE FOR THE SUBTITLE, and that is deliberate. A first cut also gave `.kl-cardline >
   .kl-section-sub` an order and a full-width basis. Two facts retired it. Measured, it earns nothing: a
   subtitle's max-content width is the measure, so it already wraps to a line of its own, and the E2E head-geometry
   guard stays green with the rule removed. And `SectionHeadSubtitleRowGuardTests` holds a standing contract that
   the ONLY desk-reaching rules on `.kl-section-sub` are the §8e-s2 opt-in ones — a second gate on this class,
   however narrow, is exactly the drift that guard exists to stop. A card line's subtitle is a plain `.kl-muted`
   paragraph and takes its own line by its own width. */
.kl-shell-main .kl-cardline > .kl-tcard-lead {
  order: -7;
  flex: 0 0 auto;
}

@media (max-width: 640px) {
  .kl-shell-main .kl-cardline > .kl-tcard-lead .kl-tcard-lead-where { display: none; }
}

/* ---- 8c. Segmented control (exclusive scope; punch-list registry/scope) -----
   <div class="kl-segmented" role="group"> <button aria-pressed> … </button> …
   One pill-group; the pressed segment is teal-filled (navy ink on teal, on-brand). */
.kl-shell-main .kl-segmented {
  display: inline-flex; flex-wrap: wrap; align-items: stretch;
  border: 1px solid var(--k-border); border-radius: var(--k-radius-md);
  overflow: hidden; background: var(--k-surface);
}
.kl-shell-main .kl-segmented > button {
  appearance: none; background: transparent; color: var(--k-text);
  border: 0; border-inline-end: 1px solid var(--k-border);
  min-block-size: var(--k-touch); padding: 0 var(--k-space-4);
  font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
  transition: background var(--k-dur-1) var(--k-ease), color var(--k-dur-1) var(--k-ease);
}
.kl-shell-main .kl-segmented > button:last-child { border-inline-end: 0; }
/* HOVER MUST NOT CLAIM THE CHOSEN SEGMENT (owner B12 review, 2026-08-05).
   This rule repaints `background` and says NOTHING about `color`, which is the .kl-btn--seal failure mode: a state
   rule that changes ONE of the two colours. On /legal-hold it out-specified the page's own scoped selected rule
   (0,3,1 against 0,3,0), so the SELECTED "Active" tab kept color:var(--k-paper) and took background:var(--k-surface-2)
   — rgb(245,247,250) on rgb(238,242,247), a measured 1.05:1. The owner watched the word and its count vanish while
   the cursor rested on the tab it had just clicked, and reappear when the cursor moved away.
   The exclusion is wrapped in :where() so it contributes ZERO specificity: the selector still weighs (0,3,1) and no
   other segmented control changes. Guarded by Req_LEGALHOLD_RetentionRuleRoomAndTabContrastTests. */
.kl-shell-main .kl-segmented > button:not(:where([aria-pressed="true"], [aria-selected="true"])):hover { background: var(--k-surface-2); }
.kl-shell-main .kl-segmented > button[aria-pressed="true"] { background: var(--k-teal-500); color: var(--k-ink-900); }

/* ---- 8d. Filter chip (selected/active state; punch-list drafts-4 / registry) -
   A toggle chip with a real selected state — <button class="kl-filterchip"
   aria-pressed="true|false">. Deliberate size (>UA default), teal-fill when on. */
.kl-shell-main .kl-filterchip {
  display: inline-flex; align-items: center; gap: 6px;
  /* UI-GUAR-036 (R2-25): a ≥44 CSS-px touch target (was 40; the live sweep flagged filter chips at NN×40). */
  min-block-size: var(--k-touch); padding: 0 var(--k-space-4);
  border: 1px solid var(--k-border); border-radius: var(--k-radius-full);
  background: var(--k-surface); color: var(--k-text);
  font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; white-space: nowrap;
  transition: background var(--k-dur-1) var(--k-ease), border-color var(--k-dur-1) var(--k-ease), color var(--k-dur-1) var(--k-ease);
}
.kl-shell-main .kl-filterchip:hover { background: var(--k-surface-2); }
.kl-shell-main .kl-filterchip[aria-pressed="true"] {
  background: var(--k-teal-500); color: var(--k-ink-900); border-color: transparent;
}

/* ---- 8e. Section header row (left-aligned title + count/actions; registry-7) - */
.kl-shell-main .kl-section-head {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-2) var(--k-space-3);
  margin-block: var(--k-space-2);
}
.kl-shell-main .kl-section-head h1, .kl-shell-main .kl-section-head h2,
.kl-shell-main .kl-section-head .kl-section-title { margin: 0; }
.kl-shell-main .kl-count {
  font-size: 13px; color: var(--k-text-muted); font-variant-numeric: tabular-nums;
}
/* Severity-tinted counts (Home rework/approval/unread) — the markup shipped kl-count--critical/--caution/--info
   with no rules behind them (TEST-EFFICACY A1 dead-variant finding); counts carry severity per owner doctrine. */
.kl-shell-main .kl-count--critical { color:var(--k-critical-fg); font-weight:600; }
.kl-shell-main .kl-count--caution { color:var(--k-caution-fg); font-weight:600; }
.kl-shell-main .kl-count--info { color:var(--k-info-fg); font-weight:600; }

/* Numeric table cells (Expenses/Medical amount columns) — the markup shipped kl-cell--num with no rule
   (TEST-EFFICACY A1 dead-variant finding); dense right-aligned tabular numerals per the owner's ledger taste. */
.kl-shell-main th.kl-cell--num, .kl-shell-main td.kl-cell--num { text-align:end; font-variant-numeric:tabular-nums; }
/* Primary/secondary actions pushed to the trailing edge of a section header (e.g. registry "New record"). */
.kl-shell-main .kl-head-actions { display: flex; flex-wrap: wrap; gap: var(--k-space-3); margin-inline-start: auto; }

/* ---- 8e-s. THE SECTION HEAD'S SUBTITLE TAKES ITS OWN LINE, BELOW THE TITLE+ACTION ROW (≤640px) ------------
   Owner ruling 2026-08-11, looking at /medical in a 393px phone window:
     "on this page on the mobile view, the add medical record should be on the same row with the main title and
      its icon labeled medical. Right now, it's on its own row underneath the description of the page."

   MEASURED at 393x660 before this rule: h1 top 81, .kl-section-sub top 131 (the full 361px content width), the
   command bar top 203 — THREE rows, head 170px tall. The head has 256px of free space on the title row and the
   button is 157px, so the row was always affordable; the subtitle's max-content width simply exceeds the line, so
   it breaks to line 2, fills it, and pushes the action to line 3.

   THIS IS A SHARED RULE BECAUSE IT IS A SHARED SHAPE, NOT A /medical QUIRK. /expenses measures 81 / 131 / 203 with
   a 170px head at the same width — identical to the pixel. Every other .kl-section-head sampled at 393
   (/certifications, /users-roles, /master-names) carries no subtitle in the head and was already on one row; none
   of them can match this selector. Fixing this per page is exactly the drift the doubled-gap incident taught us to
   stop doing (§A1, playbook §9).

   THE LEVER IS ORDER, NOT SIZE. The subtitle moves BEHIND the action bar in the flex order and takes a full-width
   basis, so the title and the action share line 1 and the prose gets line 2 to itself. Nothing shrinks: no type
   ramp, no padding, no hit area (§A6), and flex-wrap stays `wrap`, so a width where the two genuinely do not fit
   still wraps honestly instead of overflowing. Focus order is unaffected — a <p> is not focusable and the DOM
   order assistive tech reads (title → subtitle → action) is untouched; only the visual order changes.

   :has() CONFINES THE BLAST RADIUS to heads that actually have an action to share the row with. The five heads
   that carry a subtitle and no action (/medical/new, /expenses/new, /expenses/{id}/edit, /vehicles/new,
   /operator/leads) cannot match, so their spacing is deliberately left alone. Home's .kl-section-sub is not in a
   section head at all and is likewise unmatched. :has() is already a dependency of this sheet (§8b-m, §5j-a).

   DESK IS UNTOUCHED BY CONSTRUCTION. At 1400 and 1280 /medical already puts title, subtitle and action on one row
   and the owner ruled that correct — hence the media query rather than an unconditional rule. */
@media (max-width: 640px) {
  .kl-shell-main .kl-section-head:has(.kl-head-actions) > .kl-section-sub {
    order: 1;
    flex-basis: 100%;
    /* The row gap (var(--k-space-2)) already separates the two lines; the UA's 1em block margins on <p> would
       stack on top of it and re-spend the vertical space the reorder just saved (§B1), including 16px of dead
       space below the head now that the subtitle is its last item. */
    margin-block: 0;
  }
}

/* ---- 8e-s2. THE SAME SHAPE AT DESK, PER HEAD, BY OPT-IN --------------------------------------------------
   Owner ruling 2026-08-11, looking at /expenses at 1400:
     "I'd like for the icon and expenses header to have the top row, of course, and on the right, clamped in
      that same row, the actions — the record expense, the CSV, the export. And then the row below that should
      have the detail label… on its own row, below all the other stuff in the header. It actually looks exactly
      right on mobile right now."

   MEASURED at 1400 before this rule (head 1048px wide): h1 top 104 (151px), .kl-section-sub top 109 (547px),
   .kl-head-actions top 157 (424px) — the actions on the LOWER row, head 112px. 1280 is the same shape (91 / 107
   / 155, head 112). Flex breaks the line in order: title + subtitle fit (151 + 12 + 547 = 710 of 1048) and the
   424px cluster does not, so the cluster is what wraps — the mirror image of the phone defect §8e-s fixed, and
   the same lever fixes it: reorder the subtitle behind the actions and give it a full-width basis.

   WHY AN OPT-IN CLASS AND NOT A WIDENED MEDIA QUERY. The criterion is a MEASUREMENT no selector can take: the
   subtitle should drop only where the action cluster cannot share the row with it. /expenses cannot (a 424px
   cluster — Record expense, the format picker and Export — against a 547px subtitle). /medical can: ONE 157px
   action, and all three items measure on a single 56px row at both 1400 and 1280 (h1 104, sub 109, action 97),
   which the owner ruled correct. Those two heads carry IDENTICAL markup, so widening §8e-s to desk would spend
   a whole extra row on /medical to fix nothing (§B1). The opt-in is the only honest place to put the judgement,
   and it is reviewable in a diff. Every other head in the product that reaches this selector is one of those
   two — the rest either carry no .kl-section-sub in the head (/certifications, /users-roles, /master-names,
   /legal-hold, all measured single-row at 1400/1280/393 and provably unmatched) or carry no action bar
   (/medical/new, /expenses/new, /expenses/{id}/edit, /vehicles/new, /operator/leads, excluded by :has()).

   The declarations are deliberately identical to §8e-s, including :has() and the margin reset: at ≤640 the two
   rules agree, so an opted-in head has ONE behaviour at every width and the phone shape the owner approved is
   the shape that widens. Nothing shrinks — no type ramp, no padding, no hit area (§A6) — and flex-wrap stays
   `wrap`, so a width where the title and the cluster genuinely do not fit still wraps honestly. */
.kl-shell-main .kl-section-head--sub-below:has(.kl-head-actions) > .kl-section-sub {
  order: 1;
  flex-basis: 100%;
  margin-block: 0;
}

/* =============================================================================
   §W4 — COMMAND BAR: one row of buttons, always (owner ruling 2026-07-24)
   "on all page types we should only allow 1 row of buttons — only keeping out the most important buttons, and
    hiding any that would overflow into a second row into a 'more actions' button that shows them in a dropdown."

   The behaviour is CommandBar.razor + kanine-commandbar.js; this is only its presentation. Two states, switched by
   the single `kl-is-collapsed` class the measurer applies:

     FITS      — .kl-cmdbar-cluster is display:contents, so its actions ARE the bar's own flex children and render
                 byte-identically to the un-adopted markup, and the More trigger is display:none (out of the a11y
                 tree and the tab order, exactly when it would be a no-op).
     DOES NOT  — the cluster becomes an absolutely-positioned dropdown anchored to the bar's inline end, and the
                 trigger appears.

   The flex-wrap:wrap on .kl-head-actions above is deliberately LEFT ALONE. It is what makes the un-collapsed
   measurement honest (the measurer asks "do these land on more than one row?"), and it is the safe resting state
   for any bar that has not adopted the component or whose JS never ran.
   ========================================================================== */
.kl-shell-main .kl-cmdbar { position: relative; }

/* Dissolved: the cluster contributes no box, so its children are the bar's children. */
.kl-shell-main .kl-cmdbar-cluster { display: contents; }

/* The trigger only exists when the bar could not fit. */
.kl-shell-main .kl-cmdbar-more { display: none; }
.kl-shell-main .kl-cmdbar.kl-is-collapsed .kl-cmdbar-more { display: inline-flex; }

/* Collapsed: the cluster is a real box again — a dropdown panel, closed by default. */
.kl-shell-main .kl-cmdbar.kl-is-collapsed .kl-cmdbar-cluster { display: none; }
.kl-shell-main .kl-cmdbar.kl-is-collapsed .kl-cmdbar-cluster.is-open {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--k-space-2);
  position: absolute;
  inset-block-start: calc(100% + var(--k-space-1));
  inset-inline-end: 0;
  z-index: 60;                       /* the shared menu rung — above page content, below the modal overlay (80) */
  min-inline-size: 12rem;
  /* Arbitrary labels and a narrow phone: the panel caps to the viewport and scrolls INSIDE itself rather than
     pushing the page wide. Same lesson as the shell account hub (box-sizing is load-bearing here too). */
  box-sizing: border-box;
  max-inline-size: min(20rem, calc(100vw - var(--k-space-4)));
  max-block-size: min(24rem, calc(100vh - 6rem));
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--k-space-2);
  background: var(--k-surface, #fff);
  border: 1px solid var(--k-border, rgb(0 0 0 / .12));
  border-radius: var(--k-radius-md);
  box-shadow: 0 8px 24px rgb(0 0 0 / .18);
}

/* Inside the dropdown the actions are full-width rows, so a long label wraps within the panel instead of widening
   it. Touch targets are NEVER reduced to buy the row — collapsing IS how the row is bought (principle A6). */
.kl-shell-main .kl-cmdbar.kl-is-collapsed .kl-cmdbar-cluster.is-open > * {
  inline-size: 100%;
  justify-content: flex-start;
  min-block-size: var(--k-touch);
}

/* The trigger keeps the WCAG floor like every other icon+label button in a bar. */
.kl-shell-main .kl-cmdbar-more { min-block-size: var(--k-touch); align-items: center; gap: var(--k-space-2); }

/* Below the field breakpoint the trigger sheds its text label and reads as the standard overflow glyph, so the
   collapsed bar costs one 44px target instead of a wide pill on a 320px phone. */
@media (max-width: 640px) {
  .kl-shell-main .kl-cmdbar-more-label { display: none; }
  .kl-shell-main .kl-cmdbar-more { min-inline-size: var(--k-touch); justify-content: center; }
}

/* Explicit filter-panel card (a multi-class element can't lean on the [class$="-card"] whole-attribute suffix). */
/* W1 (2026-07-23): `margin-block-end: var(--k-space-4)` was REMOVED here. §8a's `.kl-page` is a flex column with
   its own `gap: var(--k-space-4)`, and a flex gap NEVER collapses with a margin — so every listing screen paid 32px
   where 16px was intended. The gap now owns the rhythm; the block-context restoration lives in §8e-w1 below. */
.kl-shell-main .kl-filterpanel {
  background: var(--k-surface);
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-lg);
  box-shadow: var(--k-elev-1);
  padding: var(--k-space-6);
}

/* ---- 8e-w1. Rhythm restoration for NON-flex containers (W1, 2026-07-23) ------
   The gap-owns-the-rhythm rule above only holds where the parent actually provides one. Probed live across all
   twelve reachable listing screens: 15 filterpanel/searchrow elements, 14 of them direct children of `.kl-page`
   (flex, 16px gap). The single exception is /users-roles, where `.kl-filterpanel` sits inside `.kl-list-pane`,
   a plain `display:block` section with no gap — there the margin was load-bearing, so it is restored explicitly.
   Keyed on the CONTAINER, not the page, so any future block-context reuse inherits the same spacing. */
.kl-shell-main .kl-list-pane > .kl-filterpanel,
.kl-shell-main .kl-list-pane > .kl-searchrow,
.kl-shell-main .kl-detail-pane > .kl-filterpanel,
.kl-shell-main .kl-detail-pane > .kl-searchrow {
  margin-block-end: var(--k-space-4);
}

/* Same doubled-gap mechanism, different selector family: §5l's `[class$="-filters"]` / `[class$="-filterdrawer"]`
   container rule also carries `margin-block-end: var(--k-space-4)` (e.g. `.kl-quick-filters` on /records). That
   family is used in many nested contexts where the margin IS load-bearing, so it is neutralised only where the
   flex gap provably already applies — as a DIRECT child of `.kl-page`. */
.kl-shell-main .kl-page > :where([class^="kl-"],[class*=" kl-"])[class$="-filters"],
.kl-shell-main .kl-page > :where([class^="kl-"],[class*=" kl-"])[class$="-filterdrawer"] {
  margin-block-end: 0;
}

/* ---- 8f. Collapsible filter card (tame the dense filter wall; registry-2) ---- */
.kl-shell-main details.kl-collapse { border: 0; }
.kl-shell-main details.kl-collapse > summary {
  display: inline-flex; align-items: center; gap: var(--k-space-2);
  min-block-size: var(--k-touch); padding: 0 var(--k-space-3);
  cursor: pointer; list-style: none; color: var(--k-accent); font-weight: 600; font-size: 14px;
  border-radius: var(--k-radius-md); inline-size: fit-content;
}
.kl-shell-main details.kl-collapse > summary::-webkit-details-marker { display: none; }
.kl-shell-main details.kl-collapse > summary::before { content: "▸"; transition: transform var(--k-dur-1) var(--k-ease); }
.kl-shell-main details.kl-collapse[open] > summary::before { transform: rotate(90deg); }

/* ---- 8g. Small layout helpers (search row + sr-only accessible name) -------- */
/* W1 (2026-07-23): `margin-block-end` REMOVED — see the note on `.kl-filterpanel` (§8e). `.kl-page`'s flex gap owns
   the rhythm; §8e-w1 restores it for the non-flex `.kl-list-pane` / `.kl-detail-pane` contexts. */
.kl-shell-main .kl-searchrow {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-3);
}
.kl-shell-main .kl-searchbox { flex: 1 1 16rem; min-inline-size: 0; inline-size: auto; }

/* ---- 8g-s. `.kl-search-shell` — THE live-search control: ONE definition, every page that has one ----------
   OWNER RULING 2026-08-11, reviewing /medical: "this page, the search icon is above the entry box on its own
   row instead of inside of it."

   WHY /medical HAD NO SHELL WHILE THREE SIBLING PAGES DID — and both of the obvious readings are wrong. The
   class was not inert (three of the four pages that use it rendered correctly), and /medical was not missing
   the `.kl-qcard` ancestor these rules key on (its chain is
   `.kl-search-shell` -> `.kl-searchrow` -> `.kl-qcard.kl-qcard--hassearch` -> `.kl-page` -> `.kl-shell-main`).
   The real mechanism: /dogs, /vehicles and /records/search each wrote the container treatment into their OWN
   `.razor.css`, so there were THREE private copies of one idiom — and Blazor stamps a scope attribute only on
   markup the component itself writes, so a fourth page adopting exactly the same markup inherited none of them.
   Measured live at 1400x900 before this promotion, the /medical shell computed `display: block`, `border: 0`,
   `padding-inline: 0`, `min-block-size: auto`, and its 20px magnifier sat 26px ABOVE the input instead of 9.3px
   inside it. A block box cannot lay two children on a row; that is the whole of the reported defect.

   The copies had ALREADY drifted, which is the second half of the argument for one definition: only
   RecordSearch's carried the `prefers-reduced-motion` cut, so the same control honoured the same accessibility
   preference on one page out of three. That cut is folded in at the foot of this block and now holds everywhere.

   WHAT IS DELIBERATELY NOT PROMOTED: the flex-ITEM sizing. `flex` and `min-inline-size` below are the BASE,
   for a shell that is not on a query-card header line (/records/search has no `.kl-qcard` and keeps the
   22rem/14rem the page sheet used to give it). How the shell shares a header line with a primary filter and the
   disclosure button is a different question, already answered per-context by §9's
   `.kl-shell-main .kl-qcard .kl-search-shell` (0,3,0) and its two media-query basis adjustments — which
   out-specify this rule and keep /dogs, /vehicles and /medical on 12rem / 8rem exactly as before. Measured
   either side of the move: /dogs and /vehicles `flex-basis` 192px at 1400 and 1280, 128px at 393, unchanged.

   THE DESCENDANT SELECTOR on the inner input is load-bearing, not style. §5b's
   `.kl-shell-main input:not([type])` (0,2,1) out-specifies a lone class, which is exactly what left /medical's
   input carrying its own 1px border, its own 48px floor and — via `.kl-shell-main input:focus` — a SECOND focus
   ring painted inside the shell. `.kl-shell-main .kl-search-shell .kl-search-q` is (0,3,0) and settles it with
   no `!important`.

   `min-block-size: var(--k-touch)` is the §A6 floor for the whole control and is never traded for space.
   Guarded by app/tests/Kanine.Architecture.Tests/SearchShellSingleDefinitionGuardTests.cs — do NOT answer a
   page's shell defect with a page-scoped copy, which is the move that produced this ruling. ---- */
.kl-shell-main .kl-search-shell {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--k-space-2);
  flex: 1 1 22rem;
  min-inline-size: 14rem;
  padding-inline: var(--k-space-3);
  min-block-size: var(--k-touch);
  background: var(--k-surface);
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-md);
  transition: border-color var(--k-dur-1) var(--k-ease),
              box-shadow var(--k-dur-1) var(--k-ease);
}

.kl-shell-main .kl-search-shell:focus-within {
  border-color: var(--k-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--k-accent) 22%, transparent);
}

.kl-shell-main .kl-search-shell-icon {
  display: block;
  flex: none;
  color: var(--k-text-muted);
}

.kl-shell-main .kl-search-shell:focus-within .kl-search-shell-icon { color: var(--k-accent); }

/* The input is borderless INSIDE the shell — the shell carries the chrome, so the pair reads as one control. */
.kl-shell-main .kl-search-shell .kl-search-q {
  flex: 1 1 auto;
  /* §A6 (measured live 2026-09-12 at 320/375/393/430/768/1400): the SHELL already stands at var(--k-touch), but
     the shell is a plain <div> and does not activate anything. The input is the control, and it measured 38.5px
     tall on every search surface — medical-search and users-search among them — so a band of about 4.7px at the
     top and the bottom of the shell looked like the search box and answered no tap. `align-self: stretch` fills
     the shell's content box with the control that owns the click. It costs no height: the shell's floor and its
     padding are unchanged, the input stays borderless and transparent, and a text input centres its own text. */
  align-self: stretch;
  min-inline-size: 0;
  min-block-size: auto;
  border: 0;
  outline: 0;
  background: transparent;
  color: var(--k-text);
  font: inherit;
  font-size: 15px;
  padding-block: var(--k-space-2);
}

/* ONE focus treatment: the shell's `:focus-within` ring, never a second ring on the inner input. */
.kl-shell-main .kl-search-shell .kl-search-q:focus {
  border: 0;
  outline: 0;
  box-shadow: none;
}

.kl-shell-main .kl-search-shell .kl-search-q::placeholder { color: var(--k-text-muted); }

/* Reduced motion: drop the border/ring micro-motion. Promoted from RecordSearch.razor.css, the only one of the
   three page copies that had it — the drift this whole block exists to end. */
@media (prefers-reduced-motion: reduce) {
  .kl-shell-main .kl-search-shell { transition: none; }
}

/* ---- 8g-w2. Invisible readiness markers (W2, 2026-07-23) --------------------
   69 `<div data-testid="…-committed">` hooks across 66 files exist purely so E2E can observe that a surface
   settled. Most render EMPTY, yet as direct flex children of `.kl-page` each one still counts as a flex item and
   therefore consumes a gap on BOTH sides — 32px of layout around nothing.
   `position: absolute` takes them out of flow while KEEPING their box, the DOM node and every `data-*` attribute
   (unlike `display: contents`, which deletes the box outright and can break anything waiting on the element).
   `:empty` is load-bearing, not decoration: the naming convention alone does NOT identify these markers — the same
   suffix is carried by three REAL content containers (`manifest-receipt-committed` wraps the whole receipt,
   `packet-builder-committed` the whole builder, `mn-demographics-committed` a visible status line). `:empty`
   excludes those automatically, and self-heals: the moment a marker gains a child it drops out of this rule and
   returns to normal flow — which is exactly the failure mode that made `display: contents` unsafe here. */
[data-testid$="-committed"]:empty { position: absolute; }

/* Accessible-name-only (visually hidden, still in the a11y tree) — e.g. a <caption> behind a visible section head. */
.kl-visually-hidden {
  position: absolute !important; inline-size: 1px; block-size: 1px;
  padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0);
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* ---- 8g-a8. DESK-ONLY controls — the phone-width export rule (§A8) ----------
   Owner ruling 2026-07-25, on the daily log: "on daily log, on mobile screen size web and maui. the filters
   button has free space to the right but the export log button displays on the next row instead (WAIT, export
   buttons dont belong on the phone size displays)". A LIST/LEDGER export is a desk task. `RecordList` already
   carried exactly that reasoning as a HEAD rule ("A desk task — hidden where the export client is not provided
   (the field head), so the registry stays usable there without the packet pipeline"); this promotes it to a
   WIDTH rule, which is what "web and maui" means — a narrow browser window on a desktop obeys it identically.

   `display: none` is load-bearing, NOT a stylistic pick. `visibility: hidden` / `opacity: 0` / an off-screen
   clip would leave the control in the TAB ORDER and in the ACCESSIBILITY TREE: a keyboard or screen-reader
   user would still reach an "Export log" button the sighted phone user cannot see — a worse defect than the
   wrapped row this closes. `display: none` drops the subtree from layout, from the a11y tree and from
   sequential focus navigation together. The shell's own hamburger and the command bar's More trigger are
   hidden the same way, for the same stated reason (§W4).

   `!important` is likewise deliberate. Consumers set their own `display` — the Expenses export cluster is a
   Blazor-SCOPED `display: flex` group (specificity 0,2,0) and lives inside a `display: contents` command-bar
   cluster. A one-way "must be ABSENT" safety rule must not lose a specificity race to a page's own layout
   declaration; §8j's `.kl-tcard-hide` already uses `!important` for the same class of guarantee.

   NOT scoped to `.kl-shell-main`: the rule must hold wherever a governed control renders, and a specificity
   bump would defeat the point of the flag. It is opt-in by class, so it can never capture anything by accident.

   Consumers must be governed markup, not decoration: this is a CAPABILITY REMOVAL, so it belongs only on
   list/ledger exports (per-item exports a handler may genuinely need on scene are deliberately NOT flagged).
   Enforced by app/tests/Kanine.Web.Tests/Req_DeskOnlyExportTests.cs. */
@media (max-width: 640px) {
  .kl-desk-only { display: none !important; }
}

/* Skip-to-content (WCAG 2.4.1 Bypass Blocks). Off-screen until focused, NOT display:none — a hidden-by-display
   link is not focusable, so it could never be reached by the keyboard user it exists for. z-index clears the
   sticky topbar (z-index:70) so it is not painted behind the chrome it is helping the user skip past. */
.kl-skip-link {
  position: fixed; inset-block-start: 0; inset-inline-start: 0; z-index: 200;
  transform: translateY(-120%);
  padding: var(--k-space-3, 12px) var(--k-space-4, 16px);
  background: var(--k-teal-500); color: var(--k-ink-900);
  font-weight: 600; text-decoration: none;
  border-end-end-radius: var(--k-radius-md, 8px);
}
.kl-skip-link:focus, .kl-skip-link:focus-visible {
  transform: translateY(0);
  outline: 2px solid var(--k-ink-900); outline-offset: -4px;
}
@media (prefers-reduced-motion: no-preference) { .kl-skip-link { transition: transform 120ms ease-out; } }

/* ---- 8h. Drafts panel (REC-056 page-major sign-off) -----------------------
   Consumes the cross-cutting primitives (kl-section-head, kl-count, kl-head-actions, kl-filterchip,
   :where([class^="kl-"],[class*=" kl-"])[class$="-row"], kl-btn). Adds only the row's column rhythm + the trailing-action push (light/outdoor proven).
   NOTE: this page's DARK-mode neutral-button/chip contrast rides the FOUNDATION dark-button issue tracked in
   ProposedUI/docs/UI-OPEN-QUESTIONS.md (affects every page incl. the signed-off records-registry) — not fixed here. */
.kl-shell-main .kl-drafts-filters { align-items: center; }
.kl-shell-main .kl-draft-row { flex-wrap: wrap; gap: var(--k-space-2) var(--k-space-4); }
.kl-shell-main .kl-draft-dog { font-weight: 700; color: var(--k-text); }
.kl-shell-main .kl-draft-start,
.kl-shell-main .kl-draft-age { color: var(--k-text-muted); font-size: 13px; font-variant-numeric: tabular-nums; }
.kl-shell-main .kl-draft-actions {
  margin-inline-start: auto; display: inline-flex; align-items: center; gap: var(--k-space-2);
}
.kl-shell-main .kl-drafts-paging {
  display: flex; align-items: center; justify-content: center; gap: var(--k-space-3);
  margin-block-start: var(--k-space-4);
}

/* =============================================================================
   8i. Shared involvement popup — GLOBAL (un-scoped) modal + tabs + count chips.
   -----------------------------------------------------------------------------
   PROMOTED from the record-build surface's page-scoped .kl-rb-modal so the new
   cross-file Involvement components (Components/Involvement/*) share ONE overlay /
   modal / tab-strip / chip — a Blazor-scoped page rule would not reach a sibling
   component's markup. Every value is an on-brand --k-* token (mode-adaptive: light
   / dark / outdoor); focus-visible rings throughout.
   ========================================================================== */
/* ==========================================================================
   THE one enforced centered-overlay primitive (UI-GUAR-033 / owner ruling §H.1
   #22). Every modal/dialog/sheet renders as a TRUE overlay through this class
   (the <KlOverlay> Blazor wrapper applies it + owns ESC/outside-click): a
   single full-viewport position:fixed inset:0 scrim, DIM at a FIXED navy
   alpha .55 (≥0.5 and MODE-STABLE — NOT a --k-text/color-mix that flips light
   in dark), centered, stacked ABOVE the shell. The shell rail/top-bar/bottom
   nav top out at z-index 60 (kl-shellnav-rail desktop 50 / mobile 60, kl-
   shellnav rail drawer 60); z-index:80 here clears all of them so a modal is
   never painted under the chrome. There is exactly ONE scrim element (the
   overlay IS the scrim — the panel is its child). Retired the dead
   .kl-dialog::backdrop (the app ships NO native <dialog>/showModal, so a
   ::backdrop on a <div> never renders — it was a scrimless no-op).
   ========================================================================== */
.kl-modal-overlay {
  position: fixed; inset: 0; z-index: 80;
  display: flex; justify-content: center;
  padding: var(--k-space-4);
  background: rgb(10 26 43 / .55); backdrop-filter: blur(2px);

  /* A dialog TALLER than the viewport was previously unreachable at both ends. The overlay is fixed with no
     overflow, so an over-tall flex child simply overflowed the scrim with no scrollbar — and because
     `align-items: center` centres the overflow, BOTH the header and the ACTIONS FOOTER were clipped off-screen
     with no way to reach them. A user could open a tall dialog and be unable to press Save.
     Caught by Playwright on the daily-log create dialog ("Element is outside of the viewport", after
     scroll-into-view and even with a forced click) once this branch added four fields to it. It is a SHARED
     defect, not a daily-log one: any dialog that outgrows the viewport had it.
     Fix: let the scrim scroll, and centre with `safe` so overflow spills only downward (reachable) instead of
     symmetrically (unreachable). `safe` degrades to normal centring on engines that lack it, which is the
     pre-existing behaviour — never worse. */
  overflow-y: auto;
  align-items: safe center;
}
/* Edge drawer / side-sheet exception (still a real scrim, just NOT centered):
   <KlOverlay Edge="..."> stamps data-edge so the auditor admits the off-center
   placement while STILL requiring the full-viewport ≥0.5 scrim. The panel pins
   to the named edge and fills the cross-axis; the scrim dim is unchanged. */
.kl-modal-overlay[data-edge] { padding: 0; }
.kl-modal-overlay[data-edge="end"] { justify-content: flex-end; align-items: stretch; }
.kl-modal-overlay[data-edge="start"] { justify-content: flex-start; align-items: stretch; }
.kl-modal-overlay[data-edge="bottom"] { align-items: flex-end; justify-content: stretch; }
.kl-modal-overlay[data-edge="top"] { align-items: flex-start; justify-content: stretch; }
/* The <KlOverlay> panel slot is layout-transparent (display:contents): it carries the
   @onclick:stopPropagation seam (so an inside-click never closes) WITHOUT inserting a box
   that would defeat the overlay's centering / edge placement — the child panel becomes the
   direct flex item. */
.kl-overlay-slot { display: contents; }
.kl-modal {
  inline-size: min(40rem, 100%); max-block-size: min(85vh, 48rem); overflow: auto;
  display: flex; flex-direction: column; gap: var(--k-space-4);
  background: var(--k-surface); color: var(--k-text);
  border: 1px solid var(--k-border); border-radius: var(--k-radius-lg);
  box-shadow: var(--k-elev-3); padding: var(--k-space-6);
}
.kl-modal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--k-space-3); }
.kl-modal-title { margin: 0; font-family: var(--k-font-display); font-size: 18px; font-weight: 800; color: var(--k-text); }

/* The tab strip — UNDERLINE tabs (owner ruling 2026-06-29): no pills, no fill. The selected tab is a navy-bold label
   with a 2px TEAL underline; inactive tabs are muted text. Teal stays a THIN accent (the brand doctrine bans a large
   teal fill — it glows/vibrates in dark mode), so this is dark/outdoor-safe where the old teal-filled pill was not. */
.kl-modal-tabs {
  display: flex; align-items: stretch; gap: var(--k-space-4); flex-wrap: wrap;
  border-block-end: 1px solid var(--k-border);
}
.kl-modal-tab {
  appearance: none; background: transparent; color: var(--k-text-muted);
  border: 0; border-block-end: 2px solid transparent; border-radius: 0;
  margin-block-end: -1px; /* overlap the strip's 1px rail so the active underline sits on it */
  min-block-size: var(--k-touch); padding: 0 var(--k-space-1);
  font: inherit; font-size: 14px; font-weight: 600; cursor: pointer;
  transition: color var(--k-dur-1) var(--k-ease), border-color var(--k-dur-1) var(--k-ease);
}
.kl-modal-tab:hover { color: var(--k-text); }
.kl-modal-tab[aria-selected="true"] { color: var(--k-text); font-weight: 700; border-block-end-color: var(--k-accent); }
.kl-modal-tab:focus-visible { outline: 2px solid var(--k-accent); outline-offset: 2px; border-radius: var(--k-radius-sm); }
.kl-modal-body { display: flex; flex-direction: column; gap: var(--k-space-4); }

/* The clickable count chip — a .kl-chip rendered as a button (resets the native button chrome). It pins its OWN
   semantic-token colours (NOT the base .kl-chip neutral gray palette, which does not flip in dark) so the count stays
   legible across light / dark / outdoor; the later single-class rule wins over .kl-chip. */
.kl-involve-chips { display: inline-flex; align-items: center; gap: var(--k-space-2); flex-wrap: wrap; }
.kl-involve-chip {
  appearance: none; cursor: pointer; font: inherit;
  color: var(--k-text); background: var(--k-surface-2); border-color: var(--k-border);
}
.kl-involve-chip:hover { border-color: var(--k-accent); background: var(--k-surface); }
.kl-involve-chip:focus-visible { outline: 2px solid var(--k-accent); outline-offset: 2px; }

/* The "+ Add entity" bar (the PARENT renders it next to the chips) + the popup's file / UoF blocks. */
.kl-involve-bar { display: flex; align-items: center; gap: var(--k-space-3); flex-wrap: wrap; }
.kl-involve-modal { inline-size: min(46rem, 100%); }
.kl-involve-files { display: flex; flex-direction: column; gap: var(--k-space-3); }
.kl-involve-file { position: relative; align-self: flex-start; overflow: hidden; cursor: pointer; }
.kl-involve-file-input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.kl-involve-files-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--k-space-2); }
.kl-involve-files-item {
  display: flex; align-items: center; gap: var(--k-space-3);
  padding: var(--k-space-3) var(--k-space-4); border: 1px solid var(--k-border);
  border-radius: var(--k-radius-md); background: var(--k-surface-2);
}
.kl-involve-files-name { font-weight: 600; color: var(--k-text); }
.kl-involve-files-remove { margin-inline-start: auto; }
/* M5 — a file on this device that waits in the upload queue. */
.kl-involve-files-pending { font-size: 13px; color: var(--k-text-muted); }
/* D79.15 — the note in place of Remove on a completed record. */
.kl-involve-files-locked { margin: 0; font-size: 13px; color: var(--k-text-muted); }

/* GEO-022 / ARCH-036 — resumable video upload + inline playback in the Files panel. */
.kl-involve-files-item { flex-direction: column; align-items: stretch; }
.kl-involve-files-row { display: flex; align-items: center; gap: var(--k-space-3); }
.kl-involve-files-video {
  width: 100%; max-height: 280px; margin-top: var(--k-space-2);
  border-radius: var(--k-radius-md); background: #000;
}
.kl-involve-file-actions { display: flex; flex-wrap: wrap; gap: var(--k-space-2); align-items: center; }
.kl-video-upload { position: relative; display: inline-flex; flex-direction: column; gap: var(--k-space-1); }
.kl-video-upload-progress { display: flex; align-items: center; gap: var(--k-space-2); min-width: 14rem; }
.kl-video-upload-track {
  flex: 1; height: 8px; border-radius: 999px; overflow: hidden;
  background: var(--k-surface-3, var(--k-border));
}
.kl-video-upload-bar { height: 100%; background: var(--k-accent); transition: width .15s ease; }
.kl-video-upload-phase { font-size: .85rem; color: var(--k-text-muted); white-space: nowrap; }
.kl-video-upload-failure { margin-top: var(--k-space-2); }
/* The two empty states. NOTE (2026-07-25): the four `kl-involve-uof-*` selectors here named
   `-stub` / `-title` / `-note` / `-icon`, and the markup has emitted `-empty` / `-empty-title` /
   `-empty-note` / `-empty-icon` since the panel was built — so every one of them was a DEAD rule
   and the Use-of-Force empty state was picking up nothing but section 5h's generic state kit.
   Repointed to the classes the component actually renders; the files variant is unchanged. */
.kl-involve-files-empty,
.kl-involve-uof-empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--k-space-1);
  text-align: center; padding: var(--k-space-6) var(--k-space-4);
  border: 1px dashed var(--k-border); border-radius: var(--k-radius-md); color: var(--k-text-muted);
}
.kl-involve-files-empty-title,
.kl-involve-uof-empty-title { margin: 0; font-weight: 700; color: var(--k-text); }
.kl-involve-files-empty-note,
.kl-involve-uof-empty-note { margin: 0; font-size: 13px; }
.kl-involve-files-empty-icon,
.kl-involve-uof-empty-icon { font-size: 28px; color: var(--k-text-muted); }

/* ---- 6.99 Detail-screen header icons (UI-GUAR-019 / R2-08) ----------------- */
/* Every detail/view screen leads its identity + each section/card header with a large
   .ki icon (owner ruling section H.1 #8). This shared utility gives the leading icon a
   consistent trailing gap + optical alignment WITHOUT each detail page re-declaring it.
   The icon scales with the heading font-size (the .ki base is font-relative), so it is
   "large" on the 16-24px section/identity headings. Accent-tinted so it reads as a
   hierarchy marker, not body text; flips per-mode via the --k-accent token. */
.kl-detail-h-icon {
  margin-inline-end: var(--k-space-2);
  color: var(--k-accent);
  font-size: 0.92em;
  vertical-align: -0.05em;
  flex-shrink: 0;
}

/* =============================================================================
   6.100 PRE-AUTH / ACCOUNT CARD FAMILY (kl-login-*) — AUTH Phase 7 visual heal.
   The sign-in card vocabulary was authored only in Login.razor.css (Blazor-SCOPED
   to Login.razor), so every OTHER page composing it — /signup, /invite,
   /account/two-factor-challenge, /account/first-login, /account/link-google|apple,
   /logout, and the /account/security forms — rendered those classes UNSTYLED
   (no navy brand band, no card width, default h1). Promoted here — the same heal
   as the explicit .kl-banner family (§5g) — so ONE authoring styles every auth
   surface; Login.razor.css is retired. Scoped under .kl-shell-main at the same
   specificity as the §5a/5b suffix families and AFTER them in source, so the
   card-shell overrides (padding:0 etc.) win by order. Token-driven throughout —
   light/dark/outdoor flip with the --k-* foundation; flat fills only (R11).
   ========================================================================== */
.kl-shell-main .kl-login {
  display:flex; flex-direction:column; gap:var(--k-space-6);
  inline-size:100%; max-inline-size:28rem; margin-inline:auto;
}

/* The card is a chromeless shell — the brand band and content own the padding
   (defeats the §5a :where([class^="kl-"],[class*=" kl-"])[class$="-card"] padding + trailing margin). */
.kl-shell-main .kl-login-card {
  inline-size:100%; padding:0; margin-block-end:0; overflow:hidden;
  background:var(--k-surface);
  border:1px solid var(--k-outline);
  border-radius:var(--k-radius-lg);
  box-shadow:0 4px 24px rgba(10, 26, 43, 0.08);
}

/* Navy brand band. --k-ink-800 is a FIXED brand token (never re-points per mode),
   so the band + its white/teal type hold their contrast in light, dark, and outdoor. */
.kl-shell-main .kl-login-brand {
  background:var(--k-ink-800); color:#fff;
  padding:var(--k-space-8) var(--k-space-6);
  display:flex; flex-direction:column; align-items:center; text-align:center;
  gap:var(--k-space-1);
  border-block-end:1px solid var(--k-ink-900);
}
.kl-shell-main .kl-login-mark { inline-size:48px; block-size:48px; margin-block-end:var(--k-space-2); }
.kl-shell-main .kl-login-title {
  margin:0; font-family:var(--k-font-display);
  font-size:1.6rem; font-weight:700; letter-spacing:-0.01em; color:#fff;
}
.kl-shell-main .kl-login-subtitle {
  margin:0; font-size:0.72rem; text-transform:uppercase; letter-spacing:0.18em;
  color:var(--k-teal-400); opacity:.9;
}

.kl-shell-main .kl-login-content { padding:var(--k-space-6); }
@media (min-width: 480px) { .kl-shell-main .kl-login-content { padding:var(--k-space-8); } }

.kl-shell-main .kl-login-view { display:flex; flex-direction:column; gap:var(--k-space-4); }
.kl-shell-main .kl-login-view--center { text-align:center; align-items:center; }

.kl-shell-main .kl-login-h2 {
  margin:0; font-family:var(--k-font-display); font-size:18px; font-weight:700; line-height:1.3;
  color:var(--k-text);
}
.kl-shell-main .kl-login-h3 { margin:0; font-size:15px; font-weight:600; color:var(--k-text); }
.kl-shell-main .kl-login-p { margin:0; font-size:14px; line-height:1.55; color:var(--k-text-muted); }
/* Prose links inside a login paragraph read accent — but a BUTTON that happens to be an <a>
   must keep its own button text colour. Same `:not([class*="-btn"])` guard as the app-wide
   `.kl-shell-main a` rule (§4, above): this rule is a narrower restatement of it that was
   authored without the guard, so at 0,2,1 it out-specified `.kl-btn--primary` (0,1,0) and
   painted var(--k-accent) TEXT on a var(--k-accent) BACKGROUND — 1:1 contrast, an invisible
   label (measured on /signup/cloud/complete's "Sign in" CTA, the post-payment landing).
   `-btn` (not `--`) is the discriminator because it also covers the un-modified `.kl-btn`
   and the kf-btn / *-btn families that §8 paints as buttons. */
.kl-shell-main .kl-login-p a:not([class*="-btn"]) { color:var(--k-accent); font-weight:600; }

/* Form rhythm: the form's own gap paces the fields (defeat the §5b family's
   per-field trailing margin so spacing is one system, not two stacked ones). */
.kl-shell-main .kl-login-form,
.kl-shell-main .kl-login-realm-form { display:flex; flex-direction:column; gap:var(--k-space-4); }
.kl-shell-main .kl-login-field { margin-block-end:0; gap:var(--k-space-1); }
.kl-shell-main .kl-login-label { color:var(--k-text); font-size:13px; font-weight:600; }

/* The collapsible recovery-code fallback (two-factor): the <summary> IS the label —
   make it read and hit like a control. */
.kl-shell-main summary.kl-login-label { cursor:pointer; display:flex; align-items:center; gap:var(--k-space-2); min-block-size:var(--k-touch); }
.kl-shell-main details[open] > summary.kl-login-label { margin-block-end:var(--k-space-1); }

/* ...and it needs a VISIBLE disclosure affordance, which the rule above accidentally removed: `display:flex` on a
   <summary> suppresses the browser's own marker, so this rendered as a plain bold sentence with nothing to suggest
   it opens. `cursor:pointer` is not discoverable — it only appears once the pointer is already on the words, and
   never at all on a touch device. That matters more here than anywhere else on the screen: this line is the ONLY
   self-service way past the second factor for someone whose authenticator is gone, and everything else in the card
   (a label, a field, a button) looks like a control while the one escape looked like a caption.
   Found while filming catalogue B4 (2026-07-30) — the beat needed a positive control to prove its click had done
   anything, which is the same difficulty a real user has. Same rotating caret §8f already uses on the collapsible
   filter card, so the product has ONE disclosure idiom rather than two. */
.kl-shell-main summary.kl-login-label { list-style:none; }
.kl-shell-main summary.kl-login-label::-webkit-details-marker { display:none; }
.kl-shell-main summary.kl-login-label::before {
  content:"\25B8"; color:var(--k-accent); transition:transform var(--k-dur-1) var(--k-ease);
}
.kl-shell-main details[open] > summary.kl-login-label::before { transform:rotate(90deg); }

/* Screen-reader-only label (pre-auth pages have no .kl-visually-hidden dependency). */
.kl-login-srlabel {
  position:absolute; inline-size:1px; block-size:1px; overflow:hidden;
  clip:rect(0 0 0 0); white-space:nowrap;
}

/* Divider + the layered agency affordances (home-realm lookup, K96 route, signup). */
.kl-shell-main .kl-login-divider {
  display:flex; align-items:center; gap:var(--k-space-4);
  color:var(--k-text-muted); font-size:0.72rem; text-transform:uppercase; letter-spacing:0.08em;
}
.kl-shell-main .kl-login-divider::before,
.kl-shell-main .kl-login-divider::after { content:""; flex:1; border-block-start:1px solid var(--k-outline); }

.kl-shell-main .kl-login-agency { display:flex; flex-direction:column; gap:var(--k-space-4); margin-block-start:var(--k-space-2); }

.kl-shell-main .kl-login-link {
  align-self:center; display:inline-flex; align-items:center; min-block-size:var(--k-touch);
  color:var(--k-accent); font-size:14px; font-weight:600; text-decoration:none;
}
.kl-shell-main .kl-login-link:hover { text-decoration:underline; }
.kl-shell-main .kl-login-link:focus-visible { outline:2px solid var(--k-accent); outline-offset:2px; border-radius:var(--k-radius-sm); }

/* login.html#button.action[2] — the password field and its visibility toggle share one relative box, so the
   toggle sits INSIDE the input's trailing padding rather than beside it (the prototype treatment). Restored
   2026-08-02 with the control; the rules came off the pre-AUTH-P3 Login.razor.css unchanged. */
.kl-shell-main .kl-login-pw { position:relative; display:flex; align-items:center; }
.kl-shell-main .kl-login-pw .kl-login-input { padding-inline-end:3rem; }
.kl-shell-main .kl-login-pw-toggle {
  position:absolute; inset-inline-end:var(--k-space-2);
  display:inline-flex; align-items:center; justify-content:center;
  min-inline-size:var(--k-touch); min-block-size:var(--k-touch);
  background:none; border:0; color:var(--k-text-muted); cursor:pointer;
}
.kl-shell-main .kl-login-pw-toggle:hover { color:var(--k-text); }
.kl-shell-main .kl-login-pw-toggle:focus-visible { outline:2px solid var(--k-accent); outline-offset:2px; border-radius:var(--k-radius-sm); }

/* login.html#button.action[6] + [9] — the lockout banner's action row (Acknowledge / Contact administrator).
   It sits INSIDE the banner, under the message, so it wraps on a narrow card rather than crushing the labels. */
.kl-shell-main .kl-login-actions {
  display:flex; flex-wrap:wrap; gap:var(--k-space-2);
  margin-block-start:var(--k-space-3);
}

/* Inside the card the view's own gap paces banners — the app-wide trailing margin would double it. */
.kl-shell-main .kl-login-view .kl-banner { margin-block-end:0; }

/* The home-realm context banner (AUTH §4) — a quiet teal-edged status band. */
.kl-shell-main .kl-login-realm {
  background:var(--k-surface-2); border-inline-start:4px solid var(--k-teal-500); color:var(--k-text);
}

.kl-shell-main .kl-login-foot {
  background:var(--k-surface-2); padding:var(--k-space-3) var(--k-space-4);
  border-block-start:1px solid var(--k-outline);
  text-align:center; font-size:12px; color:var(--k-text-muted);
}

/* Inside the auth card a note is a boxed teal-edged aside (denser than the
   app-wide muted :where([class^="kl-"],[class*=" kl-"])[class$="-note"] treatment). */
.kl-shell-main .kl-login .kl-readonly-note {
  margin:0; padding:var(--k-space-2) var(--k-space-3);
  background:var(--k-surface-2); border-inline-start:3px solid var(--k-teal-500);
  border-radius:var(--k-radius-sm); font-size:13px; color:var(--k-text-muted);
}

/* Social sign-in buttons (Login + Security): the official mark + label as one
   centered row; the inline SVG sizes itself (18px), so just align and space it. */
.kl-btn--provider { display:inline-flex; align-items:center; justify-content:center; gap:0.55rem; }
.kl-provider-logo { flex:0 0 auto; display:block; }

/* Account→Security: the auth-form vocabulary reused INSIDE settings cards — cap the
   line length so password fields don't stretch the full content column, and lay the
   provider link buttons side-by-side above their shared note. */
.kl-shell-main .kl-card .kl-login-form { max-inline-size:28rem; }
.kl-shell-main .kl-security-linkrow {
  display:flex; flex-wrap:wrap; gap:var(--k-space-3);
  margin-block-start:var(--k-space-4); margin-block-end:var(--k-space-2);
}

/* The one-time recovery-code reveal: a dashed evidence panel, codes on a mono grid. */
.kl-shell-main .kl-recovery-codes {
  margin:0; padding:var(--k-space-4); list-style:none;
  display:grid; grid-template-columns:repeat(auto-fit, minmax(9rem, 1fr)); gap:var(--k-space-2);
  background:var(--k-surface-2); border:1px dashed var(--k-border); border-radius:var(--k-radius-md);
  font-size:15px; letter-spacing:.04em;
}

/* ---- 7. Print (printouts always light) ------------------------------------ */
@media print {
  .kl-shellnav-rail, .kl-shellnav-topbar, .kl-shellnav-bottom, .kl-no-print { display:none !important; }
  .kl-shell { padding-inline-start:0 !important; }
  .kl-shell-main { margin:0 !important; padding:0 !important; }

  /* `html body`, not `body`. MEASURED 2026-09-15 with emulateMedia('print') on /reports and on
     /records/{id}: the computed body background was rgb(245,247,250), never #fff. kanine-redesign.css
     loads AFTER this file and sets its own `body { background: <three gradients>, var(--k-bg);
     background-attachment: fixed }` at the same (0,0,1) specificity, so the later rule won and the
     line below had never once applied. The whole product printed on the ambient brand wash. One extra
     type selector takes this to (0,0,2) and settles it on specificity rather than on file order --
     no `!important`, so a page sheet can still override it deliberately. The shorthand also clears
     `background-attachment: fixed`, which is what tiles a fixed wash onto every sheet. */
  html body { background:#fff; color:#000; }

  /* The skip link is `position: fixed` and parked off-screen with a transform (§ Skip-to-content).
     Chromium repeats a fixed element on EVERY printed sheet and clamps it back into the page box, so
     "Skip to main content" was printing on nearly every sheet of every surface -- measured on sheets
     1-2 of /reports and sheets 1-2 of /records/{id}. It is a keyboard affordance for the screen; paper
     has no focus ring to reveal it, so `display: none` here costs nothing and removes it from paper. */
  .kl-skip-link { display:none; }

  /* PAPER HAS NO SCROLLBAR. A screen-only scroll affordance must not survive onto the sheet, and two
     of them were doing real damage.

     1. THE MASK DELETED THE DOCUMENT. `.kl-scrollcue--fade` fades the right edge with a `mask-image`
        to say "there is more this way". Chromium composites a masked element for print and does not
        paint it into the PDF at all. PROVEN 2026-09-15 by two real `page.pdf()` runs of the same live
        /reports page: as shipped, 3 sheets and 837 glyphs with no letterhead, no classification chip,
        no table and no document footer -- a report printout with no report on it; with only the mask
        neutralised, the same 3 sheets carried 1517 glyphs and the letterhead. Nothing else changed.
     2. A SCROLL CONTAINER CANNOT PAGINATE. `overflow: auto` makes an unbreakable box: Chromium prints
        the part inside the box and drops the rest, so a table longer than its scroller loses its tail
        with no mark on the paper, and a table wider than its scroller loses its right-hand columns.
        Measured on /reports at the Letter content width: scrollWidth 1033 against clientWidth 718.

     `overflow: visible` restores normal fragmentation, so long content flows onto the next sheet and
     wide content is a layout problem the surface can then solve honestly (the report document wraps
     its header cells under print for exactly this reason).

     THE DOUBLED CLASS IS DELIBERATE. A scroll container is almost always declared in a page's own
     scoped sheet, and a Blazor scoped rule reads `.kl-reports-preview-body[b-hash]` -- also (0,2,0),
     and its bundle loads after this file, so a plain `.kl-shell-main .kl-scrollcue--fade` lost the tie
     on order and measured `overflow-x: auto` under print with the rule sitting right there. Repeating
     the class takes the overflow rule to (0,3,0), which beats the page sheet on specificity and still
     leaves a page free to override it deliberately. `!important` would take that choice away. The mask
     line needs none of this: the mask is declared in this file, so file order already settles it. */
  .kl-shell-main .kl-scrollcue--fade { -webkit-mask-image:none; mask-image:none; }
  .kl-shell-main .kl-scrollcue.kl-scrollcue,
  .kl-shell-main .kl-scrollcue--fade.kl-scrollcue--fade,
  .kl-shell-main .kl-tablewrap.kl-tablewrap { overflow:visible; }
}

/* ── FREE-plan chip (owner-directed 2026-07-18): the always-visible free-tier marker in the shell rail + topbar. ── */
.kl-shellnav-planchip{display:flex;align-items:center;gap:6px;margin:4px 14px 8px;padding:6px 10px;font-size:11px;
  font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--k-ink-900);background:var(--k-teal-400);
  border-radius:6px;text-decoration:none}
.kl-shellnav-planchip:hover{background:var(--k-teal-500)}
.kl-shellnav-planchip .ki{font-size:14px}
.kl-shellnav-planchip--top{margin:0 8px 0 0;padding:4px 8px}

/* =============================================================================
   8j. Location + weather disclosure — GLOBAL (un-scoped) accordion chrome.
   -----------------------------------------------------------------------------
   PROMOTED from RecordEnvelopeFieldsSection's page-scoped .kl-rb-envelope
   .kl-rb-locwx* rules (the .kl-rb-modal precedent above) when the accordion was
   extracted into the shared RecordLocationWeatherAccordion component so the
   quick-start scratch pad could mount the SAME disclosure the record-entry
   wizard uses. A Blazor-scoped rule reaches only the markup its own component
   renders, so the chrome had to leave the envelope sheet or be duplicated —
   this is the ONE copy, in the ONE shared CSS surface both heads load.

   The envelope's Defaults drawer and Ref. # accordion deliberately reuse this
   same chrome (they always did, by class); they keep their own scoped
   .kl-rb-defaults-* additions.

   The collapsed header PREVIEWS both values, so a working GPS autofill needs no
   open and a failed one is one tap from the manual controls. Every value is an
   on-brand --k-* token; the header clears the touch floor.
   ========================================================================== */
.kl-rb-locwx {
  padding: 0;
  border: 1px solid var(--k-border); border-radius: var(--k-radius-md);
  background: var(--k-surface); overflow: hidden;
}

/* NO SIBLING STRETCH (owner request 2026-10-02). A grid cell stretches to the height of its row by default. The
   accordions sit in the multi-column envelope grid above 640px, so an OPEN accordion stretched the closed accordion
   next to it into a tall empty bordered box. An accordion cell now sizes to its own content. Scoped above 640px:
   at 640px and below the envelope grid is display:contents inside a flex COLUMN, where align-self:start would shrink
   the accordion width. The phone layout is therefore unchanged. */
@media (min-width: 641px) {
  .kl-rb-grid > .kl-rb-locwx { align-self: start; }
}

/* HEADER — the always-visible at-a-glance display AND the tap target (≥44px). ONE line: tight vertical
   padding, the 44px floor comes from min-block-size, not stacked content, so it reads like a single field row. */
.kl-rb-locwx-head {
  appearance: none; background: none; border: 0; margin: 0; cursor: pointer;
  inline-size: 100%; min-block-size: var(--k-touch); /* the a11y floor, not a padded two-liner */
  display: flex; align-items: center; gap: var(--k-space-2);
  padding-block: var(--k-space-1); padding-inline: var(--k-space-4);
  font: inherit; color: var(--k-text); text-align: start;
}
.kl-rb-locwx-head:focus-visible {
  outline: 2px solid var(--k-accent); outline-offset: -2px; border-radius: var(--k-radius-md);
}

/* COLLAPSED — the single at-a-glance line: address half · weather half, EVEN SPLIT (owner ruling 2026-08-07).
   Both halves are flex:1 1 0 (zero basis) so the split is a fixed 50/50 of the line's content box regardless
   of either value's length — an auto basis would let content length decide the split, which is the bug this
   fixes. Each half's own furniture (pin icon on the loc side, "·" separator on the wx side) is charged to
   that half via flex:0 0 auto on the furniture and flex:1 1 auto + min-inline-size:0 on the value, so the
   TEXT shrinks/ellipsises inside its half while the icon/separator stay whole. */
.kl-rb-locwx-line {
  flex: 1 1 auto; min-inline-size: 0;
  display: flex; align-items: baseline; gap: var(--k-space-2);
}
.kl-rb-locwx-loc {
  flex: 1 1 0; min-inline-size: 0;
  display: inline-flex; align-items: baseline; gap: 4px;
}
.kl-rb-locwx-ico {
  flex: 0 0 auto; font-size: 13px; color: var(--k-text-muted); align-self: center;
}
.kl-rb-locwx-wx {
  flex: 1 1 0; min-inline-size: 0; display: inline-flex; align-items: baseline;
}
/* A hairline separator before the weather so it reads as its own segment on the shared line; it never shrinks. */
.kl-rb-locwx-wx::before {
  flex: 0 0 auto;
  content: "·"; color: var(--k-text-muted); margin-inline-end: var(--k-space-2);
}
/* OPEN — a slim uppercase title; the body renders the real controls, so the header sheds the previews. */
.kl-rb-locwx-title {
  flex: 1 1 auto;
  font-size: 12px; font-weight: 600; letter-spacing: 0.02em;
  color: var(--k-text-muted); text-transform: uppercase;
}
/* The value shrink-fits and ellipsises so a long address never wraps the header taller. */
.kl-rb-locwx-val {
  min-inline-size: 0;
  font-size: 14px; color: var(--k-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Both the address value and the weather value are the flexible part of their own half — each shrinks/ellipsises
   independently within its 50% share of the line; only the pin icon and the "·" separator stay whole (above). */
.kl-rb-locwx-loc .kl-rb-locwx-val,
.kl-rb-locwx-wx .kl-rb-locwx-val { flex: 1 1 auto; }
.kl-rb-locwx-muted { color: var(--k-text-muted); font-style: italic; }

.kl-rb-locwx-chevron {
  flex: 0 0 auto; font-size: 16px; color: var(--k-text-muted);
  transition: color var(--k-dur-1) var(--k-ease);
}
.kl-rb-locwx-head:hover .kl-rb-locwx-chevron,
.kl-rb-locwx-head[aria-expanded="true"] .kl-rb-locwx-chevron { color: var(--k-accent); }

/* BODY — the full controls, revealed on expand. A short height/opacity reveal keeps it native, no jank.
   [hidden] is what collapses it (the region always exists so aria-controls resolves); the explicit
   display:none is required because the flex declaration below would otherwise beat the UA hidden rule. */
.kl-rb-locwx-body {
  display: flex; flex-direction: column; gap: var(--k-space-4);
  padding: var(--k-space-4);
  border-block-start: 1px solid var(--k-border);
  animation: kl-rb-locwx-reveal var(--k-dur-2, 180ms) var(--k-ease) both;
}
.kl-rb-locwx-body[hidden] { display: none; }
@keyframes kl-rb-locwx-reveal {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .kl-rb-locwx-body { animation: none; }
}

/* ============================================================================
   AN ICON IS NEVER A CONTAINER (owner-reported 2026-07-22).
   ----------------------------------------------------------------------------
   The structural families above are matched by the SUFFIX of the class ATTRIBUTE
   — :where([class^="kl-"],[class*=" kl-"])[class$="-card"], :where([class^="kl-"],[class*=" kl-"])[class$="-btn"], :where([class^="kl-"],[class*=" kl-"])[class$="-badge"] and 60 more. That is a
   deliberate, useful convention for containers, but it cannot tell a container
   from an ICON whose semantic name happens to end the same way. Seventeen do:

       .ki-ms-credit-card  ends "-card"     .ki-save          ends "-save"
       .ki-ms-badge        ends "-badge"    .ki-delete        ends "-delete"
       .ki-error           ends "-error"    .ki-overflow-menu ends "-menu"      ...

   Whenever such an icon is written class-last — <span class="ki ki-ms-credit-card">
   — the attribute ends with "-card" and the icon was painted AS a card: surface
   background, 1px border, 16px radius, 24px padding, display:block. That is the
   "big white square where the icon should be" reported on the billing button: the
   glyph was rendering correctly all along, inside a white card of its own.

   Reset ONLY the container properties. Never font-size, colour or line-height —
   icons legitimately set those, and several deliberate treatments (the home
   quick-action chips, the plan chip) depend on them. Declared AFTER THE LAST
   STRUCTURAL SUFFIX RULE so source order settles the tie against every
   equal-specificity structural rule above; scoped component rules (which carry a
   b-* attribute, and so out-specify this) still win, exactly as they should.

   THAT — not "last in the file" — is the invariant, and the distinction is
   load-bearing (corrected 2026-07-25). Sections added below this point are fine
   PROVIDED they declare no `[class$="…"]` structural family; §9's query card and
   the card-list section that follow it declare none, and the two `.ki` rules they
   do carry (`.kl-qcard-toggle .ki`) set font-size / line-height / colour, which
   this block deliberately never touches and which out-specify it anyway.
   Req_Icons_NotAContainerTests enforces exactly this ordering, reading RULES and
   not the prose above — so append new sections freely, but if one ever needs a
   suffix family, move this block below it.
   ============================================================================ */
.kl-shell-main .ki {
  display: inline-block;
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  padding: 0;
  margin-block-end: 0;
}

/* ============================================================================
   §9. QUERY CARD (`.kl-qcard`) — the ONE list-surface query pattern
   ----------------------------------------------------------------------------
   Owner ruling 2026-07-25: every list/registry screen presents its query chrome
   as ONE card with ONE line —

       [ primary filter ]  [ search box ]  [ ONE "Filters" disclosure button ]

   — and everything else (the surplus filters, a search-scope picker, the
   show-deleted read toggle) lives behind that single button, inside the SAME
   card. Two cards and two disclosure controls for one region was the defect: it
   spent a whole second card of vertical space on chrome that is idle for the
   overwhelming majority of visits, and it made the phone and the desk two
   different things to learn.

   PROVENANCE. This pattern shipped first on the records registry (/records) —
   phone-only on 2026-07-24, promoted to every width on 2026-07-25 — where it
   took the query chrome from two cards (~227px) down to a single header line.
   Its CSS lived page-scoped in RecordList.razor.css. Sweeping the other seven
   registries onto it would have meant EIGHT copies of one pattern, which is
   precisely the divergence this repo has already been bitten by (RecordList and
   Dogs once carried two differently-scoped copies of the same rhythm fix — see
   Kanine.Architecture.Tests/SharedListingRhythmGuardTests). So it lives here,
   once, in the RCL that BOTH heads load. Only genuinely per-page deltas (which
   control is the primary, and how wide it is) stay page-scoped.

   OPT-IN BY WRAPPER. Every rule below is anchored to `.kl-qcard`, so a
   `.kl-searchrow` / `.kl-searchbar` / `.kl-filterpanel` anywhere else in the app
   is completely untouched.

   ANATOMY (RecordList.razor and Vehicles.razor are the reference hosts):

     <div class="kl-qcard kl-qcard--hassearch">          <- wrapper
       <section class="kl-searchrow [kl-qcard-open]">    <- HEADER line
         ...primary filter... ...search box...
         <button class="kl-qcard-toggle">Filters</button><- the ONE control
       </section>
       <section class="kl-filterpanel [kl-qcard-open]">  <- BODY (collapsed)
         <FilterAccordion HostOwnsDisclosure Expanded=... >
       </section>
     </div>

   DOM ORDER is header-then-body on purpose: a disclosure whose revealed content
   precedes its own button in the tab order is a WCAG 2.4.3 defect. Because the
   markup order already IS the visual order, no `order` reversal is needed at any
   width.

   NOTE ON THE SHARED-RHYTHM GUARD: nothing here sets `margin-block-end` on
   `.kl-filterpanel` / `.kl-searchrow`. The wrapper is the flex child of
   `.kl-page`, so the page gap still owns the rhythm (§8e-w1).
   ============================================================================ */

/* The wrapper butt-joins its two sections into a single card — `gap: 0` is the
   whole trick; `.kl-page`'s own gap still separates the card from its siblings. */
.kl-shell-main .kl-qcard {
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* Collapsed: the body is not merely empty, it is GONE — no box, no border, no
   page gap. That vanished card IS the reclaim. `display:none` is correct for a
   collapsed disclosure: the button's aria-expanded="false" already tells
   assistive tech the content is not exposed (and the FilterAccordion surplus
   inside it carries [hidden] for the same reason). */
.kl-shell-main .kl-qcard > .kl-filterpanel { display: none; }

.kl-shell-main .kl-qcard > .kl-filterpanel.kl-qcard-open {
  display: block;
  /* Butt-joined under the header: no top border (it would double the header's)
     and no top radius, so the two boxes read as ONE card. */
  border-block-start: 0;
  border-start-start-radius: 0;
  border-start-end-radius: 0;
}

/* ---- The HEADER line. The baseline leaves `.kl-searchrow` a bare flex row; inside
   a query card it becomes the card itself. `flex-wrap: wrap` is load-bearing rather
   than cosmetic — it is what lets a revealed scope picker (or a wrapped control on a
   phone) drop onto its own line INSIDE the card. `align-items: flex-end` overrides
   the shared `center` so a .kf-field primary (whose label sits ABOVE its control)
   shares a bottom edge with the search box and the button instead of floating
   centred against them. ---- */
.kl-shell-main .kl-qcard > .kl-searchrow,
.kl-shell-main .kl-qcard > .kl-searchbar {
  position: relative;             /* positioning context for any popover */
  overflow: visible;              /* a scope popover must escape the card */
  background-color: var(--k-surface);
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-lg);
  padding: var(--k-space-2);
  box-shadow: var(--k-elev-1);
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--k-space-3);

  /* ONE CARD — the header may not push the body it discloses away from itself. Found live 2026-08-09:
     `/vehicles` and `/dogs` name their header `.kl-searchbar` and each page sheet still gives it
     `margin-block-end: var(--k-space-4)` from the era when the header WAS the `.kl-page` flex child. Inside a
     query card the WRAPPER is that child, so the margin is (a) the §8e-w1 doubled gap, still live under a class
     name that guard's list does not carry, and (b) a 16px split straight through the middle of a card §A9 says
     is butt-joined. Measured header-bottom to body-top: /vehicles 16, /dogs 16, every `.kl-searchrow` page 0.
     Zeroed HERE rather than in the two page sheets so it holds for any future header, and because this selector
     is (0,3,0) — it out-specifies a page-scoped `.kl-searchbar[b-hash]` (0,2,0) without an `!important`. */
  margin-block-end: 0;

  /* ---- THE EYEBROW: the brand hairline across the TOP edge of the MERGED card (owner report
     2026-07-25). It is painted as the card's OWN top background LAYER, not as an absolutely
     positioned `::before` bar.

     WHY NOT A ::before BAR. That is what shipped, and it overhung the rounded corners: a 2px-tall
     box cannot carry a 16px corner radius, because CSS scales every radius down by the same factor
     until it fits the box's shortest side (f = 2/16), so `border-start-*-radius: var(--k-radius-lg)`
     rendered as a ~2px nub and the bar ran the card's FULL width. At the top edge the card's painted
     area is inset by up to the corner radius, so the bar's ends stuck out past the card's outline —
     measured at 9.43px of overhang per corner, at every width.

     WHY NOT `overflow: hidden` ON THE CARD. It would clip the bar to the curve perfectly and it is
     the obvious fix — but this card deliberately hosts escaping UI. The scope picker's
     `.kl-searchscope-menu` is `position: absolute` INSIDE this row, so clipping truncates it; proven
     by screenshotting the popover with `overflow:hidden` forced on and diffing the pixels (they
     differ). `overflow: visible` above stays load-bearing.

     WHY NOT INSET THE BAR BY THE RADIUS. It works, but it stops the bar ~16px short of each end
     rather than following the curve, and it has to repeat --k-radius-lg so the two can drift apart.

     A BACKGROUND LAYER is clipped to the card's own rounded border-box by construction, so it hugs
     the corner curve exactly, at any width, radius or state, needs no second token, and touches
     neither `overflow` nor the stacking order. `background-position: 0 0` is the padding box's top
     edge — the same 2px band the old ::before occupied, so nothing else moves. ---- */
  background-image: linear-gradient(
      color-mix(in srgb, var(--k-accent) 55%, transparent),
      color-mix(in srgb, var(--k-accent) 55%, transparent));
  background-size: 100% 2px;
  background-repeat: no-repeat;
  background-position: 0 0;
}

/* The panel's own top accent is neutralised INSIDE a query card — the header owns
   the top edge now. */
.kl-shell-main .kl-qcard > .kl-filterpanel::before { content: none; }

/* Open: the header loses its bottom radius so it reads as one card with the body. */
.kl-shell-main .kl-qcard > .kl-searchrow.kl-qcard-open,
.kl-shell-main .kl-qcard > .kl-searchbar.kl-qcard-open {
  border-end-start-radius: 0;
  border-end-end-radius: 0;
}

/* The search box carries the GROW on the header line (the primary is fixed-ish and
   the button is content-sized), with a floor so it never collapses to nothing. This
   overrides §8g's `flex: 1 1 16rem`, which would force a wrap on a three-control
   line at tablet width. */
.kl-shell-main .kl-qcard .kl-searchbox,
.kl-shell-main .kl-qcard .kl-search-shell {
  flex: 1 1 12rem;
  min-inline-size: 8rem;
}

/* A .kf-field primary on the header line: the form kit gives every field a
   --k-space-4 bottom margin for the STACKED grid, which on a single line would drop
   the control ~16px below the search box and the button. Zero it HERE only (the
   surplus grid's fields, which really do stack, are untouched). The width band keeps
   a select readable without letting it eat the line a page has to share. */
.kl-shell-main .kl-qcard > .kl-searchrow > .kf-field,
.kl-shell-main .kl-qcard > .kl-searchbar > .kf-field {
  margin-block-end: 0;
  flex: 0 1 15rem;
  min-inline-size: 9rem;
}

/* A non-field primary (a segmented tablist, a chip bar) sits content-sized on the
   header line and never grows into the search box's room. */
.kl-shell-main .kl-qcard-primary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--k-space-2);
  flex: 0 1 auto;
  margin-block-end: 0;
}

/* ---- THE one disclosure button. A real <button>: focusable, Enter/Space-operable
   for free, --k-touch tall (A6 — no target is ever shrunk to buy this space), in the
   quiet ghost-control language the rest of the header uses. ---- */
.kl-shell-main .kl-qcard-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--k-space-2);
  flex: 0 0 auto;
  min-block-size: var(--k-touch);
  padding: 0 var(--k-space-3);
  background: transparent;
  border: 1px solid var(--k-border);
  border-radius: var(--k-radius-md);
  color: var(--k-text);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--k-dur-1) var(--k-ease), border-color var(--k-dur-1) var(--k-ease);
}

.kl-shell-main .kl-qcard-toggle:hover {
  background: var(--k-surface-2);
  border-color: var(--k-accent);
}

.kl-shell-main .kl-qcard-toggle:focus-visible {
  outline: 2px solid var(--k-accent);
  outline-offset: 2px;
}

.kl-shell-main .kl-qcard-toggle .ki {
  font-size: 16px;
  line-height: 1;
  color: var(--k-text-muted);
}

/* B8 — a collapsed card must NEVER hide that the list on screen is narrowed. The
   accent + the count badge say "something folded in here is restricting this list";
   the count comes from the EXECUTED query, never the pending form state. */
.kl-shell-main .kl-qcard-toggle--active { border-color: var(--k-accent); }
.kl-shell-main .kl-qcard-toggle--active .kl-qcard-toggle-label { color: var(--k-accent); }
.kl-shell-main .kl-qcard-toggle--active .ki { color: var(--k-accent); }

.kl-shell-main .kl-qcard-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 18px;
  padding-inline: var(--k-space-1);
  border-radius: var(--k-radius-full);
  background: var(--k-accent);
  color: var(--k-surface);
  font-family: var(--k-font-mono);
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* ---- PHONE (max-width 640px). The collapsed header must stay ONE line:
   [search] [Filters]. A primary filter on that line would regrow the very card just
   reclaimed and, at 393px, would wrap to do it — so on a page that HAS a search box
   the primary travels WITH the disclosure: hidden while collapsed, full-width on its
   own line inside the card when open. It is not lost while collapsed; the toggle's
   accent + count badge still say the list is restricted.

   A page with NO search box keeps its primary visible at every width (`--hassearch`
   is absent there) — hiding it would leave a header line containing nothing but the
   button, which is chrome disclosing chrome. ---- */
@media (max-width: 640px) {
  .kl-shell-main .kl-qcard--hassearch > .kl-searchrow > .kf-field,
  .kl-shell-main .kl-qcard--hassearch > .kl-searchbar > .kf-field,
  .kl-shell-main .kl-qcard--hassearch > .kl-searchrow > .kl-qcard-primary,
  .kl-shell-main .kl-qcard--hassearch > .kl-searchbar > .kl-qcard-primary {
    display: none;
  }

  .kl-shell-main .kl-qcard--hassearch > .kl-searchrow.kl-qcard-open > .kf-field,
  .kl-shell-main .kl-qcard--hassearch > .kl-searchbar.kl-qcard-open > .kf-field {
    display: flex;                 /* the form kit's label-over-control column, restored */
    flex: 1 1 100%;
  }

  .kl-shell-main .kl-qcard--hassearch > .kl-searchrow.kl-qcard-open > .kl-qcard-primary,
  .kl-shell-main .kl-qcard--hassearch > .kl-searchbar.kl-qcard-open > .kl-qcard-primary {
    display: flex;
    flex: 1 1 100%;
  }

  /* The opened body's desk padding costs 24px of vertical either side of a 393px column. */
  .kl-shell-main .kl-qcard > .kl-filterpanel.kl-qcard-open {
    padding: var(--k-space-3);
  }

  /* With the primary gone, the box + the one disclosure own the header line; 8rem is
     what keeps them on ONE line at 393px. */
  .kl-shell-main .kl-qcard--hassearch .kl-searchbox,
  .kl-shell-main .kl-qcard--hassearch .kl-search-shell {
    flex: 1 1 8rem;
  }
}

/* ---- THE TIGHT BAND, 641px to 900px (measured defect, fixed 2026-07-25) ------------------------------------
   Just above the phone breakpoint the primary comes BACK onto the header line while the content column is at
   its narrowest — the rail is still a drawer, so the card gets the viewport minus `.kl-shell-main`'s fluid
   gutters (~574px of content at 641px). Three controls have to share it.

   WHY IT WRAPPED, and why the existing floors did not save it: flexbox decides LINE BREAKS from each item's
   HYPOTHETICAL main size — its `flex-basis` — and only shrinks items AFTER they are placed on a line. The bases
   sum to more than the card long before any `min-inline-size` floor is consulted:

       /records   18rem (288) + 12rem (192) + 112 button + 24 gaps = 616 > 574   -> the button wraps
       /dogs      15rem (240) + 12rem (192) + 112 button + 24 gaps = 568 ~ 574   -> wraps at the tightest widths

   So `min-inline-size: 8rem` on the search box never applied: the box was never asked to shrink, the BUTTON was
   asked to leave. Measured wrapping widths before this rule (live, 20px steps): /records 641-681, /dogs 641-661,
   /vehicles 641-661. (An earlier sweep read /records as 641-767 and the other two as clean — that run was taken
   while the Kanine.Presentation.Pages scoped-CSS bundle was 404ing, so it measured pages missing their own
   sheets. See .verify/scoped-css-delivery.mjs, which now guards that delivery.)

   THE FIX is on the BASES, not the floors, and it is a band rather than a global change because the desk widths
   were never wrong: above ~900px there is slack and the primary should keep its comfortable width. Nothing here
   shrinks a touch target — `.kl-qcard-toggle` keeps `min-block-size: var(--k-touch)` and is untouched; only the
   two SHRINKABLE text controls give up hypothetical width, and each still stops at its existing floor.
   ------------------------------------------------------------------------------------------------------------ */
@media (min-width: 641px) and (max-width: 900px) {
  /* The primary: basis down to its own floor, so it asks for exactly what it will settle for. */
  .kl-shell-main .kl-qcard > .kl-searchrow > .kf-field,
  .kl-shell-main .kl-qcard > .kl-searchbar > .kf-field,
  .kl-shell-main .kl-qcard > .kl-searchrow > .kl-qcard-primary,
  .kl-shell-main .kl-qcard > .kl-searchbar > .kl-qcard-primary {
    flex-basis: 9rem;
  }

  /* The search box keeps `flex-grow: 1`, so it still absorbs every spare pixel on the line — it simply stops
     CLAIMING 12rem up front, which is what was pushing the one disclosure button onto a second row. */
  .kl-shell-main .kl-qcard .kl-searchbox,
  .kl-shell-main .kl-qcard .kl-search-shell {
    flex-basis: 8rem;
  }
}

/* ---- §9b. THE DISCLOSED BODY IS A COMPOSED ROW — every control on it shares ONE centre line
   (owner ruling 2026-08-09, reported on /vehicles) ---------------------------------------------------------------

   OWNER: *"the show deleted checkbox could be better positioned to be in vertical alignment to the make
   dropdown."* Read as: their vertical CENTRES agree — the §A7 toggle sits on the neighbouring filter's line,
   not 32px under it. (The other available reading — left-edge alignment into a column — was NOT built; if he
   means that instead, this block is where it changes.)

   WHY IT WAS OFF, and why it is NOT a Vehicles bug. `.kl-filteraccordion-surplus` (FilterAccordion.razor.css)
   is a grid with `align-items: end`, which is already the right intent: every control in the disclosure bottom-
   aligns, and since each one is `var(--k-touch)` tall (A6) a shared bottom edge IS a shared centre line. But
   `align-items` aligns MARGIN boxes, and every neighbour the toggle sits beside carries a trailing block margin
   that `.kl-showdeleted` does not:

     • `.kl-filter-grid`  -> 16px, from §5's `[class$="-grid"]` container family;
     • `.kf-field`        -> 16px, from §5b's form-kit stacking margin;
     • `.kl-filter-pills` -> 12px, TrainingAids' own facet-pill row.

   Each one lifts its OWN control that far off the row's bottom edge while the toggle stays on it. Measured live
   at 1400px (show-deleted centre MINUS the neighbouring control's centre, before this rule):

     /vehicles +32.0   /dogs +32.0   /certifications +32.0   /medical +16.0   /training-aids +12.0

   32 where both margins stack (a `.kl-filter-grid` wrapper AND the `.kf-field` inside it), 16 where the field is
   a direct surplus child (Medical), 12 for the pills row. `/master-names` is the control group: its surplus holds
   the toggle and nothing else, so there is no row to compose and it measured, and stays, unaffected.

   THE FIX IS A SUBTRACTION, once, in the shared sheet. The surplus is a grid with its own `gap`, so a trailing
   margin on anything it lays out is redundant spacing AND the thing breaking the alignment. Zeroing it lets the
   `align-items: end` that was already there do what it says. Bounded to the two levels the surplus actually lays
   out — its items, and the fields inside a `.kl-filter-grid` item — so a control's own internals (DailyLog's
   `.kl-dl-filter-row`, AuditView's `.kf-field-error`) are untouched.

   `> *` DELIBERATELY, NOT A SUFFIX FAMILY. `[class$="-grid"]` keys on the class ATTRIBUTE ending, so one trailing
   modifier class (or a trailing space) silently drops an element out of the rule — this repo has been bitten by
   that twice. The universal child selector cannot miss a member, and it means a NEW surplus filter added on any
   page inherits the alignment instead of re-opening this defect.

   SPECIFICITY IS (0,3,0) ON PURPOSE — the `.kl-filteraccordion >` step is not decoration. A page-scoped
   `.razor.css` selector carries the component's `[b-hash]` attribute, which is worth a class: TrainingAids'
   `.kl-filter-pills { margin-block-end: var(--k-space-3) }` resolves to `.kl-filter-pills[b-…]` at (0,2,0), so a
   (0,2,0) reset here would TIE with it and lose on sheet order — the toggle would stay 12px low on exactly one
   registry, which is the "the stylesheet says it, the computed style disagrees" trap. The surplus is a direct
   child of `.kl-filteraccordion` by construction (FilterAccordion.razor), so the extra step costs nothing and is
   not a guess. `FilterDisclosureAlignmentGuardTests` fails the build if a scoped sheet re-adds one anyway.

   Reclaim, measured live at 1400px (query-card body height, before -> after): /vehicles 154 -> 122,
   /dogs 255 -> 223, /certifications 154 -> 122, /medical 138 -> 122, /training-aids 113 -> 101. That is a side
   effect of the alignment, not a second change: the removed margins were spending vertical space (B1) to break
   the row.
   ------------------------------------------------------------------------------------------------------------ */
.kl-shell-main .kl-filteraccordion > .kl-filteraccordion-surplus > *,
.kl-shell-main .kl-filteraccordion > .kl-filteraccordion-surplus > * > * {
  margin-block-end: 0;
}

/* =============================================================================
   10. CARD LIST (kl-cardlist) — the CONTAINER for off-table cards. It supplies the
   list and the card BOX only; the title LINE inside each card is section 8b-n's
   `.kl-cardline`, unchanged and un-copied.
   -----------------------------------------------------------------------------
   WHY THIS EXISTS. Four components rendered a `<ul>`/`<li>` card list whose every
   class was defined NOWHERE in `app/src` — `Involvement/UseOfForcePanel`,
   `SessionRevocationAdmin`, `MasterNameDedupQueue` and `LinkIntoRecordPanel`. They
   were unstyled block `<div>`s, so every child stacked at every width, and each one
   PASSED the wrap-and-overflow sweeps because a block element cannot wrap. That is
   conformance by ABSENCE, and it is invisible to every other check in this repo.
   (`app/tools/page-review/unstyled-class-scan.mjs` is the mechanical version of the
   hand audit that found them; it reports 400+ more classes in the same state.)

   WHAT THIS BLOCK DELIBERATELY DOES NOT DO. It does NOT define a title, a badge or
   an action slot. Section 8b-n already made A10's anatomy universal off-table
   through `.kl-cardline` + `.kl-tcard-title` / `.kl-tcard-badge` / `.kl-tcard-actions`,
   and a second spelling of the same three ideas is precisely the drift this review
   has been unwinding — two hand-rolled corner slots had already diverged by 4.5px in
   July. These four surfaces therefore wear `.kl-cardline` like every other off-table
   card in the product, and this section adds only the two things a LIST needs and a
   single card does not:

     1. THE LIST RESET. It cannot be inherited. Section 5l's `[class$="-list"]` family
        matches the whole class ATTRIBUTE, so the moment a list reads
        `class="kl-x-list kl-cardlist"` it no longer ends in `-list` and stops matching
        — adding the opt-in class is exactly what would remove the reset.
     2. THE CARD BOX. A `<tr>` gets its card chrome from section 8b-m's reflow and a
        page card from section 5a's `-card` suffix; an `<li>` in a purpose-built list
        has neither, so the surface / border / radius / padding are declared here. The
        padding matches 8b-m's collapsed row, which is what lets `--k-tcard-line` mean
        the same thing in all three places.

   Body content below the title line (`-body`, `-chips`, `-foot`) is content
   vocabulary, not anatomy, so it lives here rather than in 8b-n.
   ========================================================================== */

/* The block-rhythm wrapper a card list sits in (a panel/section that stacks blocks at
   the house gap instead of collapsing to default block margins). */
.kl-cardstack { display: flex; flex-direction: column; gap: var(--k-space-3); }

.kl-cardlist {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--k-space-3);
}

/* THE CARD BOX — a simple flex COLUMN: the `.kl-cardline` header, then the body lines.
   It needs no wrap, no ordering and no generated line break, because the card's header
   element already bounds the title line (8b-n's third deliberate difference from the
   table form). */
.kl-cardlist .kl-cardlist-item {
  display: flex; flex-direction: column; gap: var(--k-space-2);
  background: var(--k-surface); color: var(--k-text);
  border: 1px solid var(--k-border); border-radius: var(--k-radius-lg);
  box-shadow: var(--k-elev-1);
  padding: var(--k-space-3) var(--k-space-4);
}

/* TWO SCOPED TUNINGS of 8b-n's line, both MEASURED on the live harness, both written
   THROUGH the `.kl-cardlist` wrapper so they cannot reach the cards 8b-n owns. Section
   A9 clause 3 is the precedent: a genuine per-surface delta is written through the
   wrapper, never as a private re-implementation. Both are candidates for promotion into
   8b-n — raised in the handover rather than taken unilaterally.

   1. THE TITLE'S BASE WIDTH. `flex-basis: 0` is exactly right for a card whose line
      holds a title and a badge: the title asks for nothing and grows into the rest. It
      is NOT enough once a labelled ACTION SET shares the line, and the failure is not
      obvious — flexbox picks lines from base sizes, so a zero-basis title never forces
      a break, and the title is then handed only the free space left over. Measured at
      393px on the use-of-force entry (a 149px injury chip + a 131px View/Edit pair):
      the title was allotted 23px and its timestamp wrapped down SEVEN lines, making a
      120px title line and a 206px card. A 6rem base makes the title claim its share up
      front, which pushes the action slot onto a line of its own exactly when it will
      not fit — and leaves the one-line card alone where it does (the involvement panel
      still lands all three on one 74px line). It is a BASE, not a floor: `flex-shrink`
      is still 1, so nothing overflows at 320px.
   2. THE ACTION SLOT MUST BE ABLE TO WRAP ITS OWN CONTROLS. `flex: 0 0 auto` +
      `display: flex` with no wrap means a slot holding a SET cannot narrow at all.
      Measured on the dedup queue (Accept (merge) + Not the same = 329px): the slot ran
      to 346px inside a 254px card at 320px — a real horizontal overflow of the document,
      not merely an ugly line. Wrapping inside the slot, capped at the card's width, is
      what makes a multi-action card list survive a 320px screen. */
.kl-cardlist .kl-cardline > .kl-tcard-title { flex-basis: 6rem; }
.kl-cardlist .kl-cardline > .kl-tcard-actions { flex-wrap: wrap; max-inline-size: 100%; }

/* A6 ON A TITLE THAT IS ITSELF TAPPABLE. Section 8b-n gives the title no touch floor,
   which is right for a plain text title — but the involvement panel's title IS the
   record drill-in anchor, a standalone control that measured 20px on the live harness
   at 393px, well under the WCAG 2.5.8 line this repo holds. Restoring the floor is FREE
   whenever an action slot shares the line (8b-n already holds that line at 48px), and
   A6 says the trade is not on offer anyway. Scoped to `.kl-cardlist` so it cannot
   disturb the off-table cards 8b-n owns.

   DELIBERATELY NOT APPLIED TO LINKS *INSIDE* A TITLE. The dedup queue's title is the
   phrase "ANDERSON, ALICE M ~ ANDERSEN, ALICE", two links reading as one identity —
   WCAG 2.5.8's own INLINE exception (a target in a sentence or block of text) covers
   exactly that, and forcing each to 48px measured a 232px title line and a 258px card
   at 393px, because each name then wrapped as a 48px block. A rule that makes a card
   two and a half times taller to satisfy an exception the standard grants is worse for
   the thumb, not better: it pushes the next row off the screen. */
.kl-cardlist .kl-cardline .kl-tcard-title:is(a, button, [role="button"]) {
  display: inline-flex; align-items: center;
  min-block-size: var(--k-touch);
}

/* A body field — its own line under the title line. */
.kl-cardlist .kl-cardlist-body {
  min-inline-size: 0;
  font-size: 13px; color: var(--k-text-muted); overflow-wrap: anywhere;
}
/* Related chips sharing ONE row (section B2) rather than stacking one per line. */
.kl-cardlist .kl-cardlist-chips {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-2);
}
/* A field that must come LAST whatever its DOM order (A10 clause 7's foot line). */
.kl-cardlist .kl-cardlist-foot { order: 9; }

/* ── OPS-012 booking-conflict warning ────────────────────────────────────────────────────────────────────
   ONE family, shared by all three planning surfaces (Scheduler's selected-event pane, EventCreate, EditEvent)
   per the one-shared-CSS-surface ruling — a scoped .razor.css cannot cross components, and three copies of a
   warning treatment is exactly how two of them end up looking different.

   It is styled as a WARNING, not a critical error: the conflict never blocks a write (see
   ScheduleConflictDetector), so it must not wear the refusal colour. Loud enough that a planner cannot scroll
   past it, quiet enough that it does not read as a failure. */
.kl-conflict-warning {
  margin: var(--k-space-3) 0 0 0;
  padding: var(--k-space-3);
  border: 1px solid var(--k-border);
  border-inline-start-width: 4px;
  border-radius: var(--k-radius-md, 6px);
  background: var(--k-surface-2);
  color: var(--k-text);
}

.kl-conflict-lede {
  margin: 0 0 var(--k-space-2) 0;
  font-weight: 600;
}

.kl-conflict-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--k-space-1);
}

.kl-conflict-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--k-space-2);
  min-inline-size: 0;
  font-size: 13px;
}

.kl-conflict-kind {
  font-weight: 600;
  white-space: nowrap;
}

.kl-conflict-what {
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

/* The honest caveats (no roster yet / detection unavailable) are notes, not alarms. */
.kl-conflict-note,
.kl-conflict-unavailable {
  margin: var(--k-space-2) 0 0 0;
  font-size: 12px;
  color: var(--k-text-muted);
}

/* A day cell the sergeant must be able to spot WITHOUT opening anything. */
.kl-scheduler-cell--conflict {
  box-shadow: inset 0 0 0 2px var(--k-border);
}

.kl-scheduler-conflict-flag {
  position: absolute;
  inset-block-start: 2px;
  inset-inline-end: 4px;
  font-size: 11px;
  line-height: 1;
  font-weight: 700;
  color: var(--k-text);
}

/* ── §PT — Personnel-type eligibility notice (OWNER RULING 2026-08-09) ────────────────────────────────
   The one visual home for "some people are not offered here, and this is why". Shared (A1): the same
   component renders on the web head and the MAUI field head, so the rule lives here and not in a
   page-scoped sheet.

   It is a LINE, never a card (§B4): a one-sentence fact must not spend a card's vertical space, and this
   string appears under as many as eight pickers. The component renders NOTHING when there is nothing to
   say, so there is no empty box consuming a flex gap (§B1 / the ShowDeletedNotice idiom).            */
.kl-personnel-notice {
  margin: var(--k-space-1) 0 0;
  font-size: 13px;
  line-height: 1.4;
  color: var(--k-text-muted);
}

/* The EMPTY state is the load-bearing one — it stands in for a picker with no options at all, so it is
   given the same quiet border treatment an honest empty state gets elsewhere rather than reading as a
   caption on a control that is not there. */
.kl-personnel-notice--empty {
  margin-block-start: var(--k-space-2);
  padding: var(--k-space-2) var(--k-space-3);
  border: 1px dashed var(--k-border);
  border-radius: var(--k-radius-sm, 6px);
  color: var(--k-text-muted);
}

/* ============================================================================
   §12 — "YOU WERE HERE": the row a reader came back to.
   Owner ruling 2026-08-09: "...press the back arrow and be sent back to the list
   just as i had left it SO I CAN INSPECT THE NEXT ROW DOWN."

   Restoring filters and scroll gets the reader to the right pixels. This is what
   turns that into "I can carry on": the row they dipped into is findable at a
   glance, and it is FOCUSED, so the very next Down/Tab continues from there.

   IT IS NOT A SELECTION. Every registry here already has a selection idiom
   (kl-cert-row--selected and friends) that means "this row is the subject of the
   panel you are looking at". Reusing it would tell the reader something false —
   nothing is selected, they are simply back where they were. So this is a quiet
   leading edge and a barely-there wash, and it is deliberately weaker than both
   the selection state and the focus ring.

   A DATA ATTRIBUTE, NOT A CLASS, and that is the whole reason this rule reads the
   way it does. The shared sheets carry [class$="-suffix"] selector families
   (§5a's "-card", §5l's "-list", the chip-leaf family). Appending a modifier
   token to a class attribute changes what that attribute ENDS with, so an element
   silently drops out of its family in one state and not the other — trap T7, hit
   three times on 2026-08-08 alone. data-returned cannot do that: it is a
   different attribute, so no class-suffix family can see it.

   Set and cleared by kanine-list-state.js, never by a page.
   ========================================================================== */
[data-returned="true"] {
  position: relative;
  /* The wash is an inset shadow rather than a background, so it composes over a
     zebra stripe, a deleted-row tint and a status tint without fighting any of
     them for the background property. */
  box-shadow: inset 3px 0 0 0 var(--k-accent), inset 0 0 0 100vmax rgb(0 0 0 / .035);
}

html.dark [data-returned="true"] {
  box-shadow: inset 3px 0 0 0 var(--k-accent), inset 0 0 0 100vmax rgb(255 255 255 / .05);
}

/* The reader is BACK, not being alerted. A row that animates its way in would
   pull the eye away from the next row down, which is the one they came back for. */
@media (prefers-reduced-motion: no-preference) {
  [data-returned="true"] {
    transition: box-shadow 160ms ease-out;
  }
}

/* The focus ring still wins where it applies. Focus means "the keyboard is here";
   data-returned means "this is where you left off". They coincide on arrival and
   diverge the moment the reader presses Down, which is exactly the distinction
   the two visuals need to carry. */

/* ── TAID-USE: the training aid's USAGE HISTORY list (/training-aids/{id}, Details panel). ────────────────────
   The rows answer "what was this aid used for", the half a reliability challenge turns on. Each row's record
   link is a REAL touch target: a bare inline <a> measured 44px live, under the house --k-touch floor, and the
   standing rule is that a target is never shrunk to buy layout. The list itself is a plain column with the
   design-system gap (no margin on the children — flex `gap` plus a child `margin-block-end` never collapse, and
   that doubled-gap class is the one this review already paid for once). */
.kl-aid-usage-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--k-space-2);
}
.kl-aid-usage-list > li {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--k-space-3);
  flex-wrap: wrap;
}
.kl-aid-usage-list a {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--k-touch);
}

/* ── §9c — THE INCREMENTAL-LOAD TAIL (owner ruling 2026-08-10) ──────────────────────────────────────────────────
   Shared, because the ruling landed on /daily-log and /master-names within an hour and a dozen registries are
   behind them. Rendered by Kanine.Presentation/Components/ListMoreSentinel.razor; the host wraps it in whatever
   element its container needs (an <li> inside a <ul>, a <td colspan> inside a table), which is why there is no
   list-item or table styling here.

   The control keeps the full --k-touch floor: A6 is not traded for the density of a control a reader only meets
   when they have run out of list. `list-style: none` and the zeroed inline padding are for the <li> case — a
   bullet beside a Load-more button reads as a row of data that is not one. */
.kl-listmore {
  display: flex;
  justify-content: center;
  padding-block: var(--k-space-3);
  list-style: none;
}
.kl-listmore-btn {
  min-block-size: var(--k-touch);
}

/* THE DEPTH-CAP REFUSAL (owner ruling 2026-08-10). Quiet, but genuinely on the page and in the reading order — the
   whole ruling is that this list stops OUT LOUD rather than silently, so a toast, a title attribute or anything that
   scrolls away would defeat it. It is the SAME element as the polite live region (see ListMoreSentinel.razor); this
   rule is only what unhides it, which is why there is no second node to keep in step. */
.kl-listmore-live--cap {
  margin: 0;
  max-inline-size: 44ch;
  text-align: center;
  text-wrap: balance;
  color: var(--k-text-muted);
  font-size: 13px;
  line-height: 1.45;
}

/* =============================================================================
   RECORD IDENTITY (owner ruling 2026-09-13) — a subject named on a record shown as
   a FACE plus a NAME, not as the words "View custodian" / "View suspect" /
   "View subject K9".
   -----------------------------------------------------------------------------
   The CONTENT (the photo disc, the name, the unresolved degrade) is rendered by
   the shared PersonIdentity / DogIdentity components in the RCL both heads load.
   The anchor around it stays in the host page, because the UI-GUAR-003 ledger
   anchors on a LITERAL data-testid in page markup. So the treatment lives HERE,
   once, rather than being restated in each page's scoped sheet.

   THE TWO FAMILIES SHARE EVERY RULE, on purpose. The owner ruled the SAME shape
   three times — training-aid custodian, use-of-force suspect, medical subject K9 —
   so a person and a K9 are laid out by one set of declarations and a change to one
   cannot leave the other behind. They are separate class names only because the
   markup that mounts them is separate; nothing here is allowed to differ.

   A6: `min-block-size: var(--k-touch)` on the link is what makes the pressable
   span 44+ CSS px. The photo disc is 32px, so the box alone would land near 34 —
   the explicit floor is doing the work, and it costs no page density because the
   row it sits in is already taller than a bare text line.

   The disc is a FIXED 32x32 box that is reserved whether or not a photo resolves,
   so the glyph fallback and a real photograph occupy exactly the same space and
   the row cannot jump when the image lands.

   CLASS-SUFFIX FAMILIES CHECKED (the `[class$="-…"]` trap): `-link`, `-disc`,
   `-name` and `-unknown` are none of the 40+ shared suffix families declared in
   section 5 of this sheet, so these four elements take their type from these rules
   only. */
.kl-personidentity-link,
.kl-dogidentity-link {
  display: inline-flex;
  align-items: center;
  gap: var(--k-space-2);
  min-block-size: var(--k-touch);
  min-inline-size: 0;
}

.kl-personidentity-disc,
.kl-dogidentity-disc {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 32px;
  block-size: 32px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--k-surface-2);
  border: 1px solid var(--k-border);
  color: var(--k-text-muted);
  font-size: 16px;
  line-height: 1;
}

.kl-personidentity-name,
.kl-dogidentity-name {
  min-inline-size: 0;
  overflow-wrap: anywhere;
}

/* The name did not resolve. Deliberately NOT the same treatment as a resolved
   name and deliberately NOT the same as an absent subject (the host does not
   mount this at all when there is no custodian / no suspect) — a withholding and
   an absence must read differently. */
.kl-personidentity-unknown,
.kl-dogidentity-unknown {
  min-inline-size: 0;
  font-style: italic;
  color: var(--k-text-muted);
}

/* ==========================================================================
   KANINE Passport — program surfaces (package C): /signup/program, /passport/programs, /passport/terms,
   /admin/passport-program, /operator/programs. ONE home for both heads (§A1). Every class is kl-pp-* and none ends
   in a shared suffix family (-row, -list, -section, -actions, -facts…), so no family restyles them (trap T7). The
   wide public pages widen the auth card with a DATA ATTRIBUTE, never a trailing modifier class. Tokens only.
   ========================================================================== */
.kl-shell-main .kl-login[data-pp-wide="true"] { max-inline-size: min(48rem, 100%); }
.kl-shell-main .kl-pp-toc,
.kl-shell-main .kl-pp-terms-items { margin: 0; padding-inline-start: var(--k-space-6); display: flex; flex-direction: column; gap: var(--k-space-2); }
.kl-shell-main .kl-pp-terms-part { display: flex; flex-direction: column; gap: var(--k-space-3); scroll-margin-block-start: var(--k-space-6); }
.kl-shell-main .kl-pp-query {
  display: grid; gap: var(--k-space-3); align-items: end;
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
}
.kl-shell-main .kl-pp-query-go,
.kl-shell-main .kl-pp-submitbar { display: flex; flex-wrap: wrap; gap: var(--k-space-2); align-items: center; }
.kl-shell-main .kl-pp-directory,
.kl-shell-main .kl-pp-queue-items { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--k-space-3); }
.kl-shell-main .kl-pp-entry {
  display: flex; flex-direction: column; gap: var(--k-space-2); min-inline-size: 0;
  padding: var(--k-space-4); border: 1px solid var(--k-outline); border-radius: var(--k-radius-md);
  background: var(--k-surface);
}
.kl-shell-main .kl-pp-entry-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--k-space-2); min-inline-size: 0; }
.kl-shell-main .kl-pp-entry-name { font-weight: 600; overflow-wrap: anywhere; min-inline-size: 0; }
.kl-shell-main .kl-pp-detail-name { margin: 0; overflow-wrap: anywhere; }
.kl-shell-main .kl-pp-entry-meta { margin: 0; color: var(--k-text-muted); overflow-wrap: anywhere; }
.kl-shell-main .kl-pp-mono { font-family: var(--k-font-mono); font-size: 0.85em; }
.kl-shell-main .kl-pp-chips { display: flex; flex-wrap: wrap; gap: var(--k-space-2); margin: 0; }
.kl-shell-main .kl-pp-fieldset { border: 0; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--k-space-1); min-inline-size: 0; }
.kl-shell-main .kl-pp-choice {
  display: flex; align-items: center; gap: var(--k-space-3); min-block-size: var(--k-touch); cursor: pointer;
}
.kl-shell-main .kl-pp-choice > input { flex: 0 0 auto; inline-size: 18px; block-size: 18px; margin: 0; accent-color: var(--k-accent); }
.kl-shell-main .kl-pp-names { display: grid; gap: var(--k-space-3); grid-template-columns: repeat(auto-fit, minmax(min(10rem, 100%), 1fr)); }
.kl-shell-main .kl-pp-stats {
  display: grid; grid-template-columns: minmax(8rem, max-content) 1fr; gap: var(--k-space-2) var(--k-space-4); margin: 0;
}
.kl-shell-main .kl-pp-stats dt { font-weight: 600; color: var(--k-text-muted); }
.kl-shell-main .kl-pp-stats dd { margin: 0; overflow-wrap: anywhere; min-inline-size: 0; }
@media (max-width: 640px) {
  .kl-shell-main .kl-pp-stats { grid-template-columns: 1fr; }
  .kl-shell-main .kl-pp-stats dd { margin-block-end: var(--k-space-2); }
}
.kl-shell-main .kl-pp-queue,
.kl-shell-main .kl-pp-detail { display: flex; flex-direction: column; gap: var(--k-space-4); min-inline-size: 0; }

/* ==========================================================================
   KANINE Passport — issue surfaces (package D1): /dogs/{id}/passport (kl-ppi-*), /admin/passports (kl-ppl), and the
   public check page /passport/{fingerprint} (kl-ppc-*). ONE home for both heads (§A1). No class ends in a shared
   suffix family (-row, -card, -field, -banner, -note, -list, -section, -actions, -panel…), so no family restyles them
   (trap T7). A modifier is a DATA ATTRIBUTE (data-passed), never a trailing class. Tokens only.
   ========================================================================== */
.kl-shell-main .kl-ppi,
.kl-shell-main .kl-ppl { display: flex; flex-direction: column; gap: var(--k-space-4); min-inline-size: 0; }
.kl-shell-main .kl-ppi-links { margin: 0; display: flex; flex-wrap: wrap; gap: var(--k-space-2); align-items: center; }
.kl-shell-main .kl-ppi-box {
  display: flex; flex-direction: column; gap: var(--k-space-3); min-inline-size: 0;
  padding: var(--k-space-4); border: 1px solid var(--k-outline); border-radius: var(--k-radius-md);
  background: var(--k-surface);
}
.kl-shell-main .kl-ppi-h2 { margin: 0; font-size: 1.1rem; display: flex; align-items: center; gap: var(--k-space-2); }
.kl-shell-main .kl-ppi-copy { display: flex; flex-wrap: wrap; gap: var(--k-space-2); align-items: center; min-inline-size: 0; }
.kl-shell-main .kl-ppi-copy > input { flex: 1 1 16rem; min-inline-size: 0; }
.kl-shell-main .kl-ppi-decline { font-weight: 600; overflow-wrap: anywhere; }
.kl-shell-main .kl-ppi-checks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--k-space-2); }
.kl-shell-main .kl-ppi-check { display: flex; align-items: flex-start; gap: var(--k-space-3); min-inline-size: 0; }
.kl-shell-main .kl-ppi-mark { flex: 0 0 auto; font-size: 1.25rem; line-height: 1.4; color: var(--k-success); }
.kl-shell-main .kl-ppi-mark[data-passed="false"] { color: var(--k-critical); }
.kl-shell-main .kl-ppi-check-text { display: flex; flex-direction: column; gap: var(--k-space-1); min-inline-size: 0; }
.kl-shell-main .kl-ppi-check-name { font-weight: 600; overflow-wrap: anywhere; }
.kl-shell-main .kl-ppi-check-meta { color: var(--k-text-muted); overflow-wrap: anywhere; }
.kl-shell-main .kl-ppi-check-need { overflow-wrap: anywhere; }
.kl-shell-main .kl-ppi-offer { display: flex; flex-direction: column; gap: var(--k-space-3); min-inline-size: 0; }
.kl-shell-main .kl-ppi-stepper { display: inline-flex; align-items: center; gap: var(--k-space-2); }
.kl-shell-main .kl-ppi-stepper > .kl-btn { min-inline-size: var(--k-touch); min-block-size: var(--k-touch); justify-content: center; }
.kl-shell-main .kl-ppi-seatcount { min-inline-size: 3ch; text-align: center; font-weight: 600; font-variant-numeric: tabular-nums; }
.kl-shell-main .kl-ppi-price { inline-size: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.kl-shell-main .kl-ppi-price th,
.kl-shell-main .kl-ppi-price td { padding: var(--k-space-2); border-block-end: 1px solid var(--k-outline); text-align: start; }
.kl-shell-main .kl-ppi-price td:last-child,
.kl-shell-main .kl-ppi-price tfoot td { text-align: end; }
.kl-shell-main .kl-ppi-price tfoot th,
.kl-shell-main .kl-ppi-price tfoot td { font-weight: 700; border-block-end: 0; }
.kl-shell-main .kl-ppi-disclosure {
  margin: 0; padding: var(--k-space-3); border-inline-start: 4px solid var(--k-accent);
  background: var(--k-surface-2); font-weight: 600; overflow-wrap: anywhere;
}
.kl-shell-main .kl-ppi-charge { margin: 0; font-weight: 700; }
.kl-shell-main .kl-ppi-versions { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--k-space-2); }
.kl-shell-main .kl-ppi-version { display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-2); min-inline-size: 0; }
.kl-shell-main .kl-ppi-version-name { font-weight: 600; }
.kl-shell-main .kl-ppi-dialog { display: flex; flex-direction: column; gap: var(--k-space-3); inline-size: min(36rem, 100%); max-block-size: 90vh; overflow-y: auto; }
.kl-shell-main .kl-ppi-dialog form { display: flex; flex-direction: column; gap: var(--k-space-2); }
.kl-shell-main .kl-ppc-command {
  margin: var(--k-space-2) 0 0; padding: var(--k-space-3); border-radius: var(--k-radius-md);
  background: var(--k-surface-2); overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere;
  font-family: var(--k-font-mono); font-size: 0.85em;
}

/* KANINE Passport — package I (R22, R24): the vetting rows of the application forms, the operator checklist, the dog
   Trainer section, and the onboarding step panel. kl-pp-* names that end in no shared suffix family. Tokens only. */
.kl-shell-main .kl-pp-rowset {
  padding: var(--k-space-3); border: 1px solid var(--k-outline); border-radius: var(--k-radius-md); gap: var(--k-space-2);
}
.kl-shell-main .kl-pp-crit { gap: var(--k-space-2); }
.kl-shell-main .kl-pp-trainer {
  display: flex; flex-direction: column; gap: var(--k-space-3); min-inline-size: 0;
  margin-block-start: var(--k-space-4); padding-block-start: var(--k-space-4); border-block-start: 1px solid var(--k-outline);
}
.kl-shell-main .kl-pp-invitebox { display: flex; flex-direction: column; gap: var(--k-space-2); min-inline-size: 0; }
.kl-shell-main .kl-pp-sharelink { display: block; overflow-wrap: anywhere; user-select: all; }
.kl-shell-main .kl-pp-onboard {
  display: flex; flex-direction: column; gap: var(--k-space-3); min-inline-size: 0; margin-block-start: var(--k-space-2);
}
/* ==========================================================================
   KANINE Passport — claim and provenance surfaces (package D2): the claim page /passport/claim (kl-ppcl-*), and the
   provenance of an imported record or dog on RecordDetailBody, DogDetail, MedicalDetail, CertificationDetail, and
   DogDetailEdit (kl-ppv-*). ONE home for both heads (§A1). No class ends in a shared suffix family (-row, -card,
   -field, -banner, -note, -list, -section, -panel, -facts, -grid…), so no family restyles them (trap T7). A verdict
   is a DATA ATTRIBUTE (data-verdict), never a trailing modifier class. Tokens only.
   ========================================================================== */
.kl-shell-main .kl-ppcl-inline { margin: 0; display: inline-flex; }
.kl-shell-main .kl-ppcl-admins { margin: 0; padding-inline-start: var(--k-space-6); display: flex; flex-direction: column; gap: var(--k-space-1); overflow-wrap: anywhere; }
.kl-shell-main .kl-ppv {
  display: flex; flex-direction: column; gap: var(--k-space-3); min-inline-size: 0;
  margin-block: var(--k-space-4); padding: var(--k-space-4); border: 1px solid var(--k-outline);
  border-inline-start: 4px solid var(--k-accent); border-radius: var(--k-radius-md); background: var(--k-surface);
}
.kl-shell-main .kl-ppv[data-verdict="revoked"],
.kl-shell-main .kl-ppv[data-verdict="failed"] { border-inline-start-color: var(--k-critical); }
.kl-shell-main .kl-ppv-head { margin: 0; font-size: 1.1rem; display: flex; align-items: center; gap: var(--k-space-2); }
.kl-shell-main .kl-ppv-lede { margin: 0; overflow-wrap: anywhere; display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-2); }
.kl-shell-main .kl-ppv-warn {
  margin: 0; padding: var(--k-space-3); border-radius: var(--k-radius-md); font-weight: 600; overflow-wrap: anywhere;
  color: var(--k-critical-fg); background: var(--k-critical-bg);
}
.kl-shell-main .kl-ppv-dl {
  margin: 0; display: grid; gap: var(--k-space-2) var(--k-space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
}
.kl-shell-main .kl-ppv-dl > div { display: flex; flex-direction: column; gap: var(--k-space-1); min-inline-size: 0; }
.kl-shell-main .kl-ppv-dl dt { font-weight: 600; color: var(--k-text-muted); }
.kl-shell-main .kl-ppv-dl dd { margin: 0; overflow-wrap: anywhere; min-inline-size: 0; }
.kl-shell-main .kl-ppv-verdict { font-weight: 700; color: var(--k-success); }
.kl-shell-main .kl-ppv-verdict[data-verdict="not-sealed"] { color: var(--k-text-muted); }
.kl-shell-main .kl-ppv-verdict[data-verdict="failed"],
.kl-shell-main .kl-ppv-verdict[data-verdict="revoked"] { color: var(--k-critical); }
.kl-shell-main .kl-ppv-mono { font-family: var(--k-font-mono); font-size: 0.85em; overflow-wrap: anywhere; }
.kl-shell-main .kl-ppv-fps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--k-space-1); }
.kl-shell-main .kl-ppv-fps a { display: inline-flex; align-items: center; min-block-size: var(--k-touch); }
.kl-shell-main .kl-ppv-events { margin: 0; padding-inline-start: var(--k-space-6); display: flex; flex-direction: column; gap: var(--k-space-2); }
.kl-shell-main .kl-ppv-event { display: flex; flex-wrap: wrap; gap: var(--k-space-2); overflow-wrap: anywhere; }
.kl-shell-main .kl-ppv-event-when { font-variant-numeric: tabular-nums; color: var(--k-text-muted); }
.kl-shell-main .kl-ppv-links { display: flex; flex-wrap: wrap; gap: var(--k-space-2); align-items: center; }
.kl-shell-main .kl-ppv-mark { white-space: nowrap; }

/* Signer custody (owner rulings 2026-09-27) — the Sign dialog and the "confirm it is you" dialog. Each method is one
   section, divided by a rule. The two-digit match code is what a user compares with the phone, so it is large and
   monospaced. Foundation tokens only. */
.kl-shell-main .kl-usersign-method {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--k-space-2);
  padding-block-start: var(--k-space-3); border-block-start: 1px solid var(--k-border);
}
.kl-shell-main .kl-usersign-method:first-of-type { padding-block-start: 0; border-block-start: 0; }
.kl-shell-main .kl-usersign-method h3 { margin: 0; font-size: 1rem; }
.kl-shell-main .kl-usersign-method p { margin: 0; }
.kl-shell-main .kl-usersign-code {
  font-family: var(--k-font-mono); font-size: 2.25rem; font-weight: 700; line-height: 1.1; letter-spacing: 0.15em;
}
/* A panel that hosts a nested overlay ("confirm it is you" opens inside the packet seal, the formal report, the
   retention change and the aid disposal dialogs) must not keep a transform. Several panels run an entry animation
   with fill mode "both" whose last keyframe is transform: translateY(0). A kept transform makes the panel the
   containing block of every position:fixed descendant, so the nested scrim shrank to the panel and clipped the
   dialog inside it (measured 2026-09-27). Specificity (0,3,0) wins over a scoped (0,2,0) panel rule. */
.kl-shell-main .kl-dialog:has(.kl-modal-overlay),
.kl-shell-main [role="dialog"]:has(.kl-modal-overlay) { animation: none; transform: none; }

/* Signer-custody plan WS-2 — the signing key screens (/account/signing-keys, its request screen, /admin/signing-keys)
   and the signing key banner. The cards, rows and lists take the shared *-card / *-row / *-list families by suffix.
   These rules add the key code, the facts list, the action row and the muted line. Foundation tokens only. */
.kl-shell-main .kl-signerkeys { display: flex; flex-direction: column; gap: var(--k-space-4); }
.kl-shell-main .kl-signerkeys-muted { color: var(--k-text-muted); overflow-wrap: anywhere; }
.kl-shell-main .kl-signerkeys-code { font-family: var(--k-font-mono); font-weight: 600; letter-spacing: 0.05em; }
.kl-shell-main .kl-signerkeys-key-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-2); }
.kl-shell-main .kl-signerkeys-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-2); margin-block-start: var(--k-space-2); }
.kl-shell-main .kl-signerkeys-date,
.kl-shell-main .kl-signerkeys-field { display: flex; flex-direction: column; gap: var(--k-space-1); min-inline-size: 0; }
.kl-shell-main .kl-signerkeys-field textarea { inline-size: 100%; font-family: var(--k-font-mono); font-size: 0.85em; }
.kl-shell-main .kl-signerkeys-card-text {
  margin: 0; padding: var(--k-space-3); border-radius: var(--k-radius-md); background: var(--k-surface);
  font-family: var(--k-font-mono); font-size: 0.85em; white-space: pre-wrap; overflow-wrap: anywhere;
}
.kl-shell-main .kl-signerkeys-facts {
  margin: 0; display: grid; gap: var(--k-space-1) var(--k-space-4); grid-template-columns: max-content minmax(0, 1fr);
}
.kl-shell-main .kl-signerkeys-facts dt { font-weight: 600; color: var(--k-text-muted); }
.kl-shell-main .kl-signerkeys-facts dd { margin: 0; overflow-wrap: anywhere; }
.kl-shell-main .kl-signerkeys-steps { margin: 0; padding-inline-start: var(--k-space-6); }
.kl-shell-main .kl-signerkeys-request-list,
.kl-shell-main .kl-signerkeys-key-list,
.kl-shell-main .kl-signerkeys-log-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--k-space-3); }
.kl-shell-main .kl-signerkeys-log-row { display: flex; flex-wrap: wrap; gap: var(--k-space-2); overflow-wrap: anywhere; }
/* A key row holds three blocks (name + chips, the device/code line, the actions). The *-row family lays them out on one
   line, which on a phone squeezed the middle line to one character per line. Stack them, and let the actions wrap. */
.kl-shell-main .kl-signerkeys-key-row { flex-direction: column; align-items: stretch; gap: var(--k-space-1); }
.kl-shell-main .kl-signerkeys-key-row > * { min-inline-size: 0; }

/* Signer-custody plan WS-2 — the printed onboarding certificate (/admin/signing-keys/certificate). The sheet is a paper
   document first: on screen it sits in a bordered page frame, and under print the shell and the toolbar go away and the
   sheet IS the page (a named page, so no other surface inherits its margins). The class names avoid the suffix families
   of this file (-card, -row, -list, -note, -facts, -value), so no family rule reaches into the paper layout. The codes
   (fingerprint, check code, entry hash) are monospace and never break inside a group of eight, so a person can compare
   them with the kanine-verify output by eye. Foundation tokens only. */
.kl-shell-main .kl-certpage { display: flex; flex-direction: column; gap: var(--k-space-4); }
.kl-shell-main .kl-cert-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-2); }
.kl-shell-main .kl-cert-picker { display: flex; flex-wrap: wrap; align-items: center; gap: var(--k-space-2); }
.kl-shell-main .kl-cert-pickhead { font-weight: 600; color: var(--k-text-muted); }
.kl-shell-main .kl-cert-pick { overflow-wrap: anywhere; }
.kl-shell-main .kl-cert-pick[aria-current="page"] { font-weight: 700; }
.kl-shell-main .kl-cert-sheet {
  page: kl-cert;
  max-inline-size: 52rem; padding: var(--k-space-6); display: flex; flex-direction: column; gap: var(--k-space-4);
  background: var(--k-surface); border: 1px solid var(--k-border); border-radius: var(--k-radius-md);
}
.kl-shell-main .kl-cert-head { border-block-end: 2px solid var(--k-border); padding-block-end: var(--k-space-3); }
.kl-shell-main .kl-cert-title { margin: 0; font-size: 1.75rem; line-height: 1.2; }
.kl-shell-main .kl-cert-subtitle { margin: var(--k-space-1) 0 0; color: var(--k-text-muted); }
.kl-shell-main .kl-cert-replaced {
  margin: 0; padding: var(--k-space-3); font-weight: 600;
  border: 2px solid var(--k-border); border-radius: var(--k-radius-md);
}
.kl-shell-main .kl-cert-body { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--k-space-4); }
.kl-shell-main .kl-cert-factlist { margin: 0; flex: 1 1 24rem; min-inline-size: 0; display: flex; flex-direction: column; gap: var(--k-space-2); }
.kl-shell-main .kl-cert-fact {
  display: grid; grid-template-columns: minmax(8rem, 13rem) minmax(0, 1fr); gap: var(--k-space-1) var(--k-space-3);
  break-inside: avoid;
}
.kl-shell-main .kl-cert-term { font-weight: 600; color: var(--k-text-muted); }
.kl-shell-main .kl-cert-desc { margin: 0; min-inline-size: 0; display: flex; flex-direction: column; overflow-wrap: anywhere; }
.kl-shell-main .kl-cert-mono { font-family: var(--k-font-mono); letter-spacing: 0.04em; }
.kl-shell-main .kl-cert-mono .kl-cert-part { white-space: pre-wrap; }
.kl-shell-main .kl-cert-qr { margin: 0; flex: 0 0 11rem; display: flex; flex-direction: column; gap: var(--k-space-1); }
.kl-shell-main .kl-cert-qrsvg { display: block; inline-size: 11rem; block-size: 11rem; }
.kl-shell-main .kl-cert-qrcap { font-size: 0.8rem; color: var(--k-text-muted); }
.kl-shell-main .kl-cert-h2 { margin: 0 0 var(--k-space-2); font-size: 1.1rem; }
.kl-shell-main .kl-cert-sign { break-inside: avoid; border-block-start: 1px solid var(--k-border); padding-block-start: var(--k-space-3); }
.kl-shell-main .kl-cert-signfield { display: flex; align-items: flex-end; gap: var(--k-space-3); min-block-size: 2.5rem; }
.kl-shell-main .kl-cert-signname { flex: 0 0 7rem; }
.kl-shell-main .kl-cert-signline { flex: 1 1 auto; max-inline-size: 22rem; border-block-end: 1px solid currentColor; }
.kl-shell-main .kl-cert-how { break-inside: avoid; }
.kl-shell-main .kl-cert-steps { margin: 0; padding-inline-start: var(--k-space-6); display: flex; flex-direction: column; gap: var(--k-space-1); }
.kl-shell-main .kl-cert-nosecret { margin: 0; font-weight: 600; }
@media (max-width: 40rem) {
  .kl-shell-main .kl-cert-sheet { padding: var(--k-space-4); }
  .kl-shell-main .kl-cert-fact { grid-template-columns: minmax(0, 1fr); }
}
@page kl-cert { size: letter; margin: 0.5in; }
@media print {
  .kl-shell-main .kl-certpage { gap: 0; }
  /* The print block of §7 already paints the page white with black text. The sheet drops its frame and inherits it. */
  .kl-shell-main .kl-cert-sheet {
    max-inline-size: none; padding: 0; border: 0; border-radius: 0; background: transparent; color: inherit; font-size: 10.5pt;
  }
  .kl-shell-main .kl-cert-term, .kl-shell-main .kl-cert-subtitle, .kl-shell-main .kl-cert-qrcap { color: inherit; }
  .kl-shell-main .kl-cert-head, .kl-shell-main .kl-cert-replaced, .kl-shell-main .kl-cert-sign { border-color: currentColor; }
}

/* =============================================================================
   PHONE-LAYOUT BATCH (2026-10, bench Pixel 7a). New rules only; nothing above is edited.
   ========================================================================== */

/* ---- PL-1. PHONE LANDSCAPE: A COMPACT SHELL (P2) -----------------------------------------------------------------
   Bench Pixel 7a, record wizard, landscape (844x390): the content area was about 40 percent of the screen. The sticky
   top bar (57px), the persistent "Add a second way to sign" banner (about 64px with its margin), the bottom bar
   (56px) and the capture "+" took the rest. A phone in landscape has about 390px of height, so every bar costs the
   page twice what it costs in portrait.
   The query is the phone landscape band: landscape, a height of 500px or less, and the shell width band (<=1280px)
   in which the bottom bar exists. A tablet in landscape (800px or more high) is outside it.
   Budget (checked by PhoneLayoutGuardTests): top bar 49px + banner 32px + bottom bar 49px = 130px. That leaves 260px
   of 390px, which is 66 percent, above the 60 percent floor. The values are tokens, so the bar clearances that read
   --k-bottomnav-h follow with no other edit. The banner stays on screen. It states a condition ("Add a second way to
   sign" or "Confirm this device"), so it is shortened to one line and never hidden. */
@media (orientation: landscape) and (max-height: 500px) and (max-width: 1280px) {
  :root { --k-bottomnav-h: 48px; --k-topbar-h: 49px; }
  /* The top bar keeps its 48px touch targets and loses its block padding: 48px + 1px border = 49px. */
  html body .kl-shellnav-topbar { padding-block: 0; }
  /* The open drawer starts below the bar. Both heads declare 56px for the portrait bar. Follow the compact bar. */
  html .kl-shellnav-rail.is-open,
  html[data-shellrail="collapsed"] .kl-shellnav-rail.is-open { inset-block-start: var(--k-topbar-h); }
  /* The banner is one slim line. The text takes the free width and cuts with an ellipsis. The link keeps its width.
     (0,2,2) is required: the base .kl-banner:has(> a[href]) wrap rule is (0,2,1). */
  html body .kl-banner.kl-signerkeys-banner {
    flex-wrap: nowrap; align-items: center; box-sizing: border-box;
    min-block-size: 32px; margin-block-end: 0; padding-block: 0; padding-inline: var(--k-space-3);
    font-size: 12px; line-height: 1.3;
  }
  html body .kl-signerkeys-banner > span { flex: 1 1 auto; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  html body .kl-signerkeys-banner > a { flex: 0 0 auto; white-space: nowrap; }
  /* The feedback FAB gets a gutter at the right edge of the content. A landscape phone has width to spare (844px), and
     no height to spare, so the FAB cannot perch above the content. The gutter is the FAB box (48px) + its edge offset
     (1.25rem) + a gap (0.5rem), the same slot width as the wizard footer. The FAB then covers no select chevron, no
     tile, and no button. (0,2,2) beats the scoped (0,2,0) padding of the web head and the (0,1,0) rule of the MAUI head. */
  html body .kl-shell-main.kl-shell-main {
    padding-inline-end: calc(var(--k-fab-size, 48px) + 1.25rem + 0.5rem + env(safe-area-inset-right, 0px));
  }
}

/* ---- PL-2. A SELECT CUTS ITS VALUE WITH AN ELLIPSIS, BEFORE THE CHEVRON (P6) --------------------------------------
   A long option, such as "Rockingham County Sheriff ... - CODE", was cut hard at the chevron with no sign that text
   was missing. The closed select already reserves its end padding for the chevron (section 5b, --k-space-8). This adds
   the ellipsis, so the cut shows. The rule sets only the three overflow properties, so no other select style changes. */
.kl-shell-main select:not([multiple]):not([size]),
.kf-select:not([multiple]):not([size]) {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- PL-3. THE MATCH-CODE PICK OF A SIGNING APPROVAL (defect 20, beta 2026-10-02) ---------------------------------
   SigningApprovalPanel, StatementApprovalPanel and ReauthApprovalPanel show the match code among decoys, as buttons in
   a radio group. No rule styled them: the three buttons touched each other, and a tap showed no selected state. The
   group gets the normal gap. The chosen code (aria-checked="true", the ARIA state of role="radio") gets the teal fill of
   a pressed segment, a dark border, and a check mark, so the state does not depend on color alone.
   Each selector weighs (0,3,0) or more. That beats the (0,2,0) and (0,3,0) rules of .kl-btn with no !important. */
.kl-signapprove .kl-signapprove-codes {
  border: 0; margin: 0 0 var(--k-space-4); padding: 0; min-inline-size: 0;
}
.kl-signapprove .kl-signapprove-codes [role="radiogroup"] {
  display: flex; flex-wrap: wrap; gap: var(--k-space-3);
}
.kl-signapprove .kl-btn.kl-signapprove-code {
  min-inline-size: 72px; min-block-size: var(--k-touch);
  background: var(--k-surface); color: var(--k-text);
  border: 2px solid var(--k-border);
  font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums;
}
.kl-signapprove .kl-btn.kl-signapprove-code:not([aria-checked="true"]):hover { background: var(--k-surface-2); }
.kl-signapprove .kl-btn.kl-signapprove-code[aria-checked="true"] {
  background: var(--k-teal-500); color: var(--k-ink-900); border-color: var(--k-ink-900);
}
.kl-signapprove .kl-btn.kl-signapprove-code[aria-checked="true"]::before { content: "\2713\00a0"; }

/* Defect 38 (2026-10-02) — the "Sign in online" prompt of an offline PIN session (OnlineSignInPrompt, device head
   only). The text, the button and the error stack, and the button keeps the touch floor. */
.kl-online-signin-prompt { display:flex; flex-direction:column; align-items:flex-start; gap:var(--k-space-2); }
.kl-online-signin-prompt-text { margin:0; }

/* ---- PL-4. THE FIRST-LOAD SCREEN OF A K96 MIRROR (owner request 2026-10-12) ----------------------------------------
   PreparingEnvironment: one heading, one progress bar, one sentence, centered in the content area. Before this, the
   screen used the kl-gen-* classes of PacketGenerating, whose rules are SCOPED to that component in another assembly,
   so the screen rendered as raw text. The bar is determinate when the pull has a real total, and it slides when the
   total is not known yet. Reduced motion stops the slide and the width transition. Tokens only, so light, dark and
   outdoor all follow the theme. The class names avoid the broad suffix families (-status, -actions, -note), with one
   deliberate exception: "-panel" gives the panel the shared card chrome (5a). That family rule sets display:block at
   (0,2,0), so the panel's layout below is written at (0,2,0) too, and it comes later, so it wins. */
.kl-mirrorload {
  display: flex; align-items: center; justify-content: center;
  min-block-size: min(70vh, 36rem);
  padding: var(--k-space-6) var(--k-space-4);
}
.kl-mirrorload .kl-mirrorload-panel {
  inline-size: 100%; max-inline-size: 30rem; margin: 0;
  display: flex; flex-direction: column; align-items: center; gap: var(--k-space-4);
  text-align: center;
}
.kl-mirrorload-mark { font-size: 40px; line-height: 1; color: var(--k-accent); }
.kl-mirrorload[data-phase="ready"] .kl-mirrorload-mark { color: var(--k-success-fg); }
.kl-mirrorload[data-phase="stalled"] .kl-mirrorload-mark { color: var(--k-caution-fg); }
.kl-mirrorload[data-phase="failed"] .kl-mirrorload-mark { color: var(--k-critical-fg); }
.kl-mirrorload-heading {
  margin: 0; font-family: var(--k-font-display); font-size: 22px; font-weight: 700; line-height: 1.25;
  color: var(--k-text);
}
.kl-mirrorload-bar {
  position: relative; inline-size: 100%; block-size: 8px; overflow: hidden;
  border-radius: var(--k-radius-full);
  background: color-mix(in oklab, var(--k-accent) 14%, var(--k-surface-2));
}
.kl-mirrorload-fill {
  block-size: 100%; border-radius: inherit;
  background: linear-gradient(90deg, var(--k-accent), var(--k-accent-bright));
  transition: inline-size .4s ease;
}
.kl-mirrorload-bar[data-indeterminate="true"] .kl-mirrorload-fill {
  inline-size: 35%;
  animation: kl-mirrorload-slide 1.4s ease-in-out infinite;
}
@keyframes kl-mirrorload-slide {
  from { transform: translateX(-100%); }
  to { transform: translateX(290%); }
}
.kl-mirrorload-lede {
  margin: 0; max-inline-size: 26rem;
  font-size: 15px; line-height: 1.5; color: var(--k-text-muted);
}
.kl-mirrorload-links {
  display: flex; flex-wrap: wrap; justify-content: center; gap: var(--k-space-3);
}
.kl-mirrorload-links:empty { display: none; }
@media (prefers-reduced-motion: reduce) {
  .kl-mirrorload-fill { transition: none; }
  .kl-mirrorload-bar[data-indeterminate="true"] .kl-mirrorload-fill {
    animation: none; inline-size: 100%; opacity: .45;
  }
}
