/* ==========================================================================
   HAVTO Blueprint -- drop-in utilities for the authenticated MudBlazor app.

   These utilities carry the approved marketing "blueprint" language into the
   app WITHOUT hardcoding colour: every token below reads from a MudBlazor
   palette variable (emitted by ThemeConfiguration.cs), so light/dark just work.

   Load AFTER MudBlazor's stylesheet, e.g. in App.razor / _Host.cshtml:
     <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
     <link href="css/havto-blueprint.css" rel="stylesheet" />

   -- Revision R1 (implementation fixes against MudBlazor's real DOM) --
   #1  .hav-app scopes the focus ring -- put it on the MainLayout root <div>
       (NOT .hav-blueprint, which would frame the whole app).
   #2/#3 nav active + focus target the INNER .mud-nav-link (PolicyAwareNavLink
       renders <div class="mud-nav-item hav-navlink"><a class="mud-nav-link">).
   #4  --hav-text-label is darker than --hav-text-muted so small 600 labels clear AA.
   #6/#9 table selectors reach MudTable AND MudDataGrid header cells.
   #7  dark mode: MudBlazor rewrites --mud-palette-* on :root via MudThemeProvider --
       there is NO .mud-theme-dark class, so these token-based rules just work; no
       dark-specific selectors are needed (or present).

   -- Revision R2 --
   SOURCE OF TRUTH: THIS FILE, shipped as Blueprint_Claude_Design/App/
   havto-blueprint.css. Since 2026-09-16 the App copy is the source and
   wwwroot/css/havto-blueprint.css is BUILD OUTPUT (MSBuild Copy on
   BeforeBuild) -- never hand-edit wwwroot. (R2 originally stated this the
   other way round; corrected 2026-09-19 with the R3 selector attestation.)
   Adds: --hav-chart-* series + furniture tokens, .hav-chart, .hav-empty
   (dashed empty plate), brand-lockup vs UI-type separation (HAV2062),
   app-icon rules. Tagged R2 #n below.

   -- Revision R3 (vendor selector attestation, 2026-09-19) --
   Every .mud-* selector below is attested against the PINNED packages --
   MudBlazor 9.9.0, Heron.MudCalendar 4.1.0 -- and the recorded DOM fixture,
   which is what HAV2281 (spec S11.64) requires. Fourteen selectors were
   sketched from memory of the vendor DOM and matched nothing; three rules
   were therefore silent no-ops from the day they were written (today-marker,
   out-of-month dimming, disabled-tab mute). A rule that matches nothing looks
   exactly like a decision not to draw it, which is why it survived review.
   DO NOT add a .mud-* selector to this file without reading it off the
   rendered DOM or the shipped vendor stylesheet at the pinned version.
   ========================================================================== */

/* Fonts are loaded via a <link> in App.razor (the app owns font loading) -- see README.
   The @import below is kept commented as a standalone fallback, to avoid double-loading. */
/* @import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600;700&family=Barlow+Condensed:wght@500;600;700&family=IBM+Plex+Mono:wght@400&display=swap'); */

:root {
  /* Steel accent + surfaces -- mapped to the MudBlazor palette (fallbacks match ThemeConfiguration.cs) */
  --hav-accent:        var(--mud-palette-primary, #3b6ea5);
  --hav-accent-strong: var(--mud-palette-primary-darken, #2A5280);
  --hav-panel:         var(--mud-palette-surface, #ffffff);
  --hav-ground:        var(--mud-palette-background, #fafbfc);
  --hav-line:          var(--mud-palette-lines-default, #cbd5e1);
  --hav-text:          var(--mud-palette-text-primary, #1f2937);
  --hav-text-muted:    var(--mud-palette-text-secondary, #64748b);
  /* R1 #4: darker than muted (mixed toward text-primary) so 11-13px/600 furniture labels
     clear WCAG AA (4.5:1) on the light ground, where bare --hav-text-muted is ~3.9:1. */
  --hav-text-label:    color-mix(in srgb, var(--mud-palette-text-secondary, #64748b) 45%, var(--mud-palette-text-primary, #1f2937));

  /* Safety status -- the one sanctioned colour exception (ISO 3864 / ANSI Z535) */
  --hav-info:    var(--mud-palette-info, #005387);
  --hav-success: var(--mud-palette-success, #237F52);
  --hav-warning: var(--mud-palette-warning, #F9A900);
  --hav-error:   var(--mud-palette-error, #C0392B);

  /* Type -- Condensed leads, Barlow reads.
     R2 #1: --hav-font-brand exists ONLY for the lockup asset fallback. UI headings use
     --hav-font-head via .hav-h and must never borrow the lockup's tracking (HAV2062). */
  --hav-font-head:  "Barlow Condensed", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --hav-font-body:  "Barlow", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --hav-font-brand: "Barlow Condensed", "Segoe UI", Roboto, sans-serif;
  /* R8 S11.12 -- the mechanical face (HAV2137). References, codes, hashes, compared dates.
     NEVER prose, labels, names, statuses or quantities: a quantity is tabular Barlow. */
  --hav-font-mono:  "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* R2 #2 -- Chart series ramp. Steel accent + neutrals, NOT a rainbow and NOT the
     safety-status palette (status colour carries legal meaning; a volume series does not).
     Six steps max; a seventh series collapses into "Other" on --hav-chart-6.
     Use the status tokens only when the series IS the status (Open / Overdue / Closed). */
  --hav-chart-1: var(--hav-accent);
  --hav-chart-2: color-mix(in srgb, var(--hav-accent) 62%, var(--hav-panel));
  --hav-chart-3: var(--hav-accent-strong);
  --hav-chart-4: color-mix(in srgb, var(--hav-accent) 34%, var(--hav-panel));
  --hav-chart-5: var(--hav-text-muted);
  --hav-chart-6: var(--hav-line);

  /* Chart furniture */
  --hav-chart-grid:  var(--hav-line);
  --hav-chart-axis:  color-mix(in srgb, var(--hav-line) 60%, var(--hav-text-muted));
  --hav-chart-label: var(--hav-text-label);
  --hav-chart-track: color-mix(in srgb, var(--hav-line) 45%, transparent);

  /* R6 S11.5 -- Separation by SURFACE, not glare. ground -> section -> raised/input.
     One formula, theme-correct in both directions: mixing the panel toward text-primary
     DARKENS the light theme (the conventional faint field inset) and LIGHTENS the dark
     theme (the conventional lift), so the dark steps need no separate override block.
     No shadow, no second border, no glow. Zero hex -- HAV2065. */
  --hav-surface-ground:  var(--hav-ground);
  --hav-surface-section: var(--hav-panel);
  --hav-surface-raised:  color-mix(in srgb, var(--hav-panel) 94%, var(--hav-text));
  --hav-surface-input:   color-mix(in srgb, var(--hav-panel) 96%, var(--hav-text));
  /* Table chrome (header/footer bars) sits on the raised step. */
  --hav-surface-chrome:  var(--hav-surface-raised);
  /* R6 S11.5 -- a status role used as chip TEXT takes its deep step. Info was the one
     role without one; derived, so it flips with the theme like the surfaces above. */
  --hav-info-text: color-mix(in srgb, var(--hav-info) 82%, var(--hav-text));

  /* R3 -- Foreground on a filled surface is a TOKEN, never a literal.
     Measured: in dark mode white fails on all five status fills (1.67-2.77:1).
     Light mode: white passes on accent/info/success/error, FAILS on warning (1.96).
     Amber is a fill colour, never a text colour. Gate HAV2065. */
  --hav-on-accent:  #ffffff;
  --hav-on-info:    #ffffff;
  --hav-on-success: #ffffff;
  --hav-on-warning: #000000;   /* 10.70:1 -- white would be 1.96:1 */
  /* A role used as TEXT takes its deep step. The chip pattern puts status text on a
     tint of ITSELF, and every base role fails there (success 4.03, error 4.27,
     accent 4.29, amber 1.70). Fills keep the base colour, so tints are unchanged. */
  --hav-warning-text: #8A5A00;
  --hav-success-text: #1D6B45;
  --hav-error-text:   #A62F23;

  /* S11.32-S11.33 (HAV2194, HAV2195) -- the five-band judgement scale.
     Referenced from HavRiskRating.razor.css, RiskHeatMap.razor.css and
     HavRiskLevelChip.razor.css (25 call sites) and, until v1.79.0, DEFINED IN
     NONE OF THEM: the definitions sat in the app's own copy of this file, which
     a kit sync overwrites wholesale. A --hav-* token whose owner is a file the
     sync replaces has no owner. They live here now.
     HAV2194 forbids one hue held across the scale, so the five traverse
     success -> warning -> error rather than shading one colour. Fills are tints
     against --hav-panel so they flip with the theme on their own; that is why
     there is no dark override below.
     The FILL carries the band. The -text step is chosen for contrast on that
     fill from the three measured role steps above, so two adjacent bands may
     share a text step without sharing a band -- do not "fix" that by inventing
     five literals (HAV2065, Appendix B owes the measurement). */
  --hav-risk-verylow:  color-mix(in srgb, var(--hav-success) 18%, var(--hav-panel));
  --hav-risk-low:      color-mix(in srgb, color-mix(in srgb, var(--hav-success) 50%, var(--hav-warning)) 22%, var(--hav-panel));
  --hav-risk-medium:   color-mix(in srgb, var(--hav-warning) 24%, var(--hav-panel));
  --hav-risk-high:     color-mix(in srgb, color-mix(in srgb, var(--hav-warning) 45%, var(--hav-error)) 28%, var(--hav-panel));
  --hav-risk-veryhigh: color-mix(in srgb, var(--hav-error) 32%, var(--hav-panel));
  --hav-risk-verylow-text:  var(--hav-success-text);
  --hav-risk-low-text:      var(--hav-success-text);
  --hav-risk-medium-text:   var(--hav-warning-text);
  --hav-risk-high-text:     var(--hav-error-text);
  --hav-risk-veryhigh-text: var(--hav-error-text);
  /* An INVERTED surface (snackbar, tooltip, any dark bar in the light theme) flips with
     the theme, so its accent must flip too. A literal cannot -- that is HAV2065. */
  --hav-accent-inverted: #64B5F6;
  --hav-on-error:   #ffffff;

  /* R2 #3 -- Brand blue as a literal is legal in ASSET FILES and the manifest only
     (a favicon / PWA manifest / OS tile cannot read a CSS variable). Anything the app
     renders still reads --mud-palette-primary. */
  --hav-brand-blue: #3b6ea5;
}

/* --------------------------------------------------------------------------
   Blueprint frame -- square, hairline. Layer over <MudPaper Outlined>.

   v1.75.0 -- .hav-corner is DELETED (HAV2228). The registration "+" marks were a
   drawing convention that shipped as product chrome: they were reserved for KPI
   plates and panel headers, read as part of the design, and ended up on every
   container. A mark that appears on everything marks nothing.

   Remove the four <i class="hav-corner"> children wherever Razor still emits them
   -- with no rule to match they render nothing, so the markup is inert, not broken.

   .hav-blueprint stays as the hairline frame so shipped Razor keeps its borders
   during that sweep. It is now identical in effect to .hav-section; prefer
   HavSection for new work -- HavBlueprintFrame is superseded.
   -------------------------------------------------------------------------- */
.hav-blueprint { position: relative; border: 1px solid var(--hav-line); border-radius: 0; background: var(--hav-panel); }

/* --------------------------------------------------------------------------
   Kickers, rules, section headings
   -------------------------------------------------------------------------- */
.hav-kicker { font-family: var(--hav-font-head); font-weight: 600; font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: var(--hav-accent); }
.hav-rule { height: 1px; border: 0; background: var(--hav-line); margin: 10px 0 18px; }
.hav-h { font-family: var(--hav-font-head); font-weight: 600; text-transform: uppercase; letter-spacing: .01em; line-height: 1.1; color: var(--hav-text); margin: 0; }

/* S11.12 (HAV2137) -- a mechanical value: one that is retyped, compared or checked character by
   character. 12px is the SCREEN floor; print surfaces size from S15.5's points instead and do
   not use this utility. The face is already tabular, so no tnum. Long values are sized or
   linked, never word-broken. */
.hav-mono { font-family: var(--hav-font-mono); font-weight: 400; font-size: max(12px, 1em); }

/* --------------------------------------------------------------------------
   KPI stat plate -- big Barlow Condensed numeral; add --alert for attention
   -------------------------------------------------------------------------- */
.hav-kpi { position: relative; border: 1px solid var(--hav-line); background: var(--hav-panel); padding: 14px 16px; }
.hav-kpi__label   { font-family: var(--hav-font-head); font-weight: 600; font-size: 11px; letter-spacing: .07em; text-transform: uppercase; color: var(--hav-text-label); } /* R1 #4 */
.hav-kpi__value   { font-family: var(--hav-font-head); font-weight: 600; font-size: 38px; line-height: 1; margin: 6px 0 2px; color: var(--hav-text); }
.hav-kpi__caption { font-family: var(--hav-font-body); font-size: 12px; color: var(--hav-text-muted); }
.hav-kpi--alert { border-color: color-mix(in srgb, var(--hav-error) 45%, transparent); }
.hav-kpi--alert .hav-kpi__value, .hav-kpi--alert .hav-kpi__caption { color: var(--hav-error); }
/* A plate inside a frame that already draws the edge (a metrics-strip tile): no border,
   fill or padding of its own, so two edges never touch (HAV2191). Adopted v1.118.0. */
.hav-kpi--bare { border: 0; background: transparent; padding: 0; }

/* --------------------------------------------------------------------------
   Form / register section block (hairline)
   R1 #8 -- inside BaseSidePanel: put .hav-section on the MudContainer body wrapper,
   and .hav-h on the MudDrawerHeader title MudText.
   -------------------------------------------------------------------------- */
/* R7 19 Aug 2026 -- padding is 20, the S4 default. It shipped at 15, which is not on the
   spacing scale at all (1->4 2->8 3->12 4->16 6->24), so every section frame in the product was
   a hair tighter than the contract and an implementer reading the CSS got a third number
   after the spec's 20 and the blueprint's drawn 18. HAV2177's values half: the kit and the
   spec name the same class, so they state the same number. Compact (16) and feature (24)
   are page decisions, not per-panel switches. */
.hav-section { border: 1px solid var(--hav-line); background: var(--hav-panel); padding: 20px; }
/* S11.26 (HAV2151) -- a section header row: label, count, at most ONE action.
   Not insight-specific: any .hav-section carrying a count or an action uses it,
   which is why there is no .hav-insight family. The label keeps its own margin
   when used alone; inside __head the row owns the spacing. */
.hav-section__head {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 10px;
  min-height: 28px; margin-bottom: 9px;
}
.hav-section__head .hav-section__label { margin-bottom: 0; }
.hav-section__count { font-family: var(--hav-font-mono, ui-monospace), ui-monospace, monospace; font-size: 11.5px; color: var(--hav-text-muted); }
.hav-section__action { margin-left: auto; }
.hav-section__label { display: block; font-family: var(--hav-font-head); font-weight: 600; text-transform: uppercase; letter-spacing: .06em; font-size: 13px; color: var(--hav-text); margin-bottom: 9px; }

/* --------------------------------------------------------------------------
   Status & severity chips -- colour carries meaning
   -------------------------------------------------------------------------- */
.hav-status { display: inline-flex; align-items: center; gap: 5px; font-family: var(--hav-font-body); font-size: 12px; font-weight: 500; padding: 2px 10px; border-radius: 2px; }
/* R6 S11.5 -- tinted fill + text from the DEEP step of the same role, never the base
   role on a tint of itself (success 4.03, error 4.27, amber 1.70 -- all fail AA), and
   never a saturated fill for a lifecycle value. Fills keep the base colour. */
.hav-status--info    { color: var(--hav-info-text);    background: color-mix(in srgb, var(--hav-info) 14%, transparent); }
.hav-status--success { color: var(--hav-success-text); background: color-mix(in srgb, var(--hav-success) 16%, transparent); }
.hav-status--warning { color: var(--hav-warning-text); background: color-mix(in srgb, var(--hav-warning) 16%, transparent); }
.hav-status--error   { color: var(--hav-error-text);   background: color-mix(in srgb, var(--hav-error) 14%, transparent); }
.hav-status--neutral { color: var(--hav-text-label);   background: color-mix(in srgb, var(--hav-text-muted) 15%, transparent); }
/* --alert is the one saturated status: it means ACT NOW. Foreground is a token --
   a hardcoded #fff here failed dark mode at 1.67:1 (HAV2065). */
.hav-status--alert   { color: var(--hav-on-error); background: var(--hav-error); }

/* --------------------------------------------------------------------------
   Data table -- Condensed uppercase headers. Add class to <MudTable>/<MudDataGrid>.
   -------------------------------------------------------------------------- */
/* R1 #6/#9: MudTable renders <table class="mud-table-root">; MudDataGrid renders a
   <div class="mud-data-grid"> wrapping a <table>. Put Class="hav-table" on EITHER
   component -- these descendant selectors reach the header cells in both. */
.hav-table .mud-table-head th,
.hav-table .mud-table-head .mud-table-cell,
.hav-table thead th { font-family: var(--hav-font-head); text-transform: uppercase; letter-spacing: .07em; font-size: 11px; color: var(--hav-text-label); }

/* --------------------------------------------------------------------------
   Nav -- active item = accent left-rule + accent text (not a filled pill)
   Apply .hav-navlink to PolicyAwareNavLink; MudBlazor flags the inner <a> with `active`.
   -------------------------------------------------------------------------- */
/* R1 #2 (Playwright-verified): PolicyAwareNavLink renders <div class="mud-nav-item hav-navlink"><a class="mud-nav-link active">...</a></div>.
   The active flag is `active` on the INNER <a>. (R3: an earlier note claimed MudBlazor also
   keeps a "mud-nav-link-active" variant -- it does not at 9.9.0, and that variant is deleted. The other
   three variants are attested and stay.)
   Target the <a> directly. Do NOT rely on :has() (not universally supported in the target
   browsers). font-weight needs !important to beat MudBlazor's own specificity. */
.hav-navlink a.active,
.hav-navlink .mud-nav-link.active,
.hav-navlink[aria-current="page"] a {
  color: var(--hav-accent) !important;
  font-weight: 600 !important;
  border-left: 2px solid var(--hav-accent);
  background: color-mix(in srgb, var(--hav-accent) 10%, transparent);
}
.hav-nav-group-label { font-family: var(--hav-font-head); text-transform: uppercase; font-size: 11px; letter-spacing: .1em; color: var(--hav-text-muted); }

/* --------------------------------------------------------------------------
   App-bar brand -- logo + wordmark lockup (returns the HAVTO mark to the AppBar)
   -------------------------------------------------------------------------- */
.hav-app-brand { display: inline-flex; align-items: center; gap: 8px; flex: none; } /* S16.4 gap; flex:none = the lockup never shrinks (HAV2128) */
.hav-app-brand-logo { height: 22px; width: auto; display: block; flex: none; }
.hav-app-brand-wordmark { font-family: var(--hav-font-head); font-weight: 600; font-size: 18px; letter-spacing: .02em; text-transform: uppercase; color: var(--hav-text); white-space: nowrap; }

/* --------------------------------------------------------------------------
   Content width (v2) -- contain reading-oriented pages so they don't sprawl on
   wide monitors (mirrors the marketing site's ~1200px gutter). Prefer setting
   MudContainer MaxWidth (Large ~ 1280 for content/detail/settings; ExtraLarge
   ~ 1440 for wide data registers; full-bleed only for the calendar grid). Use
   .hav-page for custom layouts that don't use MudContainer. Tables scroll INSIDE
   the cap -- never widen the page.
   -------------------------------------------------------------------------- */
.hav-page { max-width: 1280px; margin-inline: auto; padding-inline: clamp(16px, 4vw, 40px); }
.hav-page--wide { max-width: 1440px; }
.hav-page--form { max-width: 900px; } /* form pages and wizards (§11.71); a wizard with a table step takes --wide on every step */
/* R10 S16 / HAV2211 -- the pre-auth measure. 480 is not a new number: it is the width
   Landing's auth card already ships, promoted from one page to the layout so the other
   ten anonymous routes stop inventing their own (400 on ForgotPassword, 350 on Logout,
   and eight with no width at all). NOT --form's 900: that is a two-column field grid's
   measure, and a pre-auth page has one column and one task. Applied on AuthLayout's
   wrapper, never per page -- eleven pages each setting a width IS the defect. */
.hav-page--auth { max-width: 480px; margin-inline: auto; padding-inline: clamp(16px, 4vw, 40px); }

/* Token-scoped external surfaces (HAV2252): 480 before token validates, 720 after.
   Applied on the layout's wrapper, never per page. */
.hav-page--external { max-width: 480px; margin-inline: auto; padding-inline: clamp(16px, 4vw, 40px); }
.hav-page--external--validated { max-width: 720px; }

/* --------------------------------------------------------------------------
   S16 -- Marketing shell. A DIFFERENT measure from the app: 1200, not 1280.
   The masthead paints full-width (background + the rule under it run edge to
   edge) while its content sits on that measure -- one padding-inline formula
   does both, and the max() means there is no breakpoint to maintain.
   MudBlazor: MudAppBar Gutters="false" + .hav-marketing-shell on the bar.
   Gutters="true" caps the bar at 1280 and it overhangs its own page (HAV2125,
   HAV2126). The formula is applied to the bar AND to its inner .mud-toolbar,
   because the toolbar is the element that actually holds the children.
   -------------------------------------------------------------------------- */
:root {
  --hav-marketing-measure: 1200px;
  /* The SAME clamp the blueprint's --edge uses. A fixed gutter here would put the
     app masthead on a different axis from the page it reproduces at every width
     but one -- the formula is only "the same formula" if the constant is too. */
  --hav-marketing-edge: clamp(20px, 5vw, 72px);
}

/* --------------------------------------------------------------------------
   R9 -- GEOMETRY TOKENS (spec S The token contract, R3).

   Every row of the spec's token contract now names a variable, so no geometry
   value in this system requires inference: the spec states the number, this
   block declares the name, and the rule below it consumes the name.

   THE RULE: a value that appears in the token contract is authored as the
   variable, never as the literal (HAV2177 direction three). Rules written
   before this block still carry their literal; they migrate as the surface is
   touched, and the literal and the variable hold the SAME number until they do
   -- a variable that disagrees with the rule beneath it is the drift this block
   exists to end.

   A token here with no consuming rule is declared, not implemented. That is a
   stated state, not a defect: the value is the contract, and the component that
   owes it is named in the spec's "Applies to" column.
   -------------------------------------------------------------------------- */
:root {
  /* Page & layout */
  --hav-page-gutter:          24px;
  --hav-page-gutter-narrow:   16px;   /* <600 */
  --hav-page-header-top:      24px;
  --hav-page-header-bottom:   16px;
  --hav-page-header-meta-gap: 4px;
  --hav-section-rhythm:       24px;   /* between section frames */
  --hav-section-inner-gap:    16px;   /* between siblings inside one */
  --hav-page-cap:             1280px;
  --hav-page-cap-wide:        1440px;
  --hav-form-cap:             900px;
  --hav-field-cap:            640px;
  --hav-prose-cap:            72ch;
  --hav-grid-gutter:          16px;   /* MudGrid Spacing="2" */
  --hav-track-alert:          320px;  /* auto-fit minmax() floors, S11.6 */
  --hav-track-card:           280px;
  --hav-track-kpi:            240px;
  --hav-track-insight:        220px;

  /* Containers */
  --hav-panel-pad:            20px;
  --hav-panel-pad-compact:    16px;
  --hav-panel-pad-feature:    24px;
  --hav-kpi-pad:              16px;
  --hav-kpi-min-h:            96px;
  --hav-toolbar-h:            56px;
  --hav-toolbar-gap:          12px;
  --hav-commit-h:             64px;   /* also set locally on .hav-commit */
  --hav-commit-pad:           16px 24px;
  --hav-appbar-h:             64px;
  --hav-appbar-h-narrow:      56px;   /* <600 */
  --hav-sidemenu-w:           260px;

  /* Buttons & actions (spec S Buttons & actions). One height per row is the rule
     the tokens exist to make cheap: read a variable, not a number, and a row
     cannot drift to two heights by accident. */
  --hav-btn-h:                40px;  /* page header, filter bar, action bar */
  --hav-btn-h-compact:        36px;  /* inside a card, panel or row */
  --hav-btn-h-touch:          44px;  /* below 600 */
  --hav-btn-pad:              0 16px;
  --hav-btn-pad-compact:      0 12px;
  --hav-btn-min-w:            88px;  /* so Save and Cancel match */
  --hav-btn-gap:              8px;   /* within a group */
  --hav-btn-gap-group:        16px;  /* between groups, before an overflow */
  --hav-btn-gap-text:         12px;  /* button to adjacent running text */
  --hav-btn-icon-gap:         8px;

  /* --------------------------------------------------------------------------
     Two tokens the spec ruled and no stylesheet declared until 1.86.0.

     The CSS column surfaced three. Adding the column was meant to be
     transcription and turned into an audit: the spec named each of these in a
     rule, nothing declared them, so every implementer who followed the rule wrote
     a literal or reached for the nearest token -- and nothing could catch it,
     because there was no name to grep for.

     The third was `--hav-divider`, a second name for the hairline this sheet
     ships as `--hav-line` -- HAV2177 in the token layer. Resolved by DELETING it
     (1.86.1): the spec's citation now reads `--hav-line`, and the alias that
     bridged them is gone. One hairline, one name.
     -------------------------------------------------------------------------- */

  /* The stronger hairline: the neutral input border (S field states) and the
     neutral status chip's outline (S14.6) -- both a drawn edge rather than a
     divider, which is why `--hav-line` is the wrong step for them.
     `lines-inputs` is MudBlazor's own semantic for exactly this border, so the
     token resolves to the value the neutral input was already drawn at rather
     than to a number picked here. */
  --hav-line-strong:          var(--mud-palette-lines-inputs, #94a3b8);

  /* The row TRACK. Defaults to 1fr: every track shares the row. v1.117.0 and earlier
     defaulted it to the field cap, which capped EVERY track -- and auto-fit counts its
     repetitions from a definite max, so repeat(auto-fit, minmax(280px, 640px)) at 942px
     resolved to one column however many items the row held.
     --hav-row-track-solo is the cap a LONE item may reach before a one-line insight
     sprawls across 1400px: the field-column cap, because one line of text reads to the
     same measure in a form or alone in a row. It applies only to a row with one child
     (see .hav-row:has(> :only-child)) or to an explicit .hav-row--solo.
     `justify-content: start` stays either way -- the cap is a track width, never a
     centring device. */
  --hav-row-track:            1fr;
  --hav-row-track-solo:       var(--hav-field-cap);

  /* Tables */
  --hav-row-h:                44px;   /* default, and the touch target */
  --hav-row-h-comfortable:    52px;
  --hav-row-h-dense:          36px;   /* read-only desktop reference only */
  --hav-thead-h:              40px;
  --hav-cell-pad:             8px 12px;
  --hav-field-grid-col-gap:   16px;
  --hav-field-grid-row-gap:   14px;

  /* Overlays */
  --hav-dialog-w-confirm:     420px;
  --hav-dialog-w-form:        600px;
  --hav-dialog-w-comparison:  840px;
  --hav-dialog-w-record:      1100px;
  --hav-dialog-pad-title:     20px 24px;
  --hav-dialog-pad-body:      24px;
  --hav-dialog-pad-actions:   16px 24px;
  --hav-dialog-action-gap:    8px;
  --hav-sidepanel-w:          420px;
  --hav-sidepanel-w-wide:     560px;
  --hav-sidepanel-header-h:   56px;
  --hav-sidepanel-body-pad:   20px;
  --hav-sidepanel-footer-h:   64px;
  --hav-menu-min-w:           200px;
  --hav-menu-max-w:           360px;
  --hav-menu-max-h:           60vh;
  --hav-menu-item-h:          36px;

  /* Interaction & state furniture */
  --hav-focus-ring-w:         2px;
  --hav-focus-ring-offset:    2px;
  --hav-hit:                  44px;
  --hav-icon-inline:          14px;
  --hav-icon:                 16px;
  --hav-icon-prominent:       20px;
  --hav-icon-appbar:          24px;
  --hav-icon-stroke:          1.5;
  --hav-icon-stroke-small:    1.7;    /* at <=14px */
  --hav-chip-h:               20px;
  --hav-chip-pad:             2px 8px;
  --hav-empty-pad:            26px 20px;
  --hav-empty-reserve:        180px;  /* page-level */
  --hav-empty-reserve-compact: 120px; /* region-level */
  --hav-skeleton-line:        12px;
  --hav-skeleton-gap:         11px;
  --hav-skeleton-dur:         1.4s;
  --hav-chart-min-h:          220px;
  --hav-chart-legend-gap:     12px;

  /* Motion (spec S Motion, R3). 240ms is the ceiling; nothing is slower. */
  --hav-dur-instant:          0ms;    /* value change, sort, filter, tab switch */
  --hav-dur-tint:             120ms;  /* hover, focus, pressed, row highlight */
  --hav-dur-disclose:         160ms;  /* expand/collapse, chip state */
  --hav-dur-popover:          200ms;  /* menu, tooltip, flyout */
  --hav-dur-panel:            220ms;  /* side panel slide, drawer */
  --hav-dur-dialog:           240ms;  /* dialog + backdrop */
  --hav-ease:                 cubic-bezier(.2, 0, .2, 1);
}

.hav-marketing-shell,
.hav-marketing-shell > .mud-toolbar {
  padding-inline: max(var(--hav-marketing-edge),
    calc((100% - var(--hav-marketing-measure)) / 2 + var(--hav-marketing-edge)));
}
.hav-marketing-page {
  max-width: var(--hav-marketing-measure);
  margin-inline: auto;
  padding-inline: var(--hav-marketing-edge);
}

/* Brand lockup -- one mark asset on both grounds (HAV2127); never a -dark pair.
   The wordmark is a DISCRETE span so it can collapse without the mark (HAV2128). */
.hav-marketing-brand { display: inline-flex; align-items: center; gap: 8px; margin-right: auto; }
.hav-marketing-brand img { width: 22px; height: 22px; display: block; flex: none; }
.hav-marketing-brand__word {
  font-family: var(--hav-font-head); font-weight: 600; font-size: 18px;
  letter-spacing: .02em; text-transform: uppercase;
  color: var(--hav-text); white-space: nowrap;
}
@media (max-width: 599px) { .hav-marketing-brand__word { display: none; } }

/* Active state, two truths (HAV2129): 'page' for a page link, 'location' for an
   in-page anchor. Both take the DEEP step -- nav links are 14px, small text. */
.hav-marketing-shell a[aria-current="page"] { color: var(--hav-accent-strong); font-weight: 600; }
.hav-marketing-shell a[aria-current="location"] { color: var(--hav-accent-strong); }

/* --------------------------------------------------------------------------
   Tab workspace (v4 -- MudBlazor 9.x DOM, verified in-browser) -- multi-panel
   MudTabs on entity detail pages / side-panel forms (NOT the 2-3 option
   segmented control). Put Class="hav-tabs" on the MudTabs COMPONENT. 9.x renders:
     <div class="mud-tabs hav-tabs">
       <div class="mud-tabs-tabbar">                  <- NOT .mud-tabs-toolbar (pre-9.x)
         <div class="mud-tabs-tabbar-inner">
           <div class="mud-tabs-tabbar-content">      <- overflow: hidden; clips vertically
             <div class="mud-tabs-tabbar-wrapper">    <- THE flex row of tabs
               <div class="mud-tooltip-root">         <- every tab is wrapped in one,
                 <div class="mud-tab mud-ripple mud-tab-panel"   so the tooltip root is
                      role="tab" tabindex="0"                    the flex CHILD, not .mud-tab
                      style="min-width:160px">       <- inline, from MinimumTabWidth
               <span class="mud-tab-slider">         <- MudBlazor's animated indicator
       <div class="mud-tabs-panels">
         <div class="mud-tab-panel mud-tab-panel-active">   <- active panel: display: contents
   Three consequences the selectors below depend on:
     (a) A TAB HEADER also carries .mud-tab-panel, so any panel rule must be scoped
         under .mud-tabs-panels or it inflates the strip (a bare .mud-tab-panel
         padding rule took tabs to ~53px tall).
     (b) The active panel is display: contents and IGNORES padding -- pad the
         .mud-tabs-panels container instead.
     (c) MinimumTabWidth defaults to "160px" and lands as an INLINE style. CSS cannot
         beat that without !important, so tabs are 160px wide unless the caller passes
         MinimumTabWidth="0" on MudTabs. Pass it wherever the label set is short.
   There is NO .hav-tab / aria-selected element in the output, so target the real
   .mud-tab* classes. Decision: HIDE the animated .mud-tab-slider and draw a static
   2px steel underline on .mud-tab-active (precise, blueprint-correct -- no sliding
   motion). Labels are Barlow Condensed uppercase ~13px; square corners throughout.
   Colour is token-only, so dark mode (--mud-palette-* rewritten on :root) just works.
   -------------------------------------------------------------------------- */
.hav-tabs .mud-tabs-tabbar { border-bottom: 1px solid var(--hav-line); min-height: 0; }
.hav-tabs .mud-tabs-tabbar-inner,
.hav-tabs .mud-tabs-tabbar-content,
.hav-tabs .mud-tabs-tabbar-wrapper { min-height: 0; background: transparent; }
.hav-tabs .mud-tab {
  font-family: var(--hav-font-head); text-transform: uppercase; letter-spacing: .04em;
  font-size: 13px; font-weight: 500; color: var(--hav-text);
  border-radius: 0; min-height: 0; padding: 10px 16px;
  border-bottom: 2px solid transparent;
  /* min-width is set INLINE by MinimumTabWidth (default 160px) -- see (c) above. */
}
.hav-tabs .mud-tab:hover { color: var(--hav-accent); background: color-mix(in srgb, var(--hav-accent) 6%, transparent); }
.hav-tabs .mud-tab.mud-tab-active { color: var(--hav-accent); font-weight: 600 !important; border-bottom-color: var(--hav-accent); }
/* Tabs are focusable DIVs (role=tab, tabindex=0), so in Chromium a MOUSE CLICK
   triggers :focus-visible -- the ring shows on click, not only on keyboard. At
   offset -2px it sat flush against the strip hairline and the tab seams and read
   as a box overlapping the neighbour; -6px frames the label area instead. */
.hav-tabs .mud-tab:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: -6px; }
/* R3: 9.x carries the generic state class .mud-disabled (the "mud-tab-disabled" name never existed). */
.hav-tabs .mud-tab.mud-disabled { color: var(--hav-text-muted); opacity: .45; }
.hav-tabs .mud-tab-slider { display: none; }               /* replaced by the static underline above */
/* Ripple guard: .mud-tab carries overflow: visible (S11.15, so a label is never cut)
   which also unclips MudBlazor's ripple -- a ~320px circle centred on a ~160px tab,
   visibly bleeding over both neighbours on click. The hover tint and the underline
   are the feedback; the ripple has no job here. */
.hav-tabs .mud-ripple-effect { display: none; }
/* Panels only -- NOT the tab headers, which share the .mud-tab-panel class. Padding
   lands on the container because the active panel is display: contents. */
.hav-tabs .mud-tabs-panels { padding-top: 18px; }
/* Overflow: 9.x renders scroll buttons as .mud-tabs-scroll-button (plural "tabs");
   .mud-tab-scroll-button was the pre-9.x name and matched nothing. Recolour to the
   accent -- the strip scrolls horizontally on its own otherwise. */
.hav-tabs .mud-tabs-scroll-button { color: var(--hav-text-muted); }
.hav-tabs .mud-tabs-scroll-button:hover { color: var(--hav-accent); }

/* Fallback for a hand-rolled (non-MudTabs) strip + the static audit mockup.
   Opt in with .hav-tabs--plain so it never fights MudTabs' own layout on .hav-tabs. */
.hav-tabs--plain { display: flex; align-items: stretch; gap: 2px; border-bottom: 1px solid var(--hav-line); overflow-x: auto; }
.hav-tab { font-family: var(--hav-font-head); text-transform: uppercase; letter-spacing: .04em; font-size: 13px; color: var(--hav-text); padding: 10px 16px; border: 0; background: transparent; border-bottom: 2px solid transparent; cursor: pointer; white-space: nowrap; }
.hav-tab:hover { color: var(--hav-accent); }
.hav-tab.active, .hav-tab[aria-selected="true"] { color: var(--hav-accent); border-bottom-color: var(--hav-accent); font-weight: 600; }
.hav-tab:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: -2px; }
.hav-tab:disabled, .hav-tab[aria-disabled="true"] { color: var(--hav-text-muted); opacity: .45; cursor: not-allowed; }
.hav-tabs__overflow { margin-left: auto; flex: none; display: inline-grid; place-items: center; width: 34px; color: var(--hav-text-muted); }

/* --------------------------------------------------------------------------
   Onboarding step header (v2) -- shell-less first-run wizard progress.
   -------------------------------------------------------------------------- */
.hav-step-header { display: flex; flex-direction: column; gap: 9px; }
.hav-step-count { font-family: var(--hav-font-head); text-transform: uppercase; letter-spacing: .06em; font-size: 12px; color: var(--hav-text-muted); }
.hav-step-progress { height: 5px; background: var(--hav-line); }
.hav-step-progress > span { display: block; height: 100%; background: var(--hav-accent); transition: width var(--hav-dur-dialog) linear; } /* §5: a determinate fill is linear (v1.108.0) */
@media (prefers-reduced-motion: reduce) { .hav-step-progress > span { transition: none; } }

/* --------------------------------------------------------------------------
   Entity detail 8/4 (v2) -- record view. Entity header + KPI plates carry the
   registration marks; standards card, main content and the right rail are plain
   hairline. Collapses to one column below ~960px.
   -------------------------------------------------------------------------- */
.hav-detail { display: grid; grid-template-columns: 8fr 4fr; gap: 20px; align-items: start; }
.hav-rail { display: flex; flex-direction: column; gap: 16px; }
/* The rail NARROWS before it moves: 4fr does that on its own down to ~960, where the
   side menu docks to icons and gives ~200px back. Stacking at 960 would throw away
   the width the dock just recovered -- the responsive ladder stacks at 760. */
@media (max-width: 760px) { .hav-detail { grid-template-columns: 1fr; } }
/* S11.8 -- A populated region takes the width (HAV2132). The grid collapses on the
   ABSENCE OF THE RAIL, not on a second parameter somebody has to remember after
   setting the first: a page with ShowAnchorPanel="false", a permission-hidden rail
   or an empty timeline would otherwise hold 4 of 12 columns of dead ground. */
.hav-detail:not(:has(.hav-rail)) { grid-template-columns: 1fr; }
.hav-detail > :only-child { grid-column: 1 / -1; }

/* --------------------------------------------------------------------------
   S11.7 -- Attribute READOUT (HAV2131). Attributes are a readout; metrics are
   plates. "Category: Safety Critical" is not a measurement and must never take
   a 38px Condensed numeral or a 96px plate. Label left, value right, hairline
   between rows. Promoted from the journey-detail note -- every detail page needs
   it, so it is not bespoke to one page. Works on MudList or plain rows.
   -------------------------------------------------------------------------- */
.hav-readout { display: flex; flex-direction: column; }
.hav-readout__row,
.hav-readout .mud-list-item {
  display: flex; align-items: baseline; gap: 12px;
  min-height: 36px; padding: 8px 0;
  border-bottom: 1px solid var(--hav-line);
  background: none;
  font-family: var(--hav-font-body); font-size: 14px;
}
.hav-readout__row:last-child,
.hav-readout .mud-list-item:last-child { border-bottom: 0; }
.hav-readout__label,
.hav-readout .mud-list-item-text { color: var(--hav-text-label); }
.hav-readout__value { margin-left: auto; text-align: right; font-weight: 600; color: var(--hav-text); }

/* --------------------------------------------------------------------------
   S11.10 -- Panel geometry (HAV2134). "Sticky footer" is a description; this is
   the geometry that makes it true. The panel spans the viewport, the commit bar
   pins to the PANEL's bottom edge, and the body is the only scroll region. The
   body's bottom padding equals the bar height, so the last field can never sit
   under it -- which is why a panel footer may pin where a table footer may not
   (S11.2). Put .hav-panel on the MudDrawer.
   -------------------------------------------------------------------------- */
.hav-panel,
.mud-drawer.hav-panel {
  display: flex; flex-direction: column;
  height: 100dvh; max-height: 100dvh;
  top: 0; bottom: 0;
  background: var(--hav-panel);
}
.hav-panel__header {
  flex: 0 0 auto; min-height: 56px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 0 20px; border-bottom: 1px solid var(--hav-line);
}
.hav-panel__body {
  flex: 1 1 auto; overflow-y: auto;
  padding: 20px 20px calc(20px + 64px);   /* clears the pinned commit bar */
}
.hav-panel__footer {
  flex: 0 0 auto; min-height: 64px;
  display: flex; align-items: center; justify-content: flex-end; gap: 8px;
  padding: 0 24px;
  border-top: 1px solid var(--hav-line);
  background: var(--hav-surface-chrome);
}

/* --------------------------------------------------------------------------
   S11.6 -- A row fills its width (HAV2130). A row of cards is a surface and a
   surface has no holes: two items in a three-column row leave a dead track
   that reads as a card which failed to load. Lay the row out from the ITEM's
   minimum width, never from a column count -- auto-fit then wraps on its own
   at any item count and any viewport, with no breakpoint to maintain.
   Use INSTEAD of MudGrid + MudItem md= wherever the count comes from a query.
   -------------------------------------------------------------------------- */
.hav-row {
  display: grid;
  gap: 16px;
  /* Self-declared, not inherited (v1.120.0). Custom properties inherit, so without this a
     .hav-row nested inside a solo row took the parent's definite cap and auto-fit collapsed it
     to one column. The solo rule below still wins on the solo row itself (later, and :has adds
     specificity). */
  --hav-row-track: 1fr;
  /* min(floor, 100%) -- a bare floor cannot shrink below itself, so a narrow
     column would overflow instead of wrapping. This collapses the floor to the
     container when the container is smaller: no media query, no breakpoint. */
  grid-template-columns: repeat(auto-fit, minmax(min(var(--hav-row-min, 280px), 100%), var(--hav-row-track, 1fr)));
  /* A short row is left-aligned and filled, never centred (two holes instead
     of one). --hav-row-track caps the track only where a lone item must not
     sprawl; it is not a centring device. */
  justify-content: start;
  align-items: stretch;
}
/* Track floors from the token contract. */
.hav-row--alert   { --hav-row-min: 320px; }
.hav-row--kpi     { --hav-row-min: 240px; }
.hav-row--insight { --hav-row-min: 220px; }
/* The lone-item cap (v1.118.0). Automatic for a row with one child; .hav-row--solo is
   the opt-in for a row whose items must ALL stay to the measure. Delete any local
   `--hav-row-track: 1fr` override once this lands -- it restates the default. */
.hav-row:has(> :only-child),
.hav-row--solo { --hav-row-track: var(--hav-row-track-solo); }
/* .hav-row--fill (v1.120.0): the opt-out for a lone-child row that is the page's structural
   shell, not an insight -- a dashboard layout row whose one child is the whole page body.
   (0,2,0) and later, so it beats .hav-row:has(> :only-child). Replaces the app's scoped
   .dashboard-layout-row exemption; delete that when this lands. Never on a row of peers. */
.hav-row.hav-row--fill { --hav-row-track: 1fr; }
/* The card IS the grid item. A wrapper carrying its own width or flex-basis
   re-creates the dead column inside the cell. */
.hav-row > * { min-width: 0; }

/* --------------------------------------------------------------------------
   Calendar / scheduling (v4 -- R3, attested against Heron.MudCalendar 4.1.0).
   Put Class="hav-cal" on <MudCalendar>. Month view renders:

     <div class="mud-cal-month-view">
       <div class="mud-cal-month-grid-header">
         <div class="mud-cal-month-header">Mon</div>        <- weekday cells
       <div class="mud-cal-month-table-holder">
         <div class="mud-cal-month-cell [cell-selected]">   <- selected is NOT mud-*
           <div class="mud-cal-month-cell-title [mud-cal-month-outside]">24</div>
           <a class="mud-cal-month-link">...</a>            ... event items ...

   Three things the v3 sketch had wrong, each of which silently disabled a rule:
     - the grid is .mud-cal-month-table-holder, not .mud-cal-month-grid;
     - out-of-month is .mud-cal-month-outside and it sits on the TITLE, not the
       cell -- cell-level dimming needs :has(.mud-cal-month-outside);
     - selected is Heron's own .cell-selected, not a mud-* class, and there is
       no aria-selected on the cell.
   TODAY: Heron 4.1.0's month view draws today internally and exposes NO class
   for it (week view is the inverse -- it ships .mud-cal-week-not-today). The
   marker is therefore OURS: the app applies .hav-cal-today via Heron's cell
   render fragment. See spec S11.65 / HAV2297 -- do not re-derive a
   "mud-cal-today" hook, it has never existed at this version.
   Square corners + hairline borders throughout; colour is token-only so dark
   mode (--mud-palette-* on :root) works.
   -------------------------------------------------------------------------- */
.hav-cal .mud-cal-month-view,
.hav-cal .mud-cal-month-table-holder { border: 1px solid var(--hav-line); background: var(--hav-line); gap: 1px; border-radius: 0; }
/* weekday header rail -- the cells inside .mud-cal-month-grid-header */
.hav-cal .mud-cal-month-grid-header .mud-cal-month-header {
  background: var(--hav-panel); padding: 7px 8px;
  font-family: var(--hav-font-head); text-transform: uppercase; letter-spacing: .06em;
  font-size: 11px; color: var(--hav-text-label);
}
/* day cells -- 1px hairline gutters come from the grid bg showing through */
.hav-cal .mud-cal-month-cell { background: var(--hav-panel); min-height: 56px; padding: 6px 8px; border-radius: 0; border: 0; }
.hav-cal .mud-cal-month-link { font-family: var(--hav-font-body); font-size: 12px; color: var(--hav-text); text-decoration: none; }
/* out-of-month -- the class is on the title, so the cell dims via :has() */
.hav-cal .mud-cal-month-cell:has(.mud-cal-month-outside) { background: color-mix(in srgb, var(--hav-text) 3%, var(--hav-panel)); }
.hav-cal .mud-cal-month-cell-title.mud-cal-month-outside { color: var(--hav-text-muted); opacity: .6; }
/* hover + selected */
.hav-cal .mud-cal-month-cell:hover { background: color-mix(in srgb, var(--hav-accent) 6%, var(--hav-panel)); }
.hav-cal .mud-cal-month-cell.cell-selected { background: color-mix(in srgb, var(--hav-accent) 10%, transparent); box-shadow: inset 0 0 0 1.5px var(--hav-accent); }
/* Today marker -- accent ring on the day number, not a filled cell.
   OURS, not the vendor's: apply .hav-cal-today to the cell from Heron's cell
   render fragment (HAV2297). The ring is on the title, so it rings the
   numeral and not the whole cell. */
.hav-cal .hav-cal-today .mud-cal-month-cell-title {
  display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 5px;
  color: var(--hav-accent); font-weight: 600; box-shadow: inset 0 0 0 1.5px var(--hav-accent);
}
.hav-cal .mud-cal-month-cell:focus-visible,
.hav-cal .mud-cal-month-link:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: -2px; }

/* Event chips -- square, per-module colour carries meaning (matches .hav-status).
   Set the module class on the event template: <div class="hav-cal-ev hav-cal-ev--incident">.
   Default (unclassed) chips read as the steel accent. */
.hav-cal-ev { display: block; margin-top: 4px; padding: 2px 6px; border-radius: 2px;
  font-family: var(--hav-font-body); font-size: 10px; line-height: 1.3;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--hav-accent); background: color-mix(in srgb, var(--hav-accent) 14%, transparent); }
.hav-cal-ev--incident   { color: var(--hav-error);   background: color-mix(in srgb, var(--hav-error) 14%, transparent); }
.hav-cal-ev--risk       { color: var(--hav-warning); background: color-mix(in srgb, var(--hav-warning) 18%, transparent); }
.hav-cal-ev--training   { color: var(--hav-info);    background: color-mix(in srgb, var(--hav-info) 14%, transparent); }
.hav-cal-ev--corrective { color: var(--hav-accent);  background: color-mix(in srgb, var(--hav-accent) 14%, transparent); }
.hav-cal-ev--inspection { color: var(--hav-success); background: color-mix(in srgb, var(--hav-success) 16%, transparent); }
.hav-cal-ev--ppe        { color: var(--hav-text-label); background: color-mix(in srgb, var(--hav-text-muted) 16%, transparent); }
/* a hard deadline reads as a solid safety-red bar, not a tint */
.hav-cal-ev--overdue { color: var(--hav-on-error); background: var(--hav-error); }

/* --------------------------------------------------------------------------
   Focus -- themed accent ring, never the browser default
   -------------------------------------------------------------------------- */
/* R1 #1/#3 -- themed keyboard focus ring (WCAG 2.4.7). Apply .hav-app to the MainLayout
   root <div> so the ring reaches every app link/button; nav focus fires on the inner
   .mud-nav-link. .hav-app is behaviour-only -- it has NO border/background (unlike
   .hav-blueprint), so it will not frame the whole app. */
.hav-app { /* app-root scope only; no visual styling */ }
.hav-app a:focus-visible,
.hav-app button:focus-visible,
.hav-navlink .mud-nav-link:focus-visible,
.hav-focus:focus-visible,
.hav-blueprint a:focus-visible,
.hav-blueprint button:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: 2px; }

/* --------------------------------------------------------------------------
   R2 #4 -- Empty state: the dashed plate, never a status alert.
   An empty region is not an event. Aliases the app's existing .empty-state-dashed
   so both names resolve to one look. Copy comes from CommonLoc, never a literal.
   -------------------------------------------------------------------------- */
.hav-empty,
.empty-state.empty-state-dashed {
  border: 1px dashed var(--hav-line);
  background: transparent;
  padding: 26px 20px;
  text-align: center;
}
.hav-empty__title {
  display: block;
  font-family: var(--hav-font-head);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .03em;
  font-size: 15px;
  color: var(--hav-text);
}
.hav-empty__body {
  display: block;
  font-family: var(--hav-font-body);
  font-size: 13px;
  line-height: 1.5;
  margin-top: 4px;
  color: var(--hav-text-muted);
}
.hav-empty__action { margin-top: 14px; }
/* Compact variant for a timeline / rail slot inside a detail page. */
.hav-empty--compact { padding: 16px 14px; }

/* --------------------------------------------------------------------------
   S11.61 -- LOADING AND COULD-NOT-LOAD (HAV2185, HAV2274).
   The four states have been specified since 1.7.1 and two of them had no class:
   --hav-skeleton-line / -gap / -dur were declared at R1 and nothing ever
   consumed them. A token with no consumer is a rule nobody can follow.
   Skeleton reserves the REAL row height so the region does not resize when the
   data lands (HAV2113's reflow argument, one state earlier). Could-not-load is
   a solid warning border, never the dashed plate: a dashed plate says nothing
   is here, and this says we do not know what is here.
   -------------------------------------------------------------------------- */
.hav-skeleton { display: grid; gap: var(--hav-skeleton-gap); }
.hav-skeleton__line {
  height: var(--hav-skeleton-line);
  background: linear-gradient(90deg,
    var(--hav-surface-section) 0%,
    color-mix(in srgb, var(--hav-line) 55%, var(--hav-surface-section)) 50%,
    var(--hav-surface-section) 100%);
  background-size: 200% 100%;
  animation: hav-skeleton-sweep var(--hav-skeleton-dur) ease-in-out infinite;
}
.hav-skeleton__line--sm { width: 40%; }
.hav-skeleton__line--md { width: 70%; }
/* A skeleton ROW holds the height of the populated row it stands in for. */
.hav-skeleton__row { display: flex; align-items: center; gap: 14px; min-height: 44px; }
.hav-skeleton__row--touch { min-height: var(--hav-kiosk-queue-row, 72px); padding: 0 18px; }
@keyframes hav-skeleton-sweep {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}
/* Motion is decoration here: the layout carries the meaning on its own. */
@media (prefers-reduced-motion: reduce) {
  .hav-skeleton__line { animation: none; background: var(--hav-surface-section); }
}

.hav-fail {
  border: 1px solid var(--hav-warning); border-left-width: 3px;
  background: color-mix(in srgb, var(--hav-warning) 8%, var(--hav-surface-section));
  padding: 18px 20px;
}
.hav-fail__title {
  display: block; font-family: var(--hav-font-head); font-weight: 600;
  text-transform: uppercase; letter-spacing: .03em; font-size: 14px;
  color: var(--hav-text);
}
.hav-fail__body { display: block; margin-top: 5px; font-family: var(--hav-font-body); font-size: 13px; line-height: 1.5; color: var(--hav-text-muted); max-width: 52ch; }
.hav-fail__action { margin-top: 14px; }
/* The cache age is a fact about the data, not an apology -- mechanical face (HAV2137). */
.hav-fail__age { display: block; margin-top: 9px; font-family: var(--hav-font-mono); font-size: 12px; color: var(--hav-text-muted); }
.hav-empty--compact .hav-empty__title { font-size: 13px; }

/* --------------------------------------------------------------------------
   R2 #5 -- Charts. Put Class="hav-chart" on <MudChart>; the series palette itself is
   set ONCE from the tokens above in ThemeConfiguration (ChartOptions.ChartPalette),
   never per page. Flat fills, square ends, hairline y-gridlines only.
   -------------------------------------------------------------------------- */
.hav-chart { font-family: var(--hav-font-body); }
/* R3: grid lines render as <line> inside the axis groups -- already covered by the
   two rules below. The "mud-charts-grid-line" / "mud-charts-gridline" hooks do not exist. */
.hav-chart .mud-chart-line { stroke: var(--hav-chart-grid); stroke-width: 1; }
.hav-chart .mud-charts-xaxis line,
.hav-chart .mud-charts-yaxis line { stroke: var(--hav-chart-axis); stroke-width: 1; }
.hav-chart .mud-charts-xaxis text,
.hav-chart .mud-charts-yaxis text,
.hav-chart text.mud-chart-axis-label {
  font-family: var(--hav-font-body);
  font-size: 11px;
  fill: var(--hav-chart-label);
}
/* Numerals read as data -- Condensed, tabular, so columns of figures align. */
/* R3: no serie-value hook exists in MudBlazor 9.x -- .hav-chart-value is ours and is the hook. */
.hav-chart-value {
  font-family: var(--hav-font-head);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  fill: var(--hav-text);
  color: var(--hav-text);
}
.hav-chart path, .hav-chart rect, .hav-chart circle { filter: none; }   /* no shadows, no bevels */
.hav-chart .mud-chart-donut circle { stroke-linecap: butt; }            /* square ends, not rounded */
.hav-chart-track { fill: var(--hav-chart-track); }
/* Legend as a chip row beneath the plot -- never a floating box over it. */
.hav-chart-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 12px; }
.hav-chart-legend > span {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--hav-font-body); font-size: 12px; color: var(--hav-text-muted);
}
.hav-chart-legend i { width: 10px; height: 10px; flex: none; display: block; }

/* --------------------------------------------------------------------------
   R2 #6 -- Brand mark & app icon.
   TWO lockups, two surfaces (S16.4, HAV2136). The SIGNATURE lockup is an SVG
   asset with the wordmark in Billet Stencil -- auth pages, exported PDF headers,
   email, app icon, favicon: surfaces with no webfont. The CHROME lockup is the
   mark + a LIVE-TEXT wordmark in the UI face -- AppBar, marketing nav, footer.
   .hav-app-brand-wordmark is the chrome rendering, NOT a fallback for the asset:
   a stencil face loses its cuts at 18px and reads as a sticker beside Barlow
   Condensed nav links, and live text inherits currentColor so one lockup serves
   both themes. Never put the asset in the AppBar.
   HAV2062 flags letter-spacing >= .1em on any heading --
   wide tracking means "the company", condensed caps mean "a heading in the product".
   Ship the icon square: iOS/Android apply their own mask. Never re-tint per theme.
   Assets live in wwwroot/img/brand/ -- 512 (maskable) / 180 (apple-touch) / 32 / 16.
   -------------------------------------------------------------------------- */
.hav-app-brand-mark { height: 24px; width: 24px; flex: none; display: block; }
.hav-app-brand-lockup, .hav-app-brand-logo { flex: none; } /* a replaced element under max-width:100% shrinks to 0 -- HAV2128 */
.hav-app-brand-lockup { width: 240px; max-width: 100%; height: auto; display: block; } /* §16.4 signature lockup: signing surfaces are fixed-width -- never collapses (v1.109.0) */
/* Below 24px the wordmark is unreadable -- mark only. */
@media (max-width: 599px) { .hav-app-brand-wordmark { display: none; } }

/* --------------------------------------------------------------------------
   R3 -- Status chips never wrap. A chip is a token, not a sentence: if the label
   doesn't fit, the label is too long. flex:none stops a chip being squeezed below
   its content width by a crowded flex row (which wraps the text outside the chip).
   -------------------------------------------------------------------------- */
.hav-chip,
.mud-chip {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  flex: none;
  border-radius: 0;
}

/* --------------------------------------------------------------------------
   R3 -- Dark theme foregrounds. MudBlazor rewrites --mud-palette-* on :root, so we
   cannot key off a class; this block is applied by the same theme switch that sets
   the palette. In dark mode every status fill lightens and white drops to 1.67-2.77:1,
   so the foreground flips to black (7.59-12.58:1). Never hardcode #fff on a fill.
   -------------------------------------------------------------------------- */
:root[data-hav-theme="dark"],
.hav-app[data-hav-theme="dark"] {
  --hav-on-accent:  #000000;
  --hav-on-info:    #000000;
  --hav-on-success: #000000;
  --hav-on-warning: #000000;
  --hav-on-error:   #000000;
  --hav-warning-text: #F9A900;
  --hav-success-text: #4ade80;
  --hav-error-text:   #fca5a5;
  --hav-accent-inverted: #3b6ea5;
}

/* .hav-chip--accent/-info/-success/-warning/-error WERE HERE and are deleted.
   They were a second chip family for the object .hav-status--* already owns, with
   the OPPOSITE contrast contract: a saturated fill of the role where .hav-status
   deliberately uses a 14-16% tint plus the deep step, because the base roles fail
   AA as text (success 4.03, error 4.27, amber 1.70 -- see the comment above).
   .hav-status--alert is the ONE saturated status in the system and it means ACT
   NOW; five more sitting one autocomplete keystroke away is the HAV2064 problem
   in a different vocabulary -- the rule was never disobeyed, it was unenforceable.
   Nothing referenced them. Use .hav-status--* for a status and a button for an
   action; do not re-add a filled chip family (HAV2065, HAV2177, S11.5, S14.4). */

/* Amber as TEXT is 1.96:1 on white -- never legal. Use the deep step. */
.hav-text-warning { color: var(--hav-warning-text); }

/* --------------------------------------------------------------------------
   R5 -- A text role is measured against EVERY ground it lands on; the worst case is
   its score. TextSecondary #64748b is 4.76 on a white panel but 4.25 on the page
   ground, where most muted text actually sits. #5b6a80 clears 4.5 on both (4.92).
   -------------------------------------------------------------------------- */
.hav-text-muted { color: var(--hav-text-muted, #5b6a80); }

.hav-text-success { color: var(--hav-success-text); }
.hav-text-error   { color: var(--hav-error-text); }
.hav-text-accent  { color: var(--hav-accent-strong); }

/* --------------------------------------------------------------------------
   R6 -- S11 Density and signal, in code. Three objects, one rule each:
   S11.1 one banner (HAV2111) * S11.2 table chrome is one object (HAV2112) *
   S11.3 empty states reserve height (HAV2113) * S11.5 separation by surface.
   All colour resolves through --hav-* / --mud-palette-* (HAV2065).
   -------------------------------------------------------------------------- */

/* S11.1 -- ONE top-of-page banner (HAV2111). The region is a SLOT, not a stack: the
   rule below hides every sibling after the FIRST IN DOM ORDER, so at most one banner
   ever renders and a second can never push the page down.
   It does NOT sort by severity -- `~` cannot, and `order:` would not help (it moves the
   painted box, not the match). Severity order is the page's obligation: contribute
   Critical before Warning before Info. A page that emits Info first shows Info and
   hides Critical, which is worse than a stack -- so HAV2111 asserts both halves.
   AI insights are not banners -- they route to an inline dashboard widget, never a
   drawer and never this slot. */
.hav-banner-region { display: grid; gap: 0; margin: 0 0 16px; }
.hav-banner-region > .hav-banner ~ .hav-banner { display: none; }
.hav-banner {
  display: flex; align-items: flex-start; gap: 12px;
  min-height: 44px; padding: 12px 16px;
  border: 1px solid var(--hav-line); border-left-width: 3px;
  background: var(--hav-surface-section);
  font-family: var(--hav-font-body); font-size: 13px; line-height: 1.45;
  color: var(--hav-text);
}
.hav-banner__title {
  font-family: var(--hav-font-head); font-weight: 600; font-size: 12px;
  letter-spacing: .07em; text-transform: uppercase; color: var(--hav-text-label);
}
.hav-banner__action { margin-left: auto; flex: none; }
/* Severity reads from the left rule + a low tint of the ground, not a saturated fill:
   a banner is page furniture, not a chip. */
.hav-banner--critical { border-left-color: var(--hav-error);   background: color-mix(in srgb, var(--hav-error) 8%, var(--hav-surface-section)); }
.hav-banner--warning  { border-left-color: var(--hav-warning); background: color-mix(in srgb, var(--hav-warning) 10%, var(--hav-surface-section)); }
.hav-banner--info     { border-left-color: var(--hav-info);    background: color-mix(in srgb, var(--hav-info) 8%, var(--hav-surface-section)); }

/* S11.2 -- Table chrome is ONE object (HAV2112). One 56px bar: search, primary action,
   filter pills. Pills wrap INSIDE the bar (the bar grows, the body never shifts);
   there is never a second stacked toolbar. */
.hav-table__header {
  display: flex; align-items: center; flex-wrap: wrap; gap: 12px;
  min-height: 56px; padding: 10px 16px;
  border: 1px solid var(--hav-line); border-bottom: 0;
  background: var(--hav-surface-chrome);
}
.hav-table__search  { flex: 1 1 240px; min-width: 200px; }
.hav-table__actions { margin-left: auto; flex: none; display: flex; align-items: center; gap: 8px; }
/* Pills claim their own line within the bar -- never the table body. */
.hav-table__filters { display: flex; flex-wrap: wrap; gap: 8px; flex: 1 1 100%; min-width: 0; }

/* thead sticks to the SCROLL CONTAINER, not the viewport: MudTable's
   .mud-table-container is the scroll port, so the header stops at the top of the table
   instead of sliding under the AppBar. The page sets the container's max-height. */
.hav-table .mud-table-container { overflow: auto; }
/* R7 19 Aug 2026 -- the header row is table chrome and takes --hav-surface-chrome, like
   __toolbar and __footer above. It used --hav-surface-section, which IS --hav-panel, so a
   .hav-table nested in a .hav-section painted its header the same colour as its own
   container: header on header, with --hav-text-label (tuned for the light ground) as the
   only signal. Found in Emergency Procedures' drill-history pane, where both tables
   declared headers and neither showed one. HAV2065 class -- a token pair resolving to no
   contrast. The intent was already stated at the token: "Table chrome (header/footer bars)
   sits on the raised step." */
.hav-table .mud-table-head th,
.hav-table .mud-table-head .mud-table-cell,
.hav-table thead th { position: sticky; top: 0; z-index: 2; background: var(--hav-surface-chrome); }

/* S11.2 -- Footer: row count + pagination, anchored beneath the last row. Explicitly
   NOT sticky -- a sticky footer covers a row on a short viewport and reads as a
   second bar, which is the thing S11.2 exists to prevent. */
.hav-table__footer {
  position: static;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  min-height: 48px; padding: 8px 16px;
  border: 1px solid var(--hav-line); border-top: 0;
  background: var(--hav-surface-chrome);
  font-family: var(--hav-font-body); font-size: 12px; color: var(--hav-text-label);
}
.hav-table__count {
  font-family: var(--hav-font-head); font-size: 11px;
  letter-spacing: .06em; text-transform: uppercase;
}

/* S11.3 -- The empty plate RESERVES height (HAV2113). An empty tile holds the height of
   the smallest populated tile in its row, so a partly-empty dashboard does not stagger;
   it never collapses. One line of centred microcopy, no illustration. The row sets
   --hav-empty-reserve from its smallest populated tile.
   INSIDE .hav-row the reserve is unnecessary: the grid equalises its cells
   (align-items: stretch), so this is for the non-grid cases -- a lone card, a flex row,
   a rail panel -- where nothing else establishes the height. */
.hav-empty,
.empty-state.empty-state-dashed {
  min-height: var(--hav-empty-reserve, 160px);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.hav-empty__body { max-width: 44ch; }
/* No illustration -- the legacy hook is neutralised rather than restyled. Icons inside
   __action are untouched (an action is not decoration). */
.hav-empty__illustration { display: none; }
.hav-empty--compact { min-height: var(--hav-empty-reserve-compact, 96px); }

/* --------------------------------------------------------------------------
   S11.14 -- The disclosure surface (HAV2139). An expander is a HEADING THAT
   OPENS, not a container: it draws no fill of its own. MudExpansionPanels
   renders each panel as MudPaper (surface fill + elevation + a divider
   pseudo-element + a margin when expanded), which is what turns four optional
   sections into four floating slabs on the page ground. Two renderings, and
   the SURFACE the group sits on decides which -- there is no third:

     .hav-disclose               on the page ground: the body opens ON the
                                 ground, 16 indent + left hairline, no border
     .hav-disclose--in-section   inside .hav-section / a plate: the body opens
                                 inside ONE hairline, fill stays transparent

   Never a second fill step inside a section (that is HAV2115 glare), never a
   gap between panels (a gap makes them cards), never a panel inside a panel.
   Put Class="hav-disclose" on the MudExpansionPanels COMPONENT.
   -------------------------------------------------------------------------- */
.hav-disclose,
.hav-disclose.mud-expansion-panels {
  display: flex; flex-direction: column;
  background: transparent; box-shadow: none; border-radius: 0;
  border-top: 1px solid var(--hav-line);
  border-bottom: 1px solid var(--hav-line);
}
.hav-disclose .mud-expand-panel {
  background: transparent; box-shadow: none; border-radius: 0; margin: 0;
}
/* MudBlazor's own divider pseudo-element and expanded margin -- both would
   re-open the gap the stack exists to close. */
.hav-disclose .mud-expand-panel::before,
.hav-disclose .mud-expand-panel::after,
.hav-disclose .mud-expand-panel-border::before { display: none; }
.hav-disclose .mud-expand-panel.mud-panel-expanded { margin: 0; }
.hav-disclose .mud-expand-panel + .mud-expand-panel { border-top: 1px solid var(--hav-line); }

/* Header: the whole row is the target (44 min), body face -- NOT condensed
   (HAV2114): these are field-group names a worker reads, not display type. */
.hav-disclose .mud-expand-panel-header {
  min-height: 44px; padding: 11px 12px; gap: 12px;
  background: transparent; border-radius: 0;
  font-family: var(--hav-font-body); font-size: 14px; color: var(--hav-text);
}
/* HOVER IS NOT A FILL (HAV2174). A disclosure header is full-bleed -- on a
   dashboard section that is a 1500px band of tinted ground appearing under the
   pointer, which reads as a selection, not an affordance, and contradicts the
   rule this component exists to state. The hover moves to the LABEL and the
   CHEVRON: they take the accent, the row keeps the ground. Same reasoning as
   the tab strip's underline -- the target may be wide, the signal is not. */
.hav-disclose .mud-expand-panel-header:hover { background: transparent; color: var(--hav-accent); }
.hav-disclose .mud-expand-panel-header:hover .mud-expand-panel-icon { color: var(--hav-accent); }
.hav-disclose .mud-expand-panel-header:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: -2px; }
/* The chevron and the body are the state. The header fill never changes. */
.hav-disclose .mud-panel-expanded > .mud-expand-panel-header { background: transparent; }
.hav-disclose .mud-expand-panel-icon { color: var(--hav-text-label); transition: transform 160ms ease, color 120ms ease; }
.hav-disclose__count { color: var(--hav-text-muted); }   /* "Witnesses (0)" */

/* MudBlazor 9 DOM (v1.119.0 correction). .mud-expand-panel-text is the HEADER's title
   wrapper -- a sibling of .mud-expand-panel-icon inside .mud-expand-panel-header. The
   body is .mud-expand-panel-content, inside .mud-collapse-container. Until v1.118.0 the
   two body rules below targeted -text, which drew the body's border round the TITLE: a
   boxed header with the chevron outside it, and a body with no frame of its own. The
   title wrapper carries nothing. */
.hav-disclose .mud-expand-panel-text { flex: 1 1 auto; min-width: 0; margin: 0; padding: 0; border: 0; background: transparent; }
/* Body, on the page ground: no fill, no border -- indent + a left hairline. */
.hav-disclose .mud-expand-panel-content {
  background: transparent;
  margin: 0 12px 20px 28px;              /* 12 header pad + 16 indent */
  padding: 0 0 0 16px;
  border-left: 1px solid var(--hav-line);
}
/* Body, inside a section: ONE hairline, transparent fill. No indent rule --
   the border is the edge, and two edges is a box inside a box. Header padding
   goes to 0 horizontally: .hav-section already supplies the frame. */
.hav-disclose--in-section .mud-expand-panel-header { padding-inline: 0; }
.hav-disclose--in-section .mud-expand-panel-content {
  margin: 0 0 14px; padding: 12px;
  border: 1px solid var(--hav-line);
  border-left-width: 1px;
}
/* A body loading state is .hav-skeleton, never MudSkeleton: MudSkeleton's text type
   renders rounded pills, and this system has no radius (S11.61, HAV2185). */
/* A disclosure never contains a disclosure (HAV2139) -- CI flags it. Until it
   is unpicked, stop the rules doubling so the defect reads as one thing. */
.hav-disclose .hav-disclose { border-top: 0; border-bottom: 0; }

/* --------------------------------------------------------------------------
   LIST ROW -- one item inside an advisory body (S11.26, S11.28, HAV2175).
   One stated finding, one verb, one hairline between rows.
   This class has been named in the spec since 1.30.0 and applied in Razor in
   three places with NO RULE BEHIND IT, so every advisory item rendered as a
   bare MudStack and the "compact" branch of BaseRecommendationsPanel changed
   nothing but the type scale. That is the whole reason the two views looked
   like the same thing twice.
   NOT a card: no fill, no radius, no shadow (HAV2139). NO hover fill -- the
   row spans the content measure and the verb is the target (HAV2174).
   -------------------------------------------------------------------------- */
.hav-list-row {
  display: flex; align-items: center; gap: 16px;
  min-height: 44px; padding: 10px 0;
  background: transparent; border-radius: 0; box-shadow: none;
}
/* Between items only. A rule under the last row doubles the body's own
   border, which is the gap-instead-of-hairline defect in reverse. */
.hav-list-row + .hav-list-row { border-top: 1px solid var(--hav-line); }
/* The finding takes the measure; the verb takes what it needs. min-width:0 or
   a long finding pushes the button off the row instead of wrapping. */
.hav-list-row__text { flex: 1 1 auto; min-width: 0; text-align: start; }
/* S11.62 (HAV2278) -- the interior is three panel-owned slots, not a caller's
   fragment. text-align is stated rather than inherited: the recommendations
   panel shipped its items centred and it reads as deliberate until the
   conforming panel is drawn beside it. The verb slot holds ONE named button
   plus, where a second action genuinely exists, an overflow menu (HAV2176) --
   never a second button and never a third dressed as a link. */
.hav-list-row__finding { display: block; font-family: var(--hav-font-body); font-size: 13px; font-weight: 600; line-height: 1.4; color: var(--hav-text); }
.hav-list-row__support { display: block; font-family: var(--hav-font-body); font-size: 12px; line-height: 1.5; color: var(--hav-text-muted); margin-top: 3px; }
.hav-list-row__support + .hav-list-row__support { margin-top: 2px; }
.hav-list-row__verb { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; }
/* S11.59 (HAV2271) -- the same row on a kiosk. 44 is a pointer's target and 72
   is a thumb's, so the queue row is a modifier rather than a second class: the
   object is identical and only the scale moves. Numbers come from the
   --hav-kiosk-* tokens so no page states one. The verb is always visible here,
   which .hav-list-row already guarantees (HAV2174) -- a hover-only target does
   not exist on this surface. Severity is a leading edge, never a row fill: a
   filled row competes with its own status chip and fails HAV2116 on export. */
.hav-list-row--touch {
  min-height: var(--hav-kiosk-queue-row, 72px);
  gap: 14px; padding: 0 18px;
  border-inline-start: 4px solid transparent;
}
.hav-list-row--touch + .hav-list-row--touch { border-top: 1px solid var(--hav-line); }
.hav-list-row--touch .hav-list-row__verb {
  min-height: var(--hav-kiosk-target, 56px); min-width: 104px;
  display: grid; place-items: center;
}
/* The leading edge takes the role colour; the row keeps its own ground. The row
   gets its OWN modifier and never .hav-status--*: that is the chip's class, and
   pairing it with a row lands the chip's tinted background on the row -- the row
   fill this archetype forbids, competing with the row's own chip and failing
   HAV2116 on export. One role, two renderings, two classes. */
.hav-list-row--sev-error   { border-inline-start-color: var(--hav-error); }
.hav-list-row--sev-warning { border-inline-start-color: var(--hav-warning); }
.hav-list-row--sev-success { border-inline-start-color: var(--hav-success); }
.hav-list-row--sev-info    { border-inline-start-color: var(--hav-info); }
.hav-list-row--sev-none    { border-inline-start-color: transparent; }
/* Under 600 the verb drops below the finding rather than squeezing it to two
   words a line. */
/* The stack is for a narrow POINTER surface. A touch row keeps its geometry at
   every width -- collapsing it trades the 72 thumb target for a block, and a
   kiosk cannot scroll to recover it. */
@media (max-width: 599px) {
  .hav-list-row:not(.hav-list-row--touch) { flex-direction: column; align-items: flex-start; gap: 8px; }
  .hav-list-row:not(.hav-list-row--touch) .hav-list-row__verb { align-self: flex-start; }
}

/* --------------------------------------------------------------------------
   S11.15 -- A tab strip belongs to its panel (HAV2140). Addendum to .hav-tabs
   above: the strip is the panel's TOP EDGE, the underline is the only active
   signal, and a label is never cut mid-word.
   -------------------------------------------------------------------------- */
.hav-tabs .mud-tab.mud-tab-active { background: transparent; }
.hav-tabs .mud-tab { white-space: nowrap; overflow: visible; text-overflow: clip; }
.hav-tabs .mud-tabs-panels { border: 0; background: transparent; }
/* >=900: the set wraps rather than scrolls, so every tab is reachable without
   a press-and-wait on an arrow button. The flex container is the
   tabbar-WRAPPER, not the tabbar -- wrapping the tabbar wrapped nothing. The
   content layer clips vertically (overflow: hidden), so a second row would be
   cut off unless it is released here too. */
@media (min-width: 900px) {
  .hav-tabs .mud-tabs-tabbar,
  .hav-tabs .mud-tabs-tabbar-inner { height: auto; }
  .hav-tabs .mud-tabs-tabbar-content { overflow: visible; height: auto; }
  .hav-tabs .mud-tabs-tabbar-wrapper { flex-wrap: wrap; height: auto; }
  .hav-tabs .mud-tabs-scroll-button { display: none; }
}
/* <900: scroll with full labels + an edge fade. The scroll region is the content
   layer (the wrapper is the row that overflows it). Arrows may assist; they are
   never the only way to reach a tab. Snap lands on the flex child, which is the
   .mud-tooltip-root each tab is wrapped in -- .mud-tab alone is a grandchild. */
@media (max-width: 899px) {
  .hav-tabs .mud-tabs-tabbar-content {
    overflow-x: auto; scroll-snap-type: x proximity; scrollbar-width: none;
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
            mask-image: linear-gradient(to right, #000 calc(100% - 24px), transparent 100%);
  }
  .hav-tabs .mud-tabs-tabbar-content::-webkit-scrollbar { display: none; }
  .hav-tabs .mud-tabs-tabbar-wrapper > .mud-tooltip-root,
  .hav-tabs .mud-tab { scroll-snap-align: start; }
}

/* --------------------------------------------------------------------------
   S11.18 -- The commit bar owns the bottom edge (HAV2143). The bar's rectangle
   is reserved: nothing floats into it, and an action label never truncates.
   .hav-commit is the page-level bar; .hav-panel__footer is the side-panel one
   (S11.10) -- same contract, same height token.
   -------------------------------------------------------------------------- */
.hav-commit {
  --hav-commit-h: 64px;
  position: sticky; bottom: 0; z-index: 3;
  display: flex; align-items: center; gap: 8px;
  min-height: var(--hav-commit-h); padding: 0 16px;
  background: var(--hav-panel); border-top: 1px solid var(--hav-line);
}
.hav-commit__spacer { margin-left: auto; }
/* v1.145.0 -- a wizard's one progress statement, centred in its commit bar (S11.71, HAV2336).
   The bar spans the stepper: inside MudStepper's flex .mud-stepper-actions a child that does
   not claim the width shrink-wraps, so the bar claims it. */
.mud-stepper-actions > .hav-commit, .hav-commit { inline-size: 100%; box-sizing: border-box; }
.hav-commit__progress { flex: 1 1 auto; display: flex; flex-direction: column; align-items: center; gap: 6px; min-width: 0; }
.hav-commit__progress-count { font-family: var(--hav-font-head); text-transform: uppercase; letter-spacing: .06em; font-size: 12px; color: var(--hav-text); white-space: nowrap; }
.hav-commit__progress-rail { display: flex; gap: 4px; inline-size: min(240px, 100%); }
.hav-commit__progress-rail > span { flex: 1; height: 4px; background: var(--hav-line-strong); }
.hav-commit__progress-rail > span[data-done] { background: var(--hav-accent); }
.hav-commit .mud-button-root,
.hav-panel__footer .mud-button-root { white-space: nowrap; flex: 0 0 auto; }
.hav-commit .mud-button-label,
.hav-panel__footer .mud-button-label { overflow: visible; text-overflow: clip; }
/* Scroll-to-top is not a control we ship (S11.18); the header, breadcrumb and
   Home already do it, and a floating circle is HAV2074 besides. Neutralised
   rather than restyled so the markup can be removed on its own schedule. */
.hav-scrolltop,
.mud-fab.hav-scrolltop { display: none; }
/* Snackbars stack ABOVE the bar, never over it. */
.hav-has-commit .mud-snackbar-location-bottom-right,
.hav-has-commit .mud-snackbar-location-bottom-left,
.hav-has-commit .mud-snackbar-location-bottom-center { margin-bottom: calc(var(--hav-commit-h, 64px) + 8px); }

/* --------------------------------------------------------------------------
   R10 S17.8 / HAV2211 -- THE KIOSK SHELL.
   A surface a stranger reads carries no authenticated chrome.

   This is the SHELL, not a content wrapper: it wraps the whole page, because
   the objects it exists to exclude -- side menu, org switcher, global search,
   notification bell, avatar -- are SIBLINGS of a content area, not children of
   one. A class on the content area cannot remove its own siblings.

   It goes on the root <div> of KioskLayout.razor, and the class is the second
   half of the fix, never the first: `@layout KioskLayout` is what stops
   MainLayout rendering. A kiosk page with no @layout directive inherits
   MainLayout and shows the nav tree and the last user's initials to a
   stranger -- a P0 privacy finding, not a styling preference.

   It BYPASSES MudContainer. MudContainer is a centred column with responsive
   gutters; a kiosk is full-bleed at a fixed 1024x768 landscape mounting, and
   its measure comes from the step column inside __body. MudMainContent's
   padding is zeroed the way AuthLayout already zeroes it.

   Two consumers, not one: VisitorCheckInKiosk.razor and TimesheetKiosk.razor.
   -------------------------------------------------------------------------- */
.hav-kiosk {
  /* S17.2's 48 does not apply: this device is mounted at chest height and used
     standing, sometimes in a glove, by somebody who has never seen it before. */
  --hav-kiosk-target:   56px;
  --hav-kiosk-commit-h: 64px;
  --hav-kiosk-queue-row: 72px;  /* HAV2163 -- the triage row, one step above commit */
  --hav-kiosk-body:     15px;
  --hav-kiosk-gap:      12px;
  --hav-kiosk-step:     clamp(28px, 3.4vw, 34px);
  --hav-kiosk-measure:  680px;  /* four fields to a step, read at arm's length */
  position: fixed; inset: 0; z-index: 4;
  display: grid; grid-template-rows: auto 1fr auto;
  background: var(--hav-ground); color: var(--hav-text);
  font-size: var(--hav-kiosk-body);
  overscroll-behavior: contain;
}
/* Head: site name, brand mark, step, idle timer. Nothing else, and no link out
   of the flow -- a link out of a kiosk is a way into the product. */
.hav-kiosk__head {
  display: flex; align-items: center; gap: 16px;
  min-height: var(--hav-kiosk-target); padding: 12px 24px;
  border-bottom: 1px solid var(--hav-line);
}
.hav-kiosk__site {
  font-family: var(--hav-font-head); text-transform: uppercase;
  letter-spacing: .04em; font-size: 18px; color: var(--hav-text);
}
.hav-kiosk__step,
.hav-kiosk__idle {
  font-family: var(--hav-font-mono); font-size: 14px;
  color: var(--hav-text-muted); white-space: nowrap;
}
.hav-kiosk__step { margin-left: auto; }
/* __body is the ONLY scroll region (HAV2189). The step column is measured here
   rather than by a page container, which is why there is no MudContainer. */
.hav-kiosk__body { overflow-y: auto; padding: 32px 24px; }
.hav-kiosk__step-col {
  max-width: var(--hav-kiosk-measure); margin-inline: auto;
  display: grid; gap: 20px;
}
.hav-kiosk__heading {
  font-family: var(--hav-font-head); text-transform: uppercase;
  font-size: var(--hav-kiosk-step); line-height: 1.1; margin: 0;
}
/* The commit is the widest object on the step, bottom-right, and does not
   scroll away. Same contract as .hav-commit at the kiosk's own height. */
.hav-kiosk__commit {
  display: flex; align-items: center; gap: var(--hav-kiosk-gap);
  min-height: var(--hav-kiosk-commit-h); padding: 0 24px;
  border-top: 1px solid var(--hav-line); background: var(--hav-surface-chrome);
}
.hav-kiosk__commit > :last-child { margin-left: auto; }
.hav-kiosk__commit .mud-button-root {
  min-height: var(--hav-kiosk-commit-h); white-space: nowrap; flex: 0 0 auto;
}
/* Every target on a kiosk is 56 and adjacent targets clear each other by 12. */
.hav-kiosk .mud-button-root,
.hav-kiosk .mud-input-control { min-height: var(--hav-kiosk-target); }
.hav-kiosk .mud-input-slot,
.hav-kiosk .mud-input-label { font-size: var(--hav-kiosk-body); }

/* --------------------------------------------------------------------------
   S11.19 -- One fill element per column (HAV2144). A row of peer tiles is as
   tall as its tallest column; each column nominates ONE region to absorb the
   difference. Nominate nothing and the slack falls to the bottom of the tile
   as dead ground -- which is what puts the third tile's footer 250px below the
   other two.

     .hav-tile-row        the peer row (equal height)
     .hav-tile-row--natural   no column can fill -> natural height instead
     .hav-tile            the column
     .hav-fill            the nominated element: takes the leftover, scrolls
     .hav-tile__footer    pinned to the bottom edge, so footers share a baseline
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   S4 / S11.19 (HAV2144) -- THE FIELD GRID.
   The arch-test signature for the two-column form. It is a class and not a
   Spacing value because MudStack Spacing sets ONE value on both axes: a 16/14
   grid is not expressible at any Spacing, and Spacing="3" is 12px, which moves
   the token. Two asymmetric gaps can only live here.
   Folds to one column at the 760 rung, with the rest of the R3 grid.
   -------------------------------------------------------------------------- */
.hav-field-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 16px;
  row-gap: 14px;
}
@media (max-width: 760px) { .hav-field-grid { grid-template-columns: 1fr; } }

/* --------------------------------------------------------------------------
   S5 -- THE ACTION ROW. Applied in 26 Razor files with no rule behind it since
   before v1.42.0, so expander headers, filter bars and detail action rows have
   been rendering as bare stacks. 12px is S5's action gap; 44px is the touch
   floor, which is why the row carries it even on the web (one geometry, both
   surfaces). Adopted at v1.79.0 -- see S11.29 on the third direction.
   -------------------------------------------------------------------------- */
.hav-action-row { display: flex; align-items: center; gap: 12px; min-height: 44px; }

/* --------------------------------------------------------------------------
   S3.1 (HAV2220) -- TRUNCATION. min-width:0 is not optional: a flex child
   without it will not shrink, so the ellipsis never appears and the value
   overflows instead. The gate also requires a title/aria-label on anything
   wearing this -- Blueprint-ArchTests.md already greps for it.
   -------------------------------------------------------------------------- */
.hav-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }

.hav-tile-row {
  display: grid; gap: var(--hav-gap, 16px);
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  align-items: stretch;
}
.hav-tile-row--natural { align-items: start; }
.hav-tile { display: flex; flex-direction: column; min-width: 0; }
.hav-tile > .mud-paper, .hav-tile > .hav-section { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
/* min-height:0 is load-bearing -- without it a flex child refuses to shrink
   below its content and the tile grows instead of the list scrolling. */
.hav-fill { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.hav-tile__footer {
  margin-top: auto; display: flex; align-items: center; gap: 8px;
  padding-top: 12px; border-top: 1px solid var(--hav-line);
}
.hav-tile__count {
  font-family: var(--hav-font-head); text-transform: uppercase; letter-spacing: .05em;
  font-size: 12px; color: var(--hav-text-muted);
}
.hav-tile__footer .hav-tile__count + * { margin-left: auto; }

/* --------------------------------------------------------------------------
   S11.20 -- A timeline's axis is time (HAV2145). Time column, hairline rows,
   a rail that spans node to node, one square mark. Not cards, not discs, not
   a hue per row.
   -------------------------------------------------------------------------- */
.hav-feed { list-style: none; margin: 0; padding: 0; }
/* 88 fits "dd MMM  HH:mm"; --hav-feed-time narrows to 56 under a day header,
   where the entries carry the clock only. The width follows the format. */
.hav-feed { --hav-feed-time: 88px; }
.hav-feed--grouped { --hav-feed-time: 56px; }
.hav-feed--time-120 { --hav-feed-time: 120px; }
.hav-feed__item {
  position: relative; display: grid; grid-template-columns: var(--hav-feed-time) 1fr;
  align-items: start; gap: 12px; padding: 10px 0 10px 20px;
  border-top: 1px solid var(--hav-line);
}
.hav-feed__item:first-child { border-top: 0; }
/* The rail: full height per row, clipped to the mark centre on the first and
   last rows so it never runs off into records that are not there. */
.hav-feed__item::before {
  content: ""; position: absolute; left: 3px; top: 0; bottom: 0;
  width: 1px; background: var(--hav-line);
}
.hav-feed__item:first-child::before { top: 17px; }
.hav-feed__item:last-child::before  { bottom: auto; height: 17px; }
.hav-feed__item:first-child:last-child::before { display: none; }
.hav-feed__mark {
  position: absolute; left: 0; top: 14px; width: 7px; height: 7px;
  border: 1px solid currentColor; background: transparent; color: var(--hav-text-muted);
}
.hav-feed__mark--attention { background: currentColor; color: var(--hav-text); }
.hav-feed__time {
  font-family: var(--hav-font-mono); font-size: 12.5px; line-height: 1.5;
  color: var(--hav-text-muted); white-space: nowrap; padding-top: 1px;
}
.hav-feed__kicker {
  display: block; font-family: var(--hav-font-head); text-transform: uppercase;
  letter-spacing: .05em; font-size: 11.5px; color: var(--hav-text-muted);
}
.hav-feed__text { font-size: 13.5px; line-height: 1.45; color: var(--hav-text); }
.hav-feed__day {
  position: sticky; top: 0; z-index: 1; padding: 8px 0 6px 20px;
  background: var(--hav-panel);
  font-family: var(--hav-font-head); text-transform: uppercase; letter-spacing: .06em;
  font-size: 12px; color: var(--hav-text-label);
}

/* --------------------------------------------------------------------------
   S11.21 -- The scope tree (HAV2146). MudTreeView ships a MudPaper-ish item with
   its own icon slot, its own hover fill and 40px of built-in indent. The tree
   is an INDEX: register geometry, one glyph vocabulary, body-face labels, mono
   counts. Put Class="hav-tree" on the MudTreeView.
   -------------------------------------------------------------------------- */
.hav-tree, .hav-tree.mud-treeview { background: transparent; }
.hav-tree .mud-treeview-item-content {
  min-height: 36px; gap: 8px; padding: 0 8px 0 0; border-radius: 0;
  font-family: var(--hav-font-body); font-size: 14px; color: var(--hav-text);
}
@media (pointer: coarse) { .hav-tree .mud-treeview-item-content { min-height: 44px; } }
.hav-tree .mud-treeview-item-content:hover { background: color-mix(in srgb, var(--hav-accent) 6%, transparent); }
.hav-tree .mud-treeview-item-content:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: -2px; }
/* Indent + guide hairline: the same grammar as disclosure (HAV2139), because a
   tree row and an open panel express the same idea. */
/* v1.121.0: every child group, first level included. The old .mud-treeview .mud-treeview selector
   missed the first-level group, which kept MudBlazor's own indent (~17px, not 20). One hairline
   per open branch is the guide rule of S11.21 -- it is not an elbow connector. Verify the group
   class against the pinned MudBlazor before porting (S11.64). */
.hav-tree .mud-treeview-group { margin: 0 0 0 20px; padding: 0; border-left: 1px solid var(--hav-line); }
/* One glyph vocabulary, in currentColor -- never a folder, a pin and a triangle
   for three levels of one hierarchy (HAV2141). */
.hav-tree .mud-treeview-item-icon .mud-icon-root { color: currentColor; font-size: 17px; }
.hav-tree .mud-treeview-item-arrow .mud-icon-root { color: var(--hav-text-label); }
/* v1.121.0: a leaf keeps its arrow slot, empty, so labels at one depth align. Collapsing it
   pulled leaf labels 16px left of their branch siblings -- indentation lying about depth. */
.hav-tree .mud-treeview-item-arrow { flex: 0 0 16px; width: 16px; }
.hav-tree .mud-treeview-item-arrow:empty { visibility: hidden; }
/* The label is text. Blue is for a link, not for a row. */
.hav-tree .mud-treeview-item-label { color: inherit; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The count is a quantity: mono, muted, right, no chip, no border. Use this, never .hav-count
   (the side-menu marker, which carries a fill) inside a tree. */
.hav-tree__count { margin-left: auto; font-family: var(--hav-font-mono); font-size: 12.5px; color: var(--hav-text-muted); }
/* Selection = the register's row-selected treatment, not a second invention. */
.hav-tree .mud-treeview-item-content.mud-treeview-item-selected {
  background: color-mix(in srgb, var(--hav-accent) 8%, transparent);
  box-shadow: inset 2px 0 0 var(--hav-accent);
}

/* --------------------------------------------------------------------------
   PERMISSION MATRIX (S11.22, HAV2147)
   A hundred settings holding the SAME ordered value are one table, not a
   hundred cards in three levels of accordion. Capability down, roles across
   in scale order; a minimum-role threshold reads as a run -- hollow below it,
   filled from it rightward -- so a column answers "what can this role do?"
   and a row answers "what does this capability need?" on one surface.
   Plain <table class="hav-matrix">; MudDataGrid has no cell-as-radio row.
   -------------------------------------------------------------------------- */
.hav-matrix { width: 100%; border-collapse: collapse; font-family: var(--hav-font-body); }
.hav-matrix thead th {
  position: sticky; top: 0; z-index: 1; background: var(--hav-panel);
  font-family: var(--hav-font-head); text-transform: uppercase;
  font-size: 10.5px; letter-spacing: 0.07em; color: var(--hav-text-muted);
  font-weight: 400; text-align: left; padding: 9px 12px;
  border-bottom: 1px solid var(--hav-line);
}
.hav-matrix__role { width: 64px; text-align: center; }
.hav-matrix tbody td { padding: 0 12px; border-bottom: 1px solid var(--hav-line); }
.hav-matrix tbody tr:hover td { background: color-mix(in srgb, var(--hav-text) 3%, transparent); }
.hav-matrix__cap { font-size: 13px; color: var(--hav-text); min-height: 40px; }
/* Module is a group header ROW, never a second expander (HAV2139). */
.hav-matrix__group td {
  font-family: var(--hav-font-head); text-transform: uppercase;
  font-size: 11px; letter-spacing: 0.06em; color: var(--hav-text);
  padding: 8px 12px; background: color-mix(in srgb, var(--hav-text) 4%, transparent);
}
/* Counts are quantities: mono, muted, no chip, no per-category hue (HAV2141). */
.hav-matrix__count { font-family: var(--hav-font-mono); font-size: 12px; color: var(--hav-text-muted); }
/* The cell is the control -- one row is a radiogroup of ordered cells. */
.hav-matrix__cell {
  all: unset; display: grid; place-items: center; width: 100%; min-height: 40px;
  cursor: pointer; color: var(--hav-text);
}
.hav-matrix__cell:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: -2px; }
.hav-matrix__mark { width: 10px; height: 10px; border: 1.5px solid currentColor; }
.hav-matrix__mark--on { background: currentColor; }
.hav-matrix__mark--off { opacity: 0.35; }
/* S11.58 (HAV2269) -- the correlation cell's four-step mark. The permission
   matrix encodes a threshold, so two steps answer it; a correlation cell
   encodes a magnitude and needs four. The steps are GEOMETRY -- empty, outline,
   half, solid -- because the mark is drawn and never typed (HAV2223: Barlow
   ships no U+25CB/U+25D0/U+25CF, so a typed mark is a fallback font or a
   notdef box), and because the pack this feeds prints in greyscale (HAV2117).
   Drop every fill below and the four steps still read. */
.hav-matrix__mark--empty   { border-color: var(--hav-line-strong); background: none; }
.hav-matrix__mark--outline { background: none; }
.hav-matrix__mark--half    { background: linear-gradient(90deg, currentColor 50%, transparent 50%); }
.hav-matrix__mark--solid   { background: currentColor; }
/* Geometry is the LEVEL; colour is the ROLE, and the role has to be set rather
   than inherited -- .hav-matrix__cell sets color:var(--hav-text), so a mark
   relying on currentColor alone draws every level in body ink. Text-step
   foregrounds (Appendix B), because the mark reads as ink and not as a fill. */
.hav-matrix__mark--role-info    { color: var(--hav-info-text); }
.hav-matrix__mark--role-warning { color: var(--hav-warning-text); }
.hav-matrix__mark--role-error   { color: var(--hav-error-text); }
.hav-matrix__mark--role-none    { color: var(--hav-line-strong); }
/* HAVTO's default is marked IN PLACE -- a notch, not a column restating it. */
.hav-matrix__cell--default { position: relative; }
.hav-matrix__cell--default::after {
  content: ""; position: absolute; bottom: 6px; left: 50%; transform: translateX(-50%);
  width: 14px; height: 1px; background: var(--hav-text-muted);
}
/* The helper sentence appears ONCE, under the header (HAV2133). */
.hav-matrix__help {
  font-size: 11.5px; color: var(--hav-text-muted); padding: 8px 12px;
  border-bottom: 1px solid var(--hav-line);
}
@media (pointer: coarse) { .hav-matrix__cell, .hav-matrix__cap { min-height: 44px; } }

/* --------------------------------------------------------------------------
   NARROW VIEWPORT (S11.24-S11.25, HAV2149-HAV2150)
   The R3 ladder governs the GRID. A page whose grid is already one column
   passes it while spending its whole first screen on preamble. What sheds
   first is the page's explanation of itself -- and the narrow web is the same
   page, never the phone app and never a different page.
   Note .hav-page needs no rule here: its clamp(16px,4vw,40px) already floors
   the gutter, and adding a breakpoint would re-introduce the step (S11.6).
   -------------------------------------------------------------------------- */
/* Guidance is rail content that was never in the rail. The ladder already
   ruled: below the main content, never above it. */
.hav-explain { order: 99; }
@media (max-width: 599px) {
  /* Shed 2 + 3 -- the description moves into the header's help affordance;
     the org line is already in the app bar's switcher, four rows above. */
  .hav-page-head__desc,
  .hav-page-head__context { display: none; }
  /* Shed 1 -- a two-level breadcrumb wraps to two lines to say where the
     reader already is. One back affordance replaces it. */
  .hav-page-head__crumbs > :not(.hav-crumb-back) { display: none; }
}
/* One sticky region below 760: app bar 56 + commit bar 64 + a sticky filter
   bar + a sticky table header leaves ~500px. The commit bar wins (S11.18). */
@media (max-width: 760px) {
  .hav-table thead th,
  .hav-filterbar,
  .hav-tabs .mud-tabs-tabbar { position: static; }
  .hav-panel__footer { position: sticky; bottom: 0; }
}
/* HAV2150 answer 3 -- the values stay, the reason sits under them. */
.hav-readonly-note {
  font-family: var(--hav-font-body); font-size: 11.5px;
  color: var(--hav-text-muted); padding: 8px 12px;
}
/* HAV2150 answer 2 -- the matrix cannot narrow, so it SUBSTITUTES: the
   register form, capability + effective role. Same fallback S11.22 names
   above seven roles. Markup carries .hav-matrix__effective on every row. */
.hav-matrix__effective { display: none; }
@media (max-width: 760px) {
  .hav-matrix thead,
  .hav-matrix__role,
  .hav-matrix__cell { display: none; }
  .hav-matrix,
  .hav-matrix tbody,
  .hav-matrix tr,
  .hav-matrix td { display: block; width: auto; }
  .hav-matrix tr { border-bottom: 1px solid var(--hav-line); }
  .hav-matrix tbody td { border-bottom: 0; padding: 8px 12px; }
  .hav-matrix__cap { min-height: 0; }
  .hav-matrix__effective {
    display: block; font-family: var(--hav-font-mono); font-size: 12px;
    color: var(--hav-text-muted); padding: 0 12px 9px;
  }
}

/* --------------------------------------------------------------------------
   The advisory callout -- a companion utility for S11.26, not a rule of its
   own (no gate). A finding stated in place: a role
   edge, a tinted ground, a heading and one line. It is NOT a MudAlert (Info
   must mean "something changed") and NOT a .hav-section (a section is a
   container; this is a statement).

   Why it exists: the dashboard's *Alignment warnings* block was hand-rolled
   with baked light literals -- a cream fill and a near-black heading -- which
   in dark mode render as a muddy grey band with 2:1 text on it (HAV2065).
   There was no kit primitive to reach for, so one was written. BOTH the fill
   and the foreground are role tokens; nothing here is a literal, and the
   colour is carried by the EDGE, with the fill only lifting the block off
   the ground.
   -------------------------------------------------------------------------- */
.hav-callout {
  border: 1px solid var(--hav-line);
  border-left: 3px solid var(--hav-text-muted);
  background: color-mix(in srgb, var(--hav-text) 4%, transparent);
  padding: 12px 15px;
}
.hav-callout__title {
  display: block; font-family: var(--hav-font-body); font-size: 13px;
  font-weight: 600; color: var(--hav-text);
}
.hav-callout__body {
  display: block; font-family: var(--hav-font-body); font-size: 12.5px;
  line-height: 1.5; color: var(--hav-text-muted); margin-top: 3px;
}
.hav-callout__action { margin-top: 10px; }
/* Role modifiers. The tint is a color-mix of the SAME token that draws the
   edge, so both grounds resolve from one declaration -- there is no light
   value and dark value to keep in step. */
.hav-callout--info    { border-left-color: var(--hav-info);    background: color-mix(in srgb, var(--hav-info) 9%, transparent); }
.hav-callout--warning { border-left-color: var(--hav-warning); background: color-mix(in srgb, var(--hav-warning) 9%, transparent); }
.hav-callout--error   { border-left-color: var(--hav-error);   background: color-mix(in srgb, var(--hav-error) 9%, transparent); }
.hav-callout--success { border-left-color: var(--hav-success); background: color-mix(in srgb, var(--hav-success) 9%, transparent); }
.hav-callout--accent  { border-left-color: var(--hav-accent);  background: color-mix(in srgb, var(--hav-accent) 8%, transparent); }
/* A callout never carries a hue on its TEXT -- the edge is the role, and
   body copy on a 9% tint cannot pass AA in the role's own colour (S11.5). */
.hav-callout .hav-callout__title { color: var(--hav-text); }

/* --------------------------------------------------------------------------
   The count marker -- a utility, not a rule of its own (no gate). A register
   count in the side
   menu, a group total, an "n items" marker: quantities. .hav-status--alert is
   the one saturated fill in the system and it means ACT NOW; spending it on
   "48 incidents" leaves nothing louder for the incident that is actually
   overdue, and paints a healthy register red.
   -------------------------------------------------------------------------- */
.hav-count {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; padding: 1px 6px;
  font-family: var(--hav-font-mono); font-size: 11px; line-height: 1.5;
  color: var(--hav-text-label);
  background: color-mix(in srgb, var(--hav-text) 8%, transparent);
  border-radius: 2px;
}
/* The one case a count MAY take the error role: it counts overdue work, and
   the number is not zero. A zero never carries a signal (HAV2138). */
.hav-count--overdue { color: var(--hav-error-text); background: color-mix(in srgb, var(--hav-error) 14%, transparent); }
.hav-count--zero { color: var(--hav-text-muted); background: color-mix(in srgb, var(--hav-text) 6%, transparent); }

/* --------------------------------------------------------------------------
   §11.69 Boot splash (HAV2309) + §11.70 reconnect banner (HAV2310) -- v1.108.0
   The splash paints before MudThemeProvider runs, so every var resolves to the
   fallback ThemeConfiguration.cs holds; the dark ground is read from
   html[data-hav-boot="dark"], set by hav-boot.js before first paint.
   -------------------------------------------------------------------------- */
.hav-boot { position: fixed; inset: 0; z-index: 2000; display: grid; place-items: center; padding: 24px; box-sizing: border-box; background: var(--hav-ground); }
html[data-hav-boot="dark"] .hav-boot { background: var(--mud-palette-background, #0f172a); }
/* The plate is paper in both themes (§16.4): at boot --hav-panel is its light fallback. */
.hav-boot__plate { width: min(360px, 100%); box-sizing: border-box; padding: 40px 36px; display: flex; flex-direction: column; gap: 20px; background: var(--hav-panel); border: 1px solid var(--hav-line); }
.hav-boot__line { display: flex; justify-content: space-between; gap: 12px; font-family: var(--hav-font-body); font-size: 14px; line-height: 1.4; color: var(--hav-text-muted); }
.hav-boot__line > :last-child { font-variant-numeric: tabular-nums; }
.hav-boot__fail { display: flex; flex-direction: column; align-items: flex-start; gap: 14px; font-family: var(--hav-font-body); font-size: 15px; color: var(--hav-text); }
/* Removal does not depend on the script: the first layout render hides the splash. */
body:has([data-hav-layout]) .hav-boot { display: none; } /* every layout root carries data-hav-layout (§11.69, v1.111.0) -- never a list of layouts */

.hav-reconnect { position: fixed; top: 0; left: 0; right: 0; z-index: calc(var(--mud-zindex-appbar, 1300) + 1); min-height: var(--mud-appbar-height, 64px); box-sizing: border-box; padding: 8px 24px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; background: var(--hav-panel); border-bottom: 1px solid var(--hav-line); color: var(--hav-text); font-family: var(--hav-font-body); font-size: 14px; }
.hav-reconnect[hidden] { display: none; }
/* §11.70: one band at a time -- the in-shell connectivity band is stale while the circuit is down */
body:has(.hav-reconnect:not([hidden])) [data-hav-connectivity] { display: none; }
.hav-reconnect__muted { color: var(--hav-text-muted); }
.hav-reconnect__action { margin-left: auto; }
/* Vendor leak (v1.117.0): MudBlazor.min.css ships
     #components-reconnect-modal button { margin: 40px auto !important; }
   sized for its own modal. The banner mounts inside #components-reconnect-modal, so that rule
   inflates .hav-reconnect to ~128px. Match its ID specificity and !important -- the one sanctioned
   !important in this block, because the vendor rule cannot be beaten any other way (HAV2198). */
#components-reconnect-modal .hav-reconnect__action { margin: 0 0 0 auto !important; border-radius: 0; }
.hav-reconnect__action, .hav-boot__action { font-family: var(--hav-font-head); font-weight: 600; font-size: 14px; text-transform: uppercase; letter-spacing: .04em; padding: 7px 14px; border: 1px solid var(--hav-accent-strong); border-radius: 0; background: transparent; color: var(--hav-accent-strong); cursor: pointer; transition: background-color var(--hav-dur-tint) cubic-bezier(.2,0,.2,1); }
.hav-reconnect__action:hover, .hav-boot__action:hover { background: color-mix(in srgb, var(--hav-accent) 10%, transparent); }
.hav-reconnect__action:focus-visible, .hav-boot__action:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: 2px; }

/* --------------------------------------------------------------------------
   Settings hub rail -- S11.74 (HAV2316). A hub past seven destinations. Our own
   markup (nav + NavLink), no vendor selectors: the rail is not MudTabs.
     <div class="hav-settings">
       <nav class="hav-settings__rail" aria-label="Settings">
         <span class="hav-settings__group">Governance</span>
         <a class="hav-settings__item" href="?tab=audit" aria-current="page">Audit log</a>
       </nav>
       <section class="hav-settings__panel">...</section>
     </div>
   Below 900 the rail is replaced by one .hav-disclose above the panel (S11.74).
   -------------------------------------------------------------------------- */
.hav-settings { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 16px; align-items: start; }
.hav-settings__rail {
  position: sticky; top: calc(var(--mud-appbar-height, 64px) + 12px);
  display: flex; flex-direction: column; padding-bottom: 8px;
  border: 1px solid var(--hav-line); background: var(--mud-palette-surface);
}
.hav-settings__group {
  font-family: var(--hav-font-head); text-transform: uppercase; font-size: 10.5px; letter-spacing: .09em;
  color: var(--hav-text-muted); padding: 12px 12px 4px;
}
.hav-settings__item {
  display: flex; align-items: center; min-height: 36px; padding: 0 12px;
  font-family: var(--hav-font-body); font-size: 13px; color: var(--hav-text); text-decoration: none;
}
.hav-settings__item:hover { color: var(--hav-accent); background: color-mix(in srgb, var(--hav-accent) 5%, transparent); }
.hav-settings__item[aria-current="page"] {
  color: var(--hav-accent); font-weight: 500;
  box-shadow: inset 2px 0 0 var(--hav-accent); background: color-mix(in srgb, var(--hav-accent) 8%, transparent);
}
.hav-settings__item:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: -2px; }
.hav-settings__panel { min-width: 0; }
@media (pointer: coarse) { .hav-settings__item { min-height: 44px; } }
@media (max-width: 899.98px) {
  .hav-settings { grid-template-columns: minmax(0, 1fr); }
  .hav-settings__rail { display: none; }
}
/* (0,3,0): the picker is a .hav-disclose MudExpansionPanels, and .hav-disclose.mud-expansion-panels
   (0,2,0) plus the app's .hav-app .mud-expansion-panels both set display:flex -- a single-class rule
   loses to both and the picker never hides, pushing the rail into the panel column. */
@media (min-width: 900px) {
  .hav-settings .hav-settings__picker.mud-expansion-panels,
  .hav-settings .hav-settings__picker.hav-disclose,
  .hav-settings .hav-settings__picker { display: none; }
}

/* --------------------------------------------------------------------------
   S11.52 -- The notification stream (HAV2216). Adopted from the app in v1.118.0.
   A grouped row is a summary: kicker + latest title + count + chevron; expanding
   reveals the member list. The row control is a plain button, so the family resets
   its chrome and the type scale stays on the feed's own classes.
   Unscoped: the panel renders in a popover portal outside .hav-app.
   -------------------------------------------------------------------------- */
.hav-notif__actions { display: inline-flex; align-items: center; gap: 2px; margin-left: auto; opacity: 0; }
.hav-notif:hover .hav-notif__actions,
.hav-notif:focus-within .hav-notif__actions { opacity: 1; }
@media (hover: none) { .hav-notif__actions { opacity: 1; } } /* touch: no hover, verbs stay reachable */
.hav-notif__content { display: grid; gap: 4px; min-width: 0; }
.hav-notif__head { display: flex; align-items: flex-start; gap: 8px; }
.hav-notif__open {
  flex: 1 1 auto; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-start;
  gap: 8px; min-width: 0; text-align: left; cursor: pointer;
  background: none; border: 0; padding: 0; font: inherit; color: inherit;
}
.hav-notif__open .hav-feed__text { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hav-notif__open:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: 2px; }
.hav-notif__count { font-family: var(--hav-font-mono); font-size: 11.5px; color: var(--hav-text-muted); white-space: nowrap; }
.hav-notif__count::before, .hav-notif__nearest::before { content: " | "; margin-right: 8px; }
.hav-notif__chevron { color: var(--hav-text-label); }
.hav-notif__nearest { font-size: 12px; color: var(--hav-text-label); white-space: nowrap; }
.hav-notif__message { font-size: 13px; line-height: 1.45; color: var(--hav-text-muted); }
.hav-notif .hav-notif-members { list-style: none; margin: 4px 0 0; padding: 0; border-top: 1px solid var(--hav-line); }
.hav-notif-members__item { display: flex; align-items: center; gap: 8px; }
.hav-notif-members__row {
  flex: 1 1 auto; display: flex; flex-wrap: wrap; align-items: baseline;
  gap: 4px 10px; min-width: 0; text-align: left; cursor: pointer;
  background: none; border: 0; padding: 8px 0; font: inherit; color: inherit;
}
.hav-notif-members__row:focus-visible { outline: 2px solid var(--hav-accent); outline-offset: 2px; }
.hav-notif-members__subject { font-size: 13px; color: var(--hav-text); }
.hav-notif-members__date { font-family: var(--hav-font-mono); font-size: 12px; color: var(--hav-text-muted); white-space: nowrap; }
.hav-notif-members__org { font-size: 12px; color: var(--hav-text-label); }
.hav-notif-members__actions { flex: 0 0 auto; display: inline-flex; align-items: center; opacity: 0; }
.hav-notif-members__item:hover .hav-notif-members__actions,
.hav-notif-members__item:focus-within .hav-notif-members__actions { opacity: 1; }
@media (hover: none) { .hav-notif-members__actions { opacity: 1; } } /* touch: no hover, verbs stay reachable */
.hav-notif-members__message { font-size: 12px; color: var(--hav-text-muted); }
/* S9.11 stream sections: "Needs you" / "Earlier" sit above the day headings. */
.hav-feed__day.hav-feed__section { color: var(--hav-text); padding-top: 14px; }
.hav-feed__text--muted { color: var(--hav-text-muted); } /* Earlier group heads read muted */
