﻿/* The application's one global stylesheet, loaded from Pages/_Host.cshtml.

   🔴 W13-11 · IT USED TO BE AN OVERRIDE SHEET AND IS NOW THE ONLY ONE. It was layered LAST over a
   third-party shell's own stylesheet so these rules would win; that shell, its sheet and the packages
   under it are gone, and nothing is beneath this file any more.

   Scope discipline is unchanged and still the point: the palette belongs in Theming/, emitted as
   --biztech-* custom properties by UiThemeTokens, and components belong in Components/Ui/. What is
   left here is only what CSS has to own - the two pieces of host-page chrome that live outside the
   Blazor component tree, and the validation styles Blazor's own form components reference by class
   name. */

/* ── The layer scale · T29 N7 · guidelines §6 ─────────────────────────────────────────────────
   R6.1: never write a raw z-index, use a layer. Before T29 this file held five bare numbers and the
   only way to know what any of them was competing with was to read the comment beside it.

   🔴 DECLARED HERE, IN THIS FILE, and that is load-bearing rather than tidy. The circuit chrome below
   renders when NO COMPONENT HAS EMITTED A THEME - that is the whole condition it exists for - so it
   cannot read a custom property MudBlazor emits, for the same reason R3.1 exempts it from var()
   colours. A `:root` block in a static stylesheet has no such problem: it is applied by the browser
   from the file, with or without a live circuit.

   MudBlazor owns 1100-1600 and we do not enter it (R6.2): Drawer 1100, Popover 1200, AppBar 1300,
   Dialog 1400, Snackbar 1500, Tooltip 1600. Verified as rendered on 2026-08-05 by reading
   --mud-zindex-* off :root in the running app, which agrees with the reflection reading in §6. */

:root {
    /* In-flow content. Declared for completeness - nothing sets it, and nothing should need to. */
    --biztech-layer-base: 0;

    /* Board column headings. 2 rather than 1 because .biztech-board-card:hover applies a transform,
       and a transformed element paints in the same layer as a positioned one - at 1 a hovered card
       would ride over the heading (R6.4). */
    --biztech-layer-sticky: 2;

    /* The board's edge fades. */
    --biztech-layer-scrim: 3;

    /* Controls that must sit above a scrim - the board's two scroll buttons. */
    --biztech-layer-affordance: 4;

    /* 🔴 W0-59 · the navigation drawer as an OVERLAY, below sm only, and its own scrim.

       Two layers rather than reusing `scrim` and `affordance`, because those two are the BOARD's - the
       edge fades and the scroll buttons - and /board is precisely the page where the drawer overlays
       something. At 3 and 4 the drawer would open underneath the fades it is covering, and the scroll
       buttons would ride over the scrim.

       Above both of them and below `skip-link`, which is the ordering that matters: one Tab from a fresh
       load must reach the skip link even with the drawer open, or the first thing a keyboard user meets
       is painted under an overlay they did not ask for. */
    --biztech-layer-drawer-scrim: 5;
    --biztech-layer-drawer: 6;

    /* 🔴 W0-65 · FINDING 1. THE BANNER, PINNED - AND THE ORDERING IS WHY THIS IS A TOKEN.

       ABOVE the drawer and its scrim, because below sm the drawer is `position: fixed` from
       --biztech-layer-drawer and inset below the app bar - so an overlay painted over the banner would
       cover the toggle that opened it, and an operator would have to guess their way out.

       BELOW skip-link for T29 N8's reason, unchanged: one Tab from a fresh load reaches the skip link
       even with the banner pinned over the page.

       7 rather than reusing `drawer`: two elements at one layer paint in tree order, and the banner is
       EARLIER in the DOM than the drawer - so equal layers would put the overlay drawer on top of the
       banner, which is the ordering this token exists to refuse.

       🔴 AND IT MAKES UiMenu.razor.css:9-11's CLAIM TRUE. That sheet already says its popover lives
       "inside the app bar's own stacking context", which was not so while the bar was unpositioned; a
       z-index here is what creates that context. Measured 2026-08-31: a sticky banner paints above a
       z-index: 2 sticky heading beneath it with or without a layer of its own, so this declaration is
       about the order being STATED rather than about today's geometry happening not to overlap. */
    --biztech-layer-appbar: 7;

    /* T29 N8 · the skip link, and it needs a layer of its own rather than `affordance`. It is only
       visible while focused, and at that moment it must clear Studio's app bar (1300) and anything
       MudBlazor has open above it - a tooltip at 1600 is the highest. A skip link painted under the
       chrome it skips is not one. Below the circuit chrome, which outranks everything (R6.3). */
    --biztech-layer-skip-link: 1700;

    /* #blazor-error-ui. See the block below for why this is our value rather than MudBlazor's. */
    --biztech-layer-circuit-error: 1900;

    /* The reconnect modal. ⚠️ NOT what renders: MudBlazor.min.css declares
       `#components-reconnect-modal { z-index: 9999 !important }`, which beats this whatever the
       source order (measured 2026-08-05). This is the floor for a missing MudBlazor stylesheet -
       exactly the degraded state this chrome exists for - and it keeps our own two circuit layers
       ordered correctly with respect to each other. */
    --biztech-layer-circuit-lost: 2000;
}

/* ── The focus ring, once · T29 N6 · R3.10 ────────────────────────────────────────────────────
   WCAG 2.1.1's other half: reaching a control by keyboard is worth nothing if you cannot see where
   you are. This used to live down in the board section, which made it look like a board rule; it is
   an application rule that the board happened to be the first to need.

   🔴 THE SELECTOR LIST IS THE AUDIT. Every element in this application that is interactive and NOT a
   MudBlazor component has to appear here, because MudBlazor rings its own and nothing else will ring
   these. Today that list is exactly three, and adding a fourth custom interactive element without
   adding it here is the bug this consolidation exists to make obvious.

   :focus-visible, not :focus - a mouse click must not leave a ring behind on a board that is on
   screen in front of an audience. `outline`, not `box-shadow` - the card's hover rule already spends
   box-shadow, and an outline cannot be clipped by the column's overflow.

   The colour is safe by construction: Primary is a tested pair against both Background and Surface
   at 4.5:1, stricter than the 3:1 WCAG 1.4.11 asks of an indicator.

   🔴 W0-52 · THE COLOUR MOVED FROM --mud-palette-primary TO --biztech-palette-primary, AND NOTHING
   ELSE IN THIS RULE CHANGED. It is the same value: both projections read BizTechPaletteDefaults, and
   BizTechCssVariablesTests asserts every role matches the record. What moves is WHO EMITS IT.
   --mud-palette-* is written at runtime by MudThemeProvider, which W0-53 unmounts; --biztech-* is
   written by Components/Ui/UiThemeTokens, mounted first in BOTH layouts, which survives it. Left on
   the MudBlazor token, this ring would resolve to nothing on the day that provider goes - and an
   outline with no colour is an outline nobody can see.

   Safe to move today because the token is emitted on every BizTech page already, and because
   AssigneeFormKeyboardTests reads this ring back from COMPUTED STYLE in a real browser: a token that
   failed to resolve fails that test rather than shipping. That test is also the one that proves this
   rule still beats MudBlazor's `button:focus{outline:none}`, which is (0,1,1) against this rule's
   (0,2,0) and stays linked on this document until W0-53.

   🔴 W0-52 ADDS THE SEAM'S CONTROLS TO THE LIST, AND app.css IS WHERE THEY HAVE TO GO. A ring
   declared in a component's own scoped sheet would work - and would be the second declaration this
   rule exists to prevent, invisible to anyone auditing the list. Global rather than scoped is
   R14.1 layer 4's third case, stated as that rule asks: these classes are named by components, but
   the RULE is an application-wide one that no single component owns.

   They must also out-specify MudBlazor's `button:focus{outline:none}`, which is still linked on this
   document until W0-53. A class plus a pseudo-class is (0,2,0) against that rule's (0,1,1), which is
   the same margin by which .biztech-skip-link already wins in a real browser - measured under a real
   Tab keypress on 2026-08-11, and re-measured by UiGalleryKeyboardTests on every seam control. */
.biztech-board-card:focus-visible,   /* the card - a real <button> since T24 round 3 */
.biztech-board:focus-visible,        /* the scroll container - tabindex="0" role="region" */
.biztech-table-scroll:focus-visible, /* W0-69 · the case list's, same three attributes */
.biztech-skip-link:focus-visible,    /* T29 N8 - only ever visible in this state */
.biztech-button:focus-visible,       /* W0-52 · the seam's button */
.biztech-icon-button:focus-visible,  /* W0-52 · the seam's icon button */
.biztech-menu-trigger:focus-visible, /* W0-52 · the disclosure trigger */
.biztech-menu-item:focus-visible,    /* W0-52 · one action in the panel */
.biztech-dialog-cancel:focus-visible,/* W0-52 · the dialog's own two buttons - plain <button>s */
.biztech-dialog-confirm:focus-visible,
.biztech-input:focus-visible,         /* W0-52 · every field the seam renders */
.biztech-choice-control:focus-visible,/* W0-52 · a checkbox or a radio */
.biztech-switch-control:focus-visible,/* W0b-01 · the seam's switch. appearance: none takes the user
                                         agent's own indicator with the rest of its appearance, so this
                                         ring is the only one it has */
.biztech-nav-item:focus-visible,      /* W0-53 · a drawer item. Blazor's NavLink renders an <a>,
                                         so the seam never sees it and cannot ring it */
.biztech-link-icon:focus-visible,     /* W0-53 · the console's open-in-a-new-tab anchor. An <a>, so
                                         it is interactive, is NOT a seam component, and is exactly
                                         the "fourth custom element" the note above is about */
.biztech-link:focus-visible,          /* W0-63 · the console's text link */
.biztech-chrome-page:focus-visible,   /* 🔴 W0-63 · S3. The tenant footer's page links, on EVERY page.
                                         An <a> in TenantChrome.razor.css, so it is the FIFTH of the
                                         custom elements .biztech-link-icon's note names, and this being
                                         a LIST rather than a rule that could find them is why it was
                                         missed - The_focus_ring_is_declared_once reads the list.

                                         🔴 MEASURED UNDER A REAL TAB PRESS, AND THE FIRST
                                         MEASUREMENT WAS WRONG. The planning probe called element.focus()
                                         and read `outline-style: none`, which says nothing:
                                         :focus-visible does not match PROGRAMMATIC focus on an anchor.
                                         Walked to with the keyboard on the same document, with this rule
                                         deleted and then restored: WITHOUT it the link wore
                                         `1px auto rgb(16, 16, 16)` at 1px - the USER AGENT's own ring -
                                         and with it `2px solid rgb(15, 76, 129)` at 2px, identical to
                                         .biztech-nav-item. So the defect is the house ring being absent
                                         and the browser's standing in for it, in a colour no palette
                                         controls - the same class as finding 4 - and NOT the WCAG 2.4.7
                                         failure the plan called it. Fixed either way; recorded honestly. */
.biztech-education-link:focus-visible, /* W7-02 · a content card's link out. An <a>, so interactive
                                          and not a seam component - the same case
                                          .biztech-link-icon above is */
.biztech-education-player:focus-visible, /* W7-02 · the native <video>/<audio> on a card. `controls`
                                            makes the element itself a tab stop, and the browser's
                                            own outline is what R3.11 replaces */
.biztech-tab:focus-visible,            /* 🔴 W0-73 · a tab-row entry. Blazor's NavLink renders
                                          an <a>, so it is interactive without being a seam component
                                          - the same case .biztech-nav-item above is, and found the
                                          same way: UiGalleryKeyboardTests reported six of these
                                          holding focus with "auto 1px", the browser's own outline.
                                          GLOBAL rather than in UiTabs.razor.css, and that is forced -
                                          this list is the ONE declaration R3.10 allows, and a second
                                          one in a scoped sheet is the thing the list exists to
                                          prevent (its own note above says so). */
.biztech-session-chat__new:focus-visible, /* 🔴 W6-44 · the chat log's "N new" pill. A real <button>
                                             (R8.8) and NOT a seam component, so the seam never sees it
                                             and cannot ring it - the same case .biztech-nav-item and
                                             .biztech-link-icon are already here for. It is declared
                                             here rather than in SessionChat.razor.css because app.css
                                             holds exactly ONE ring, shared, and a second one in a
                                             scoped sheet is what
                                             The_focus_ring_is_declared_once_and_covers_every_custom_-
                                             interactive_element exists to catch */
.biztech-session-chat-log:focus-visible,  /* 🔴 W6-44 · the transcript. R12.2 makes a fixed-height
                                             scroll container a tab stop, and a tab stop with no
                                             visible ring is R3.11's failure - a keyboard reader would
                                             have no way to tell they had reached the thing the arrow
                                             keys are about to move */
.biztech-table-row-actionable:focus-visible { /* W0-52 · a row that opens something (MudTable's is
                                                 mouse-only: its rows carry no tabindex at all) */
    outline: 2px solid var(--biztech-palette-primary);
    outline-offset: 2px;
}

/* ── Skip link · T29 N8 · R12.5 ───────────────────────────────────────────────────────────────
   One per layout, and every layout that renders one is ours: MainLayout and AnonymousLayout both
   spell it in markup.

   There was a third case until W0-51, and it is why this rule is GLOBAL rather than scoped. A layout
   shipped by the retired designer shell offered no seam to put a skip link in, so
   wwwroot/js/biztech-appbar.js injected one from script - markup no BizTech component rendered, which
   no scoped stylesheet can reach. That module is deleted and the layout is gone. The rule stays
   global for the reason that survives, which R14.1 layer 4 also allows: on /i/{key} the link sits in
   a layout whose scoped sheet would not reach it either.

   Hidden by CLIPPING rather than display:none or visibility:hidden - both of those remove the element
   from the tab order, which is the one thing it must stay in. This is the standard clip-rect idiom:
   present, focusable, and occupying no space until it is focused. */
.biztech-skip-link {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;

    /* It is a <button> rather than an anchor - see AnonymousLayout.razor for the <base href> trap
       that decided that - so the user-agent button defaults have to come off, the same four the
       board card undoes further down this file. */
    font: inherit;
    cursor: pointer;
    background: none;
    border: none;

    /* Above Studio's app bar (1300) and anything MudBlazor may have open - a tooltip at 1600 is the
       highest. A skip link painted underneath the chrome it skips is not one. */
    z-index: var(--biztech-layer-skip-link);
}

/* Focused: a real, readable control pinned to the top-left corner. The focus ring itself comes from
   the one shared declaration above, not from here. */
.biztech-skip-link:focus-visible {
    width: auto;
    height: auto;
    overflow: visible;
    clip-path: none;

    top: 0.5rem;
    left: 0.5rem;
    padding: 0.5rem 1rem;

    background: var(--biztech-palette-surface);
    color: var(--biztech-palette-primary);
    border: 1px solid var(--biztech-palette-outline);
    border-radius: var(--biztech-radius-default);
    text-decoration: none;
}

/* ── Numeric columns · T29 N5 + N20 · R4.5 ────────────────────────────────────────────────────
   Tabular figures AND a shared right edge, in one class, because either alone buys nothing. The
   point of tabular-nums is that digits line up vertically, which needs the right edge; and a
   right-aligned column of proportional figures still jitters.

   Global rather than scoped: every site is a cell or a caption rendered by a MudBlazor component
   (MudTh, MudTd, MudText, MudChip), which a component-scoped attribute does not reach - R14.1's
   layer-4 case, stated as that rule asks.

   Text columns stay left-aligned. This is for a COLUMN or a vertical STACK of numbers - not for a
   number inside a sentence, which is why ParticipantLinks' "Revoked {ts} by {who}" does not take it. */
.biztech-numeric {
    font-variant-numeric: tabular-nums;
    text-align: right;
}

/* ── Centred section text · W3-09 · F6.18 ─────────────────────────────────────────────────────
   A form section may declare that its own heading and guidance are centred. 21 of the client's 742
   sections do; the other 721 never see this class, and a section declaring nothing renders the exact
   markup it rendered before this card.

   🔴 GLOBAL RATHER THAN SCOPED, AND NOT BY PREFERENCE. One call site is UiText's own <p> - markup
   rendered by a CHILD component, which Blazor's CSS isolation does not give AssigneeForm's scope
   attribute - so a rule in AssigneeForm.razor.css would be rewritten to
   `.biztech-text-center[b-xxxxxxxxxx]` and match nothing at all. R14.1's layer-4 case, stated as that
   rule asks, and the same argument .biztech-numeric records above.

   🔴 NOT IN THE UTILITY LAYER BELOW AND NOT IN UtilityClassTests' TABLE, because MudBlazor 9.0.0
   declares no text-alignment utility at all - measured against its own stylesheet, not assumed - and a
   value column naming a class that does not exist would be a lie in a table whose whole claim is that
   it was measured. `justify-center` and `mt-6` DO exist there, so those two are rows.

   The section's FIELDS keep their own alignment: this is the heading and the guidance, never a label or
   a control. A centred input label is a legibility regression rather than a presentation choice. */
.biztech-text-center {
    text-align: center;
}

/* ── A selected row · W0-52 · R3.12 ───────────────────────────────────────────────────────────
   🔴 A SELECTED ROW, AND A DEFECT FOUND WHILE BUILDING THIS.

   Guidelines R3.12 reads "Selected rows use mud-selected-item", and Inquiries.razor sets exactly
   that through RowClassFunc. Measured 2026-08-12 against MudBlazor 9.0.0's stylesheet: ONE rule in
   the whole 602 KB mentions that class, and it is `:not(.mud-selected-item)` - an exclusion. Nothing
   anywhere styles it. So the selected case in the operator console is painted by nothing today, and
   has been since T16.

   Not fixed here - W0-52 converts no page (that is W0-53's, on the file that sets the class) - but
   the replacement is supplied so that conversion has something to point at, and so the finding is
   written down where the next person meets it.

   Colour AND an edge marker, because R12.7 asks for a second channel: an inset shadow rather than a
   border, so a selected row does not move 2px sideways relative to its neighbours. The tint is
   derived with color-mix (R13.6 already makes it load-bearing) so it follows the palette and dark
   mode instead of adding a role. */
.biztech-selected {
    background: color-mix(in srgb, var(--biztech-palette-primary) 8%, transparent);
    box-shadow: inset 2px 0 0 var(--biztech-palette-primary);
}

/* ── The data table · W0-52 · R9.8, R13.3 ─────────────────────────────────────────────────────
   🔴 GLOBAL RATHER THAN SCOPED, AND R14.1 LAYER 4'S OWN WORDING IS THE REASON: "markup rendered by
   a CHILD component". A table's elements come from three places - the <table>, <thead> and <tr> from
   UiTable, every <th> from UiTh, every <td> from UiTd, and in the hand-written mode the rows from the
   calling page. A scoped sheet stamps only the elements ITS OWN component rendered, so
   UiTable.razor.css would style the table and the rows and reach not one cell. There is no layer
   below this one that can hold a whole table.

   Flat, per W0-52's D1: rules rather than fills, no zebra striping, no shadow. The heading rule is
   Outline (3:1 in both modes, R3.7) and the row rules are OutlineSubtle, so the header separates from
   the body without the body looking like a grid of boxes.

   The row height is the sum of its parts rather than a declared number: 8px + a .875rem line + 8px is
   about 36px, which is R5.7's dense console. A coarse pointer grows the ACTIONABLE ones to 44px in
   the responsive section below - the rows are the only table element anybody taps. */

.biztech-table {
    inline-size: 100%;
    border-collapse: collapse;

    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-body2-size);
    font-weight: var(--biztech-font-body2-weight);
    line-height: var(--biztech-font-body2-line-height);
    letter-spacing: var(--biztech-font-body2-letter-spacing);
    color: var(--biztech-palette-text-primary);
}

/* Subtitle2 is .875rem/600 - the same size as the body text, heavier. A column heading is a label for
   what is under it, not a heading in the page's outline, so it does not take a step up the scale. */
.biztech-th {
    padding: 8px 12px;
    text-align: start;
    vertical-align: bottom;
    color: var(--biztech-palette-text-secondary);
    border-block-end: 1px solid var(--biztech-palette-outline);

    font-size: var(--biztech-font-subtitle2-size);
    font-weight: var(--biztech-font-subtitle2-weight);
    line-height: var(--biztech-font-subtitle2-line-height);
    letter-spacing: var(--biztech-font-subtitle2-letter-spacing);
}

.biztech-td {
    padding: 8px 12px;
    vertical-align: middle;
    border-block-end: 1px solid var(--biztech-palette-outline-subtle);
}

/* 🔴 HOVER ONLY WHERE A CLICK DOES SOMETHING, and that is a deliberate narrowing of what the pages do
   today: all three tables set MudBlazor's Hover="true", including CaseTypes and ParticipantLinks,
   whose rows are not clickable. A row that lights up under the pointer and then ignores it is an
   affordance that lies. UiTable adds this class only when OnRowClick has a handler.

   :not(.biztech-selected) so hovering the selected case does not repaint it grey - the selection tint
   is the more important of the two, and losing it under the pointer is how an operator loses their
   place in the list. */
.biztech-table-row-actionable {
    cursor: pointer;
}

.biztech-table-row-actionable:not(.biztech-selected):hover {
    background: color-mix(in srgb, currentcolor 4%, transparent);
}

/* The empty state stands in for the whole table (see UiTable.razor), so it needs the padding the
   table's own cells would have given it. Colour is inherited from the caller's UiText. */
.biztech-table-empty {
    padding: 8px 12px;
}

/* ── Circuit chrome ───────────────────────────────────────────────────────────────────────────
   Both of these divs are in Pages/_Host.cshtml, outside any component, so no component-scoped
   stylesheet can reach them.

   R5 depends on the reconnect modal being visible: if /_blazor cannot upgrade through Caddy, the page
   renders once and then silently freezes. "Silently" is the part this styling removes - blazor.server.js
   toggles these classes, and if they are invisible the failure looks like an application bug. */

#components-reconnect-modal {
    display: none;
    position: fixed;
    inset: 0;
    z-index: var(--biztech-layer-circuit-lost);
    align-items: center;
    justify-content: center;
    background: rgba(15, 23, 32, 0.55);
}

#components-reconnect-modal.components-reconnect-show,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected {
    display: flex;
}

.biztech-reconnect-dialog {
    background: #fff;

    /* 🔴 W0-71 · ADR 0026 D-D. 6px -> 8px, AND IT STAYS A LITERAL ON PURPOSE.

       R5.5 forbids restating this number anywhere else, and every seam component reads
       var(--biztech-radius-default) instead - which is why D-D needed no stylesheet edit at all and why
       Radius_is_read_from_the_variable_rather_than_restated stayed green through W0-69.

       This block is R3.1's exemption: it renders when the CIRCUIT IS DEAD, which is the one condition
       under which no component has emitted a theme, so a var() here would resolve to NOTHING and the
       dialog would lose its shape at the moment it is the only thing on the screen. Do not "fix" this
       into a variable - that is the failure mode the exemption exists to prevent.

       So the number moves by hand or it does not move. It moved, because leaving one surface in the
       application at Material's 6px - visible only when the circuit dies - bought nothing.
       PortalStyleSheetTests.The_reconnect_dialog_carries_the_current_radius_as_a_literal pins both
       halves: the value, and that it is not a var(). */
    border-radius: 8px;
    padding: 1.5rem 2rem;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
    color: #12181f;
    font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* blazor.server.js sets exactly one of these three classes on the modal; each paragraph shows only
   under its own. Without these rules all three messages would show at once. */
.biztech-reconnect-show,
.biztech-reconnect-failed,
.biztech-reconnect-rejected {
    display: none;
    margin: 0;
}

#components-reconnect-modal.components-reconnect-show .biztech-reconnect-show,
#components-reconnect-modal.components-reconnect-failed .biztech-reconnect-failed,
#components-reconnect-modal.components-reconnect-rejected .biztech-reconnect-rejected {
    display: block;
}

/* T29 · N18. This bar shipped at z-index 1000, which is BELOW ALL SIX of MudBlazor's layers -
   Drawer 1100, Popover 1200, AppBar 1300, Dialog 1400, Snackbar 1500, Tooltip 1600 (MudTheme.ZIndex
   defaults, read off the 9.0.0 assembly by reflection 2026-08-04). So a dialog's scrim covered the
   error bar outright and the snackbar shared its bottom edge.

   🔴 MEASURED IN THE BROWSER 2026-08-05, and it is not what the backlog assumed. MudBlazor.min.css
   ALREADY declares `#blazor-error-ui { z-index: 9999 }` - no !important - and this file loads after
   it at equal specificity, so OUR value is the one that renders. The old 1000 was therefore not a
   value nobody had set: it was actively overriding a correct one and dragging the bar under the
   dialog. Deleting our declaration entirely would work, and it is still wrong - see below.

   1900 rather than nothing, and rather than 9999:

     · Above MudBlazor's whole 1100-1600 band, which is what R14.4 asks for. This element and the
       reconnect modal are "the last line" - what tells an operator the application is dead rather
       than merely slow - and a last line underneath a dialog is not one.
     · BELOW the reconnect modal, which must win when both are showing: a lost circuit is the more
       specific diagnosis and it is the one carrying a Reload button. That ordering is load-bearing
       and cannot be left to the default, because Pages/_Host.cshtml renders this div AFTER the modal
       - so at an equal z-index the LATER element paints on top and the two invert. Inheriting
       MudBlazor's 9999 for both would do exactly that.

   The modal's own layer is MudBlazor's, not ours: it declares
   `#components-reconnect-modal { z-index: 9999 !important }`, which beats this file's 2000 whatever
   the source order. Our 2000 survives as the floor if MudBlazor's stylesheet ever fails to load -
   which is precisely the degraded state this chrome exists for. */
#blazor-error-ui {
    color-scheme: light only;
    background: lightyellow;
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: var(--biztech-layer-circuit-error);
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

/* ── The login screen ─────────────────────────────────────────────────────────────────────────
   GONE, W0-51, and this note is the record rather than a placeholder.

   Three things used to live here or be generated into the host pages for /login. All three worked
   around one fact: the sign-in screen shipped by the retired designer shell was built from RADZEN
   components and so could not read the theme. There was a `.login-page { --rz-* }` projection
   (Theming/RadzenLoginVariables), a rule hiding a duplicated wordmark, and a rule hiding the shell's
   own package version - "3.7.1" - from the product's front door.

   BizTech owns /login now (Components/Pages/Login.razor, on AnonymousLayout), so it takes the
   palette the ordinary way and renders one wordmark and no dependency version because that is what
   it is written to render. Nothing in this stylesheet names Radzen any more, which is one of the
   card's Done-whens rather than a side effect.

   🔴 W13-11 CLOSED THE LAST PART OF THIS. A second sign-in screen used to survive on the island,
   rendering in stock colours, and this note used to end by forbidding a rule that reached in to fix
   it. The island is gone, so there is one sign-in screen and no coupling left to forbid. */

/* 🔴 W0-59 · ONE RULE COMES BACK, AND IT IS R14.1 LAYER 4's DECLARED CASE.

   /login is on SignInLayout since this card, and its submit button is full width - a 72px button
   floating at the left edge of a 24rem panel reads as one option among several, and there is only one.

   IT CANNOT BE A SCOPED RULE and it must not be a Class= on the caller. Blazor's CSS isolation scopes a
   sheet to the elements of ITS OWN component, so SignInLayout.razor.css cannot reach a <button> that
   UiButton rendered - "markup rendered by a CHILD component" is exactly the layer-4 case R14.1 asks you
   to name, and it is the same reason .biztech-field and .biztech-input below are global for this page's
   hand-written password field. And R8.7 accepts Class= for SPACING UTILITIES only; a width is not one.

   Scoped to the panel rather than to the page, so it is the SURFACE that decides - a second button added
   to this panel would be a design question, and a second button anywhere else is unaffected. */
.biztech-signin-panel .biztech-button {
    inline-size: 100%;
}

/* ── Blazor form validation ───────────────────────────────────────────────────────────────────
   Blazor's own EditForm components emit these class names. Kept from the T02 template because T17's
   assignee form needs them; the Bootstrap-shaped rules that came with the template were dropped,
   since MudBlazor replaces Bootstrap entirely and nothing referenced them.

   T24: the three hand-picked hex values these carried are gone. #26b050 was 2.83:1 against white -
   below the 3:1 WCAG 1.4.11 asks of a control's border - and #e50000 was a second, unrelated red
   sitting next to MudBlazor's own. Both now read the theme, so they follow dark mode too. */

.valid.modified:not([type=checkbox]) {
    outline: 1px solid var(--biztech-palette-success);
}

.invalid {
    outline: 1px solid var(--biztech-palette-error);
}

.validation-message {
    color: var(--biztech-palette-error);
}

/* ── T24 follow-up · a restyle of the designer's chrome ──────────────────────────────────────
   GONE, W13-11, and this note is the record rather than a placeholder.

   🔴 TWO RULES LIVED HERE AND BOTH ARE DELETED BECAUSE THEIR SUBJECT IS. They set a right border
   and a header separator on `.mud-drawer` - the nav drawer of the retired designer shell, on its
   own routes. The card that added them was an exception on purpose: design spec §8.3 and T24 §7
   both say a stock designer screen ships as-is, and the client asked for the restyle directly on
   2026-07-31 after reviewing the running app, so it was their reversal to make.

   That drawer no longer renders anywhere. The component library it came from is not referenced by
   any project, `--mud-palette-divider` is written by nothing, and the routes are gone. A rule that
   outlives the thing it was aimed at is I15's confidently-wrong selector in stylesheet form - the
   reason W0-53 gave for deleting two of the original four - so all that is left goes the same way.

   BizTech's own drawer is unaffected and always was: Components/Layout/MainNavMenu.razor.css
   declares its spacing and its seam directly. */

/* ── T16 · the inquiry console ────────────────────────────────────────────────────────────────
   Two rules, and both are about the demo being readable from the back of a room (§9.4 beat 1:
   "watch that link go dead while the others keep working").

   A revoked participant STAYS on the list - F4 returns revoked rows deliberately, because revocation
   is a fact recorded on the row rather than a deletion - so "dead" has to be visible rather than
   inferred from an absence. Colour alone would not carry it, hence the strike-through as well.

   These are global rather than component-scoped because the <tr> this class sits on is rendered by
   ParticipantLinks INSIDE UiTable's ChildContent, and a scoped stylesheet's attribute lands on its
   own component's elements only. (Before W0-53 the same sentence named MudSimpleTable. The substrate
   changed; the reason did not.) */

/* T24 replaced `opacity: 0.6` here with a named colour, and that is a correctness fix rather than a
   tidy-up. Opacity composites EVERY descendant - which is why this rule needed a second one to
   exempt the chip inside it - and it landed the row at 3.28:1 against white, below AA. An opacity
   applied to an inherited colour is also untestable: there is no value to measure. A named colour is
   both.

   🔴 W0-53 MOVED THE TOKEN'S SPELLING AND NOT ITS VALUE. --mud-palette-text-secondary is written at
   runtime by MudThemeProvider, which this card unmounts; --biztech-palette-text-secondary is emitted
   by Components/Ui/UiThemeTokens from the same BizTechPalette. Left on the MudBlazor token, a revoked
   row would lose its colour on the day that provider goes - and keep only the strike-through, which
   is the half R12.7 says cannot carry a state on its own. */
.biztech-participant-revoked {
    color: var(--biztech-palette-text-secondary);
    text-decoration: line-through;
}

/* ── W0-41 · the access rules console ─────────────────────────────────────────────────────────
   A grant that is not currently in effect - revoked, expired, or not open yet - on the by-user view's
   two grant tables.

   🔴 GLOBAL RATHER THAN COMPONENT-SCOPED, FOR THE REASON THE PARTICIPANT RULE ABOVE GIVES AND FOR NO
   OTHER: the class arrives through UiTable's RowClass and lands on a <tr> that UiTable renders, so
   AccessRules.razor's own scoped attribute never reaches it. This is not a case of preferring a global.

   🔴 AND IT IS A SECOND CLASS RATHER THAN A REUSE OF THE ONE ABOVE. Two declarations are duplicated,
   deliberately: that class says "this participant's link is dead", this one says "this grant does not
   currently allow anything", and they are separate facts on separate surfaces that will diverge the
   first time either needs a state the other does not have. Naming one of them after the other is how a
   later reader ends up styling a grant table by editing a rule about participants.

   The strike-through is REINFORCEMENT, never the carrier. R12.2: colour is never the only signal, and
   neither is a line - each row also says "In effect: No" in words, and the resource rows name their
   window state beside it. T24's finding applies here too: `opacity` would composite every descendant
   and lands the row below AA, so this is a named colour that a contrast check can actually measure. */
.biztech-access-withdrawn {
    color: var(--biztech-palette-text-secondary);
    text-decoration: line-through;
}

/* The access key is a 36-character GUID and the link that carries it is the thing an operator reads
   aloud and pastes elsewhere. Give the column room and keep the URL on one line. */

.biztech-participant-link {
    min-width: 26rem;
}

/* W0-53 · the open-in-a-new-tab anchor beside each link.

   An <a>, not a UiIconButton, because it NAVIGATES - see ParticipantLinks' header. That means it
   arrives with none of the seam's icon-button styling, and it cannot borrow it either: the seam's
   .biztech-icon-button lives in UiIconButton.razor.css, which Blazor scopes to that component's own
   elements. So the box is declared here, in the console's own block, matching the 32px small icon
   button it sits next to.

   The focus ring is NOT declared here. app.css has exactly one, shared (N6), and this element is on
   its selector list. */
.biztech-link-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 32px;
    min-block-size: 32px;
    border-radius: var(--biztech-radius-default);
    color: var(--biztech-palette-text-secondary);
    text-decoration: none;
}

.biztech-link-icon:hover {
    color: var(--biztech-palette-primary);
    background: color-mix(in srgb, currentcolor 8%, transparent);
}

/* 🔴 W0-63 · THE CONSOLE'S TEXT LINK, AND IT IS GLOBAL FOR A MEASURED REASON.

   This was `.biztech-form-open` in FormTemplates.razor.css, and it reached NOTHING. The class sits on a
   Blazor NavLink, and Blazor stamps its scope attribute only on the elements a component renders
   ITSELF - so the rule compiled to `.biztech-form-open[b-h5zhbzlkmi]` and the <a> that NavLink renders
   carried no such attribute. Measured in a browser 2026-08-20: `scopedAttr: []`, and the link painted
   rgb(0, 0, 238) light / rgb(158, 158, 255) dark, UNDERLINED - the user agent's own link, in a colour no
   palette controls. MainNavMenu.razor.css records the identical trap and answers it with ::deep; a GLOBAL
   class needs neither, and R14.1 layer 4 already holds two anchors here for the same reason - a text link
   is an application rule no single component owns.

   It also read `--biztech-primary`, which is declared nowhere: every palette token is
   `--biztech-palette-<role>`. Two bugs in three lines, and the token half is now swept by
   TokenSufficiencyTests.Every_biztech_variable_read_has_a_declaration.

   Underlined on HOVER rather than at rest, which is the affordance FormTemplates.razor.css asked for and
   never got. The focus ring is not declared here: app.css has exactly one, shared (N6), and this element
   is on its selector list above. */
.biztech-link {
    color: var(--biztech-palette-primary);
    text-decoration: none;
}

.biztech-link:hover {
    text-decoration: underline;
}

/* ── T18 · the case board ─────────────────────────────────────────────────────────────────────
   §8.2.4's centrepiece: business stages as columns, read from the back of a room.

   Global rather than component-scoped, and that is forced rather than stylistic. The flex
   container's children are rendered by a CHILD component (BoardCard), and a scoped stylesheet's
   generated attribute lands on its own component's elements only - so a scoped rule here would
   style the column shells and miss every card inside them. Same reason T16's revoked-row rule is
   global. */

/* ROUND 3 · the frame the board scrolls inside. It exists so the edge fades and the two scroll
   buttons can sit against the board's edges without scrolling away with the columns - an ::after on
   the scroller itself is laid out inside the scrolled content and travels with it.

   The three data-* attributes are written by wwwroot/js/biztech-board.js on scroll and on resize.
   Everything below is keyed off them, and that is why this needs script at all: a fade at the left
   edge while the board is ALREADY at its left edge advertises content that is not there, and which
   edge you are against is only knowable at runtime. Absent the module - if it fails to load - no
   attribute is set, so no fade and no button renders, and the board is exactly what it was. */
.biztech-board-viewport {
    position: relative;
}

/* The fades. Both are suppressed at their own edge and when there is nothing to scroll, which is the
   whole point of the runtime flags. pointer-events: none so they never eat a click on a card. */
.biztech-board-viewport::before,
.biztech-board-viewport::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: 2.5rem;
    pointer-events: none;
    z-index: var(--biztech-layer-scrim);
}

.biztech-board-viewport::before {
    left: 0;
    background: linear-gradient(to right, var(--biztech-palette-background), transparent);
}

.biztech-board-viewport::after {
    right: 0;
    background: linear-gradient(to left, var(--biztech-palette-background), transparent);
}

.biztech-board-viewport:not([data-scrollable])::before,
.biztech-board-viewport:not([data-scrollable])::after,
.biztech-board-viewport[data-at-start]::before,
.biztech-board-viewport[data-at-end]::after {
    display: none;
}

/* The buttons, in the vertical middle of the board's own height rather than the page's. Hidden by
   default and revealed only once the module says the board scrolls - so a two-column board, or a
   board on a phone, shows nothing. */
.biztech-board-scroll {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: var(--biztech-layer-affordance);
    display: none;
}

.biztech-board-viewport[data-scrollable] .biztech-board-scroll {
    display: block;
}

.biztech-board-viewport[data-at-start] .biztech-board-scroll-start,
.biztech-board-viewport[data-at-end] .biztech-board-scroll-end {
    display: none;
}

.biztech-board-scroll-start {
    left: -0.5rem;
}

.biztech-board-scroll-end {
    right: -0.5rem;
}

/* W0-53 · the button inside the wrapper above, which was a filled MudIconButton.

   Hand-written rather than UiIconButton because these two are aria-hidden and tabindex="-1" by
   design - a mouse-only duplicate of a keyboard path that already exists - and the seam's icon
   button requires an accessible name, correctly. See Board.razor for the whole reasoning.

   Filled rather than bordered, which is what MudBlazor's Variant.Filled rendered: this sits ON the
   board's own tinted columns and has to read as an affordance floating above them rather than as a
   column edge. No focus ring here and none needed - it cannot be focused. */
.biztech-board-scroll-button {
    display: flex;
    align-items: center;
    justify-content: center;
    inline-size: 32px;
    block-size: 32px;
    padding: 0;

    border: 1px solid var(--biztech-palette-outline);
    border-radius: 999px;
    background: var(--biztech-palette-surface);
    color: var(--biztech-palette-text-primary);
    box-shadow: var(--biztech-shadow-raised);
    cursor: pointer;
}

.biztech-board {
    display: flex;
    gap: 0.75rem;
    overflow-x: auto;

    /* Pairs with the buttons: a click moves exactly one column, and scroll-behavior is what makes
       that read as a move rather than a jump. biztech-board.js overrides it to 'auto' under
       prefers-reduced-motion (WCAG 2.3.3), which is why it is set on the element rather than
       globally - a media query here could not reach the scrollBy call. */
    scroll-behavior: smooth;

    /* Columns STRETCH to a common height. This reverses the original "hang from the top", and the
       reason it changed is that columns are now panels: an empty panel two thirds the height of its
       neighbour reads as broken, where an empty column with no panel simply read as empty. The
       concern that drove the original - a column with eleven cards leaving ten columns of
       whitespace - is handled by min-height on the column instead, which sets a floor rather than
       matching the tallest. */
    align-items: stretch;
    padding-bottom: 0.75rem;

    /* ROUND 3. The board was 204px tall in a 791px viewport with 299px blank beneath it - it used a
       quarter of the screen it had, and every column was a stub.

       min-height on the CONTAINER rather than height on the columns, because align-items: stretch
       above already makes the columns match the flex line. One declaration therefore sets all nine,
       and columns still grow past it when a stage fills up.

       min() and not a flat 60vh: on a tall monitor 60vh is a 900px column of mostly nothing. 34rem
       is the point past which more height stops buying more cards on screen. */
    min-height: min(60vh, 34rem);
}

/* A FIXED width, not a flex basis. Columns must not reflow as counts change: the demo advances a
   case while the board is on screen (§8.2), and a layout that reshuffled on every Refresh would
   make the audience lose the column they were watching.

   The panel is T24's follow-up. Measured 2026-07-31: border-width computed 0px, so columns had NO
   boundary of any kind - and with the count chip right-aligned 16px from the NEXT column's title
   (and 159-200px from its own), the header row read as "Not started | 0 Saved | 1 Assigned". Every
   count appeared to belong to the following column, the last column's count looked orphaned, and a
   card sitting correctly inside its column looked displaced. The panel and the header change in
   Board.razor fix that together. */
.biztech-board-column {
    flex: 0 0 17rem;
    min-width: 17rem;

    /* A tint DERIVED from the palette rather than a new role. color-mix keeps BizTechPalette at 22
       roles and still follows dark mode, where a hard-coded tint would not - and reaching for
       MudBlazor's own --mud-palette-background-gray would reintroduce exactly what T24 removed: a
       token nothing in this repository sets. Cards are `surface`, so they lift off this; the page
       is `background`, so the panel's edge reads against it. */
    background: color-mix(in srgb, var(--biztech-palette-outline) 9%, var(--biztech-palette-background));

    /* lines-default (the Outline role), NOT divider. T24 §2.3 reserves the 3:1-tested token for
       boundaries something depends on seeing, and this is precisely the boundary that was missing. */
    border: 1px solid var(--biztech-palette-outline);
    border-radius: var(--biztech-radius-default);

    /* T29 N22. Was 0.625rem - 10px, neither a 4px-scale multiple nor a named constant. Rounded up to
       12px rather than down to 8px: the column is a fixed 17rem and the cards inside are width:100%,
       so this trades 4px of card width for a panel edge that reads as deliberate. Re-measured on the
       running board after the change. */
    padding: 0.75rem;

    /* A floor, so an empty column still reads as a column. Kept for the phone-stacked case below,
       where the container's min-height is switched off. */
    min-height: 10rem;

    /* ROUND 3. A ceiling and a scrollbar, so a full stage scrolls INSIDE its column instead of
       stretching the board and pushing the other eight columns' headers off the top of the screen.
       16rem is the measured chrome above the board - page heading, filter panel, gutters - plus a
       little breathing room at the bottom.

       🔴 W0-65 · RE-MEASURED 2026-08-31 AGAINST THE PINNED BANNER, AND IT DOES NOT MOVE.

       Measured on the running stack at 1440x900 with .biztech-appbar now `position: sticky`: the
       column's top at 253.94, so the ceiling of 100vh - 16rem = 644px puts its bottom at 897.94 in a
       900px viewport - 2.06px of margin, which is the "little breathing room" this comment already
       claimed. The arithmetic: 64 (banner) + 16 (UiContainer's block padding) = 80, which is where the
       h1 starts, + 173.94 of heading block and filter panel = 253.94.

       AND THE REASON A STICKY BANNER CHANGED NOTHING, which is worth stating because the card expected
       it to: the banner has ALWAYS occupied 64px above the board at scroll 0, because grid row one
       always reserved its height. `sticky` changes what happens when the page SCROLLS, and at >= sm
       /board does not scroll - measured scrollHeight 900 against clientHeight 900, because the columns
       scroll inside themselves, which is what this ceiling exists to make them do.

       Below sm the media block at the end of this file sets `max-height: none` - measured "none" at 375,
       where the columns stack and the page scrolls (scrollHeight 4226). So this constant is a >= sm
       rule, and PortalStyleSheetTests.LayoutConstants keeps its row unchanged. */
    max-height: calc(100vh - 16rem);
    overflow-y: auto;
}

/* The column's own header: label and count together, with a rule under them. Pairs with the
   left-packed flex in Board.razor - the CSS cannot fix the binding on its own, because what bound
   the count to the wrong label was justify-space-between in the markup.

   ROUND 3: sticky, now that the column scrolls. A column of fifteen cards whose heading has scrolled
   away is a list of cases belonging to nothing.

   `background: inherit` picks up the column's color-mix tint, so cards pass BEHIND the heading
   rather than through it. The `sticky` layer is 2 rather than 1 because .biztech-board-card:hover
   applies a transform, and a transformed element paints in the same layer as a positioned one - at
   1 a hovered card would ride over the heading. See the layer scale at the top of this file. */
.biztech-board-column-header {
    border-bottom: 1px solid var(--biztech-palette-outline-subtle);
    padding-bottom: 0.5rem;

    position: sticky;
    top: 0;
    z-index: var(--biztech-layer-sticky);
    background: inherit;
}

/* The whole card is the click target (one target, no nested links to mis-hit on stage), so it has
   to look like one.

   ROUND 3: the card is now a real <button> (see BoardCard.razor for why). The first four
   declarations undo the user-agent button defaults a bare <button> starts with - the centred 13px
   Arial and the shrink-to-fit width.

   🔴 W0-53 · THE SURFACE MOVED HERE FROM MUDBLAZOR'S CLASSES, and it had nowhere else to go. The card
   carried `mud-paper mud-paper-outlined` by hand, which supplied background, border, radius and ink;
   the seam's equivalent is UiPaper, and UiPaper's class is declared in ITS OWN scoped sheet, which
   cannot reach an element UiPaper did not render. So the four declarations are written out here, with
   the same tokens and the same values UiPaper uses - R14.1 layer 4's third case, and the alternative
   was a component rendering a card it has no other business inside. */
.biztech-board-card {
    display: block;
    width: 100%;
    font: inherit;
    text-align: inherit;

    background: var(--biztech-palette-surface);
    border: 1px solid var(--biztech-palette-outline);
    border-radius: var(--biztech-radius-default);
    color: var(--biztech-palette-text-primary);

    cursor: pointer;
    transition: box-shadow 120ms ease-in-out, transform 120ms ease-in-out;
}

/* The one elevation this application spends, and the token moved with the rest: --mud-elevation-8 is
   MudThemeProvider's to emit and this card unmounts it. --biztech-shadow-raised is the same value
   from UiThemeTokens. */
.biztech-board-card:hover {
    transform: translateY(-1px);
    box-shadow: var(--biztech-shadow-raised);
}

/* Round 3. "No cases", centred in the space the column now has rather than tucked under the
   heading - at 34rem tall an empty column is mostly empty space, and a caption clinging to the top
   of it reads as a loading state that stalled. */
.biztech-board-empty {
    display: block;
    text-align: center;
    padding-top: 2rem;
}

/* A stage key no case type declares (§6.4, T21). The heading shows the raw key, and this is what
   stops that reading as a deliberate label.

   Keeps its opacity where the revoked row lost one, and the difference is the point: this is a
   WEIGHT applied to an already-legible ink, not a way of expressing a colour. It measures 7.19:1
   light and 8.20:1 dark. */
.biztech-board-undeclared {
    font-style: italic;
    opacity: 0.75;
}

/* T26's session badge MOVED OUT of this file, T29 N17 -> Components/Pages/AssigneeForm.razor.css.
   It styles a <span> in that page's OWN markup, so a component-scoped sheet reaches it and R14.1
   says to use the highest layer that can. This file is for host-page chrome, third-party classes,
   and markup rendered by a CHILD component - the badge was none of those.

   Left as a signpost rather than deleted silently, because `.biztech-session-badge` is the kind of
   global class name someone greps for. */

/* ── T24 · responsive ─────────────────────────────────────────────────────────────────────────
   Before T24 this file had ZERO @media queries: responsive behaviour was entirely MudBlazor's grid.
   That is still where the structural work belongs - the pages already use xs/sm/md - and these four
   blocks only cover what the grid cannot reach, because they are about rules CSS owns outright.

   The breakpoints are MudBlazor's own (sm 600px, md 960px), so a page never disagrees with its grid
   about where a layout changes. */

/* 1. The board on a phone. Eight side-by-side columns is not a phone layout at any width, so below
      sm the row becomes a stack - honest rather than a compromise.

      At 600px AND ABOVE the fixed 17rem non-reflowing column is UNCHANGED, and that is load-bearing:
      the demo advances a case while the board is on screen, and a layout that reflowed on every
      Refresh would make the audience lose the column they were watching. The demo is presented on a
      laptop. */
@media (max-width: 599.98px) {
    .biztech-board {
        flex-direction: column;
        overflow-x: visible;

        /* Round 3's viewport-filling height is a SIDE-BY-SIDE idea. Stacked, it would only pad the
           first column out to 60vh and push the other eight below the fold. */
        min-height: 0;
    }

    .biztech-board-column {
        flex: 1 1 auto;
        min-width: 0;

        /* 🔴 W0-71 · R13.8, AND THE NUMBERS ARE MEASURED RATHER THAN REASONED.
           Found by BoardViewportTests on its first run - the suite paid for itself the same way
           InquiriesViewportTests did for W0-69, and on the other of the two pages W0-51 shipped this on.

           There is no universal border-box reset in this stylesheet, so `width: 100%` here resolved
           against the CONTENT box and the padding and border were then added OUTSIDE it. Measured in a
           browser at 375px, in both modes:

               #biztech-main          left=0   right=375  width=375
               .biztech-container     left=0   right=375  width=375   padding 16/16
               .biztech-board         left=16  right=359  width=343
               .biztech-board-column  left=16  right=385  width=369   <- 343 + 12+12 + 1+1

           So every stacked column laid out 10px past the landmark, and .biztech-main's
           `overflow-x: hidden` CLIPPED that rather than scrolling it - R13.8's own defect, in the
           stacked phone layout nobody had opened in a browser.

           SCOPED TO THIS MEDIA BLOCK ON PURPOSE. Outside it the column is `flex: 0 0 17rem` with
           `min-width: 17rem`, and 17rem is a MEASURED value - the comments on .biztech-board record
           re-measurements of the board against it. Making the base rule border-box would silently
           narrow every desktop column by 26px and move geometry W0-63 and W0-65 both measured. The
           declaration that needs border-box is `width: 100%`, and it only exists here. */
        box-sizing: border-box;
        width: 100%;

        /* Round 3's per-column scroller comes off too. Stacked, each column would become its own
           little scroll box inside the page's scroll - nine nested scrollers on a touch screen,
           which is worse than the long page it replaces. */
        max-height: none;
        overflow-y: visible;
    }

    /* And with no scroll container to stick inside, `top: 0` would pin every heading to the VIEWPORT
       instead - nine headings stacking up at the top of the screen as you scroll past them. */
    .biztech-board-column-header {
        position: static;
    }
}

/* 2. The participant link column. 26rem is 416px inside a MudSimpleTable, which forces horizontal
      scroll on any phone. The width is justified on a desktop - an operator reads a 36-character
      GUID aloud and pastes it elsewhere, so it must not wrap mid-token - but it needs a narrow-screen
      escape, and below md wrapping the URL beats scrolling the table. */
@media (max-width: 959.98px) {
    .biztech-participant-link {
        min-width: 0;
        overflow-wrap: anywhere;
    }
}

/* 3. Touch targets on /i/{key}, and the reason this whole step is small.
      Size.Small is the house style, and it is RIGHT for the operator console - an inquiry list is read
      at a desk with a mouse. It is wrong for /i/{key}, the one page an outsider opens on a phone to
      sign with a finger.

      🔴 W0-59 RE-AIMED THIS BLOCK, AND IT HAD MATCHED NOTHING SINCE W0-53. Every selector here named a
      MudBlazor class - .mud-button-root, .mud-icon-button, .mud-input-control .mud-input-slot,
      .mud-select .mud-input-slot, .mud-checkbox > .mud-button-root, .mud-radio > .mud-button-root -
      and that card converted /i/{key} to the seam. So R5.8's 44px minimum has applied to no element on
      that page since it merged.

      Nothing caught it, and the two near misses are worth naming: TokenSufficiencyTests refuses a
      --mud-* VARIABLE rather than a rule that names a MudBlazor CLASS, and
      The_responsive_rules_are_present only asked whether `.biztech-anon` appeared inside the query -
      which it did, as the SCOPE of every dead selector. It is I15's confidently-wrong affordance in
      stylesheet form, and it is the same class of finding W0-53 deleted two rules for.

      Keyed on (pointer: coarse) rather than a width, unchanged: a touch laptop needs this and a narrow
      desktop window does not. Scoped to .biztech-anon so the console is untouched. */
@media (pointer: coarse) {
    .biztech-anon .biztech-input,
    .biztech-anon .biztech-icon-button {
        min-block-size: 44px;
    }

    /* 🔴 W3-26 · THE SIZE CLASS HAS TO BE NAMED HERE, AND R14.1a IS THE REASON.

       This read `.biztech-anon .biztech-button` and reached no button on this page. UiButton.razor.css
       declares `.biztech-button-small { min-block-size: 32px }` and `.biztech-button-medium
       { min-block-size: 40px }` - the SAME property this rule sets. Scoped CSS adds a [b-xxxxx]
       attribute to the last compound only, so those are (0,2,0), exactly the weight of the selector
       above them; and _Host.cshtml links BizTech.Portal.styles.css AFTER app.css by decision, so at
       equal specificity the later sheet won. Naming the size class makes this (0,3,0) and settles it.

       Measured in a browser on 2026-08-18 under an emulated coarse pointer, at 375 and at 1440: the
       signature pad's Clear button and the form's "Show me" both 35px tall against R5.8's 44px.

       🔴 WHY THE ICON BUTTON WAS FINE AND THIS WAS NOT, because the difference is worth knowing.
       UiIconButton.razor.css declares `block-size: 32px`, a DIFFERENT property - and a min-* floor
       beats a fixed size whatever the cascade says, so that one never entered the fight. One seam
       component chose `min-block-size` and the other chose `block-size`, and only one of them silently
       lost. The same is true of `.biztech-input` above, which no scoped sheet floors.

       🔴 AND THIS IS W0-59's FINDING ONE LEVEL DOWN. That card found this block naming MudBlazor
       classes and therefore matching nothing, and added
       PortalStyleSheetTests.The_touch_target_block_reaches_the_seams_controls to stop it recurring.
       That test asks whether the SELECTOR is present. It was present, and still reached nothing - a
       substring in a stylesheet cannot see the cascade. Only a browser can, which is why
       AssigneeFormViewportTests.Every_tap_target_meets_the_touch_minimum measures the rendered box. */
    .biztech-anon .biztech-button.biztech-button-small,
    .biztech-anon .biztech-button.biztech-button-medium {
        min-block-size: 44px;
    }

    .biztech-anon .biztech-icon-button {
        min-inline-size: 44px;
    }

    /* The checkbox and radio hit areas are the label's, not the 20px glyph's - which is why this names
       .biztech-choice (the row) rather than .biztech-choice-control (the box). */
    .biztech-anon .biztech-choice {
        min-block-size: 44px;
    }
}

/* 4. The anonymous panel's padding. 2rem each side plus the main's 1rem is 96px of a 375px screen -
      more than a quarter of it spent on whitespace, on the page with the most to read. */
@media (max-width: 599.98px) {
    .biztech-anon-panel {
        padding: 1rem;
    }

    .biztech-anon-main {
        padding: 1rem 0.5rem;
    }
}

/* 5. The table on a phone · W0-52 · R9.8, R13.3 · "table -> DataLabel cards".
      Five columns of case data do not fit 375px, and a horizontally scrolling table is a table
      nobody scrolls: the columns past the fold are simply not read. So below sm each row becomes a
      card whose cells each carry their own heading, rendered from the data- attribute UiTd emits.

      🔴 THIS IS WHY UiTable, UiTh AND UiTd RESTATE THEIR ARIA ROLES. Changing `display` on a table
      element strips its implicit role in Chrome and Safari - the elements stay, the TABLE goes, and
      a phone user gets five unrelated lines where a desktop user gets an announced row. The explicit
      roles survive the display change and say exactly what the elements already said at every other
      width. Nothing in this block is safe without them.

      Scoped to .biztech-table-reflow, which UiTable puts only on the templated mode. A hand-written
      table's cells carry no labels to reflow with, so restructuring one would produce a stack of
      unlabelled values - worse than the wrapping it does instead. */
@media (max-width: 599.98px) {
    /* The whole table becomes blocks, tbody included: leaving the row group as `table-row-group`
       around block rows makes the browser rebuild anonymous table boxes and the reflow half-applies. */
    .biztech-table-reflow,
    .biztech-table-reflow tbody {
        display: block;
    }

    /* The headings are not deleted - each cell is about to render its own copy, but the header row is
       still what a screen reader reads, and display:none would take it out of the accessibility tree.
       Same clip idiom as the skip link, for the same reason: present, occupying no space.

       🔴 inset-inline-start IS LOAD-BEARING, AND ITS ABSENCE TURNED dev RED ON LINUX FOR FIVE MERGES.
       The skip link's idiom is safe there because a <button> is a LEAF: its label is a text node, so
       nothing inside it has a box of its own. A <thead> is a SUBTREE. `overflow: hidden` clips what is
       PAINTED and changes nothing about layout, so the <tr> and <th> inside kept their full geometry -
       and `white-space: nowrap` laid that geometry out as one unwrapped line.

       PortalDrawerTests.No_content_is_clipped_by_the_main_landmark walks every element under the
       landmark, so it saw those cells. Whether it FAILED came down to the font: the same header
       measured 418px against a 375px limit on a GitHub Ubuntu runner and 358px on Windows. CI was red
       and every developer's machine was green, which is the worst shape a failure can take.

       Moving the box off the inline start takes the whole subtree out of the landmark's geometry
       instead of merely painting over it. Logical rather than `left`, so RTL moves it the other way.
       The clip stays: it is what keeps the 1px box itself from being a visible speck. */
    .biztech-table-reflow .biztech-table-head {
        position: absolute;
        inset-inline-start: -10000px;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .biztech-table-reflow .biztech-table-row {
        display: block;
        margin-block-end: 8px;
        padding: 8px;
        border: 1px solid var(--biztech-palette-outline);
        border-radius: var(--biztech-radius-default);
    }

    /* The row is the card now, so its cells give up the rules and the horizontal padding they had as
       columns. Alignment resets too, and .biztech-numeric's own comment is the argument: a shared
       right edge is only worth something against a COLUMN of figures, and stacked there is no column.
       The tabular figures stay; the right edge goes. */
    .biztech-table-reflow .biztech-td {
        display: block;
        padding-inline: 0;
        border-block-end: 0;
        text-align: start;
    }

    /* The heading, repeated per cell. Caption size and secondary ink, so the label reads as the
       smaller of the two lines and the value is the thing seen first. */
    .biztech-table-reflow .biztech-td[data-label]::before {
        content: attr(data-label);
        display: block;
        color: var(--biztech-palette-text-secondary);
        font-size: var(--biztech-font-caption-size);
        font-weight: var(--biztech-font-caption-weight);
        line-height: var(--biztech-font-caption-line-height);
        letter-spacing: var(--biztech-font-caption-letter-spacing);
    }
}

/* 6. Rows you can tap · W0-52 · R5.8.
      Block 3 exempts the operator console from the 44px minimum by decision - an inquiry list is read
      at a desk with a mouse. A CLICKABLE ROW is the exception to that exception: it is the only
      target in a table, it spans the width of the screen, and on a touch laptop or a tablet it is hit
      with a finger. 12px + a .875rem line + 12px is 44px, reached with padding so no height is
      declared and nothing is forced taller than its content on a mouse.

      Keyed on the pointer rather than the width, like block 3, and it applies at every width for the
      same reason: the input device decides this, not the viewport. */
@media (pointer: coarse) {
    .biztech-table-row-actionable .biztech-td {
        padding-block: 12px;
    }
}

/* 7. The app bar's developer switch on a phone · W0b-01.
      The app bar is one nowrap flex row - menu button, wordmark, spacer, theme toggle, developer switch,
      account menu - and six items plus five 12px gaps plus 24px of inline padding do not fit a 375px
      viewport once the switch carries a visible caption. Something has to give, and the caption is the
      only part of the control that is not the control.

      🔴 CLIPPED, NOT display: none. The <label> IS the switch's accessible name - UiSwitch takes no
      AriaLabel precisely because it has a visible one - so removing it from the accessibility tree would
      leave a switch that announces nothing at all. Worse than a switch with no caption, and invisible to
      every test in this repository. The clip-rect idiom is .biztech-skip-link's and
      .biztech-table-reflow's heading row's, for exactly this reason: present and readable by assistive
      technology, occupying no space.

      SCOPED TO THE APP BAR. UiSwitch is a seam control - /ui renders one, and a form built by a later
      card may - so a global rule would take the caption off a phone form where there is room for it and
      no reason to.

      Same breakpoint as blocks 1, 4 and 5, so this file never disagrees with itself about where the
      layout changes. */
@media (max-width: 599.98px) {
    .biztech-appbar .biztech-switch-label {
        position: absolute;
        inline-size: 1px;
        block-size: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }
}

/* ── T24 · reduced motion ─────────────────────────────────────────────────────────────────────
   .biztech-board-card:hover is the ONLY animation in this application, and it was unguarded. Goal 5
   asks for "no flashy motion"; honouring the OS setting is the version of that which is a
   requirement rather than a preference (WCAG 2.3.3).

   The cursor and the box-shadow stay - the card must still read as a click target. Only the movement
   goes. */
@media (prefers-reduced-motion: reduce) {
    .biztech-board-card {
        transition: none;
    }

    .biztech-board-card:hover {
        transform: none;
    }

    /* Round 3 added a second animation: scroll-behavior: smooth on the board. biztech-board.js
       already checks this setting before its own scrollBy, but that only covers the two buttons -
       this covers the arrow keys, the scrollbar and anything else that moves the scroller. */
    .biztech-board {
        scroll-behavior: auto;
    }
}

/* ── T25 · the JSON editor's own input ────────────────────────────────────────────────────────
   R14.1 LAYER 4, and the reason is the one the rule names: the <textarea> this styles is rendered
   by MudTextField, a CHILD COMPONENT, so FormJsonEditor.razor.css cannot reach it - a scoped sheet
   only stamps elements in its own component's markup. ::deep would reach it, but FormJsonEditor has
   more than one root element, which is exactly the case ::deep does not handle predictably.

   Layout only, no colour and no size: the field's ink and its border come from MudTheme (layer 1),
   and R5.4 puts every raw length on the 4px scale - which the size this wanted is not on. The
   monospace face is what makes an indented document readable, not a smaller one.

   A font stack rather than var(--mud-typography-monospace-family): MudBlazor 9.0.0 emits no such
   variable, and a var() resolving to nothing falls back to the proportional body face - the one
   thing this rule exists to prevent. */
.biztech-form-json textarea {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

    /* A JSON document has meaningful leading whitespace, and a wrapped line that re-indents itself
       reads as a different nesting level than it is. */
    white-space: pre;
    overflow-x: auto;
}

/* ── The seam's field shell · W0-52 · R9.1 (rewritten), R5.7 ──────────────────────────────────
   🔴 GLOBAL RATHER THAN SCOPED, AND R14.1 LAYER 4's SECOND CASE IS EXACTLY THIS: "markup rendered
   by a CHILD component". Six input components share one field shell - a label above a bordered box -
   and Blazor's CSS isolation scopes a sheet to its OWN component's elements, so a rule written in
   UiTextField.razor.css cannot reach the <select> UiSelect renders. The alternatives are six copies
   of the same forty lines, which drift, or this. Stated as R14.1 asks.

   THE LABEL SITS ABOVE THE BOX (decision D1). R9.1 said "floating MudBlazor labels" and is rewritten
   in the same edit that takes the guidelines to 4.0: a floating label is a Material-2 device that
   overlaps its own border, needs a notch cut in it, and reads as a placeholder until it moves. Above
   the field it is always legible, always in the accessibility tree in the same place, and it costs
   no animation.

   R5.7's density: 40px is the console default, matching what a dense MudBlazor input rendered, so
   W0-53's conversion does not resize every form in the application. */

/* 🔴 W0-69 · ADR 0026 D-G. THE ONLY WIDTH DECLARATION FOR A FORM IN THIS APPLICATION, and until this
   card there was none at all.

   Measured 2026-09-03: no form stylesheet capped width, and UiContainer declares no max-inline-size on
   purpose (R5.3), so a single-column form's fields spanned about 1,400px at 1440. §9's Forms rules said
   nothing about width, and R4.6's 46rem prose measure exempts "working surfaces (board, case table)"
   without ever saying which side a form falls on.

   60rem is 960px, which is R13.1's `md` breakpoint: a form is never wider than the width at which its
   own grid goes two-up. Reads the token rather than restating the length - R5.5's rule for the radius,
   applied to a measure - which is also what lets ONE declaration reach every form surface.

   NOT on .biztech-container: the board and the case table are wrapped in one and are meant to be wide,
   so a cap there would narrow two working surfaces while looking like a form fix. And not on
   CaseTypeFormEditor, which is a two-pane workspace rather than a form - its panes take the measure and
   it does not. */
.biztech-form {
    max-inline-size: var(--biztech-form-measure);
}

/* 🔴 W0-69 · A WIDE TABLE SCROLLS INSIDE ITSELF RATHER THAN BEING CLIPPED BY THE LANDMARK.

   Measured 2026-09-03 by InquiriesViewportTests at 768px in both modes: the case list laid out at
   1,032px inside a 768px landmark, and .biztech-main declares overflow-x: hidden - so two columns were
   clipped rather than scrolled and unreachable by any means. R13.8's own sentence: "overflow-x: hidden
   is not a responsive strategy - it hides the symptom and the diagnostic together."

   768 is the gap by construction: R13.3's reflow makes the table labelled cards below sm (600), and
   above ~1,030 every column fits. Between the two it is a real table that does not.

   The element carries tabindex="0" role="region" and a label (R12.2), so what scrolls is reachable by
   keyboard as well as by pointer - and it is in the focus-ring list above beside .biztech-board, which
   is the in-repo precedent this follows. */
.biztech-table-scroll {
    overflow-x: auto;
}

.biztech-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-inline-size: 0;
}

.biztech-field-label {
    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-caption-size);
    font-weight: var(--biztech-font-caption-weight);
    line-height: var(--biztech-font-caption-line-height);
    letter-spacing: var(--biztech-font-caption-letter-spacing);
    color: var(--biztech-palette-text-secondary);
}

/* 🔴 W6-53 · design §4. A LABEL THAT CARRIES A GLYPH, WHICH THE PLAIN LABEL CANNOT.

   .biztech-field-label is a block, so UiSelect.LabelIcon's svg landed on its own line above the word
   and the label became two lines tall. The specimen sets the pair inline:

     .mk-toolbar-label { display:inline-flex; align-items:center; gap:6px }

   6px rounds to 4px, for the reason SessionParticipantRail.razor.css records at length:
   PortalStyleSheetTests refuses a raw length off the 4px scale unless it is load-bearing at that exact
   value. Applied only where LabelIcon put it, so every other field label is unchanged. */
.biztech-field-label-icon {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

/* 🔴 W6-53 · design §4. A FIELD WHOSE LABEL SITS BESIDE ITS CONTROL, NOT ABOVE IT.

   .biztech-field stacks the label over the input, which is right for a form and wrong for a toolbar:
   the specimen's row is one line tall and reads

     [mic] [cam] | globe Interpretation [Off] --------- [Leave video]

   with .mk-toolbar-label as inline text before the select. Stacked, the label made the whole toolbar
   two lines tall and pushed everything after it down - measured in Chrome on 2026-09-09.

   🔴 THE LABEL STAYS A <label for>, WHICH IS THE WHOLE POINT OF DOING IT THIS WAY. The specimen's
   mock has a bare span and an unlabelled select; moving the text out of the <label> to copy that would
   cost the control its accessible name (R12.2). Turning the FIELD into a row keeps one label, one
   name, one control, and puts them where the specimen puts them.

   Opt-in through Class=, so every other field in the application is untouched. */
.biztech-field-inline {
    /* 🔴 flex-direction IS THE WHOLE RULE, AND LEAVING IT OUT MADE THIS DO NOTHING. .biztech-field
       above is ALREADY display:flex - it is a COLUMN - so a rule that only re-declared `display` changed
       no pixel, and the label stayed stacked over the select on both session panes. Found by driving it
       in Chrome on 2026-09-09 after the first attempt shipped. */
    flex-direction: row;
    align-items: center;
    gap: 0.25rem;
}

.biztech-field-inline .biztech-field-label {
    margin: 0;
    white-space: nowrap;
}

/* 🔴 W6-53. THE CONTROL KEEPS ENOUGH ROOM FOR ITS OWN PLACEHOLDER.

   .biztech-input is inline-size: 100%, which in a COLUMN meant "the field's width" and in a row means
   "whatever the flex basis leaves" - so the library search shrank to 185px and rendered
   "Search to show a resourc", measured in Chrome at 1911px on 2026-09-09. A placeholder that is cut off
   is R9.2's blocking reason unreadable beside its own control.

   12rem = 192px, the same scale value UiSelect's own .biztech-milestone-control uses for the same job
   one surface along, and it is a MINIMUM rather than a width: flex-shrink still applies below it when
   the toolbar wraps, which is what keeps R13.8 satisfied at 375px. */
.biztech-field-inline .biztech-input {
    min-inline-size: 12rem;
}

/* 🔴 W6-53 · SHARED BY BOTH SESSION PANES. Moved out of AgentSession.razor.css when
   SessionViewer needed the identical rule; a scoped sheet cannot be shared, and a second copy
   would be one rule in two files (R8.2). Its original reasoning follows, unchanged.

   AgentSession.razor keeps the wrapper element; SessionViewer.razor now carries one too. */
/* 🔴 THE PAGE'S ONE HEADING, PRESENT AND NOT PAINTED.

   Design §3 and specimens 1–6 open the stage at the toolbar and the artefact. What was above them —
   an `h1` repeating the case's subject, a `SESSION: {reference}` badge and a "You are hosting as X"
   line — is why the first screen at 1264×615 held no presenter control: measured 2026-09-06, page
   height 1,876px with Take presenter at y = 637 on a 615px viewport. The subject and the reference are
   both in the band now (`SessionStageBar.Reference`), so painting them again here would be the same
   fact twice, 40px apart.

   The heading itself stays, because R12.4 wants a document outline and
   `PortalPageShellTests.Every_page_opens_at_h1` reads this file for one. Hidden by CLIPPING rather than
   `display: none` or `visibility: hidden` — both of those remove the element from the accessibility
   tree, which is the one thing it is here for. This is app.css's own idiom, used by
   `.biztech-skip-link` and `.biztech-labels-in-headers` and given the same reasoning in both.

   The rule is on the WRAPPER rather than on the `h1`. `UiText` renders inside a child component, so a
   scoped rule aimed at the heading itself compiled to `h1[b-agentsession]` and matched nothing - which
   is also why this could not stay in a scoped sheet once a second page needed it (R8.2). */
.biztech-session-heading {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* W13-09 · A TABLE OF FIELDS WHOSE COLUMN HEADERS ALREADY NAME THEM.

   The Blueprint editor's tables put a UiTextField in every cell, and UiTextField.Label is
   EditorRequired - correctly, because a <th> is not an accessible name for an input inside the cell.
   The result on screen was every cell repeating its own column: "Key / Key", "Label / Label", six
   times a row. Measured in a browser on 2026-09-05; it is most of what made those tables look
   unfinished.

   So the label is hidden FROM SIGHT and kept for everything else. Not display:none and not
   visibility:hidden - both remove it from the accessibility tree, which is the one thing it is there
   for. This is the clip-path idiom UiLifecycleMap already uses for its route list.

   Scoped to the opt-in class rather than applied to tables generally: a table whose cells are NOT
   named by their headers still needs its labels, and ManagedRecordList is full of them. */
.biztech-labels-in-headers .biztech-field-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The required mark is aria-hidden and paired with aria-required on the control itself: a screen
   reader announces "required" from the input, and an asterisk read aloud as "star" is noise. */
.biztech-field-required {
    color: var(--biztech-palette-error);
    margin-inline-start: 4px;
}

/* W0-37 · the field-level validation message. The seam renders it with role="alert" and points the
   control's aria-describedby at it, so this rule is only the appearance - see UiTextField.Error.

   🔴 R12.7, "never colour alone": this is colour PLUS a sentence, which is why there is no icon and no
   border trick here. The text itself is the second signal, and it says what is wrong rather than that
   something is. The colour is the same token .biztech-field-required already uses, so a palette change
   moves the asterisk and the message together. */
.biztech-field-error {
    margin: 0;
    color: var(--biztech-palette-error);

    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-caption-size);
    font-weight: var(--biztech-font-caption-weight);
    line-height: var(--biztech-font-caption-line-height);
    letter-spacing: var(--biztech-font-caption-letter-spacing);
}

/* W6-19 · a standing caption under a field, saying something true about it rather than something wrong
   with it. UiTimestampField is the first caller and uses it to name the ZONE a reading is in.

   🔴 NOT .biztech-field-error IN A DIFFERENT COLOUR, and the separation is the point: an error is
   role="alert" and transient, a hint is neither, and giving them one class would make a screen reader
   announce "Times shown in Australia/Sydney" as an alert every time the form re-rendered. Both are named
   in aria-describedby, hint first, which is the order a reader needs them in.

   The muted ink and the caption scale are the same tokens .biztech-field-error uses, so a palette change
   moves the two together. */
.biztech-field-hint {
    margin: 0;
    color: var(--biztech-palette-text-secondary);

    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-caption-size);
    font-weight: var(--biztech-font-caption-weight);
    line-height: var(--biztech-font-caption-line-height);
    letter-spacing: var(--biztech-font-caption-letter-spacing);
}

/* W0-37 · a declared field the generated form may not edit, shown rather than disabled.

   A disabled input reads as "not yet" where the truth is "never" - R9.14 the wrong way round - and
   these are never editable: an `init`-only property (W0-35 D5, so a fact such as CreatedAt cannot be
   forged through a generated form) or a Timestamp or Reference kind (W0-37 D3). So the value renders as
   text at the same size the input's would, under the same label. */
.biztech-readonly-value {
    margin: 0;
    /* W0-63 · finding 6. The padding reads the token too, and it has to: a 20px line box plus 8+8 of
       padding is 36, which would render 36 under a 32px floor. */
    min-block-size: var(--biztech-control-height);
    padding: var(--biztech-control-padding-block, 8px) 0;

    color: var(--biztech-palette-text-primary);

    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-body2-size);
    font-weight: var(--biztech-font-body2-weight);
    line-height: var(--biztech-font-body2-line-height);
    letter-spacing: var(--biztech-font-body2-letter-spacing);
}

.biztech-input {
    box-sizing: border-box;
    inline-size: 100%;
    /* W0-63 · finding 6. 40px at :root, 32px under .biztech-shell - so the console compacts and the
       anonymous surfaces do not, without either being named here. */
    min-block-size: var(--biztech-control-height);
    padding: var(--biztech-control-padding-block, 8px) 12px;
    margin: 0;

    border: 1px solid var(--biztech-palette-outline);
    border-radius: var(--biztech-radius-default);
    background: var(--biztech-palette-surface);
    color: var(--biztech-palette-text-primary);

    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-body2-size);
    font-weight: var(--biztech-font-body2-weight);
    line-height: var(--biztech-font-body2-line-height);
    letter-spacing: var(--biztech-font-body2-letter-spacing);
}

.biztech-input:hover:not(:disabled) {
    border-color: var(--biztech-palette-primary);
}

.biztech-input:disabled {
    color: var(--biztech-palette-text-disabled);
    border-color: var(--biztech-palette-outline-subtle);
    cursor: default;
}

.biztech-input-multiline {
    min-block-size: 80px;
    resize: vertical;
}

/* A checkbox and a radio are the two controls whose box is the control. Sized on the scale, and
   accent-color is what paints a native control in the brand without replacing it - R8.8's "do not
   reimplement a native control that already works", and the reason neither of them is a styled div. */
.biztech-choice {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    /* W0-63 · finding 6. */
    min-block-size: var(--biztech-control-height);
    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-body2-size);
    line-height: var(--biztech-font-body2-line-height);
    color: var(--biztech-palette-text-primary);
}

/* 🔴 W0-37 · A MODIFIER, NOT A CHANGE TO `.biztech-choice`, AND THE REASON IS A TEST THAT WENT RED.
   The box and its label sit on one line and an error message has to go UNDER them - without a wrap it
   becomes a third flex item and reads as a caption beside the label, which a browser pass on /ui found
   with every file test green.

   The first fix put `flex-wrap: wrap` on `.biztech-choice` itself. That class is shared: `UiRadioOption`
   renders one per option, so the wrap reached every radio group in the application and
   `AssigneeFormKeyboardTests.Arrow_keys_move_within_the_radio_group` went red on /i/{key}. Scoping it to
   a class only `UiCheckbox` adds, only when it has a message, touches neither radio options nor a
   checkbox that is fine. */
.biztech-choice-invalid {
    flex-wrap: wrap;
}

.biztech-choice-invalid .biztech-field-error {
    flex-basis: 100%;
}

.biztech-choice-control {
    inline-size: 20px;
    block-size: 20px;
    margin: 8px 0 0;
    accent-color: var(--biztech-palette-primary);
    flex: none;
}

.biztech-choice-label {
    padding-block-start: 8px;
    min-inline-size: 0;
}

/* A radio group is a <fieldset>, and its <legend> is the question. The user agent gives both a
   border and padding of their own; both are declared away here rather than left to MudBlazor's `*`
   reset, which W0-53 removes. */
.biztech-choice-group {
    border: 0;
    margin: 0;
    padding: 0;
    min-inline-size: 0;
}

.biztech-choice-group-legend {
    padding: 0;
    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-caption-size);
    font-weight: var(--biztech-font-caption-weight);
    line-height: var(--biztech-font-caption-line-height);
    color: var(--biztech-palette-text-secondary);
}

/* W0b-01 · a switch. A native <input type="checkbox"> with role="switch", painted as a track and a
   thumb: the browser keeps the keyboard model, the label association and `disabled`, and the role is
   what makes it announce "switch, on" rather than "checkbox, checked".

   🔴 appearance: none IS WHY accent-color IS NOT HERE. .biztech-choice-control names one colour and
   leaves the rendering to the user agent, which is R8.8's whole point - but a track and a thumb are not
   a tick box, so this one's paint is ours. With the appearance goes the user agent's focus indicator,
   which is why .biztech-switch-control is in the shared focus ring at the top of this file.

   🔴 THE GROUND IS A CUSTOM PROPERTY, NOT A PALETTE ROLE. When the switch is on the track is filled with
   currentcolor, so the thumb has to be cut out of whatever is BEHIND the control - Surface on /ui,
   AppbarBackground in the app bar. Painting it either one of those makes it invisible on the other, and
   this is the only mechanism that lets one component serve both grounds without a Style parameter.

   currentcolor throughout, so the control inherits the ink of wherever it is mounted - OnAppbar in the
   app bar, TextPrimary on a page - and follows dark mode without naming a role of its own. */
.biztech-switch {
    --biztech-switch-ground: var(--biztech-palette-surface);

    display: flex;
    align-items: center;
    gap: 8px;
    /* W0-63 · finding 6. */
    min-block-size: var(--biztech-control-height);
    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-body2-size);
    line-height: var(--biztech-font-body2-line-height);
}

.biztech-appbar .biztech-switch {
    --biztech-switch-ground: var(--biztech-palette-appbar-background);
}

.biztech-switch-control {
    appearance: none;
    position: relative;
    flex: none;
    inline-size: 36px;
    block-size: 20px;
    margin: 0;
    border-radius: var(--biztech-radius-pill);
    background: color-mix(in srgb, currentcolor 32%, transparent);
    cursor: pointer;
}

/* The thumb. 4px inside a 20px track leaves a 12px circle centred, and every number is on the 4px
   scale R5.4 requires. */
.biztech-switch-control::before {
    content: "";
    position: absolute;
    inset-block-start: 4px;
    inset-inline-start: 4px;
    inline-size: 12px;
    block-size: 12px;
    border-radius: var(--biztech-radius-pill);
    background: currentcolor;
}

.biztech-switch-control:checked {
    background: currentcolor;
}

/* 36 - 4 - 12 = 20, so the thumb ends the same 4px from the far edge that it started from the near one.
   No transition: the position IS the non-colour cue WCAG 1.4.1 asks for, and this application has one
   animation, guarded at the end of this file. */
.biztech-switch-control:checked::before {
    inset-inline-start: 20px;
    background: var(--biztech-switch-ground);
}

.biztech-switch-control:disabled {
    cursor: default;
    color: var(--biztech-palette-text-disabled);
}

.biztech-switch-label {
    min-inline-size: 0;
}

/* 🔴 W0-65 · FINDING 3 · THE BREADCRUMB, PROMOTED - AND THE FILE IT CAME FROM ASKED FOR THIS BY NAME.

   Moved whole from Components/Pages/AdminConsole.razor.css, which stated its own exit condition in
   writing: "Nothing else in this application has a breadcrumb yet - §16's audit records item 7.4,
   'No breadcrumbs', as an open gap … The day a second surface grows one, this moves to app.css."
   AdminConsole, CasePage, /presentations/{id} and the three create/edit routes compose it. That day is
   this card, and the audit gap it closes is 2026-08-04-frontend-ui-ux-audit.md:624.

   🔴 IT HAD TO MOVE RATHER THAN BE COPIED. PageBreadcrumb renders the <nav>, the <ol> and the <li>s, so
   a sheet scoped to AdminConsole cannot reach ANY of them - the rule would have resolved to nothing on
   the very page it was written for. This is R14.1 layer 4's declared case: markup rendered by a child
   component. No ::deep is needed, because PageBreadcrumb writes these elements itself rather than
   nesting them inside another component.

   🔴 AND IT NEEDS NO UtilityClassTests ROW, WHICH IS WORTH STATING BECAUSE THE CARD ASKED FOR ONE (D7).
   That file's `Utilities` table maps a biztech- utility to THE MUDBLAZOR CLASS IT REPLACED, and
   Every_utility_replaces_a_class_the_markup_actually_uses fails any row whose value nothing in the
   markup names. There is no MudBlazor breadcrumb class, so a row would be RED. Nothing is lost:
   DeclaredAnywhere() scans app.css and every *.razor.css, so
   Every_biztech_class_the_markup_names_is_declared_somewhere was satisfied before the move and is
   satisfied after it. PortalStyleSheetTests
   .The_breadcrumb_is_an_application_rule_rather_than_one_pages_scoped_sheet is what pins the move
   itself. */

.biztech-breadcrumb {
    display: flex;

    /* 🔴 nowrap, AND IT IS THE WHOLE POINT OF THIS BLOCK. This declared `wrap`, which was fine only
       because every trail was two items long: measured on the running console 2026-09-01, a two-item
       trail is 19.91px on one line at 1440 AND at 375, but a realistic three-item trail
       (Case types / CONCIERG-20260831-9FC236C946BC / Form) took 47.81px over TWO LINES at 375.

       A breadcrumb is chrome above the thing somebody came for. Chrome that grows a second line pushes
       the content down by its own height, and it does it on the narrowest screen - where there is least
       room and where nobody is looking at the trail anyway. So it does not wrap: it truncates, and below
       sm it collapses. Both are declared here rather than left to the page. */
    flex-wrap: nowrap;
    align-items: center;
    gap: 8px;

    /* min-inline-size on a flex CONTAINER that is itself a flex item, because the default `auto` floors
       it at its content's width - so `overflow: hidden` below would never engage and the trail would
       push its parent wide instead of truncating. This is the declaration that makes the rest work. */
    min-inline-size: 0;
    overflow: hidden;

    /* The <ol>'s own reset. It is an ordered list because a trail IS ordered - a screen reader
       announcing "list, 3 items" and the position within it is the whole reason not to use a <div> -
       but the numbers themselves would be noise beside the separators below. */
    list-style: none;
    margin: 0;
    padding-inline-start: 0;

    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-caption-size);
    line-height: var(--biztech-font-caption-line-height);
}

/* Each step is itself a flex row, so its separator sits OUTSIDE the truncated text - see the ellipsis
   rule below, which is why this matters rather than being a spelling choice. */
.biztech-breadcrumb li {
    display: flex;
    align-items: center;
    min-inline-size: 0;
}

/* 🔴 THE TEXT TRUNCATES, THE SEPARATOR DOES NOT. text-overflow works on the element that clips, so
   putting it on the <li> would let an over-long label eat its own "/" and run the next crumb straight on.
   The <a> and the leaf are the elements that clip; the ::before hangs off the <li> beside them.

   flex-shrink is left at its default 1 on every step INCLUDING the leaf, deliberately. Pinning the leaf
   would make a long leaf push the ancestors out of the container entirely rather than share the squeeze,
   and the ancestors are the part a person needs when the leaf is already named by the <h1> below. */
.biztech-breadcrumb a,
.biztech-breadcrumb [aria-current="page"] {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The separator is generated content on every item but the first, rather than a character typed into
   the markup. Two reasons, and the second is the one that matters: a literal "/" between the <li>s
   would be read aloud by a screen reader as part of the trail, and CSS-generated content in a ::before
   is not in the accessibility tree the same way - the list structure is what announces the relationship,
   and the slash is decoration for people who can see it.

   flex: none so the separator is never itself squeezed to nothing - a trail whose slashes vanished under
   pressure would read as one run-on name. */
.biztech-breadcrumb li + li::before {
    content: "/";
    flex: none;
    margin-inline-end: 8px;
    color: var(--biztech-palette-text-secondary);
}

.biztech-breadcrumb a {
    color: var(--biztech-palette-primary);
    text-decoration: none;
}

/* R3.11: never `outline: none`. The underline on hover is ADDED to the focus ring rather than replacing
   it - a keyboard user gets the ring from the document's own focus style, and a mouse user gets the
   affordance that says this one is a link and the leaf beside it is not. */
.biztech-breadcrumb a:hover {
    text-decoration: underline;
}

/* The current page. Secondary ink and no link affordance, because it is where the operator already is -
   R9's rule that a control which cannot take you anywhere should not look like one that can. */
.biztech-breadcrumb [aria-current="page"] {
    color: var(--biztech-palette-text-secondary);
}

/* 🔴 BELOW sm THE TRAIL COLLAPSES TO `parent / here`, AND THAT IS THE "easy back" RATHER THAN A
   SEPARATE AFFORDANCE.

   :nth-last-child(-n + 2) is the last two steps - the immediate parent and the leaf. Every ancestor
   above them is hidden, so however deep a route gets, the narrow rendering is the two-item shape already
   measured at 19.91px on one line. Truncation above is the safety net for a pathological label; this is
   the rule that means it is rarely needed.

   🔴 THE PARENT LINK IS THE BACK, AND NO ARROW IS ADDED. R9.17 says the way back is one
   PageBreadcrumb and never an arrow character, and a `‹` bolted on below sm would be a second idiom
   arriving at exactly the width where there is least room for one. What a person needs is a named,
   tappable destination, and that is what is left standing.

   THE LEAF IS KEPT rather than hidden with the rest, which is the accessibility half: aria-current="page"
   is the trail's answer to "where am I", and dropping it at narrow width would take that answer away on
   the device most likely to be read aloud.

   display: none rather than a container query or JS: the trail is chrome, the breakpoint is the one this
   application already names in four places, and a hidden ancestor is correctly absent from the
   accessibility tree rather than announced and invisible.

   NOT sized up for (pointer: coarse). R5.8 exempts the console by decision - see its note beside the
   coarse block above - and this component renders on console pages only. The day a breadcrumb appears on
   /i/{key}, it needs a touch target and this comment is wrong. */
@media (max-width: 599.98px) {
    .biztech-breadcrumb li:not(:nth-last-child(-n + 2)) {
        display: none;
    }
}

/* ── The utility layer · W0-52 · R5.1, R8.7 ───────────────────────────────────────────────────
   🔴 WHY THIS EXISTS, AND WHY IT IS NOT THE "UTILITY-CLASS LIBRARY" THE CARD FORBIDS.

   R5.1 says spacing comes from utility classes and R8.7 says a seam component accepts Class= for
   exactly that. Both rules were written when those classes came from MudBlazor.min.css - and W0-53
   unlinks that stylesheet. Measured on this tree: the markup leans on twenty-five of them, led by
   mud-text-secondary (29 uses), mb-4 (24), align-center (22) and d-flex (20). On the day that
   <link> goes, every one of them stops existing and every page's spacing collapses at once.

   So this is the supply side of two rules that already exist, not a new way of writing CSS. It is
   MEASURED rather than designed: every class below replaces one the markup uses today, the values
   are read off MudBlazor 9.0.0's own stylesheet so nothing moves by a pixel at conversion, and
   UtilityClassTests fails the build on a utility declared here that no markup needs - which is the
   property that stops a measured layer becoming a framework.

   Prefixed, deliberately. An unprefixed .mb-4 would silently shadow MudBlazor's while both sheets
   are linked - the quiet override ADR 0013 spent 78 measured selectors objecting to - and the prefix
   makes W0-53's rename greppable rather than a judgement call.

   LOGICAL PROPERTIES, not margin-bottom/padding-left. Same result in LTR, correct in RTL, and W0-29
   puts a multi-language model on the roadmap. MudBlazor's own utilities are physical; this is the
   one place the layer deliberately does not copy it, because copying it would bake a direction into
   ninety call sites that W0-53 is about to touch anyway.

   ────────────────────────────────────────────────────────────────────────────────────────────────
   🔴 W0-59 · S4 · THIS LAYER IS LAST IN THE FILE, AND THAT POSITION IS THE FIX.

   R14.1 orders the styling layers: utility classes in markup are LAYER 2, this file's global rules
   are LAYER 4, "go one layer down only when the one above cannot reach". In the built output that
   order was inverted, and it was inverted twice over.

   Measured on /ui, 2026-08-13. NINE utilities computing 0:

     biztech-pa-4 on .biztech-paper          padding-top: 0px
     biztech-mb-2 / mb-4 / my-6 on .biztech-text, .biztech-alert, .biztech-divider,
     .biztech-grid, .biztech-choice-group    margin: 0px

   Two different mechanisms produced that:

     1. A SCOPED SHEET. Blazor's CSS isolation rewrites `.biztech-paper { padding: 0 }` as
        `.biztech-paper[b-xxxxxxxxxx]`, which is (0,2,0) against a utility class's (0,1,0). And
        _Host.cshtml links BizTech.Portal.styles.css AFTER this file on purpose, saying so in its own
        comment - so the scoped rule won on specificity and would have won on order too. Those
        defensive `margin: 0` / `padding: 0` declarations are deleted in this same commit: they were
        written against MudBlazor's `*` reset going away, and the elements that genuinely need one
        (h1-h6, p, hr, form controls) are now reset once by UiThemeTokens instead.

     2. THIS FILE'S OWN ORDER. `.biztech-choice-group` is a <fieldset> and legitimately declares
        `border: 0; margin: 0; padding: 0`. It sat LATER in this file than the utility layer, so at
        equal specificity source order decided and the reset won. Moving the layer here is what fixes
        that half, and it fixes it for every future rule as well rather than one selector at a time.

   The visible consequence was that EVERY UiPaper in the application rendered with no padding: "New
   case" on /inquiries, "New case type" on /case-types, "Customer Inquiry Form" on /i/{key} and "A
   paper" on /ui all had their title flush against the panel border, in both modes, at every width.
   That is the card's own sentence - "the whole application now reads one step tighter than it did" -
   with its mechanism found.

   🔴 WHY NO TEST SAW IT, which is the part worth remembering. UtilityClassTests asks whether every
   declared utility is NAMED by the markup, and every one of these was: the class was present on the
   element and simply did nothing. A class that is present and inert passes every check in this
   repository. UtilitySpecificityTests is the guard that closes it.

   NOT `!important`: R14.2 permits it only for beating a third-party utility class, and this was
   beating our own. Position is the honest instrument. */

.biztech-mb-1 { margin-block-end: 4px; }
.biztech-mb-2 { margin-block-end: 8px; }
.biztech-mb-3 { margin-block-end: 12px; }
.biztech-mb-4 { margin-block-end: 16px; }

.biztech-mt-1 { margin-block-start: 4px; }
.biztech-mt-2 { margin-block-start: 8px; }
.biztech-mt-3 { margin-block-start: 12px; }
.biztech-mt-4 { margin-block-start: 16px; }

/* 🔴 W0-59 · biztech-mb-6, NOT biztech-my-6, AND THE RENAME IS THE POINT. Every call site was a page's
   HEADER ROW, where `margin-block: 24px` supplied both the gap under the header AND the gap above it -
   and the second one is the container's job since R5.10. Left as `my`, the two would read 48px. */
.biztech-mb-6 { margin-block-end: 24px; }

/* W3-09 · F6.18. An emphasised section's heading row sits one step wider above than an ordinary one.
   24px is the 6th step of R5.1's 4px base, and MudBlazor 9.0.0's own `.mt-6,.my-6` is 24px - measured
   against its stylesheet, as this layer's table requires. `mt` rather than `my`: the gap BELOW a heading
   is biztech-mb-1's, whatever the section declares. */
.biztech-mt-6 { margin-block-start: 24px; }

/* 🔴 W0-63 · THREE DEAD CLASSES, FOUND BY THE SWEEP THIS COMMIT ADDS, AND NONE OF THEM IS THIS
   CARD'S SIX. biztech-pa-3 was named at AgentSession.razor:113 and :179, biztech-my-3 at :202 and
   biztech-mr-2 at Inquiries.razor:363 - and app.css declared none of the three, so those four elements
   rendered with no padding and no margin at all. Nothing could see it: Every_class_the_markup_names_has_an_answer
   only reads classes that do NOT start with biztech-, and Every_biztech_utility_the_markup_names_is_declared
   only reads names already in the table. A biztech- class that nothing declares fell between the two.

   This file already carries the same trap caught once, thirty lines down: "biztech-mb-6, NOT
   biztech-my-6, AND THE RENAME IS THE POINT". my-3 is that one, missed. */
.biztech-mr-2 { margin-inline-end: 8px; }

/* 🔴 W6-50 · biztech-my-3 IS GONE, AND THE GUARDRAIL ABOVE IS WHAT DELETED IT. Its one call site was
   AgentSession's rail divider, between the finish step and the panel loop. This card made the rail's
   children siblings - each already a bordered UiPaper carrying its own biztech-mb-4 - so the divider
   went, and UtilityClassTests.Every_utility_replaces_a_class_the_markup_actually_uses went red on the
   next run: "these utilities are not measured against anything in the markup". Its own instruction was
   "delete the rule", and this layer's whole rule is that it grows by measurement rather than by
   anticipation - which cuts both ways or it is not a rule. */
.biztech-pa-2 { padding: 8px; }
/* W0-63 · finding 6. The console's filter panels, one step down from pa-4. Four call sites in the
   commit that adds it - Inquiries, Board, AccessRules and AssignPanel - and pa-4 keeps its own dozen
   elsewhere, so neither utility becomes unused. */
.biztech-pa-3 { padding: 12px; }
.biztech-pa-4 { padding: 16px; }
.biztech-px-4 { padding-inline: 16px; }
.biztech-py-2 { padding-block: 8px; }

.biztech-d-flex { display: flex; }
.biztech-d-block { display: block; }

/* 🔴 W0-63 · FINDING 3's OTHER HALF AND FINDING 6's, IN ONE DECLARATION, AND THE PROPERTY IS THE
   WHOLE POINT.

   `min-block-size` is a FLOOR and the button's line box is above it: --biztech-font-button-* is .875rem
   at line-height 1.75 = a 24.5px line box, so a small button rendered 24.5 + 4+4 padding + 1+1 border =
   34.5 against its declared 32, and a medium one 42.5 against its declared 40. Both measured exactly on
   2026-08-20. Reading the token alone would leave the console's buttons at 34.5 while its fields render
   32 - so "declared equals rendered" needs a DEFINITE size.

   block-size is the property UiIconButton already chose, which is why the icon button measured exactly
   32.0 while the small button measured 34.5; the W3-26 note in this file's (pointer: coarse) block
   records that difference and why only one of the two ever lost a cascade fight. Nothing else declares
   block-size on .biztech-button, so there is no cascade to win here - and .biztech-button already carries
   align-items: center and white-space: nowrap, so a 32px box centres a 24.5px line box (32 - 2 of border
   = 30 of content) without clipping.

   Under .biztech-shell, not in UiButton.razor.css: /i/{key}'s buttons must not move by a pixel - the
   card's ## Do not - and they stay at 42.5 and 34.5 because nothing outside the console gains a
   block-size at all. */
.biztech-shell .biztech-button {
    block-size: var(--biztech-control-height);
}

.biztech-align-center { align-items: center; }
.biztech-align-start { align-items: flex-start; }
/* 🔴 W0-63 - FINDING 3. Six call sites in the commit that adds it - the five console rows that
   pair a labelled field with a button, and /ui's copy of the same row, which is the one a browser
   measures. A field carries a caption-scale label above its box and a button does not, so `center` over
   the two left their bottoms 14.7px apart; `flex-end` lines up the edge a reader actually looks at.

   🔴 W0-65 - FINDING 4. A SIXTH ROW, AND THE ONE CONDITION UNDER WHICH THIS UTILITY IS THE WRONG ANSWER.

   CasePage's *Move this case on* row joined the five: it is a plain flex div rather than a UiGrid, so
   W0-63's sweep could not see it and the row went on centring a labelled UiSelect against three buttons.
   The sweep reads what a row HOLDS now, whatever element carries it.

   AND THE LIMIT, WRITTEN DOWN RATHER THAN DISCOVERED LATER: a field carrying an `Error` renders
   .biztech-field-error BELOW its control, so bottom alignment puts the button level with the error
   MESSAGE rather than with the input. `align-end` is right for a row whose field cannot be invalid and
   wrong the moment one can be - and the day a row pairs an errorable field with a button, this needs a
   different mechanism (aligning the control boxes rather than the wrappers), not another call site.

   Swept 2026-08-31: none of the nine `biztech-align-end` rows in this application passes `Error` to a
   field. Every `Error=` call site is ManagedRecordForm (seven, in a form grid), ManagedReferenceField,
   four AccessCodeGate mounts and /ui's "Invalid inputs" section, and not one of those is an aligned row.
   So the condition is recorded here and no utility is added for a case the application does not have -
   a PortalRowAlignmentTests case would have to invent the row it measures, which is a test asserting a
   fixture rather than the application. */
.biztech-align-end { align-items: flex-end; }
.biztech-justify-space-between { justify-content: space-between; }

/* W3-09 · F6.18. A centred section's heading row is a flex container, so centring it is justify-content;
   text-align on a flex row moves nothing. Both classes land on the same element. */
.biztech-justify-center { justify-content: center; }
.biztech-flex-wrap { flex-wrap: wrap; }
.biztech-flex-none { flex: none; }
.biztech-gap-1 { gap: 4px; }
.biztech-gap-2 { gap: 8px; }
.biztech-gap-3 { gap: 12px; }
.biztech-gap-4 { gap: 16px; }

/* The one colour utility, and the most-used class in the application. Reads the seam's token rather
   than MudBlazor's, so it survives W0-53 unmounting MudThemeProvider. */
.biztech-text-secondary { color: var(--biztech-palette-text-secondary); }

/* W7-02 · F9.5, F9.6. The native <video> and <audio> elements on an education card.

   🔴 GLOBAL RATHER THAN COMPONENT-SCOPED, ON §8'S OWN REASONING. The players are hand-written markup in
   VideoItem.razor and RecordingItem.razor - the seam publishes no player primitive - and §8 records why
   such markup wears the seam's GLOBAL classes: ".biztech-field, .biztech-input and friends live in
   app.css rather than a scoped sheet precisely so hand-written markup cannot drift from what the seam
   renders." Two scoped sheets holding one identical rule is the drift that warns about.

   A <video> defaults to 300px wide whatever box it is in, so without this a player is narrower than its
   own card on a laptop. max-inline-size, not width, so it cannot push the grid column it sits in - the
   defect EducationItemViewportTests exists to catch. No colour and no height: the aspect ratio is the
   media's, and forcing one would letterbox somebody's recording. */
.biztech-education-player {
    display: block;
    inline-size: 100%;
    max-inline-size: 100%;
}
