/* 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. */

/* 🔴 W8-25 · THE SCROLLBAR'S GUTTER IS RESERVED, AND A BROWSER MEASUREMENT IS WHY.

   Reported as "a noticeable jump when you switch tabs" on the agent launcher, and the tab strip turned
   out to have nothing to do with it. Measured on the running stack at 1536px: the Start tab's page is
   735px and fits the viewport exactly; a work tab's is 852px and does not. So the scrollbar appears,
   `documentElement.clientWidth` drops 1536 → 1521, and every centred container re-centres inside the
   narrower box - the h1, the strip and the whole page slide 8px LEFT on the press, and back on the way
   out.

   This is not the launcher's defect and it is not fixable there: the scrollbar belongs to the DOCUMENT,
   so any page whose content crosses the viewport height does the same thing. `scrollbar-gutter: stable`
   reserves the space whether or not the bar is showing, which is what makes a page that grows stay
   still.

   🔴 ON `html` AND NOT ON A COMPONENT, for the same reason: this is the element that scrolls.
   Overlay scrollbars (macOS, touch) take no space and are unaffected - the property reserves a gutter
   only where the platform paints a classic bar, so nothing changes on the platforms that never had the
   problem. */
/* 🔴 FROM sm UP, AND NOT ON A PHONE — WHICH A VIEWPORT SUITE MEASURED RATHER THAN A PREFERENCE.
   A reserved gutter costs 15px of layout width whether or not a bar is painted. Above sm that is a
   fair price for a page that stays still; at 375px it is 15px of a 375px screen spent on a scrollbar
   the platform does not paint there — iOS and Android both use overlay bars, so the jump this rule
   exists to stop cannot happen on a phone in the first place.

   Measured: ResourceLibraryViewportTests found the library's toolbar row 12px past the landmark at
   375, because the landmark had become 360 while the row was still laid out for the width the page was
   authored at. R13.1's boundary, and the narrow end of it is where the gutter buys nothing. */
@media (min-width: 600px) {
    html {
        scrollbar-gutter: stable;
    }
}

/* ── 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;

    /* W27-06 (plan D4, D19) · the snackbar host, above the console's app bar (7) and the portal's sticky header
       (2), so a toast is never under either. It also paints over the rail and the overlay drawer: a transient
       message that closes on a click is the accepted trade, and it reverses W0-68's first "Do not" by decision.
       Below the skip link, which must clear everything while it is focused. */
    --biztech-layer-snackbar: 8;

    /* 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-segmented-control:focus-visible + .biztech-segmented-option, /* W21-04 · a segment: the radio
                                         is clipped out of sight, so its label wears the ring */
.biztech-swatch-control:focus-visible, /* W21-04 · the colour input beside a hex box */
.biztech-snackbar-action:focus-visible, /* W21-04 · a snackbar's one action - a plain <button> */
.biztech-theme-workspace-scroll:focus-visible, /* W21-04 · the theme settings pane - tabindex="0" role="region" */
.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-filter-chip-clear:focus-visible, /* 🔴 the applied-filter chip's own ×. A real <button>
                                         the case list renders itself, because the seam's smallest
                                         icon button is taller than the chip it would sit in */
.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-nav-group-label:focus-visible, /* 🔴 14.0 · a drawer CAPTION, which became a <summary> when
                                         the groups learned to collapse. A summary is focusable and
                                         operable by keyboard the moment it exists, so it is a control
                                         that was already in the tab order before it was on this list -
                                         wearing the user agent's ring while the items beneath it wore
                                         the house one. This is the list, not a rule that could find
                                         them: LibraryFolderTree's own summary is the standing
                                         counter-example and is worth its own card rather than a
                                         silent addition here */
.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-assistant-source-open:focus-visible, /* the assistant's Open on a cited source - an <a> drawn as a button */
.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-knowledge-library-row:focus-visible,  /* 🔴 W7-23 · the reader's tree rows - a <summary> or an
                                                  <a>, both interactive and neither a seam component */
.biztech-knowledge-library-mode:focus-visible, /* 🔴 W7-23 · the Collection / Type toggle's native
                                                  <button>s - the seam's text button has no pressed state */
.biztech-assistant:focus-visible,              /* 🔴 W18-03 · the assistant panel takes focus when it
                                                  opens, because it sits after <main> in the DOM - this
                                                  ring is how a keyboard reader sees where they landed */
.biztech-assistant-citation:focus-visible,     /* 🔴 W18-03 · a citation chip - a native <a>, because a
                                                  staff chip opens a document in a new tab and the seam's
                                                  anchor carries no target */
.biztech-assistant-sources-summary:focus-visible, /* 🔴 W18-03 · the Sources disclosure's <summary> */
.biztech-process-diagram-summary:focus-visible, /* 🔴 W19-03 · the Process tab's diagram <summary> */
.biztech-nav-docs:focus-visible,               /* 🔴 W24-01 · the drawer foot's Docs link - a NavLink <a>,
                                                  the same case .biztech-nav-item is; found by
                                                  UiGalleryKeyboardTests wearing "auto 1px" */
.biztech-docs-index-card:focus-visible,        /* W24-01 · a docs index card - an <a> */
.biztech-docs-contents-list a:focus-visible,   /* W24-01 · a page's "On this page" links */
.biztech-docs-body a:focus-visible,            /* W24-01 · every link a docs page draws - prose, cards,
                                                  steps, callouts, matrix cells - none a seam component */
.biztech-logo-other-summary:focus-visible,     /* W28-21 · the logo card's Other logo slots <summary> */
.biztech-docs-deep-summary:focus-visible,      /* W24-01 · the In depth <summary>, a control the moment it
                                                  exists, as .biztech-nav-group-label's note says */
.biztech-docs-matrix-scroll:focus-visible,     /* W24-01 · a matrix's sideways scroll box - R12.2's tab stop,
                                                  the same case .biztech-table-scroll is */
.biztech-docs-body pre:focus-visible,          /* W24-01 · a page's code block and table - each scrolls
                                                  sideways in its own box, so each is a tab stop (R12.2) */
.biztech-docs-body table:focus-visible,
.biztech-assistant-docs a:focus-visible,       /* W24-01 · the panel's "Read the docs" link */
.biztech-drawer-brand:focus-visible,           /* 🔴 W19-14 · the console drawer's logo, a link home - a native <a>,
                                                  the drawer's head now that the app bar is a phone's only */
.biztech-admin-chat-box:has(.biztech-chat-composer__box:focus-visible), /* 🔴 W22-08 · the Setup assistant's
                                                  question box: the card is the control a reader sees, so the
                                                  ring is round the card and the field inside draws none - R3.11's
                                                  one exemption, in AdminAssistantChat.razor.css */
.biztech-assistant-body:focus-visible,         /* 🔴 W18-03 · the panel's one scroller, a labelled tab
                                                  stop by R12.2's own prescription */
.biztech-portal-start-now:not(.biztech-portal-start-title):focus-visible, /* 🔴 W25 walk · a portal card that starts
                                                  its case on a press - an app icon, or an image card's action. A
                                                  native <button>, not a seam component; it was ringed in
                                                  PortalCard.razor.css, the second declaration this list forbids */
.biztech-portal-start-title:focus-visible::after, /* 🔴 W25 walk · the same, when the card's heading holds the button:
                                                  the ring is drawn on its stretched layer, round the whole card the
                                                  press reaches, rather than round the title's words */
.biztech-case-card:focus-visible,     /* W19-17 · the launcher's case card, which is one link now */
.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;
}

/* 🔴 A FIELD ALREADY HAS A BORDER, SO ITS RING DOES NOT STAND OFF FROM ONE.

   Focusing the case list's search box drew a SECOND box around the first. That is the shared ring doing exactly what it is told - 2px,
   offset 2px - which reads correctly around a button, a card or a table row, because none of them
   has a 1px outline of its own 2px further in. A field does, so the eye gets border, gap, ring.

   The ring is NOT a second declaration of the focus indicator, which is what the list above exists
   to prevent: it is the same ring, at the same 2px, moved onto the edge it was standing off from.
   Thickness and colour are unchanged, so R3.11 and WCAG 2.4.11 are answered by the same pixels as
   before.

   🔴 THE BORDER GOES TRANSPARENT RATHER THAN TAKING THE RING'S COLOUR, AND THE FIRST VERSION DID
   THE LATTER. 1px border plus 2px ring in one colour is a 3px edge, which was reported back as too
   heavy - correctly: the field then wore three times the weight of the outlined button beside it.
   Transparent leaves exactly the ring, so a focused field is 2px like every other focused control.
   The field's own background shows through the border box, so nothing gaps.

   One selector covers the seam's whole field family: UiTextField's <input>, its <textarea> and
   UiSelect's <select> all carry .biztech-input, measured on the running console.

   AFTER the block above, and that is load-bearing rather than tidy: the two selectors have the
   same specificity, so source order is what decides which outline-offset applies. */
.biztech-input:focus-visible {
    border-color: transparent;
    outline-offset: 0;
}

/* 🔴 W22-08 · THE SETUP ASSISTANT'S QUESTION BOX TAKES THE FIELD'S RULE. Its ring is round the card rather than the field inside
   (R3.11's one exemption), and the card has a 1px edge as a field does: standing 2px off it, the ring drew a second box round
   the first. On the edge, with the edge transparent, it is one line. And 1px, not 2px: round a card this size the full ring
   read as a heavy frame - the primary colour on the card's own edge is the focus, said once. After the shared ring, for the
   same source-order reason as the rule above. */
.biztech-admin-chat-box:has(.biztech-chat-composer__box:focus-visible) {
    border-color: transparent;
    outline-width: 1px;
    outline-offset: 0;
}

/* ── The page title's focus ───────────────────────────────────────────────────

   🔴 W16-01 DECLARED AN `outline: none` HERE AND W0-79 REMOVED IT, BECAUSE THE TWO CARDS FIXED THE
   SAME DEFECT TWO WAYS AND ONLY ONE OF THEM CAN BE RIGHT.

   Both diagnosed it identically, and correctly: <FocusOnNavigate Selector="h1"> in App.razor stamped
   tabindex="-1" on the heading at runtime and focused it; Chrome counts a PROGRAMMATIC focus as
   :focus-visible; and it drew the USER AGENT's own ring, in a colour no palette controls. W16-01 kept
   the focus move and suppressed the ring on that one selector. W0-79 removed the focus move.

   With nothing focusing the heading, the suppression had no subject: dead CSS whose only remaining
   effect was to keep a documented hole in R3.11, which bans `outline: none` outright. So it goes, and
   PortalStyleSheetTests' exemption list is empty again.

   🔴 WHAT THAT COSTS, STATED RATHER THAN BURIED - AND IT IS THE HALF W16-01 DELIBERATELY AVOIDED.
   A keyboard or screen-reader user no longer lands on the destination's heading after an in-circuit
   navigation; they resume from wherever the last interaction left them. That is an R12.3 / R12.4
   reduction, it is W0-79's decision rather than an oversight, and §12 records it. What remains is the
   skip link on every layout (R12.5), still the first tabbable element on every document.

   If that trade is judged wrong, the way back is the guidelines - and W16-01's suppression is the
   alternative already written down. */



/* ── 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);
}

/* 🔴 W14-03 · A NUMERIC HEADER, AND THIS RULE EXISTS BECAUSE THE ONE ABOVE SILENTLY BEAT THE UTILITY.

   UiTh's own header tells a caller to put Class="biztech-numeric" on the header as well as on its
   cells — R4.5's shared right edge. Doing that did nothing: `.biztech-numeric` is declared at :272 and
   `.biztech-th { text-align: start }` at :351, both single-class and therefore EQUAL SPECIFICITY, so
   the later one won. Every numeric header in this application was left-aligned over right-aligned
   figures, and the class that was supposed to fix it rendered exactly like no class at all.

   🔴 IT WAS NOT NOTICED BECAUSE NOTHING COULD SEE IT. Inquiries.razor and UiGallery.razor already wrote
   that class on a header; a bUnit test resolves no stylesheet, so asserting the class is PRESENT passes
   either way. RoleEditorViewportTests.A_count_header_is_right_aligned_over_its_figures reads the
   computed value out of a real browser, which is the only thing that can say which rule won.

   ADDITIVE rather than a reorder: moving `.biztech-numeric` below `.biztech-th` would change the
   cascade for every other property it carries and for every element that wears it. Two classes is
   (0,2,0) against (0,1,0), so this wins on specificity and nothing else moves. */
.biztech-th.biztech-numeric {
    text-align: right;
}

.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);
}

/* W31-02 · D6. An open item's detail row: set apart from the rows by the surface tint, and padded like
   a panel because what it holds is a block, not a cell value. min-width: 0 lets a scrolling child
   (a stack trace) scroll inside the cell instead of widening the table. */
.biztech-table-detail > td {
    padding: 12px 16px;
    min-width: 0;
    max-width: 0;
    background: color-mix(in srgb, currentcolor 3%, 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;
}

/* W28-21 · a link inside an admin reply's running text is underlined at rest, so it is told from its words by more than colour. */
.biztech-link.biztech-assistant-prose-link {
    text-decoration: underline;
}

.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 EDGE FADES ARE GONE TOO.

   They were two absolutely positioned pseudo-elements, 2.5rem of the page's own background fading to
   transparent over each edge of the board, each suppressed at the edge it pointed past and when there
   was nothing to scroll. The argument for them was T24 round 3's: measured 2,544px of columns in a
   1,248px viewport, five of nine stages off-screen, "and the only affordance was a hairline scrollbar".

   That premise is no longer this board's. The scrollbar under the columns is not a hairline - it is
   the browser's own, and it is the affordance every horizontally scrolling surface on the web uses -
   and the scroller is focusable with role="region", so the arrow keys drive it. What the fade added on
   top of those was a soft edge over the LAST COLUMN's cards, which is what was reported: a column
   greyed out at the side of a board reads as a column that is disabled rather than as one that
   continues.

   🔴 WHAT GOES WITH THEM IS THE WHOLE RUNTIME-FLAG MECHANISM ON THIS PAGE. data-scrollable,
   data-at-start and data-at-end existed to answer "which edge am I against", because a fade at an edge
   you are already at claims content that is not there. With no fade and no button, nothing on the board
   reads those flags, so biztech-board.js stops reporting them - a stylesheet hook nothing keys off is
   the same defect as a class nothing styles. The tab row still uses them and biztech-scroller.js is
   untouched. */

/* 🔴 THE TWO SCROLL BUTTONS AND THEIR RESERVED GUTTER ARE BOTH GONE. The pair read as dated chrome,
   and the gutter reserved for them sat empty on arrival.

   The empty space was this block working as designed, and the design was W16-18's fix for a real
   defect: measured on /board at 768px, a card and the forward control overlapped by 32 x 32px - the
   WHOLE control - and document.elementFromPoint at its centre returned the control rather than the
   card beneath it, so a pointer landing on what looked like a case opened nothing. A z-index could
   not help (a control UNDER the cards it scrolls past is a control nobody can press), so the viewport
   reserved `padding-inline: 2.5rem` under the scrollable flag - the edge fade's own width - to give
   the control ground that was not the board.

   But the rule keyed on the start flag hid the BACKWARD control while the board was at its start,
   which is how every board opens. So the reserved 40px was empty on arrival and grew a button only
   once the reader had scrolled past the thing it points back to.

   🔴 NOTHING ACCESSIBLE GOES WITH THEM, AND THAT IS WHY THIS IS A DELETION RATHER THAN A REDESIGN.
   The pair was aria-hidden with tabindex="-1" by design - a mouse-only duplicate of a path that
   already works, since the scroller below carries tabindex="0" and role="region" and the arrow keys
   drive it. WCAG 2.1.1 was satisfied by the region before these existed and is satisfied by it now.
   The edge fades above stay, keyed on the same three runtime flags, and are the part that says
   "there is more this way" without taking 80px to say it.

   The tab row's own pair is the same pattern on UiTabs and is deliberately untouched: that seam is
   composed by several pages, so it is W18-01's rather than a walk of this one's, and a tab row is a
   line of text rather than a 2,544px board.

   (Selectors are described rather than quoted here. PortalStyleSheetTests reads this file for the
   rules that make the runtime flags mean something, and a name written in a comment would satisfy an
   assertion about a rule that no longer exists - the trap UiQrCodeRenderTests records for a URL.) */

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

    /* 🔴 KEPT, THOUGH THE BUTTONS IT WAS WRITTEN FOR ARE GONE. It read "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", and the scrollBy call it paired with went with them.

       It still has callers, and they are the ones that mattered for WCAG 2.1.1 all along: this
       scroller is focusable, so the arrow keys, PageUp/PageDown, Home and End all drive it, and
       every one of those is a programmatic scroll the browser animates under this property. The
       reduced-motion override below applies to it unchanged. */
    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.

       🔴 IT WAS `min(60vh, 34rem)` AND THAT CEILING IS REVERSED. The columns stopped short of the
       space below them, and the requirement is that the COLUMNS scroll rather than the page.

       The 34rem arm was argued as "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". That is true of the
       CARDS and false of the COLUMN: a column is a panel with a border, and a panel that stops 150px
       short of the space it is standing in reads as a box that failed to fill rather than as a
       deliberate size. Measured 2026-09-24 at 1680x867: the board stopped at y=860 in an 867px
       viewport - and because 34rem is a fixed length it stopped at 860 at EVERY viewport height,
       leaving 164px blank at 1024 and overflowing a short one by 9px.

       🔴 THE SAME EXPRESSION AS THE COLUMN'S CEILING BELOW, WHICH IS WHAT MAKES THE COLUMNS SCROLL
       RATHER THAN THE PAGE. Floor and ceiling being one value means a column is EXACTLY the height
       available, so a full stage scrolls inside its own panel and the document does not scroll at all.

       🔴 THE VALUE IS MEASURED AT RUNTIME, AND THE calc() IS THE FALLBACK. This is the third answer to
       one report and the first two were both lengths: `min(60vh, 34rem)` stopped at the same y at every
       viewport height, and `calc(100vh - 20rem)` - walked a rem at a time in a browser until the
       document stopped scrolling - was still reported as leaving space at the bottom.

       A constant cannot be right, because the number it has to encode is the chrome ABOVE the board
       plus the board's own padding plus the platform's scrollbar, and those differ by deployment, by
       density and by operating system. The distance from an element's top to the foot of the viewport
       is not expressible in CSS, so biztech-board.js measures it and writes it here.

       🔴 THE DEFAULT IS DECLARED HERE RATHER THAN WRITTEN AS A var() FALLBACK, and TokenSufficiencyTests
       asked for that: it sweeps every --biztech-* a stylesheet READS for somewhere it is DECLARED, and
       a property whose only writer is a script has no declaration to find. Declaring it costs nothing -
       biztech-board.js sets it as an INLINE style, which beats any stylesheet rule - and it turns a
       value that lived only in JavaScript into one a reader of this file can see.

       The declared value is what renders before the module's first frame, and what renders for good if
       the import fails (R9.11 - this page degrades rather than errors). It is the walked constant, so
       the failure mode is the previous behaviour rather than a collapsed board.

       🔴 block-size, NOT min-height, AND THAT IS WHAT MAKES THE COLUMNS SCROLL INSTEAD OF THE PAGE. A
       floor lets the tallest column push the board past the fold; a definite size does not, and
       `align-items: stretch` above then hands that exact height to every column - so the ceiling the
       column used to declare for itself is now simply the height it was given, and its own
       `overflow-y: auto` is what a full stage scrolls inside. One declaration, and the two can no
       longer disagree. */
    --biztech-board-block-size: calc(100vh - 20rem);
    block-size: var(--biztech-board-block-size);
}

/* 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;

    /* 🔴 W19 · THE SURFACE, NOT A TINT. The column was a tint derived from the palette -
       color-mix(outline 9%, background) - so cards (`surface`) lifted off it. Against a page that is
       already grey the board read as grey on grey, so the column is the surface now, as the tables'
       cards and the portal's are: its outline edge below is what separates it from the page, and the
       cards carry an edge of their own to stay distinct on it (.biztech-board-card).

       Still a palette token, so it follows dark mode and costs no new role - and never MudBlazor's own
       --mud-palette-background-gray, a token nothing in this repository sets, which is what T24 removed. */
    background: var(--biztech-palette-surface);

    /* The tables' card edge (outline-subtle), so the board and the case list read as one set. The column
       was on the full Outline role, which drew it visibly heavier than the table cards beside it. The
       column is still bounded: it is a surface panel on the grey page, and the edge only finishes it. */
    border: 1px solid var(--biztech-palette-outline-subtle);
    /* W19 · the tables' card corners, twice the theme's radius. */
    border-radius: calc(var(--biztech-radius-default) * 2);

    /* 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.

       🔴 RE-MEASURED 2026-09-24 AND MOVED 16rem -> 20rem, BECAUSE IT IS NOW A FLOOR AS WELL AS A
       CEILING. The rule above gives the board the same expression, so this value is no longer "the
       chrome above the board plus a little breathing room" - it is the whole of what is NOT column, and
       what is below the column counts as much as what is above it: the board's own 0.75rem padding and
       the horizontal scrollbar both live inside the board's box, under the column.

       Walked in a browser at 1440x900, 1680x867 and 1280x1024, stepping the constant a rem at a time
       and reading documentElement.scrollHeight - clientHeight after each:

           16rem -> 52px of page scroll      19rem -> 4px
           17rem -> 36px                     20rem -> 0px
           18rem -> 20px

       20rem is the first value at which the document does not scroll at any of the three, which is the
       reported requirement in as many words: "if there are no task don't make whole page scroll.
       individual columns will scroll."

       🔴 AND IT IS THE FALLBACK NOW, NOT THE ANSWER. The space at the bottom was reported AGAIN against
       the walked constant, which is what sent this to a runtime measurement - see the rule above for
       why no length can be right here. The column reads the same property the board does, so the two
       cannot come to disagree about how tall the column may be, and both degrade to the walked value
       together if the module never loads.

       🔴 AND IT IS `100%` NOW, WHICH IS THE SAME NUMBER ARRIVED AT WITHOUT RESTATING IT. The board
       carries a definite block-size and stretches its columns to it, so the column's ceiling IS the
       board's content box - and the board's own padding and the horizontal scrollbar are inside that
       box rather than added below it, which is the arithmetic every fixed constant here got wrong.
       Below sm the media block at the end of this file still sets `max-height: none`, where the
       columns stack and the page scrolls. */
    max-height: 100%;
    /* W19 · the column no longer scrolls - its cards do, in .biztech-board-column-cards under the heading, so the
       scrollbar starts where the cards start. The column is a stack of the heading and that box, and the ceiling
       above is what bounds the box. */
    display: flex;
    flex-direction: column;
}

/* 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 made it sticky, because the column scrolled and a column of fifteen cards whose heading has scrolled
   away is a list of cases belonging to nothing. W19: the column's cards scroll in a box of
   their own under it now (.biztech-board-column-cards), so the heading is simply outside what scrolls - it stays
   put without sticking, and no card can pass behind or through it. flex: none so the stack never squeezes it. */
.biztech-board-column-header {
    flex: none;
    border-bottom: 1px solid var(--biztech-palette-outline-subtle);
    padding-bottom: 0.5rem;
}

/* 🔴 W19 · THE COLUMN'S CARDS, AND THE ONE THING IN THE COLUMN THAT SCROLLS.

   It takes the column's slack under the heading - min-block-size: 0, or a flex item's floor is its content and it
   would never scroll. It reaches out over the column's own 12px inline padding and gives it back as its own, so a
   card's resting shadow, its hover lift and app.css's focus ring - drawn 4px outside it - have room inside the
   scroller rather than being clipped at its edge; the 4px of block padding is the same room at the top and bottom.

   The bar is thin and inset: WebKit and Blink draw the thumb with a transparent border clipped away, so it floats
   2px in from the column's edge in a lighter ink than the outline. Firefox, which has no ::-webkit-scrollbar, takes
   the thin standard bar in the same ink - and only Firefox does, because in Blink a scrollbar-color or
   scrollbar-width turns the ::-webkit-scrollbar rules off. */
.biztech-board-column-cards {
    flex: 1;
    min-block-size: 0;
    overflow-y: auto;
    margin-inline: -0.75rem;
    padding: 4px 0.75rem;
}

.biztech-board-column-cards::-webkit-scrollbar {
    width: 12px;
}

.biztech-board-column-cards::-webkit-scrollbar-track {
    background: transparent;
}

.biztech-board-column-cards::-webkit-scrollbar-thumb {
    border: 2px solid transparent;
    border-radius: var(--biztech-radius-pill);
    background: color-mix(in srgb, var(--biztech-palette-outline) 55%, transparent) padding-box;
}

.biztech-board-column-cards::-webkit-scrollbar-thumb:hover {
    background: var(--biztech-palette-outline) padding-box;
}

@supports not selector(::-webkit-scrollbar) {
    .biztech-board-column-cards {
        scrollbar-width: thin;
        scrollbar-color: color-mix(in srgb, var(--biztech-palette-outline) 55%, transparent) transparent;
    }
}


/* 🔴 THE COUNT PILL, NARROWED - AND THE TYPE IS DELIBERATELY UNTOUCHED.

   The pill read as oversized beside its own label. UiChip's own sheet
   gives every chip `padding: 2px 8px` and the caption size. Around a single digit that is a 26px pill
   carrying 7px of ink, which is what reads as big. Narrowing the horizontal padding and dropping the
   block padding to 0 takes it to a disc that fits its digit, and the pill radius then reads as a badge
   rather than as a button.

   🔴 WHAT IS NOT CHANGED IS THE FONT SIZE, AND THAT IS A RULE RATHER THAN RESTRAINT. R4.7: "a chip
   carries a name or a count read aloud, so it wraps rather than truncating - and never shrinks below
   the 12px floor to fit". A count is exactly that, and 12px is already the floor.

   🔴 SO THIS IS AS SMALL AS THE PILL GOES, AND IT WAS REMOVED ENTIRELY ONCE ON THE WAY TO ESTABLISHING
   THAT. Asked again for smaller, the chip was replaced by a plain number - and the answer to THAT was
   "put the pill back", because the plain number lost the zero/non-zero colour, which is what makes
   "which stage is filling up" readable down a row of nine headings without reading nine digits. Both
   reports are satisfied by the same rule: the box is the smallest a 12px count can be enclosed in.

   A min-inline-size so a 1 and a 9 are the same disc, and two digits widen it rather than two discs
   sitting at different sizes down the row. 5x4 on the base unit (R5.1).

   🔴 4px AND NOT 6px, AND A GUARDRAIL DECIDED THAT. The first draft wrote 6px and PortalStyleSheetTests
   refused it: "a raw length is either a multiple of the 4px base unit or a named layout constant, and
   these are neither: app.css: 6px." It was right - there is nothing measured about 6 here, it was a
   number that looked about right.

   Here rather than in UiChip.razor.css because the class arrives from a CALLER: Blazor stamps a scoped
   sheet's attribute on its own component's elements, so UiChip's sheet can style .biztech-chip but not
   a name Board.razor invented. R14.1 layer 4, third case. */
.biztech-board-count {
    padding: 0 4px;
    min-inline-size: 20px;
    justify-content: center;
}

/* 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-radius: var(--biztech-radius-default);
    color: var(--biztech-palette-text-primary);

    /* The fifth user-agent button default, named now that nothing else is overriding it. The rule's
       header lists four - "the centred 13px Arial and the shrink-to-fit width" - and the border was a
       silent fifth: `border: 1px solid var(--biztech-palette-outline)` was doing two jobs, drawing the
       card's edge AND overriding whatever the browser would have drawn. With the edge gone, the
       override has to be said.

       🔴 MEASURED, AND IT IS NOT WHAT IT LOOKS LIKE. "shadow look like a border" was reported against
       this card, and the obvious explanation was that deleting the edge had restored Chromium's
       `2px outset ButtonBorder`. It had not: read off the running board with this declaration removed
       and the project rebuilt, the computed border was `0px none` either way. So this line is an
       explicit reset for the user agents that DO draw one rather than the fix for that report - and it
       is kept for that reason, not for the one it was added for. */
    /* 🔴 W19 · AN EDGE AGAIN, NOW THE COLUMN IS THE SURFACE TOO. On a white column the card's own fill is no
       boundary at all, so it takes an edge under the same resting shadow - halfway from outline-subtle to outline,
       because outline-subtle all but vanished on white. It is still the explicit border the note above wants
       said, so no user agent's own is drawn. Was `border: 0`. */
    border: 1px solid color-mix(in srgb, var(--biztech-palette-outline) 50%, var(--biztech-palette-outline-subtle));

    /* 🔴 A RESTING SHADOW WHERE THE BORDER WAS, AND THIS IS A DEPARTURE FROM ADR 0026 D-E RATHER THAN
       AN APPLICATION OF IT. The card's border was to become a shadow.

       D-E licenses a shadow for the top layer and for things that float OVER content they do not push
       aside, and says everything else takes a border. A resting case card is in flow. So this is the
       decision changing, not the rule being read differently, and InFlowElevationTests carries the row
       that records it - which is why that list is an allow-list with reasons rather than a comment.

       🔴 WHAT WAS MEASURED BEFORE DOING IT, because R6.6 says a border is what carries depth on dark
       and the obvious worry is that a shadow cannot. Read off the running board 2026-09-24, card
       background against its column's:

           light   1.16:1          dark   1.02:1

       The card's own fill is not a boundary in either mode - in dark it is not a boundary at all. So
       SOMETHING has to draw the edge, and with the border gone that is the shadow, on a ground it has
       almost no luminance distance from. A deeper column tint was measured as an alternative and does
       not reach: at 40% mix the card still reads 1.59:1 light and 1.48:1 dark, against WCAG 1.4.11's
       3:1 for a boundary that carries meaning. A subtler border token was measured too -
       outline-subtle lands at 1.08:1 and 1.26:1, which is a border nobody can see.

       The old border was 2.88:1 light and 3.57:1 dark, so the light figure was already under 1.4.11's
       3:1 before this change - a finding this rule inherits rather than creates.

       Raised rather than overlay: raised is the shallower of the seam's two steps, which leaves the
       deeper one for the hover - so a resting card and a hovered one differ by degree rather than by
       kind, and no third token had to be invented for either. */
    box-shadow: var(--biztech-shadow-raised);

    cursor: pointer;
    transition: box-shadow var(--biztech-motion-duration, 120ms) ease-in-out, transform var(--biztech-motion-duration, 120ms) ease-in-out;
}

/* 🔴 THE REFERENCE READS AS AN IDENTIFIER RATHER THAN AS A SENTENCE.

   It is a 31-character machine token - PRESENTA-20260922-D61840C0DE1C - sat in the same 12px secondary
   caption as "Version 1" beneath it, so two different KINDS of fact were drawn identically. The
   tabular figures and the slight tracking are what a reader needs to take a token in character by
   character, which is the whole reason this line exists: §8.2.4 calls this board "the centrepiece …
   read from the back of a room", and the reference is the string an operator reads aloud on stage.

   NOT a monospace stack: R4.2 gives this application one font family, and a second one introduced for
   one line on one card is a type decision taken in a stylesheet. Tabular figures are a FEATURE of the
   family already in use, which is the same thing .biztech-numeric asks for.

   No truncation and no nowrap - BoardCard's own header records why at length: the wrap broke the token
   mid-identifier, which is precisely what an operator must not do while reading it out, and one
   unwrapped line at 12px was the fix. Nothing here reintroduces a clip. */
.biztech-board-card-ref {
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
}

/* 🔴 ONE METADATA ROW WHERE THERE WERE THREE LINES, and the separator is drawn rather than typed.

   The version, the case type and - before this - the assignee each rendered as a full-width caption
   block, so a card spent three lines on facts that fit on one. They are facts ABOUT the case rather
   than the case itself, and set as a row with separators they read as metadata, which is what they
   are.

   🔴 THE MIDDOT IS A ::before ON EVERY CHILD BUT THE FIRST, WHICH IS THE WHOLE REASON IT IS HERE AND
   NOT IN THE MARKUP. The board renders the type line only in the all-types view, so a typed separator
   would leave a dangling middot with nothing after it on a filtered board - the same class of defect
   as a chip that shows when it has nothing to say. `:not(:first-child)` asks the question the markup
   cannot: is there something before me on this line?

   flex-wrap, because the column is a fixed 17rem and a long type name must drop to a second line
   rather than push the card's box wider than the panel holding it (R13.8). */
.biztech-board-card-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    margin-block-start: 4px;
}

.biztech-board-card-meta > *:not(:first-child)::before {
    content: "·";
    margin-inline-end: 4px;
    color: var(--biztech-palette-text-secondary);
}

/* 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. */
/* 🔴 THE HOVER IS A DEEPER SHADOW NOW, NOT A FIRST ONE. The resting card carries
   --biztech-shadow-raised since the border came off, so a hover that spent the same token would be a
   lift with no change at the bottom of it - the transform would be the only signal, and 1px of
   translate is not a signal anybody notices.

   --biztech-shadow-overlay is the seam's deeper step and is what UiDialog and the snackbar spend. It
   is borrowed here rather than a third token being invented: R5.5's argument for reading a token
   rather than restating a value applies to depth as much as to radius, and two steps is all this
   board needs.

   🔴 THE TRANSFORM STAYS AND IS NOT DECORATION. R12.7 wants a second channel, and "the shadow got a
   bit deeper" is exactly the kind of difference somebody may not see. It is also D-F's ONLY animation
   subject, and W0-71 forbids reopening D-F - removing the lift would empty that decision. */
.biztech-board-card:hover {
    transform: translateY(-1px);
    box-shadow: var(--biztech-shadow-overlay);
    /* W19 · the edge takes the primary container on hover, as a portal tile's does. */
    border-color: var(--biztech-palette-primary-container);
}

/* 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 a screenful and push the other eight below the fold.

           🔴 block-size TOO, NOW THAT THE WIDE RULE SETS A DEFINITE ONE. `min-height: 0` alone used to
           be the whole of this, because the wide rule declared a floor; a definite size has to be
           unset here or eight stacked columns would be crammed into one screenful and scroll inside a
           box the height of the viewport. The custom property biztech-board.js writes is still on the
           element - it is simply not read at this width. */
        min-height: 0;
        block-size: auto;
    }

    .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;
    }

    /* W19 · the cards box is the per-column scroller now, so it is the one that comes off. The heading's
       `position: static` that stood here went with the sticky it undid. */
    .biztech-board-column-cards {
        overflow-y: visible;
    }
}

/* 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 AND ON A TABLET · W0-52 · R9.8, R13.3 · "table -> DataLabel cards".

      🔴 W16-17 · THIS MOVED FROM sm (599.98px) TO md (959.98px), AND THE OLD NUMBER WAS THE DEFECT.

      This block's breakpoint and the DRAWER's were the same number, so there was no intermediate
      state: at 599px a clean card list and no nav; at 600px the drawer claimed 241 of 600 pixels and
      a 689px table was laid out into the 344 that remained - TWICE its column. Still 1.35x at 768 and
      45px over at 900. Measured on the running stack across 21 widths on 2026-09-18.

      🔴 md, AND NOT A NEW NUMBER. 959.98px is MudBlazor's md and block 2 above already keys the
      participant-link column on it - so this file still never disagrees with itself about where the
      layout changes, which is the property this section's own header claims. A hand-picked ~945 or
      ~1024 would have bought one page's arithmetic at the cost of a sixth breakpoint nobody could
      check; the plan's D3 records both alternatives and why they were refused.

      🔴 THE DRAWER'S BREAKPOINT DOES NOT MOVE WITH IT, and that is the point rather than an
      inconsistency. MainLayout.razor.css and biztech-viewport.js still key the nav on 599.98px: the
      defect was the two numbers being IDENTICAL, which left no width at which the nav was present and
      the table still fitted. They are now nested - cards up to md, nav from sm - and the band between
      them is the one that was broken.

      🔴 .biztech-table-scroll STAYS AND IS NOT REDUNDANT. Columns are configuration, so a case type
      declaring more of them can still exceed its column above md; the scroll region is the honest
      fallback there. What changes is that no column set this repository declares needs it.

      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 md 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: 959.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;
    }

    /* W31-02 · D6. The detail row follows its item's card as a block of its own; max-width resets the
       desktop rule that keeps a long stack from widening the table, because a block cannot widen one. */
    .biztech-table-reflow .biztech-table-detail,
    .biztech-table-reflow .biztech-table-detail > td {
        display: block;
        max-width: none;
    }

    .biztech-table-reflow .biztech-table-detail {
        margin-block: -8px 8px;
    }

    .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);
    }
}

/* 5a. The ONE table that reflows at sm instead · W16-17.

       Block 5 above moved the reflow to md for every table in the application. `LibraryRows` is the
       single exception and `UiTableReflow` carries the whole argument: every one of its rows holds a
       `UiMenu`, and measured at 768px with those rows reflowed neither /library's nor the document
       viewer's row menu could be pressed at all - `.biztech-menu-scrim` intercepted every click, and
       five browser facts timed out. UiMenu anchors its panel by ordinary absolute positioning and
       ManagedRecordList.razor records that fixing that "belongs on its own card".

       🔴 AN UNDO ACROSS ONE BAND RATHER THAN A SECOND COPY OF BLOCK 5. Duplicating those five rules
       under a second class is forty lines that have to be kept in step by hand, and the first edit that
       forgot one would produce a half-reflowed table with no error. This restores the table display
       across 600-959.98px and leaves block 5 to do everything below 600, so the exception owns ONE
       band and the shared rules stay shared.

       `revert` rather than a typed value, so each element returns to the user agent's own display -
       `table`, `table-row-group`, `table-row`, `table-cell` - which is what it would have had if block
       5 had never matched. Writing those four out would be four chances to pick the wrong one.

       Equal specificity to block 5's rules by construction, and LATER in the file, which is what makes
       it win. R14.2's `!important` is not reached for: position is the honest instrument, and app.css's
       utility-layer note makes the same argument at greater length. */
@media (min-width: 600px) and (max-width: 959.98px) {
    .biztech-table-reflow-sm,
    .biztech-table-reflow-sm tbody,
    .biztech-table-reflow-sm .biztech-table-row,
    .biztech-table-reflow-sm .biztech-td {
        display: revert;
    }

    /* The heading row comes back into flow and out of its off-screen box. All four declarations are
       block 5's, reverted together: a <thead> left at `position: absolute` while its table was a table
       again would lay the headings over the first row. */
    .biztech-table-reflow-sm .biztech-table-head {
        position: revert;
        inset-inline-start: revert;
        width: revert;
        height: revert;
        overflow: revert;
        white-space: revert;
    }

    /* And the per-cell heading goes, because the real <thead> is back above it. `none` rather than an
       empty string: an empty ::before still generates a box. */
    .biztech-table-reflow-sm .biztech-td[data-label]::before {
        content: none;
    }
}

/* 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 and 4, 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. It used to be the
       belt to biztech-board.js's braces - that module checked this setting before its own scrollBy -
       and with the two buttons and that call gone, this rule is now the ONLY thing honouring the
       preference on the board. It covers what it always covered: the arrow keys, the scrollbar and
       anything else that moves the scroller. */
    .biztech-board {
        scroll-behavior: 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;
}

/* 🔴 A SECTION'S NAME INSIDE A PANEL, AND IT HAD TO STOP LOOKING LIKE A FIELD LABEL.

   "Identity" over a field labelled "Kind" read as one two-word phrase. That is typographic rather than
   a wording problem: the first draft used Subtitle2 - 600 at .875rem - and `.biztech-field-label` is
   600 at .875rem too, so a heading and the label directly under it were the same object to the eye.
   Overline is the scale's own answer, and the seam calls it "an uppercase eyebrow": .75rem, 500,
   uppercase, with .08333em of tracking. Nothing in this application labels a field that way.

   🔴 THE LINE HEIGHT IS OVERRIDDEN, WHICH IS THE HALF THAT IS NOT FREE. The Overline slot carries
   `line-height: 2.66` - a Material figure for an eyebrow standing alone in a bar - which puts 20px of
   air inside a 12px line. On a heading 12px above its own controls that reads as a gap nobody asked
   for. `normal` rather than a number, so nothing here restates a length (R5.4).

   Tight to what it names - 12px, three units on the base unit - because a heading that floats
   equidistant between two groups belongs to neither.

   HERE RATHER THAN IN A SCOPED SHEET because five components render one of these: the create panel's
   two sections and the Process tab's four tables, which are separate components a scoped rule cannot
   reach (I19). R14.1 layer 4's third case. */
.biztech-section-title {
    display: block;
    line-height: normal;
    margin-block-end: 12px;
}

/* 🔴 A SEGMENTED TRACK: ONE OF THESE IS ON. The console's shape for a set of tabs or views that
   switch in place, rather than a filled button beside plain ones - Filled + a brand colour is what
   UiButton gives a page's primary ACTION, so an open tab drawn that way was the same object to the eye
   as the thing to press.

   HERE RATHER THAN IN A SCOPED SHEET because it is shared by components a scoped rule cannot reach
   (I19): the create panel's Details/Process/Delivery strip and the case type editor's. /inquiries'
   Active/Archived switch is the rule's origin and still carries its own copy as .biztech-view-switch.

   A segment that is a <button> is on through Pressed, which UiButton.razor.css draws. */
.biztech-segmented {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 2px;
    border: 1px solid var(--biztech-palette-outline-subtle);
    border-radius: var(--biztech-radius-default);
}

/* A segment that is a LINK is on through aria-current="page": the case type editor's tabs are routes
   (R9.18), and aria-pressed belongs to a button. The same tint and edge as UiButton's pressed rule,
   and why both is recorded there - the tint alone is 1.19:1 on dark.

   Here rather than in UiButton.razor.css because it is the TRACK that makes a current link a toggle;
   Current on a lone link - a pager, a nav entry - is not one and must not be drawn as one, and a
   scoped sheet cannot name the track it sits in. (0,3,0) outranks the scoped `.biztech-button-text`
   ground (0,2,0), which the pressed rule could only beat on source order. */
.biztech-segmented .biztech-button[aria-current="page"] {
    background: color-mix(in srgb, var(--biztech-palette-primary) 12%, transparent);
    color: var(--biztech-palette-primary);
    border-color: var(--biztech-palette-primary);
}

/* 🔴 THE PRESSED BUTTON'S TINT IS IN UiButton.razor.css, AND IT WAS HERE FOR ONE BUILD.

   It lost a specificity race it could not win. This file's `.biztech-button[aria-pressed="true"]` is
   (0,2,0); UiButton's own `.biztech-button-text` compiles to `.biztech-button-text[b-xxxxxxxxxx]`,
   which is (0,2,0) as well - and the scoped bundle is linked after app.css, so `background:
   transparent` won on source order and the open tab stopped being tinted at all.

   That is R14.1a's subject read from the other side. The seam publishes `Pressed`, so what `Pressed`
   LOOKS like is the seam's, in the sheet that already decides what every other variant looks like -
   where the two rules sit in one cascade and neither has to out-specify the other.

   CaseTypesViewportTests.The_open_tab_is_painted_differently_from_the_closed_ones is what holds it
   now, and it is a BROWSER fact for the reason this note exists: a source sweep asserted the rule was
   here and passed while it applied to nothing. */

/* 🔴 ONE SECTION OF A PANEL, AND THE HAIRLINE IS WHAT MAKES FOUR OF THEM READ AS FOUR.

   The Process tab is four of these and they ran together: a heading, a centred block, then the next
   heading with nothing between them, so the eye read one column of alternating text rather than four
   sections. The create form's Details tab had already answered this - a rule between its groups - and
   the two tabs are the same panel, so they answer it the same way.

   THE ADJACENT SIBLING, NOT :not(:first-child), and that is what makes it work across components. The
   four tables are four separate components rendered as siblings; `:first-child` asks about the
   PARENT's children, which is what CaseTypePanel renders into that branch, and any wrapper added later
   would silently move every rule. `+` asks only "is there one of these directly before me", which is
   the question being asked.

   🔴 MORE ABOVE THE LINE THAN BELOW IT, AND THE ASYMMETRY IS THE POINT. 16px either side left the
   section's last line sitting close to the rule under it. 32 above and 16 below puts the gap where a
   reader needs it - after the thing they have just finished reading - and still starts the next
   section clear of the line. 8 x 4 and 4 x 4 on the base unit (R5.1).

   🔴 AND EVERY PIXEL OF IT IS PAID FOR, BECAUSE THIS TAB HAS NO SLACK. Measured on /case-types/new at
   1440x866 with every section empty: 24px above the line is the exact height of the viewport, 32px
   scrolls 18px, 40px scrolls 42. So the three rules that fund it are part of this one - the section
   header tightens to its own content (4px, below), the action row's separation comes down from 48 to
   32, and the tab strip's own margin from 16 to 12. A later edit that takes one of those back takes
   this with it, and CaseTypesViewportTests is what says so.

   `outline-subtle` rather than `outline`: this divides related things inside one panel, and T24 §2.3
   reserves the 3:1-tested token for boundaries something depends on seeing. */
.biztech-section + .biztech-section {
    margin-block-start: 32px;
    padding-block-start: 16px;
    border-block-start: 1px solid var(--biztech-palette-outline-subtle);
}

/* 🔴 A SECTION HEADING WITH AN ACTION BESIDE IT, AND THE TITLE'S MARGIN HAS TO GO WHEN IT IS IN A ROW.

   The rule above gives the title 12px under it, which is right when it STANDS ABOVE what it names -
   the create form's two sections. The Process tab's four put the title in a flex row beside an Add
   control, and a bottom margin on a flex item does not push anything down there: it grows the ROW.
   Measured on /case-types/new, the four header rows were 44px tall for a 32px control - 12px each of
   space doing nothing, on the tab reported as scrolling with nothing on it.

   So the row owns its own spacing (I18) and the title inside it owns none. Here rather than in a
   scoped sheet for the reason the title itself is here: four separate components render one of these,
   and a scoped rule reaches its own component's markup only (I19).

   🔴 4px UNDER THE ROW, NOT 8, WHICH IS THE OTHER HALF OF SEPARATING THESE SECTIONS. A heading belongs
   to the content under it, so the space WITHIN a section should be smaller than the space between two -
   tighten inside, loosen outside. It also pays for the 8px the rule above added over the hairline,
   which is what keeps four empty sections on one screen. */
.biztech-section-header {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-block-end: 4px;
}

.biztech-section-header > .biztech-section-title {
    margin-block-end: 0;
}

/* 🔴 WHAT A LIST SHOWS WHEN IT HAS NOTHING TO SHOW, AND IT IS HERE BECAUSE THE THIRD SURFACE ASKED.

   The case list drew it first: an empty tab had shown a line of body text against the left margin, then
   "No cases on this page." under it, then a disabled Previous and Next - three pieces of furniture for
   "there is nothing here". The People tab copied the rules two cards later and its own sheet recorded
   the debt in as many words: "two surfaces is not yet a pattern (R8.2 wants three) ... if a third
   surface needs it, that is the moment to move all three." The compliance record is the third, so this
   is that moment and both copies are deleted.

   It lives in app.css rather than in a component because I19 is what made the copies necessary: a
   scoped rule reaches its own component's markup and nothing else, so a name declared in one sheet
   cannot be reused from another however identical the rules. app.css has no scope attribute, which also
   means the mark below needs no ::deep - a descendant selector reaches UiIcon's own svg from here.

   48px of block padding rather than a height: the space is the table's or the grid's, and a fixed
   height would be a second opinion about how tall an empty list is. 12 x 4 on the base unit (R5.1). */
.biztech-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;

    padding: 48px 16px;
    text-align: center;
}

/* 🔴 THE DENSE VARIANT, FOR A COLUMN THAT HOLDS SEVERAL OF THESE AT ONCE.

   48px above and below is right for the block's first three surfaces because each is the ONLY empty
   state on its screen: it stands in the space rows would have used, and a caption clinging to the top
   of that space reads as a loading state that stalled.

   The Process tab holds FOUR, stacked. The same padding there is about 560px of mostly air, and a page
   with nothing on it scrolled - which is the opposite of what the block is for. Nothing is reserving
   that space in the first place; a section with no rows is simply short.

   NO block padding at all, and the two lines close to 4px - so the block still reads as one centred
   object rather than as a heading with a sentence after it.

   🔴 IT WAS 24, THEN 12, AND THE PADDING WAS NEVER WHERE THE SPACE BELONGED. Shrinking it made the
   sections touch - each block tight under its own heading and tight above the next one, so four
   sections read as one column of alternating text. What separates SECTIONS is `.biztech-section`'s
   hairline, which is the Details tab's own answer; once the line is there the block needs none of its
   own, and dropping it is what pays for the space around the line.

   A MODIFIER rather than a second block, so the two cannot drift: everything about how this reads -
   the column, the centring, the mark's size - is the rule above, and this changes only what depends on
   how many of them there are. */
.biztech-empty-state-dense {
    padding-block: 0;
    gap: 4px;
}

/* The mark is secondary ink: it labels the block, it does not announce it. */
.biztech-empty-state-mark {
    display: flex;
    color: var(--biztech-palette-text-secondary);
}

/* 🔴 THE SIZE IS SET ON THE SVG AND THE WRAPPER IS WHY THAT WORKS. UiSize stops at Medium - 24px -
   which is a row's glyph rather than a block's mark. */
.biztech-empty-state-mark .biztech-icon {
    inline-size: 32px;
    block-size: 32px;
}

/* The action sits a step further down than the sentence: 8px is the gap between the lines of the block
   above it, and a button reading as one more line of that stack is what this avoids. 4 x 4 (R5.1). */
.biztech-empty-state-action {
    margin-block-start: 8px;
}

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

/* A LABEL IS THE SIZE OF THE THING IT NAMES, AND TELLS ITSELF APART BY WEIGHT.

   This read the caption facets - .75rem at weight 400 - against a control rendering body2's .875rem at
   the same weight, so every label in the application was a sixth smaller than its own value and dimmer
   than it besides. Reported on /library/new as "labels are too small compared to input text"; the
   defect was the seam's rather than that page's, which is why the fix is here and not there.

   Body2's size, line height and letter spacing, so a label and the value beneath it sit on ONE scale
   step and the field reads as a named thing rather than as a grey whisper over a black one.
   Subtitle2's 600 is what then separates them: a label is distinguished by weight, which survives at
   any size, rather than by being small, which only works while the reader has good eyes.

   🔴 THE FACETS ARE READ FROM TWO SLOTS ON PURPOSE, WHICH R4.3 OTHERWISE FORBIDS. There is no
   `--biztech-font-label-*` slot, and inventing one would be a type scale change - UiThemeTokens emits
   the scale, DesignDirectionDocumentTests gates it, and every reader of it would have to be swept. A
   label is subtitle2's weight at body2's measure, and saying exactly that in two lines is cheaper and
   more honest than a seventh slot that no <UiText> could ever ask for.

   The secondary ink is unchanged: 600 at .875rem is still small text under WCAG, so the pair keeps the
   4.5:1 PaletteContrast already measures - a label does not become large text by gaining a weight.

   What moves with it, and had to: .biztech-portal-create-choice offsets a checkbox by the height of
   the label block a checkbox does not have (I20), computed from the tokens rather than typed. It now
   reads body2's, because this is the box it is standing in for. */
.biztech-field-label {
    font-family: var(--biztech-font-family);
    font-size: var(--biztech-font-body2-size);
    font-weight: var(--biztech-font-subtitle2-weight);
    line-height: var(--biztech-font-body2-line-height);
    letter-spacing: var(--biztech-font-body2-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. */
/* 🔴 A TABLE OF CONTROLS HAS FIXED COLUMNS, AND AN ERROR MESSAGE IS WHY THIS ONE DOES.

   Under `table-layout: auto` a column is as wide as its content demands, and an error message is
   content. Measured on the stage editor's On-entry row: the template field was 491px wide while it
   carried "A notify action names the notification template it sends.", and 370px the moment a valid
   key was typed and the message went - so a field changed size as an operator used it, and every
   column beside it moved to make room.

   `fixed` sizes the columns from the header row and the available width instead, so nothing an
   operator types or fixes moves anything. Measured after: 273px empty and 273px filled.

   🔴 THE min-inline-size IS WHAT KEEPS R13.8 TRUE. `inline-size: 100%` alone would make the table
   exactly its container at every width - so at 375px six columns of selects would be 55px each and
   .biztech-table-scroll would have nothing to scroll, which is that rule's own defect wearing a
   different cause. 44rem is a floor: above it the table fits and the columns share the room, below it
   the table stays 44rem and the region scrolls, exactly as it does today.

   On the opt-in class rather than on every table, for the reason that class already gives: this is
   about tables whose cells hold form controls.

   🔴 `table.` IS LOAD-BEARING, AND ITS ABSENCE PUT A SCROLLBAR UNDER AN EMPTY SECTION. UiTable renders
   its EmptyContent INSTEAD of the table and carries the caller's Class onto that wrapper, so a bare
   `.biztech-labels-in-headers` matched a plain <div> holding one centred sentence - and gave it a
   44rem floor plus `inline-size: 100%` on top of the wrapper's own padding. Measured: the On-entry
   region scrolled 24px at 1440 and 316px at 768, with nothing in it. A floor for columns has no
   meaning on a box that has none. */
table.biztech-labels-in-headers {
    table-layout: fixed;
    inline-size: 100%;
    min-inline-size: 44rem;
}

/* 🔴 A COLUMN THAT HOLDS A CHECKBOX DOES NOT NEED A SIXTH OF THE TABLE.

   `table-layout: fixed` divides the width equally when nothing says otherwise, and on the stages table
   that gave Timer - one checkbox - and Order - two 32px icon buttons - the same room as Key, Label and
   Role. The cost lands on the widest thing in the row: a role's refusal wrapped to SIX lines inside its
   column while two columns beside it sat mostly empty.

   Under fixed layout the first row's cells decide, so a width on the <th> is what sets the column and
   nothing needs to be repeated per row.

   🔴 A CLASS ON THE HEADER RATHER THAN :nth-child(), which is the other way to say this and the one
   that breaks quietly. These tables differ - six columns on stages, seven on transitions, three on a
   stage's actions - so a positional rule would need one selector per table and would move the moment a
   column is inserted. The cell says what it holds; the sheet says how much room that needs.

   TWO WIDTHS RATHER THAN ONE, BECAUSE THE COLUMNS HOLD DIFFERENT THINGS. 5rem takes "Armed" beside its
   box and a pair of 32px controls with the row's own 12px padding either side; the trailing column also
   holds a word - Edit - so at 5rem its two controls wrap onto separate lines. The rest of the table
   divides what is left, which is what the fixed layout was for. */
th.biztech-col-compact {
    inline-size: 5rem;
}

th.biztech-col-actions {
    inline-size: 7rem;
}

/* The other direction: a cell holding a whole condition rather than one value. The transitions
   table's guard is up to three controls side by side, so it takes the space the compact columns
   beside it gave back. */
th.biztech-col-wide {
    inline-size: 16rem;
}

/* 🔴 A TABLE OF CONTROLS TOP-ALIGNS ITS CELLS, AND AN ERROR MESSAGE IS WHY.

   `.biztech-td` is `vertical-align: middle`, which is right for cells of text: a one-line cell reads
   level with a two-line one beside it. It is wrong for cells of CONTROLS. The moment one field reports
   a problem its cell grows by the height of that message, and every control in the row re-centres
   against the taller cell - so on the stage editor's On-entry row the Does select sat lower than the
   With field beside it, for a message that belonged to neither.

   Top, so a row's controls start on one line and a message hangs below the field it belongs to, moving
   nothing. AssignPanel reached the same answer from the other side with `biztech-align-start`, and its
   own note records what the middle-aligned version looked like: "every field at a different height".

   On the opt-in class rather than on .biztech-td, for that class's own stated reason: this is about
   tables whose cells hold form controls, and a table of text still wants its cells centred. */
.biztech-labels-in-headers .biztech-td {
    vertical-align: top;
}

.biztech-labels-in-headers .biztech-field-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* 🔴 W14-01 · A CONTROL WHOSE PLACEHOLDER ALREADY SAYS ITS NAME.

   The presenter's toolbar carried two grey words between its controls: `Show`, in front of a select
   whose placeholder reads "Choose something to show", and `Library`, in front of a box whose
   placeholder reads "Search to show a resource". Each was a shorter version of the sentence two
   centimetres to its right, and together they were about 110px of the row that made it wrap onto a
   second line.

   🔴 THIS REVERSES W6-53 · DESIGN §4 SPECIMEN 3 - "the label IS the inline word Show" - on
   Krishan's instruction of 2026-09-11, and it is written down rather than quietly done. What that
   ruling was actually fixing is untouched: it took a SENTENCE off the top of the control and made the
   row one line tall, and the row is still one line. This takes the remaining stub off the side.
   .biztech-field-inline stays on both controls, because the field is still a row.

   🔴 THE LABEL ELEMENT STAYS, WHICH IS WHY THIS IS A CLASS AND NOT A DELETED PARAMETER.
   UiTextField.Label and UiSelect.Label are the controls' ACCESSIBLE NAMES (R12.2); a `placeholder` is
   not one - it is not exposed as a name, and it disappears the moment somebody types. Hidden from sight
   and kept for everything else, which is `.biztech-labels-in-headers`' sentence above, verbatim, for the
   same reason: a label whose job is already done by something else on screen.

   Not display:none and not visibility:hidden - both remove it from the accessibility tree, which is the
   one thing it is there for. The clip-path idiom .biztech-skip-link and .biztech-labels-in-headers
   already use.

   Opt-in through Class=, so every other field in the application is untouched. */
.biztech-label-in-placeholder .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);
}

/* 🔴 THE DATALIST ARROW SITS IN THE MIDDLE OF THE BOX, NOT ON THE TEXT'S BASELINE.

   A field with Suggestions renders `list=`, and Chromium draws its own indicator for that - on hover
   and on focus only, which is why a screenshot of the resting field shows nothing at all. It is laid
   out against the text BASELINE, so in a 32px control with a 14px face it sat visibly below centre.

   `block-size: 100%` gives it the content box's height and its own padding is what then decides where
   the glyph lands inside that - so the reset is the working half rather than the sizing. Measured by
   capturing the field at each candidate: `vertical-align: middle` moved it not at all, and
   `block-size` without the padding reset moved it back down.

   🔴 A USER-AGENT PSEUDO-ELEMENT, AND CHROMIUM'S ALONE. Firefox and Safari draw no indicator for a
   datalist at all, so this straightens an arrow those browsers never show. That is the price of the
   native control, and the control is the right one: BlueprintSuggestions records why in as many words
   - any template key is legal and the list is a hint, so "a datalist is the right control and a select
   is not".

   Beside the input's own box rather than in a scoped sheet, because that box is declared here too -
   UiTextField has no sheet of its own, and a rule that has to out-specify one should sit next to it.

   🔴 ONLY ON A FIELD WITH A LIST. A date field's calendar icon is the same pseudo-element, and the 100% height
   drew it at nothing, so a date field had no icon to press and its calendar could not be opened. */
.biztech-input[list]::-webkit-calendar-picker-indicator {
    /* 🔴 box-sizing BECAUSE THE SWEEP ASKED, AND IT IS RIGHT TO ASK. A percentage height beside a
       padding declaration is the pairing that renders taller than the space it was given the moment
       the parent gains a height - CaseCard.razor.css records what that cost when an unrelated fix
       activated it. The padding here is 0, so nothing would overflow today; the rule is about what the
       next edit to these three lines does, not about this one. */
    box-sizing: border-box;
    block-size: 100%;
    margin: 0;
    padding: 0;
}

.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);
}

/* 🔴 NOT WHILE IT IS FOCUSED, AND THE `:not(:focus-visible)` IS THE WHOLE OF THAT SENTENCE.

   A focused field grew a heavier edge when the pointer crossed it.
   It is a specificity race, not a size change. This selector is (0,3,0) - `:not()` takes its
   argument's weight - against (0,2,0) for the focus rule six hundred lines up, so hovering a
   FOCUSED field repainted the border underneath the ring and the 2px edge became 3px again.

   Excluded here rather than out-specifying it up there: while a field is focused the ring owns its
   edge, and saying so at the hover site is the half a reader of THIS rule needs. Raising the other
   rule's weight would leave two selectors silently competing for one border.

   :focus-visible rather than :focus, to match the rule it defers to: a field focused by a mouse
   click draws no ring, so hover must still colour its border. */
.biztech-input:hover:not(:disabled):not(:focus-visible) {
    border-color: var(--biztech-palette-primary);
}

/* A field with a message under it has an edge in the same red, so the eye finds it before reading. The
   field had only the sentence, and a long form's unanswered field read like every answered one until the
   person reached its caption. The :hover arm matches the hover rule's weight and comes after it, so hovering
   a wrong field keeps it red; focus still hands the edge to the ring. The sentence is still the signal that
   does not rely on colour (R12.7). */
.biztech-input[aria-invalid="true"]:not(:focus-visible),
.biztech-input[aria-invalid="true"]:hover:not(:focus-visible) {
    border-color: var(--biztech-palette-error);
}

/* 🔴 THE SELECT KEEPS THE BROWSER'S BEHAVIOUR AND LOSES ITS PAINT, WHICH IS THE SWITCH'S BARGAIN.

   It read as a misalignment beside the search box. The boxes are identical - measured at the same
   top, the same 32px, the same padding - and what differed was the user agent's own arrow: its
   size, its grey and its spacing, next to controls drawing UiIcons.ExpandMore.

   `appearance: none` takes that arrow, and UiSelect renders ours in its place. What it does NOT
   take is anything that matters: the keyboard model, the announced role, the platform picker on a
   phone and the touch target are the element's, not its appearance's. The open list is still the
   platform's and cannot be reached from here at all.

   The inline-end padding is the chevron's 20px box plus the 12px gutter every field already has,
   so the longest option cannot run under the mark. 8 x 4 on the base unit (R5.1). */
select.biztech-input {
    appearance: none;
    padding-inline-end: 32px;
}

/* The open list as UiMenu's panel, where the browser lets a select's list be styled: a surface with the outline edge
   and the raised shadow, rows of 40px in Body2, and the chosen row's tick at the end. It is still the <select>, so the
   keyboard, the role and a phone's own picker are unchanged. A browser without base-select keeps the platform's list,
   which is how every select was drawn before. */
@supports (appearance: base-select) {
    select.biztech-input,
    select.biztech-input::picker(select) {
        appearance: base-select;
    }

    /* UiSelect draws its own chevron. */
    select.biztech-input::picker-icon {
        display: none;
    }

    select.biztech-input::picker(select) {
        box-sizing: border-box;
        margin-block: 4px;
        max-block-size: min(20rem, 60vh);
        overflow-y: auto;
        /* A transparent track: the platform's square one covered the panel's right-hand corners. */
        scrollbar-color: var(--biztech-palette-outline) transparent;
        scrollbar-width: thin;
        padding: 4px;
        border: 1px solid var(--biztech-palette-outline);
        border-radius: var(--biztech-radius-default);
        background: var(--biztech-palette-surface);
        color: var(--biztech-palette-text-primary);
        box-shadow: var(--biztech-shadow-raised);
    }

    /* border-box: an option is content-box, so the padding stood on top of the 40px and each row was 56. */
    select.biztech-input option {
        box-sizing: border-box;
        display: flex;
        align-items: center;
        gap: 12px;
        min-block-size: 40px;
        padding: 8px 12px;
        border-radius: var(--biztech-radius-default);
        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);
    }

    select.biztech-input option:hover:not(:disabled),
    select.biztech-input option:focus-visible {
        background: color-mix(in srgb, currentcolor 8%, transparent);
    }

    select.biztech-input option:disabled {
        color: var(--biztech-palette-text-disabled);
    }

    select.biztech-input option::checkmark {
        order: 1;
        margin-inline-start: auto;
    }
}

/* 🔴 ONE FILTER BAR ON EVERY CONSOLE LIST. The board, the presentations, the knowledge search and the library each
   drew theirs differently - labelled fields in a card, labels above with no card, labels beside, a field and a button
   - so a reader met five patterns for one job. Each now sits in UiToolbar's card, the case list's: a search box with
   its glyph, then the narrowings, each saying its empty state in words ("All categories").

   The labels are CLIPPED, not removed, the case list's rule: they keep their `for`, so every control keeps its
   accessible name. Global rather than scoped because the field and its label are UiTextField's and UiSelect's. */
.biztech-filterbar-search .biztech-field-label,
.biztech-filterbar-field .biztech-field-label,
.biztech-filterbar-labels .biztech-field-label,
.biztech-filterbar-labels .biztech-choice-group-legend {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

/* The search box: 28rem at most, the W19 lists' width, and it shrinks on a phone. */
.biztech-filterbar-search {
    position: relative;
    flex: 0 1 28rem;
    min-inline-size: 0;
}

/* The glyph over the box's start, with the typed text moved past it. Positioned, so it paints over the unpositioned
   input with no z-index of its own. */
.biztech-filterbar-search-glyph {
    position: absolute;
    inset-inline-start: 12px;
    inset-block-start: 0;
    display: flex;
    align-items: center;
    block-size: var(--biztech-control-height);
    color: var(--biztech-palette-text-secondary);
    pointer-events: none;
}

.biztech-filterbar-search .biztech-input {
    padding-inline-start: 40px;
}

/* A narrowing is as wide as what it says, up to 240px - the case list's and the library's, so no dropdown is a long
   box around two words. */
.biztech-filterbar-field {
    flex: 0 1 auto;
    min-inline-size: 0;
    max-inline-size: 240px;
}

/* The shell is only a positioning context; the field's own box is the select's. */
.biztech-select-shell {
    display: block;
    position: relative;
}

/* 🔴 pointer-events: none IS LOAD-BEARING: without it the chevron is a 20px dead zone over the
   one control an operator aims at, and a click there does nothing at all. */
.biztech-select-chevron {
    position: absolute;
    inset-inline-end: 12px;
    inset-block-start: 50%;
    translate: 0 -50%;

    display: flex;
    pointer-events: none;
    color: var(--biztech-palette-text-secondary);
}

/* A disabled control's mark is disabled ink, or it reads as the one live thing in a dead row. */
.biztech-input:disabled ~ .biztech-select-chevron {
    color: var(--biztech-palette-text-disabled);
}

.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;
}

/* 🔴 W14-01 · THE FILE PICKER'S OWN BUTTON, WHICH THE BROWSER DRAWS AND NOTHING HERE HAD EVER
   TOUCHED.

   Photographed on /decks/{id} on 2026-09-11: three stacked fields, each a labelled box in this
   application's own type and colour, and each containing a grey system "Choose File" in the platform's
   default face. Three of them on one screen, and they read as a form that was never finished.

   🔴 THE NATIVE CONTROL IS KEPT AND ONLY PAINTED, which is R8.8 - "do not reimplement a native
   control that already works" - and the same call `.biztech-choice` already makes with accent-color two
   rules down. The picker dialog, the keyboard behaviour, the announced role and the file name the browser
   writes beside the button are all the platform's and stay that way. ::file-selector-button is the one
   pseudo-element that exists for this, and it is the only part being restyled.

   Outlined rather than filled, and it takes UiButton's outlined declarations by hand rather than by
   class: a pseudo-element cannot carry a class, so this is the one place a button's look is written
   twice. Outlined because the field is a field - the page's primary action is Upload, beside it, and a
   filled picker would out-shout the control that actually does the work. */
.biztech-input[type="file"] {
    /* The native button is taller than a line of text; the field breathes round it instead of clipping. */
    padding-block: calc(var(--biztech-control-padding-block, 8px) / 2);
}

.biztech-input[type="file"]::file-selector-button {
    margin-inline-end: 12px;
    padding: 4px 12px;

    border: 1px solid currentcolor;
    border-radius: var(--biztech-radius-default);
    background: transparent;
    color: var(--biztech-palette-primary);

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

    cursor: pointer;
}

.biztech-input[type="file"]:hover:not(:disabled)::file-selector-button {
    background: color-mix(in srgb, currentcolor 8%, transparent);
}

/* A disabled field's button follows it rather than staying live-looking beside a dead control. */
.biztech-input[type="file"]:disabled::file-selector-button {
    color: var(--biztech-palette-text-disabled);
    cursor: default;
}

/* 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%;
}

/* 🔴 W16-12 · 6 · THE HINT HAS THE SAME LAYOUT PROBLEM AS THE ERROR AND TAKES THE SAME ANSWER. It is a
   third flex item on a row of two, so without a wrap it reads as a caption BESIDE the label - which is
   exactly what a browser pass on /ui found for the error message with every file test green.

   A SECOND MODIFIER RATHER THAN WIDENING `.biztech-choice-invalid`, for that rule's own reason: the
   base class is shared with UiRadioOption, and the modifier is what keeps the wrap off every radio
   group in the application. The two never apply together, because UiCheckbox renders the hint only when
   there is no error - a control must not show a rule and a complaint about that rule at once. */
.biztech-choice-hinted {
    flex-wrap: wrap;
}

.biztech-choice-hinted .biztech-choice-hint {
    flex-basis: 100%;
}

/* Caption, secondary: it is standing copy about the control rather than something wrong with it, so it
   takes .biztech-field-hint's weight rather than .biztech-field-error's colour. */
.biztech-choice-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);
}

.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;
}

/* W21-04 · THE SEGMENTED CHOICE. A fieldset of native radios (UiRadioGroup's model) whose labels are
   the segments: the radio is clipped to one pixel rather than display: none, so it keeps focus, the
   arrow keys and the label click, and :checked / :focus-visible on it style the label beside it.
   Every length is a scale step; the segment is a control's height, like a button. */
/* 🔴 position: relative IS THE CONTAINING BLOCK THE CLIPPED RADIOS NEED. Without it each radio is
   placed against the initial containing block at its static position, outside any scrolling ancestor:
   a column of segmented choices inside a scroller stretched the DOCUMENT by its own height, and the
   page scrolled past the end of the shell into blank space. */
.biztech-segmented-options {
    position: relative;
    display: inline-flex;
    flex-wrap: wrap;
    gap: 4px;
}

.biztech-segmented-control {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: 0;
    opacity: 0;
    pointer-events: none;
}

.biztech-segmented-option {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-block-size: var(--biztech-control-height);
    padding-inline: 12px;
    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);
    line-height: var(--biztech-font-body2-line-height);
    cursor: pointer;
}

.biztech-segmented-control:checked + .biztech-segmented-option {
    border-color: var(--biztech-palette-primary);
    background: var(--biztech-palette-primary);
    color: var(--biztech-palette-on-primary);
}

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

/* W21-04 · THE SWATCH FIELD. A native colour input beside a hex box: the swatch is a control's height
   and a control's radius, so the row reads as one field with two halves. The hex box is as wide as
   seven monospace digits need and no wider. */
.biztech-swatch-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

.biztech-swatch-control {
    inline-size: 40px;
    block-size: var(--biztech-control-height);
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--biztech-palette-outline);
    border-radius: var(--biztech-radius-default);
    background: transparent;
    cursor: pointer;
}

/* The browser paints its own swatch inside the input, inset and with its own rounded border - a second
   radius inside the first. Its wrapper's padding and its border come off, so the colour fills the
   input edge to edge and the input's one radius, clipped by overflow above, is the only corner seen. */
.biztech-swatch-control::-webkit-color-swatch-wrapper {
    padding: 0;
}

.biztech-swatch-control::-webkit-color-swatch {
    border: 0;
    border-radius: 0;
}

.biztech-swatch-control::-moz-color-swatch {
    border: 0;
    border-radius: 0;
}

.biztech-swatch-control:disabled {
    cursor: default;
}

.biztech-swatch-hex {
    inline-size: 112px;
    font-family: var(--biztech-font-mono-family);
}

/* 🔴 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;
    }

    /* 🔴 W20-06 · AND THE PARENT LOSES ITS SEPARATOR, which `li + li` still gives it with its hidden
       predecessor in the DOM. Every trail was two steps until the composer's three- and four-step ones,
       so the narrow trail never showed it - measured on the stack as `/ ● BizTech Demo / ● Welcome home`. */
    .biztech-breadcrumb li:nth-last-child(2)::before {
        content: none;
    }
}

/* The theme's back button before a portal page's breadcrumb (PageBreadcrumb; portalInnerPages.backButton): a round button in
   the primary, leading to the trail's previous step. Hidden unless the theme shows it - --biztech-portal-back-button is
   none unset - and drawn only in the portal shell, where the row around the trail exists at all. The trail beside it keeps
   its own truncation: it is the row's flexible part. */
.biztech-breadcrumb-row {
    display: flex;
    align-items: center;
    gap: 12px;
    min-inline-size: 0;
}

.biztech-breadcrumb-row > .biztech-breadcrumb {
    flex: 1 1 auto;
}

.biztech-breadcrumb-back {
    display: var(--biztech-portal-back-button, none);
    flex: none;
    align-items: center;
    justify-content: center;
    inline-size: 32px;
    block-size: 32px;
    border: 1px solid var(--biztech-palette-primary);
    border-radius: var(--biztech-radius-pill);
    background: var(--biztech-palette-surface);
    color: var(--biztech-palette-primary);
}

.biztech-breadcrumb-back:hover {
    background: var(--biztech-palette-primary);
    color: var(--biztech-palette-on-primary);
}

/* R13.4 · the coarse-pointer 44px floor: on a phone this is the control most often pressed. */
@media (pointer: coarse) {
    .biztech-breadcrumb-back {
        inline-size: 44px;
        block-size: 44px;
    }
}

/* 🔴 W20-06 · A SECTION'S HOOK FOR THE COMPOSER'S PREVIEW, AND IT LAYS OUT NOTHING. PortalItemOutlet wraps each
   section in this so the preview can find it by id; `display: contents` takes the wrapper's own box away, so the
   section's elements take part in their parent's layout exactly as they did - what readers see is unchanged.
   Global rather than scoped because the preview's mark below reaches into it from another component. */
.biztech-portal-item {
    display: contents;
}

/* The preview's mark on the selected item: an outline, not a shadow (the design direction), offset clear of the
   item's own border, in the primary role held to 3:1 against both grounds. DASHED, so it never reads as the focus
   ring - which is the one solid 2px outline in this file (R3.10) - when a reader tabs through the preview. */
[data-composer-highlight] {
    outline: 2px dashed var(--biztech-palette-primary);
    outline-offset: 4px;
    border-radius: var(--biztech-radius-default);
}

/* 🔴 W20-06 · D4 · THE LEVEL MARKER: a dot before a crumb's label, coloured by the level the step sits at.
   Decoration only - aria-hidden in the markup, and the level is said in words by the heading's chip and the
   outline (R12.7). flex: none so a squeezed trail truncates the label, never the dot. The ink is the
   element's own `color`, painted through currentColor, so one token per level is the whole rule. */
.biztech-crumb-marker {
    flex: none;
    inline-size: 8px;
    block-size: 8px;

    /* R5.5's pill radius: on an 8px square it is a circle, and it is the one radius not read from a variable. */
    border-radius: 999px;
    background: currentColor;
    margin-inline-end: 4px;
}

.biztech-crumb-marker-portal {
    color: var(--biztech-palette-text-secondary);
}

.biztech-crumb-marker-tile {
    color: var(--biztech-palette-primary);
}

.biztech-crumb-marker-section {
    color: var(--biztech-palette-secondary);
}

.biztech-crumb-marker-child {
    color: var(--biztech-palette-tertiary);
}

/* W19-17 · A portal's footer page (About, Contact, Privacy, Terms...) on W19's quiet card: the subtle edge, twice the theme's
   radius and more room inside. Only the panel that holds one - the sign-in forms and /i/{key} keep their panel. Here rather than in
   AnonymousLayout.razor.css: that sheet's scope cannot see the article's class, which PortalStaticPageBody renders. */
.biztech-anon-panel:has(.biztech-static-page) {
    border-color: var(--biztech-palette-outline-subtle);
    border-radius: calc(var(--biztech-radius-default) * 2);
    padding: 40px;
}

@media (max-width: 599.98px) {
    .biztech-anon-panel:has(.biztech-static-page) {
        padding: 24px;
    }
}

/* ── 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);
}

/* 🔴 W18-03 · R5.7. The assistant panel pairs its question box with an Ask button on one row, and in
   PortalLayout the panel is not under .biztech-shell - so without this rule that button is 42.5px beside
   a 40px box. The panel is never on /i/{key}, so W0-63's "not a pixel there" still holds. A rule of its
   own rather than a second selector on the one above, which a test finds by its exact text. */
.biztech-assistant .biztech-button {
    block-size: var(--biztech-control-height);
}

/* The portal's pages, likewise: a medium button took its 42.5px line box beside a 40px field, so no portal button was
   the height the theme's Control height names. Small keeps its 32. Not on /i/{key}, which is AnonymousLayout's. */
.biztech-portal-shell .biztech-button-medium {
    block-size: var(--biztech-control-height);
}

.biztech-portal-shell .biztech-button-small {
    block-size: 32px;
}

.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%;
}

/* W19-07 · ADR 0026 D-E · ONE LEVEL OF LIFT. A card's shadow is the theme's Depth (--biztech-shadow-card), and only the
   panels that sit on the page cast it: anything inside a card, a bar, a table panel, a dialog or a menu is flat, or
   shadows inside a shadowed panel stack into a muddy edge. Set on the container's children so the container keeps
   its own; custom properties inherit, so it reaches every depth below. */
:where(.biztech-paper, .biztech-toolbar, .biztech-theme-section, .biztech-dialog, .biztech-dialog-panel, .biztech-menu-panel, .biztech-filters-panel) > * {
    --biztech-shadow-card: none;
}

/* W19-07 · MOTION THE THEME CAN BE SEEN TO SET. Accordions snapped open and tab panels swapped in one frame, so the
   Motion setting reached almost nothing on a page. An opened accordion's content, and a panel marked
   .biztech-appear when it mounts, now fade and rise 4px in at the theme's duration: opacity and transform only, per
   R6.7, so nothing lays out twice. `none` is 0ms, and the OS's reduced-motion choice turns it off below.

   🔴 THE ACCORDION ANIMATES ITS ::details-content, NOT ITS CHILDREN. The first version put the animation on
   `details[open] > :not(summary)`, and it played on the first open only: a closed <details> hides its content with
   content-visibility, which skips style recalculation inside it, so the children never lost the animation while
   closed and had nothing to restart on the next open. The pseudo-element itself is styled whether open or not, so
   [open] adds and removes its animation every time. It also reaches every accordion whatever its children are -
   the drawer's groups wrap theirs. */
@keyframes biztech-appear {
    from {
        opacity: 0;
        transform: translateY(-4px);
    }
}

details[open]::details-content,
.biztech-appear,
.biztech-appear-children > * {
    animation: biztech-appear var(--biztech-motion-duration, 120ms) ease-in-out;
}

/* The same for a wrapper that must add no box - a tab panel whose children sit in their parent's grid or flex. A
   display: contents element draws nothing, so its own animation would be invisible; its children carry it. Keyed by
   the caller on the tab, so the children are new on every switch. */
.biztech-appear-children {
    display: contents;
}

/* W19-09 · WHAT OPENS OVER THE PAGE TAKES THE SAME MOTION. Every UiMenu (the account menu, the organisation
   switcher), the case list's Filters panel and every dialog appeared in one frame, so the theme's Motion setting
   reached accordions and tab panels and none of these. Each restarts the animation on every open without help: a
   menu panel mounts when it opens, the Filters panel leaves `display: none` when `hidden` goes, and showModal()
   adds the dialog's [open]. A dialog's dimmed backdrop fades with it; the menu and Filters scrims are transparent,
   so there is nothing to fade. Opening only - closing unmounts or hides, as the drawer's did before it needed
   allow-discrete. */
@keyframes biztech-fade {
    from {
        opacity: 0;
    }
}

.biztech-menu-panel,
.biztech-filters-panel,
dialog.biztech-dialog[open] {
    animation: biztech-appear var(--biztech-motion-duration, 120ms) ease-in-out;
}

dialog.biztech-dialog[open]::backdrop {
    animation: biztech-fade var(--biztech-motion-duration, 120ms) ease-in-out;
}

/* W19-09 · AN ARRIVING PAGE TAKES IT TOO. MainLayout stays mounted across a navigation, so what is new after a click is
   the page under .biztech-page - and an element is only animated when it is inserted, so a page re-rendering in place
   does not replay it. The wrapper draws no box (display: contents), so its children carry the animation, as
   .biztech-appear-children's do.

   🔴 GATED ON data-motion="ready", THE DRAWER'S GATE AND FOR THE DRAWER'S REASON. The interactive render replaces the
   prerendered page, and without the gate every page load would fade in a second time over content already on screen.
   MainLayout sets the attribute after its first render, so only what arrives after that moves. */
.biztech-page {
    display: contents;
}

.biztech-shell[data-motion="ready"] .biztech-page > * {
    animation: biztech-appear var(--biztech-motion-duration, 120ms) ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
    details[open]::details-content,
    .biztech-appear,
    .biztech-appear-children > *,
    .biztech-menu-panel,
    .biztech-filters-panel,
    dialog.biztech-dialog[open],
    dialog.biztech-dialog[open]::backdrop,
    .biztech-shell[data-motion="ready"] .biztech-page > * {
        animation: none;
    }
}

/* W19-11 · A PANEL INSIDE A COMPOSER SETTING GROUP - the copy form, the rows-and-columns editor and the three add
   forms. They were UiPapers with the outline border, nested in a group that already draws one, so each read as a
   card inside a card; three of them were bordered and the add-page form was a bare block under a rule. A tint on
   the group's surface and no border of its own sets each one apart without a second frame; its depth stays the
   theme's card shadow (ADR 0026 D-E). The setting body is in the selector for the specificity: UiPaper's own
   scoped rule is (0,2,0) and the bundle's order is not ours. */
.biztech-composer-setting-body .biztech-paper.biztech-subpanel {
    border-color: transparent;
    border-radius: calc(var(--biztech-radius-default) * 2);
    background: color-mix(in srgb, var(--biztech-palette-text-secondary) 6%, var(--biztech-palette-surface));
}

