/* Shared kernel component styles -- tokens only, no hard-coded colors, per BLAZOR-REBUILD-SPEC.md's
   DESIGN SYSTEM -- BINDING section (0-DESIGN, 2026-08-04). Built to
   docs/for-claude-code/design/COMPONENT-SPECS.md; that document wins wherever this file and it
   disagree. --st- names throughout (not the deprecated aliases in tokens.css), since this file is
   the migration target, not a consumer that still needs the bridge. */

/* #blazor-error-ui ships hidden by default in every Blazor WASM template's own stylesheet; only
   the framework's boot JS ever removes the hidden state, on a genuine unhandled exception. This
   rule has to exist SOMEWHERE in the app's CSS or the div (a plain block-level <div> with no other
   styling) is visible on every single page load regardless of whether anything actually failed. */
#blazor-error-ui {
    display: none;
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    background: var(--st-danger-subtle);
    color: var(--st-danger-on-subtle);
    padding: var(--st-space-3) var(--st-space-4);
    box-shadow: var(--st-shadow-md);
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    float: right;
}

/* 0-LOADER (2026-08-05): the boot/reload splash's own styling moved to an inline <style> block in
   index.html's <head> -- a real Playwright run against beta proved it must never depend on an
   external stylesheet's network round-trip (see index.html's own comment on that block for the
   full story: the "render-blocking <link> means always-applied-before-paint" assumption didn't
   hold in practice). Nothing here anymore; kept as a marker so a future reader searching for
   ".loading-progress" finds this note instead of wondering where it went. */

/* ── Shell: top bar + left nav (COMPONENT-SPECS.md §8-9) ──────────────────────────────────────
   The chrome is dark in BOTH themes -- the product's signature, not a dark-mode artefact -- so it
   uses --st-surface-chrome / --st-text-on-chrome and the *-on-chrome state tokens throughout,
   never the theme-flipping --st-surface/--st-border. .st-on-chrome (on .shell-topbar and
   .shell-nav) swaps the global :focus-visible ring for the on-chrome variant (tokens.css). */

.shell {
    display: flex;
    flex-direction: column;
    height: 100vh;
}

.shell-topbar {
    display: flex;
    align-items: center;
    height: var(--st-topbar-h);
    flex: 0 0 var(--st-topbar-h);
    padding: 0 var(--st-space-4);
    background: var(--st-surface-chrome);
    color: var(--st-text-on-chrome);
}

.shell-brand {
    flex: 0 0 calc(var(--st-nav-w) - var(--st-space-4));
    display: flex;
    align-items: center;
    gap: var(--st-space-2);
    font-family: var(--st-font-ui);
    font-weight: 600;
    font-size: var(--st-text-base);
    color: var(--st-text-on-chrome);
    text-decoration: none;
}

.shell-brand-mark {
    flex: 0 0 20px;
    width: 20px;
    height: 20px;
    background-color: currentColor;
    -webkit-mask: var(--menu-icon-url) no-repeat center / contain;
    mask: var(--menu-icon-url) no-repeat center / contain;
}

/* Task 28 (2026-08-04): hamburger that opens the phone nav drawer -- hidden on desktop, where the
   nav is always visible as the 210px sidebar and there's nothing to open. */
.shell-hamburger {
    display: none;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    margin-right: var(--st-space-2);
    border: none;
    border-radius: var(--st-radius-sm);
    background: transparent;
    color: var(--st-text-on-chrome);
    font-size: var(--st-text-lg);
    cursor: pointer;
}

.shell-hamburger:hover {
    background: var(--st-surface-hover-on-chrome);
}

/* COMPONENT-SPECS.md §8 "Page name" -- documented in the topbar anatomy since before ScreenHeader
   existed, never actually populated until 0-REVIEW-9 #4 (2026-08-06). min-width: 0 + overflow: hidden
   on the row (flex: 1 1 auto, the only flexible item between the fixed-width brand and the
   margin-left: auto actions) is what lets the description truncate with ellipsis instead of pushing
   the account name/icons off the edge of the topbar. */
.shell-page-name {
    display: flex;
    align-items: baseline;
    gap: var(--st-space-2);
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    margin-left: 30px;
}

/* W550 (2026-08-17, OldApp): `flex: 0 0 auto` + `white-space: nowrap` made this element
   UNSHRINKABLE, so a long screen name pushed the page wider than the phone. The E2E evidence names
   it directly -- on the account-list screen at a 375px viewport this span measured width 349 with a
   right edge at 521, i.e. it alone overflowed by 146px before any other topbar element was counted.
   Its own sibling `.shell-page-name-description` has always ellipsised; the title never did, for no
   stated reason. It now shrinks and ellipsises the same way, which is also what the ribbon wants
   visually -- a truncated screen name still tells you where you are, a horizontally scrolling page
   does not. */
.shell-page-name-title {
    font-family: var(--st-font-ui);
    font-size: var(--st-text-lg);
    font-weight: 400;
    color: var(--st-text-on-chrome);
    white-space: nowrap;
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.shell-page-name-sep {
    color: var(--st-text-on-chrome-muted);
    flex: 0 0 auto;
}

.shell-page-name-description {
    font-size: var(--st-text-sm);
    color: var(--st-text-on-chrome-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

/* W550: `margin-left: auto` alone left this cluster free to exceed the bar -- it had no
   `min-width: 0`, so its own children's min-content width became a floor the flex container could
   not go below, and the excess became document scrollWidth (the exact thing
   `E2EConfig.AssertNoHorizontalOverflowAsync` measures). */
.shell-topbar-actions {
    display: flex;
    align-items: center;
    gap: var(--st-space-1);
    margin-left: auto;
    min-width: 0;
}

.shell-signout-btn {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    height: var(--st-control-h-sm);
    border: 1px solid var(--st-border-on-chrome);
    border-radius: var(--st-radius-sm);
    background: transparent;
    color: var(--st-text-on-chrome);
    padding: 0 var(--st-space-2);
    cursor: pointer;
}

.shell-signout-btn-icon {
    display: none;
}

.shell-signout-btn:hover {
    background: var(--st-surface-hover-on-chrome);
}

/* 0-REVIEW-9 #7 (2026-08-06), COMPONENT-SPECS.md §8 "Icon button": 35px circle, transparent, glyph
   in --st-text-on-chrome. Generic enough to reuse for any future topbar icon (search/notifications),
   not just the theme control. */
.shell-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 35px;
    height: 35px;
    border: none;
    border-radius: var(--st-radius-pill);
    background: transparent;
    color: var(--st-text-on-chrome);
    font-size: var(--st-text-base);
    line-height: 1;
    cursor: pointer;
}

/* A105 (Mazhar 08-20): bell/search were the only two topbar icons rendered as color-emoji Unicode
   glyphs (🔔/🔎), which ignore CSS `color` entirely -- every other topbar icon is either a
   text-presentation Unicode symbol or (like this) a currentColor-stroked SVG, same technique
   MenuIcon.razor already uses for the nav rail, so the fix reuses the existing pattern instead of
   inventing a new one. */
.shell-icon-svg {
    width: 18px;
    height: 18px;
    stroke: currentColor;
    flex-shrink: 0;
}

.shell-icon-btn:hover {
    background: var(--st-surface-hover-on-chrome);
}

.shell-icon-btn[aria-expanded="true"] {
    background: var(--st-surface-active-on-chrome);
}

.shell-theme-menu-wrap {
    position: relative;
}

/* Same "real, focusable, labeled backdrop button" reasoning as .shell-nav-scrim (COMPONENT-SPECS.md
   §7's scrim precedent) -- click-outside-to-close without a JS interop listener. */
.shell-menu-scrim {
    position: fixed;
    inset: 0;
    z-index: 999;
    border: none;
    padding: 0;
    background: transparent;
    cursor: default;
}

/* R4 B1 (Mazhar, 2026-08-17): "Top-ribbon Settings menu: text area wider -- tiny labels currently wrap
   into two rows per option." The 140px floor was sized for the theme menu's three one-word options
   (Light/Dark/System) and then inherited by the Settings menu in ROUND 2, whose entries are real
   sec_menu labels ("Account Settings", "Change Password"). Those wrapped, and because the item's height
   is pinned to --st-tap-min the second line had nowhere to go -- so it read as clipped, not as wrapped.

   Two changes, and both are needed: `width: max-content` lets the panel take the width its LONGEST
   label actually needs instead of a guessed constant, and `white-space: nowrap` on the item is what
   makes that measurement honest -- without it the browser is free to wrap inside the fixed height
   again the moment a label grows. The max-width keeps a long label from running off a narrow screen;
   past that it ellipsises, with the full text still on the item's title attribute. */
.shell-theme-menu {
    position: absolute;
    top: calc(100% + var(--st-space-1));
    right: 0;
    z-index: 1000;
    display: flex;
    flex-direction: column;
    min-width: 200px;
    width: max-content;
    max-width: min(340px, calc(100vw - var(--st-space-4)));
    padding: var(--st-space-1);
    background: var(--st-surface-overlay);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-md);
    box-shadow: var(--st-shadow-md);
}

.shell-theme-menu-item {
    display: flex;
    align-items: center;
    gap: var(--st-space-2);
    height: var(--st-tap-min);
    padding: 0 var(--st-space-2);
    border: none;
    border-radius: var(--st-radius-sm);
    background: transparent;
    color: var(--st-text-primary);
    font-family: var(--st-font-ui);
    font-size: var(--st-text-sm);
    text-align: left;
    cursor: pointer;
    /* B1: see .shell-theme-menu -- the item must refuse to wrap for max-content to mean anything. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.shell-theme-menu-item:hover {
    background: var(--st-surface-hover);
}

.shell-theme-menu-item-active {
    color: var(--st-accent-fill);
    font-weight: 600;
}

/* ROUND 2 (2026-08-15): the Settings+Admin topbar menu reuses this whole block, so it needs the two
   shapes the theme menu never had -- a group heading (that menu has one flat list; this one shows
   "Setting" and "Admin" as separate sections) and an inert row for a child sec_menu names but this app
   has no screen for yet. The inert row is deliberately NOT a disabled <button>: it is a <span>, so it is
   skipped by tab order entirely rather than being a focus stop that does nothing. Same "(coming soon)"
   treatment the left rail already gives the identical entry -- one convention, two surfaces. */
.shell-theme-menu-heading {
    display: block;
    padding: var(--st-space-2) var(--st-space-2) var(--st-space-1);
    color: var(--st-text-secondary);
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.shell-theme-menu-heading:not(:first-child) {
    border-top: 1px solid var(--st-border);
    margin-top: var(--st-space-1);
}

.shell-theme-menu-item-inert {
    color: var(--st-text-secondary);
    cursor: default;
}

.shell-theme-menu-item-inert:hover {
    background: transparent;
}

.shell-user-email {
    font-size: var(--st-text-xs);
    color: var(--st-text-on-chrome-muted);
    max-width: 16rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* NIGHT-QUEUE full-unpark (2026-08-06): persistent "you're in the read-only demo" indicator --
   a filled pill (not plain text like .shell-user-email) so it reads as a status, not just a label. */
.shell-demo-badge {
    font-size: var(--st-text-xs);
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--st-text-on-fill);
    background: var(--st-accent-fill);
    border-radius: var(--st-radius-pill);
    padding: 2px var(--st-space-3);
    white-space: nowrap;
}

/* A37 (2026-08-18): the bell's unread count -- a small pill pinned to the icon-button's corner,
   .shell-icon-btn given position:relative implicitly via inline-flex + this child's absolute
   positioning (no layout change to the button itself needed). */
.shell-icon-btn {
    position: relative;
}

.shell-notification-badge {
    position: absolute;
    top: -2px;
    right: -2px;
    min-width: 16px;
    height: 16px;
    padding: 0 3px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    font-weight: 700;
    line-height: 1;
    color: var(--st-text-on-fill);
    background: var(--st-danger-fill, #d33);
    border-radius: var(--st-radius-pill);
}

.shell-notification-menu {
    min-width: 280px;
}

/* A97 (2026-08-19/20): the global search panel -- same .shell-theme-menu shell every other topbar
   control's dropdown already uses, wider than the default 200px floor since result labels (report
   names, sub-account names) run longer than "Light"/"Dark"/"System". */
.shell-search-menu {
    min-width: 280px;
    max-width: min(360px, calc(100vw - var(--st-space-4)));
}

.shell-search-input {
    height: var(--st-control-h-sm);
    margin: 0 0 var(--st-space-1);
    padding: 0 var(--st-space-2);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-sm);
    background: var(--st-surface);
    color: var(--st-text-primary);
    font-family: var(--st-font-ui);
    font-size: var(--st-text-sm);
}

.shell-search-result {
    justify-content: space-between;
    gap: var(--st-space-2);
}

.shell-search-result-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.shell-search-result-kind {
    flex-shrink: 0;
    color: var(--st-text-secondary);
    font-size: var(--st-text-xs);
}

.shell-notification-item {
    height: auto;
    min-height: var(--st-tap-min);
    white-space: normal;
}

.shell-notification-item-unread {
    font-weight: 600;
}

.shell-notification-item-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* 0-UNPARK (2026-08-06): Terms/Support -- unconditional topbar utilities, same pattern as the old
   app's TopBar.razor (see AppShell.razor's own comment at the call site for why these aren't in the
   sec_menu-driven sidebar). Plain text links, not the bordered .shell-signout-btn treatment -- two
   more bordered boxes next to the theme select/sign-out would visually compete with the actual
   session-critical controls for a pair of low-frequency utility links. */
.shell-topbar-link {
    font-size: var(--st-text-xs);
    color: var(--st-text-on-chrome-muted);
    text-decoration: none;
    white-space: nowrap;
}

.shell-topbar-link:hover {
    color: var(--st-text-on-chrome);
    text-decoration: underline;
}

.shell-body {
    flex: 1;
    display: flex;
    min-height: 0;
}

.shell-nav {
    width: var(--st-nav-w);
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    /* A36 (2026-08-18): explicit, not relied-on-as-default -- both rail states (expanded and the
       70px collapsed rail) share this single rule, so top-level rows must pack from the top with no
       extra distributed space regardless of item count. Row height itself is --st-nav-item-h -- the
       only lever left for "compact" is this property and the gap below. (--st-nav-item-h was 44px =
       --st-tap-min under R4 B17's tap-target ruling; A181, 2026-08-20 night, explicitly overrode that
       pin to 36px -- see tokens.css's own comment on the token for the full history.) */
    justify-content: flex-start;
    /* Bug ledger #23 (2026-08-11): Mazhar's own words, live on beta -- "I asked for gap between menu
       rows. Now it is too much... just increase a little than what was before. It was not too bad."
       "Before" was 0 (rows flush, matching old app's own .Nov-Body -- checked directly, no gap/margin
       between .Nav-Body-Item rows there at all). The full --st-space-1 (4px) step overshot what he
       pictured; there's no smaller token on the scale to fall back to, so this is a deliberate,
       narrow exception below it -- same class of exception this file's own literal-colour rules take
       elsewhere, not a token violation by accident.
       A36 (2026-08-18): "reduce vertical gap between menu items by 30%" -- 2px * 0.7 rounds to 1px.
       Row HEIGHT is untouched by THIS rule. --st-nav-item-h was pinned to --st-tap-min (44px, "a
       top-level row is always a tap target") through R4 B17 and A102; A181 (Mazhar 2026-08-20 night)
       explicitly overrode that pin to 36px -- see tokens.css's own comment on --st-nav-item-h for the
       full history. --st-nav-subitem-h was pinned to the same 44px by R4 B17 too, but A102 (Mazhar
       2026-08-20) reversed that specific piece back to 40px once he saw it live, then A179 took it to
       32px -- see tokens.css's own comment on --st-nav-subitem-h for the full history. This gap
       is still the only safe lever for "vertical gap between items" either way. */
    gap: 1px;
    padding: var(--st-space-3);
    background: var(--st-surface-chrome);
    border-right: 1px solid var(--st-border-on-chrome);
    overflow-y: auto;
    /* Bug ledger #19 (2026-08-10, Mazhar, REOPENED "reported before"): the rail's own collapse/
       expand (the «/» toggle snapping between --st-nav-w and --st-nav-w-collapsed) had no
       transition at all -- width changed in a single frame. This is a DIFFERENT animation from the
       accordion group's own open/close (already 0.3s eased, PEN-2 item 8) -- that one was fine; this
       one was never built. Matches old app's own NavMenu.css exactly (`transition: width 0.3s
       ease` on both its expanded and collapsed rail rules). */
    transition: width 0.3s ease;
}

/* R4 A44 (Mazhar 08-18): the draggable width handle, straddling the rail's own border-right so it
   reads as part of the edge rather than a separate stripe -- 6px wide, centred on the 1px border via
   the negative margins, flex: 0 0 auto so .shell-body's flex layout never stretches or shrinks it. Its
   own drag mechanics live in nav-resize.js; this is purely the hit target and cursor affordance. */
.shell-nav-resizer {
    flex: 0 0 6px;
    margin-left: -3px;
    margin-right: -3px;
    cursor: col-resize;
    z-index: 1;
}

/* Close button + scrim only ever render/matter on the phone drawer (COMPONENT-SPECS.md §9 Phone) --
   hidden here, made visible inside the 768px breakpoint below. */
.shell-nav-close {
    display: none;
}

.shell-nav-scrim {
    display: none;
}

/* 0-MENU hardening (2026-08-05): MenuIcon.razor's own masking technique, ported verbatim from the
   old app's MenuIcon.razor/shared.css (same sec_menu.IconName key, same static SVG-per-key file) --
   background-color: currentColor means the icon always matches whatever color: the wrapping
   .shell-nav-item/.shell-nav-group/.shell-nav-subitem already sets, no per-theme icon variant
   needed. */
.menu-icon-masked {
    background-color: currentColor;
    -webkit-mask: var(--menu-icon-url) no-repeat center / contain;
    mask: var(--menu-icon-url) no-repeat center / contain;
}

/* ROUND 2 / bug 56 A/B (2026-08-15): the inline-SVG variant. It must occupy EXACTLY the box a masked div
   occupies, because the experiment is "two adjacent rows, one variable" -- a size difference would give
   Mazhar a second thing to react to and make the answer unreadable. Sizing comes from .shell-nav-icon
   below, which both variants carry; this rule only supplies what an <svg> needs and a <div> does not.
   Note there is deliberately NO background-color and NO mask here: removing those two is the entire point
   of the variant. `stroke: currentColor` is inherited from the element's own attribute, so the ONE
   variable the two rows still share is the colour -- which is what makes "neither paints" a readable
   result (currentColor is the fault) rather than an ambiguous one. */
.menu-icon-inline {
    display: block;
    fill: none;
    overflow: visible;
}

.shell-nav-icon {
    flex: 0 0 1.3rem;
    width: 1.3rem;
    height: 1.3rem;
}

/* 0-REVIEW-9 #6 (2026-08-06): a long name ("Interest Income") wrapped to two lines inside the fixed
   210px rail -- no nav item/sub-item label had nowrap+ellipsis, only the fixed row height clipped
   the wrapped second line. flex: 1 1 auto + min-width: 0 lets the label actually shrink inside its
   flex row (the browser default min-width: auto on a flex child blocks ellipsis otherwise); the
   native title= tooltip every item already carries (AppShell.razor, since 0-MENU hardening) covers
   "genuinely long names" once truncated. */
.shell-nav-trigger-label,
.shell-nav-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    text-align: left;
}

/* COMPONENT-SPECS.md §9 "Header"/"Divider" -- the collapse toggle's own row, above everything else
   in the rail. flex: 0 0 48px matches the top bar's own height so the toggle visually aligns with it. */
.shell-nav-header {
    flex: 0 0 48px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--st-space-2);
    padding: 0 0 0 var(--st-space-3);
}

/* 0-MENU-2 #1 (2026-08-06): signed-in account name, prod's own placement (beside the collapse
   arrow). Same shrink-and-ellipsis rule as every other nav label (see .shell-nav-trigger-label's
   own comment) -- a long store name must not push the toggle button out of the rail. Hidden on the
   collapsed rail alongside every other label, .shell-nav-collapsed's existing rule below. */
/* SPRINT-QA 2026-08-14 #12: the store/account name in the topbar, beside Support and Billing.
   max-width rather than flex:1 -- the topbar actions sit at the right edge and a long store name
   must not push Support/Billing/theme off it. .shell-nav-account-name below is no longer rendered
   by AppShell; it is left in place because the collapsed-rail rules still reference it, and
   deleting dead CSS belongs to a real sweep, not to a QA pass. */
/* R4 B3 (Mazhar, 2026-08-17): "Store name at upper right: shift left / ensure fully visible." The 18ch
   cap was set when this moved into the topbar and it ellipsises most real store names -- and because the
   actions cluster is right-aligned, the missing characters are the ones nearest the right edge, which is
   what "shift left" describes from the outside: the name looks pushed off the screen. Widening the cap
   grows the span leftwards into the empty middle of the ribbon rather than pushing Support/Billing/gear
   off the right, since those are laid out after it in the same flex row.

   The vw term is what keeps that true on a narrow window, where 32ch would be most of the ribbon; the
   right margin stops the name butting straight into the "Switch account" link. Ellipsis stays as the
   last resort for a genuinely enormous name, with the full value already on the title attribute. */
.shell-topbar-account-name {
    max-width: min(32ch, 30vw);
    margin-right: var(--st-space-2);
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 600;
    color: var(--st-text-on-chrome);
}

.shell-nav-account-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 600;
    color: var(--st-text-on-chrome);
}

.shell-nav-collapse-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--st-tap-min);
    height: var(--st-tap-min);
    border: none;
    border-radius: var(--st-radius-sm);
    background: transparent;
    color: var(--st-text-on-chrome-muted);
    font-size: var(--st-text-base);
    cursor: pointer;
}

.shell-nav-collapse-toggle:hover {
    background: var(--st-surface-hover-on-chrome);
    color: var(--st-text-on-chrome);
}

.shell-nav-divider {
    height: 1px;
    width: calc(100% - 16px);
    margin: 0 8px 8px;
    background: var(--st-border-on-chrome);
    opacity: 0.5;
}

/* Bug ledger #56 (2026-08-13, MEASURED on beta with a logged-in browser, not read from source --
   source reading had already failed twice here). Mazhar: "Dashboard still without icon and height
   still short", and only ever on Dashboards and Reports. Those two are the ONLY top-level rows that
   render as a bare <a class="shell-nav-item"> (AppShell.razor's `entry.Route is not null` branch);
   every other row is a <button> WRAPPED in .shell-nav-group-wrap. .shell-nav is a flex COLUMN with
   overflow-y:auto, so its direct children are flex items with the default flex-shrink:1 -- when the
   rail's rows overflow, `height: 44px` is only a hypothetical size and the item is shrunk down to
   its own min-content, which for these rows is the 18px icon box. The group rows escape it purely
   by accident of nesting: the WRAP shrinks, but the button inside it is not a flex item, so its
   height stands. Measured on beta: link branch 18.19px vs group branch 44px, a 25.81px gap, with
   the icon present and its mask asset 200 on every row -- so this was never a missing icon or a
   missing sec_menu value (both eliminated by query on stage AND prod), it is a squashed row that
   reads as one. flex-shrink:0 on both branches makes the declared height the real height. */
.shell-nav-item {
    display: flex;
    align-items: center;
    gap: var(--st-space-2);
    height: var(--st-nav-item-h);
    flex-shrink: 0;
    padding: 0 var(--st-space-3);
    border-radius: var(--st-radius-sm);
    color: var(--st-text-on-chrome);
    text-decoration: none;
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 500;
    /* A55 (2026-08-18): padding is animatable, justify-content is not. The collapsed override
       below used to flip justify-content: flex-start -> center in the same frame .shell-nav-collapsed
       is toggled, while .shell-nav's own width keeps easing over 0.3s -- so the icon snapped to
       "centered in the FUTURE 70px rail" on frame 1, then got visibly dragged/re-centered every frame
       as the container kept shrinking underneath that fixed center. That drag is the reported
       "icons spread out / running" during collapse/expand. Fix keeps justify-content: flex-start in
       both states and instead transitions padding-left to the same centering value the collapsed
       rule computes, in lockstep with the width transition (same duration/easing) -- see the
       .shell-nav-collapsed override below, which no longer touches justify-content at all. */
    transition: background var(--st-duration-fast) var(--st-ease), padding 0.3s ease;
}

.shell-nav-item:hover {
    background: var(--st-surface-hover-on-chrome);
}

.shell-nav-item.active {
    background: var(--st-surface-active-on-chrome);
    /* LOGIN-SPEC §9.2: this is a GRAPHIC (the active-page indicator), not a focus ring that
       happens to be active -- --st-accent-on-chrome is the honest name for it now. */
    box-shadow: inset 3px 0 0 var(--st-accent-on-chrome);
}

.shell-nav-item-disabled {
    display: flex;
    align-items: center;
    gap: var(--st-space-2);
    height: var(--st-nav-item-h);
    /* Bug #56, same reason as .shell-nav-item above: also a direct flex child of .shell-nav. */
    flex-shrink: 0;
    padding: 0 var(--st-space-3);
    color: var(--st-text-on-chrome-muted);
    opacity: var(--st-disabled-opacity);
    cursor: default;
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    /* A55: same padding-transition fix as .shell-nav-item above -- this row shares the collapsed
       override selector too, so it needs the same lockstep animation. */
    transition: padding 0.3s ease;
}

.shell-nav-item-disabled small {
    font-size: var(--st-text-xs);
}

/* Item 1 (2026-08-07 pen, 0-429-REFRESH): the "never a dead end" banner -- AppSession retries a 429
   automatically before this ever shows; this is specifically the still-failed case, so it needs to
   read as recoverable (a real button), not as one more wall of red text on top of an already-empty
   nav. */
.shell-nav-session-error {
    display: flex;
    flex-direction: column;
    gap: var(--st-space-2);
    padding: var(--st-space-3);
    margin-bottom: var(--st-space-2);
    border-radius: var(--st-radius-sm);
    background: var(--st-surface-hover-on-chrome);
    color: var(--st-text-on-chrome);
    font-size: var(--st-text-xs);
}

.shell-nav-session-error p {
    margin: 0;
}

.shell-nav-session-error .kernel-btn {
    align-self: flex-start;
}

/* Group + sub-item (COMPONENT-SPECS.md §9) -- 0-MENU (2026-08-05): the spec already documented
   these tokens/metrics, this is the first screen that actually needed them (the nav was a flat list
   until this pass). */
.shell-nav-group {
    width: 100%;
    justify-content: space-between;
    background: transparent;
    border: none;
    cursor: pointer;
}

.shell-nav-chevron {
    display: inline-flex;
    flex-shrink: 0;
    color: var(--st-text-on-chrome-muted);
    transition: transform var(--st-duration-fast) var(--st-ease);
}

.shell-nav-chevron-open {
    transform: rotate(90deg);
}

/* Positioning anchor for the collapsed rail's flyout (COMPONENT-SPECS.md §9 "Collapsed behaviour")
   -- .shell-nav-children becomes position: absolute against THIS element when collapsed, so the
   flyout opens flush against its own trigger, not pinned to the top of the whole rail. */
.shell-nav-group-wrap {
    position: relative;
    /* Bug #56: this wrap IS a flex item of .shell-nav and was being shrunk below its own button's
       44px, so the buttons were overlapping their wrappers whenever the rail overflowed. The symptom
       hid behind the fact that the BUTTON kept its height (it is not a flex item), which is exactly
       why only the two bare-link rows looked wrong. Pin it so the wrap is always as tall as what it
       contains and .shell-nav's own overflow-y:auto does the scrolling instead of the rows. */
    flex-shrink: 0;
}

/* PEN-2 item 8 (2026-08-08): old app's own submenu transition is `max-height 0.3s ease-in-out,
   opacity 0.3s ease-in-out` (SellerTally.WebUI.Blazor/wwwroot/Styles/Shared/Layout/NavMenu.css) --
   matched here exactly. 500px is a generous fixed ceiling (not a JS-measured scrollHeight, which
   old app's own CSS doesn't appear to use either) -- comfortably larger than any real group's child
   list, so the transition always runs its full visual duration against actual content height rather
   than snapping to it. Requires the element to always be in the DOM (AppShell.razor no longer wraps
   this in @if) -- a CSS transition can't animate something that doesn't exist yet. */
.shell-nav-children {
    display: flex;
    flex-direction: column;
    /* Bug ledger #23 / A36 (2026-08-18): same rhythm as .shell-nav's own row gap above -- kept in
       sync deliberately so top-level rows and their sub-items read as one consistent list, not two
       different densities. */
    gap: 1px;
    max-height: 0;
    opacity: 0;
    overflow: hidden;
    transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
}

/* R4 B17: the second half of the same fix. flex-shrink: 0 on the sub-item stops the squash, but with
   the ceiling left at 500px Setup's ~30 rows would then be CLIPPED by the overflow: hidden above --
   trading a congested menu for a truncated one. 1600px clears the tallest real group (30 rows at 44px
   plus 2px gaps = ~1380px) with headroom for a few more sec_menu inserts. It is still a fixed ceiling
   rather than a JS-measured scrollHeight, matching the old app's own NavMenu.css that this transition
   was copied from -- raising the cap does not change the animation's character, since for a 3-item
   group the max-height was already an order of magnitude past the content at 500px. */
.shell-nav-children.shell-nav-children-open {
    max-height: 1600px;
    opacity: 1;
}

/* R4 B17 (2026-08-17) -- THE CAUSE of "Setup submenu rows are congested", and why it was Setup and
   only Setup. `height` on a flex child is not a floor: .shell-nav-children is a flex COLUMN whose own
   height is capped at max-height (below), so once its children total more than that cap the browser
   shrinks every one of them to fit, because the default flex-shrink is 1. Setup has ~30 sec_menu
   children (parent_id 8000: 23 in the baseline plus 7 from the 08-13 kind-coverage migration) = well
   over 1200px of rows squeezed into a 500px box, i.e. each row squashed to roughly a third of its
   stated height. Every other group is small enough to fit under the cap and therefore looked correct,
   which is exactly why this read as a Setup-specific padding problem and was "fixed" per-menu twice.
   flex-shrink: 0 is the same fix bug #56 already applied to .shell-nav-item, .shell-nav-item-disabled
   and .shell-nav-group-wrap -- the sub-item was simply the one it missed. */
.shell-nav-subitem {
    display: flex;
    align-items: center;
    gap: var(--st-space-2);
    height: var(--st-nav-subitem-h);
    flex-shrink: 0;
    padding: 0 var(--st-space-3) 0 40px;
    border-radius: var(--st-radius-sm);
    color: var(--st-text-on-chrome);
    text-decoration: none;
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 500;
    transition: background var(--st-duration-fast) var(--st-ease);
}

.shell-nav-subitem:hover {
    background: var(--st-surface-hover-on-chrome);
}

.shell-nav-subitem.active {
    background: var(--st-surface-active-on-chrome);
    box-shadow: inset 3px 0 0 var(--st-accent-on-chrome);
}

.shell-nav-subitem.shell-nav-item-disabled {
    height: var(--st-nav-subitem-h);
    padding: 0 var(--st-space-3) 0 40px;
}

/* COMPONENT-SPECS.md §9 "Collapsed behaviour" (0-MENU hardening, 2026-08-05): labels hidden (never
   shrunk), icons stay full size and centred, the active item keeps its 3px marker (untouched --
   .shell-nav-item.active above already applies regardless of width), every trigger gets a native
   title= tooltip (already on every item, not just collapsed, see AppShell.razor), and a group opens
   as a flyout instead of expanding inline. .shell-nav-trigger-label is deliberately a DIFFERENT class
   from the sub-items' own .shell-nav-label -- the flyout's contents must keep their labels even while
   the rail itself is collapsed, so the hide-on-collapse rule below must not reach inside
   .shell-nav-children at all. */
.shell-nav-collapsed {
    width: var(--st-nav-w-collapsed);
}

.shell-nav-collapsed .shell-nav-trigger-label {
    display: none;
}

/* A32 (2026-08-18): this override still stacked the header into a column at `flex: 0 0 auto`
   for the account name that used to live here -- SPRINT-QA 2026-08-14 #12 moved that name into
   the topbar, so this div now holds only the collapse toggle in BOTH states. The stale override
   still ran, making the collapsed header ~12-16px taller than the expanded header's fixed
   `flex: 0 0 48px`. Toggling .shell-nav-collapsed swaps that height in the same frame the width
   transition starts, so every row below the header snapped up/down at that instant -- read as
   "menu items run upward" during expand, since it landed at the same moment as the visible
   300ms width animation. Height now matches the base rule exactly in both states (inherited,
   not overridden here), so there is nothing to snap. */
.shell-nav-collapsed .shell-nav-header {
    justify-content: center;
    padding: 0;
}

.shell-nav-collapsed .shell-nav-account-name {
    flex: 0 0 auto;
    max-width: 100%;
    font-size: 10px;
    line-height: 1.2;
    text-align: center;
}

.shell-nav-collapsed .shell-nav-chevron {
    display: none;
}

.shell-nav-collapsed > .shell-nav-item,
.shell-nav-collapsed > .shell-nav-item-disabled,
.shell-nav-collapsed .shell-nav-group-wrap > .shell-nav-item {
    /* A55: no justify-content override anymore -- flex-start (the base rule's default, unset here
       on purpose) plus a padding-left computed to center the icon in the COLLAPSED rail's own
       70px width. Because padding transitions and justify-content does not, this glides in lockstep
       with .shell-nav's own width transition instead of snapping to a future center and being
       dragged as the rail narrows. calc() matches --st-nav-w-collapsed and .shell-nav-icon's width
       exactly so this stays correct if either token changes. */
    padding: 0 calc((var(--st-nav-w-collapsed) - 1.3rem) / 2);
    gap: 0;
}

/* Bug #17 (2026-08-10): `position: fixed`, not `absolute` -- .shell-nav has `overflow-y: auto` for
   its own scrollbar, which clips ANY absolutely-positioned descendant that extends past the 70px
   collapsed rail's own right edge (confirmed live: the flyout opened, class and opacity both
   correct, but was never actually visible on screen). `fixed` escapes ALL ancestor overflow
   clipping by definition, so top/left are no longer expressible as a CSS offset from the trigger --
   AppShell.razor's OnGroupMouseEnter measures the real trigger position via nav-flyout.js and binds
   it as an inline style instead. Max-height-based clipping isn't meaningful here (there's no layout
   height to reclaim by collapsing it), so this overrides the base rule's height/overflow approach
   with a plain fade + slight rise instead, closed by default via opacity/visibility (not max-height)
   so this flyout can also be revealed on HOVER (PEN-2 item 1, AppShell.razor's OnGroupMouseEnter)
   without a click first. */
.shell-nav-collapsed .shell-nav-children {
    position: fixed;
    min-width: 200px;
    background: var(--st-surface-overlay);
    box-shadow: var(--st-shadow-md);
    border-radius: var(--st-radius-md);
    padding: var(--st-space-2);
    z-index: 20;
    /* R4 B17: was `max-height: none; overflow: visible`, which was fine while every group was small.
       Setup's ~30 children make this card ~1380px tall -- taller than most laptop screens, so the
       bottom half of the group became unreachable in the collapsed rail (position: fixed means the
       rail's own overflow-y cannot scroll it either). Bounded to the viewport with its own scroll.
       Kept off the base .shell-nav-children rule deliberately: there, overflow: hidden IS the collapse
       animation, and giving it a scrollbar would break it. */
    max-height: calc(100vh - var(--st-topbar-h) - var(--st-space-4));
    overflow-y: auto;
    visibility: hidden;
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
}

.shell-nav-collapsed .shell-nav-children.shell-nav-children-open {
    visibility: visible;
    opacity: 1;
    transform: translateY(0);
    transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0s;
}

/* Bug #17 follow-up, found while verifying the position:fixed fix live: `.shell-nav-subitem`'s
   *-on-chrome tokens (color, hover/active background) are tuned for sitting on the DARK rail
   background (the inline-accordion case) -- but the collapsed flyout is `--st-surface-overlay`, a
   LIGHT popover card, not chrome. Left unpaired, this rendered literally white text on a white
   background: invisible, not just low-contrast. Re-paired to the same *-primary/*-hover tokens
   every other light-surface popover in this file uses. Left padding also drops the 40px inline-
   accordion indent -- that aligns a sub-item under its own parent icon in the rail; a standalone
   floating card has no parent icon to align under, so it read as a large empty gap instead. */
.shell-nav-collapsed .shell-nav-children .shell-nav-subitem {
    color: var(--st-text-primary);
    padding-left: var(--st-space-3);
}

.shell-nav-collapsed .shell-nav-children .shell-nav-subitem:hover {
    background: var(--st-surface-hover);
}

.shell-nav-collapsed .shell-nav-children .shell-nav-subitem.active {
    background: var(--st-surface-active);
    box-shadow: inset 3px 0 0 var(--st-accent);
}

.shell-main {
    flex: 1;
    min-width: 0;
    overflow: auto;
    padding: var(--st-space-4);
    background: var(--st-surface-app);
}

/* 0-LAYOUT (2026-08-04), rule 4: "Content has a max width -- nothing stretches just because the
   monitor is wide. Full-width is EARNED: data grids and report tables only." Two variants because
   data screens and form screens earn different widths -- a screen applies exactly one to its own
   root element. Neither is used by Dashboard (waits for DASHBOARD-SPEC.md, item 0x) or Login (its
   own spec already encodes correct desktop layout via .lg-shell). */
.st-page-wide {
    max-width: 1400px;
    margin: 0 auto;
}

.st-page-narrow {
    max-width: 800px;
    margin: 0 auto;
}

@media (max-width: 768px) {
    /* Task 28 (2026-08-04): CI's mobile-viewport check measured 89-98px of real horizontal overflow on
       every authenticated screen -- traced to .shell-topbar, not the nav or DataGrid (neither was
       independently overflowing at 375px in direct testing). .shell-brand's flex-basis pins it to the
       desktop NAV's width so the wordmark aligns above the sidebar -- on mobile the nav isn't a sidebar
       anymore, so that fixed 194px is pure waste and the single largest contributor. */
    .shell-brand {
        flex: 0 1 auto;
    }

    /* 0-REVIEW-9 #4 (2026-08-06): "keep phone behavior sane -- description may drop on narrow
       widths." Title stays (it's short and the whole reason the ribbon exists); the description is
       the long, droppable part -- topbar space is already tight against the account name/icons at
       this width. */
    .shell-page-name-sep,
    .shell-page-name-description {
        display: none;
    }

    .shell-page-name {
        margin-left: var(--st-space-2);
    }

    .shell-user-email {
        display: none;
    }

    /* W550 (2026-08-17, OldApp): the same reasoning that already hid .shell-user-email here. At 375px
       the topbar measured scrollWidth 469 against a 375 viewport -- 94px over -- on EIGHT separate
       E2E screens, all with the identical element list, so it is one bug in the shell rather than
       eight page bugs. The account name is the widest purely-informational thing left in the cluster
       (`max-width: min(32ch, 30vw)`), and every remaining sibling is a CONTROL -- Support, Billing,
       Settings, theme, sign out -- which must stay reachable. So the label yields and the controls
       keep their width. Whose books you are in is still on the account screen and in the drawer; on a
       phone it was already truncated to a few characters here, which is not worth 94px of broken
       layout on every authenticated screen. */
    .shell-topbar-account-name {
        display: none;
    }

    /* W550 SECOND PASS (2026-08-17, OldApp). The first pass (68a5818) made the page-name title
       shrinkable and gave .shell-topbar-actions min-width: 0. It helped -- the uniform 469px dropped
       -- but the run for `cfd3aeb` still reports overflow, and on the CLEANEST screen it is now
       404px against a 375 viewport with .shell-signout-btn as the ONLY overflowing element. That
       number is the topbar's own min-content width, and 29px is the deficit to close.

       Where the 404 comes from, added up rather than guessed: 16+16 bar padding, a 35px hamburger,
       the 20px mark plus ~85px of "SellerTally" wordmark, then the actions cluster, every member of
       which is a control that must stay reachable -- Support, Billing, the Settings gear (35), the
       theme button (35), Sign out (43) -- plus five 4px gaps. Only two of those are not controls:
       the bar's own padding and the wordmark.

       So those two yield, and nothing clickable does:
       (a) the wordmark TEXT goes, the 20px brand MARK stays -- the anchor keeps its "Go to
           dashboard" aria-label, and the drawer header carries the product name anyway, so the
           identity is not lost, it is just not spending 85px of a 375px bar;
       (b) the bar padding halves, 16px -> 8px each side;
       (c) .shell-page-name's margin-left goes with it -- at this width the title is already the
           first thing that shrinks, so its 8px indent buys nothing.
       Together that is ~101px of headroom against a 29px deficit, which is deliberate slack: the
       cluster grows by one 35px control every time a topbar utility is added, and this bar has
       gained three since it was last measured.

       WHAT THIS DOES NOT EXPLAIN, stated so the next pass does not read a partial fix as a whole
       one: the overflow is 404 on SubAccounts but 487 on SecAccountList and 464 on CoaTree, with an
       identical topbar on all three. A topbar-only cause would be the SAME number on every screen.
       So there is a SECOND, per-screen contributor -- most likely page content whose width the
       topbar then inherits by stretching -- and the shell-* elements the evidence names on the wider
       screens are symptoms of it, not the cause. This change should take the 404-class screens
       green; if the wider ones stay red with a smaller number, that is the confirmation, not a
       regression. Read the run for THIS sha by run header in ci-e2e-failures.txt, never by tail. */
    .shell-brand-text {
        display: none;
    }

    /* W550 THIRD PASS (2026-08-18, OldApp): the second pass's ~101px headroom estimate did not hold --
       `.shell-signout-btn` was still the sole (or largest) overflowing element on every re-measured
       screen, 23-75px over depending on page, confirming the row's own "second, per-screen contributor"
       prediction rather than refuting the wordmark/padding fix. Rather than chase more px off a
       text-label button, give it the same icon-only treatment `.shell-hamburger`/`.shell-icon-btn`
       already use at this width: the glyph (matching the ThemeGlyph unicode convention) replaces the
       "Sign out"/"Exit demo" text, `aria-label` on the button keeps it accessible, and the control
       shrinks from a variable-width text button to a fixed 35px circle -- the one element this row
       named as the floor becomes as small as every other topbar icon. */
    /* W550 FIFTH PASS (2026-08-19, OldApp, bug 65 continuation): third pass sized this circle to
       match `.shell-icon-btn`'s THEN-current 35px, but fourth pass shrank `.shell-icon-btn` itself to
       32px (bell/gear/theme) without touching this selector -- `.shell-signout-btn` has its own class,
       not `.shell-icon-btn`, so the fourth pass's shrink never reached it. Matching it closes the same
       3px per icon the fourth pass already banked for its siblings; verified by grep that AppShell.razor
       renders this button as `class="shell-signout-btn"` only, no shared class, so this was the one
       cluster member the fourth pass's rule couldn't have reached. */
    .shell-signout-btn {
        width: 32px;
        height: 32px;
        padding: 0;
        justify-content: center;
        border-radius: var(--st-radius-pill);
    }

    .shell-signout-btn-icon {
        display: inline;
    }

    .shell-signout-btn-label {
        display: none;
    }

    /* W550 FOURTH PASS (2026-08-19, OldApp, bug 65): A37 (feb3d8b) added the notification bell to
       `.shell-topbar-actions` AFTER third pass's budget was measured -- one more 35px `.shell-icon-btn`
       plus its gap, ~39px, is exactly the kind of growth the second pass's own comment warned about
       ("the cluster grows by one 35px control every time a topbar utility is added"). Stage E2E on
       `acd05f5` shows the overflow back, 30-47px over on most signed-in screens (NewTransactionPage,
       ChangePasswordPage, ReportPicker, CoaTree) -- consistent with the bell alone, not a new
       per-screen cause. Recovering it from icon spacing rather than hiding a control again: every
       `.shell-icon-btn` in the cluster (bell, settings gear, theme) shrinks 35px -> 32px and the
       cluster's own gap halves, 4px -> 2px. With up to 4 icon buttons and 5 gaps in the cluster that
       is up to ~22px back, plus the bar padding below drops once more, 8px -> 4px each side (8px more)
       -- ~30px combined against the ~39px bell added. NOT verified against a live E2E run (this
       session cannot use a browser) -- next stage run is the check. SecAccountListPage's much larger
       gap (83px over) is unexplained by the bell alone and still matches third pass's own "second,
       per-screen contributor" prediction; if this pass leaves it red, that confirms a page-specific
       cause there, not a regression in this fix. */
    .shell-topbar-actions {
        gap: 2px;
    }

    .shell-icon-btn {
        width: 32px;
        height: 32px;
    }

    .shell-topbar {
        padding: 0 var(--st-space-1);
    }

    .shell-page-name {
        margin-left: 0;
    }

    .shell-hamburger {
        display: inline-flex;
    }

    /* COMPONENT-SPECS.md §9 Phone: "the nav becomes an overlay drawer... position: fixed, width:
       --st-nav-w, height: 100vh, z-index: 1000, transform: translateX(-100%) -> translateX(0)".
       Replaces the earlier always-visible wrapped-row stopgap (shipped same night to kill the
       overflow bug fast) with the real spec behavior -- hidden by default, opened via the
       hamburger, not permanently eating vertical space above every screen's content. */
    .shell-nav {
        position: fixed;
        inset: 0 auto 0 0;
        width: var(--st-nav-w);
        z-index: 1000;
        flex-direction: column;
        flex-wrap: nowrap;
        transform: translateX(-100%);
        transition: transform var(--st-duration-slow) var(--st-ease);
        border-right: 1px solid var(--st-border-on-chrome);
        border-bottom: none;
        box-shadow: var(--st-shadow-md);
    }

    .shell-nav.shell-nav-open {
        transform: translateX(0);
    }

    /* R4 A44: the rail is `position: fixed` here (removed from .shell-body's flex flow), so the
       resizer -- still an ordinary flex sibling -- would otherwise show up as an empty 6px column
       ahead of .shell-main. Dragging a fixed-overlay drawer's width is not a phone interaction either
       way (COMPONENT-SPECS.md §9 draws no resize affordance for it). */
    .shell-nav-resizer {
        display: none;
    }

    .shell-nav-close {
        display: flex;
        align-items: center;
        justify-content: center;
        align-self: flex-end;
        width: var(--st-tap-min);
        height: var(--st-tap-min);
        margin-bottom: var(--st-space-2);
        border: none;
        border-radius: var(--st-radius-sm);
        background: transparent;
        color: var(--st-text-on-chrome);
        font-size: var(--st-text-base);
        cursor: pointer;
    }

    .shell-nav-close:hover {
        background: var(--st-surface-hover-on-chrome);
    }

    /* Backdrop: a real <button>, not a bare div, so it's natively focusable/clickable/labeled --
       matches .st-modal-scrim's own reasoning (COMPONENT-SPECS.md §7) applied to the drawer. */
    .shell-nav-scrim {
        display: block;
        position: fixed;
        inset: 0;
        z-index: 999;
        border: none;
        padding: 0;
        background: var(--st-scrim);
        cursor: pointer;
    }

    .shell-nav-item,
    .shell-nav-item-disabled,
    .shell-nav-subitem {
        height: var(--st-tap-min);
    }

    /* COMPONENT-SPECS.md §9: "The drawer never coexists with the collapsed 70px state -- that
       variant is desktop-only." The toggle only makes sense on desktop; hidden here. Belt-and-
       suspenders against the state itself: isNavCollapsed lives in Blazor memory, not tied to
       viewport width, so a user who collapses on desktop and then resizes/rotates down to phone
       width (no page reload, same live SPA session) must not end up with a squashed 70px drawer --
       every collapsed-only visual effect is neutralised here regardless of the class still being
       present in the DOM. */
    .shell-nav-header {
        display: none;
    }

    .shell-nav.shell-nav-collapsed {
        width: var(--st-nav-w);
    }

    .shell-nav-collapsed .shell-nav-trigger-label {
        display: block;
    }

    .shell-nav-collapsed .shell-nav-chevron {
        display: inline-flex;
    }

    .shell-nav-collapsed > .shell-nav-item,
    .shell-nav-collapsed > .shell-nav-item-disabled,
    .shell-nav-collapsed .shell-nav-group-wrap > .shell-nav-item {
        justify-content: flex-start;
        padding: 0 var(--st-space-3);
        gap: var(--st-space-2);
    }

    /* PEN-2 item 8 (2026-08-08): on phone, "collapsed" renders inline (position:static above), not as
       the desktop flyout -- so it needs the desktop flyout rule's opacity/visibility/transform
       CLOSED-state properties overridden back to the base rule's max-height approach, otherwise
       visibility:hidden (unlike display:none) still reserves full layout height while "closed,"
       permanently pushing every nav item below it down regardless of expand state. */
    .shell-nav-collapsed .shell-nav-children {
        position: static;
        background: transparent;
        box-shadow: none;
        padding: 0;
        max-height: 0;
        overflow: hidden;
        visibility: visible;
        transform: none;
        transition: max-height 0.3s ease-in-out, opacity 0.3s ease-in-out;
    }

    .shell-nav-collapsed .shell-nav-children.shell-nav-children-open {
        max-height: 500px;
        opacity: 1;
    }
}

/* Error boundary */

.error-boundary-fallback {
    max-width: 28rem;
    margin: var(--st-space-6) auto;
    padding: var(--st-space-5);
    text-align: center;
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-md);
    box-shadow: var(--st-shadow-md);
}

.error-boundary-title {
    font-family: var(--st-font-ui);
    font-weight: 600;
    font-size: var(--st-text-lg);
    margin: 0 0 var(--st-space-2);
    color: var(--st-text-primary);
}

.error-boundary-detail {
    color: var(--st-text-secondary);
    margin: 0 0 var(--st-space-4);
}

.error-boundary-reload {
    height: var(--st-control-h);
    padding: 0 var(--st-space-4);
    background: var(--st-accent-fill);
    color: var(--st-text-on-fill);
    border: 1px solid var(--st-accent-fill);
    border-radius: var(--st-radius-sm);
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    cursor: pointer;
}

/* ── Loading spinner (PEN-2 item 10, 2026-08-08) ───────────────────────────────────────────────
   Was N shimmering placeholder rows (COMPONENT-SPECS.md §5's original skeleton spec) -- Mazhar
   wants a real wait indicator instead, app-wide. One shared component (LoadingSkeleton.razor,
   ~20 screens already use it), so replacing what it renders here fixes every screen at once,
   nothing per-screen to touch. */

.loading-spinner {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--st-space-3);
    padding: var(--st-space-6) 0;
}

.loading-spinner-ring {
    width: 32px;
    height: 32px;
    border: 3px solid var(--st-border-subtle);
    border-top-color: var(--st-accent);
    border-radius: 50%;
    animation: loading-spinner-spin 0.8s linear infinite;
}

.loading-spinner-text {
    color: var(--st-text-muted);
    font-size: var(--st-text-sm);
}

@keyframes loading-spinner-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    .loading-spinner-ring {
        animation: none;
    }
}

/* ── Empty state (COMPONENT-SPECS.md §4) ───────────────────────────────────────────────────── */

.empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--st-space-3);
    padding: 40px var(--st-space-5);
    text-align: center;
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-md);
}

.empty-state-title {
    font-family: var(--st-font-ui);
    font-size: 15px;
    font-weight: 600;
    color: var(--st-text-primary);
    margin: 0;
}

.empty-state-detail {
    font-size: var(--st-text-sm);
    color: var(--st-text-muted);
    max-width: 34ch;
    margin: 0;
}

/* Auth screens (Login etc.) -- pre-boot chrome doesn't apply here (no shell yet), plain surface. */

.auth-card {
    max-width: 24rem;
    margin: var(--st-space-6) auto;
    padding: var(--st-space-5);
    background: var(--st-surface);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-md);
    box-shadow: var(--st-shadow-md);
}

.auth-title {
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xl);
    font-weight: 700;
    margin: 0 0 var(--st-space-4);
    color: var(--st-text-primary);
}

.auth-field {
    display: flex;
    flex-direction: column;
    gap: var(--st-space-1);
    margin-bottom: var(--st-space-3);
}

.auth-field label {
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 500;
    color: var(--st-text-secondary);
}

.auth-field input {
    height: var(--st-control-h);
    font-family: var(--st-font-text);
    font-size: var(--st-text-sm);
    padding: 0 10px;
    border: 1px solid var(--st-border-input);
    border-radius: var(--st-radius-sm);
    background: var(--st-surface);
    color: var(--st-text-primary);
}

.auth-field input:hover {
    border-color: var(--st-text-secondary);
}

.auth-field input:focus-visible {
    outline: 2px solid var(--st-focus-ring);
    outline-offset: 1px;
    border-color: var(--st-accent);
}

.auth-submit {
    width: 100%;
    height: var(--st-control-h);
    background: var(--st-accent-fill);
    color: var(--st-text-on-fill);
    border: 1px solid var(--st-accent-fill);
    border-radius: var(--st-radius-sm);
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    cursor: pointer;
    transition: background var(--st-duration-fast) var(--st-ease);
}

.auth-submit:hover:not(:disabled) {
    background: var(--st-accent-fill-hover);
}

.auth-submit:disabled {
    opacity: var(--st-disabled-opacity);
    cursor: not-allowed;
}

.auth-error {
    color: var(--st-danger-on-subtle);
    background: var(--st-danger-subtle);
    box-shadow: inset 3px 0 0 var(--st-danger);
    padding: var(--st-space-3);
    border-radius: var(--st-radius-sm);
    font-size: var(--st-text-sm);
    margin-bottom: var(--st-space-3);
}

/* Charts (Kernel/Components/Charts/*.razor) -- shared here rather than per-component CSS isolation
   because BarChart/LineChart/DonutChart intentionally reuse the same class names for the same
   visual language (one axis style, one label style, everywhere a chart appears). Chart colours are
   categorical only (--st-chart-*) and never carry meaning -- COMPONENT-SPECS.md's own rule. */

.chart-svg {
    width: 100%;
    height: auto;
    max-height: 220px;
}

.chart-axis-line {
    stroke: var(--st-border-strong);
    stroke-width: 1;
}

.chart-axis-line-zero {
    stroke: var(--st-text-muted);
    stroke-dasharray: 3 3;
}

.chart-axis-label {
    fill: var(--st-text-secondary);
    font-size: 9px;
    font-family: var(--st-font-ui);
}

.chart-bar {
    fill: var(--st-accent);
}

.chart-bar-negative {
    fill: var(--st-danger);
}

.chart-line {
    fill: none;
    stroke: var(--st-accent);
    stroke-width: 2;
    stroke-linejoin: round;
    stroke-linecap: round;
}

.chart-area-fill {
    fill: var(--st-accent);
    opacity: 0.18;
}

.chart-point {
    fill: var(--st-surface);
    stroke: var(--st-accent);
    stroke-width: 1.5;
}

.donut-chart-wrap {
    display: flex;
    align-items: center;
    gap: var(--st-space-4);
    flex-wrap: wrap;
}

.chart-donut-svg {
    width: 120px;
    height: 120px;
    max-height: none;
    flex-shrink: 0;
}

.chart-donut-slice {
    stroke: var(--st-surface);
    stroke-width: 1;
}

.chart-legend {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--st-space-1);
    flex: 1;
    min-width: 140px;
}

.chart-legend-item {
    display: flex;
    align-items: center;
    gap: var(--st-space-2);
    font-size: var(--st-text-xs);
    color: var(--st-text-secondary);
}

.chart-legend-swatch {
    width: 10px;
    height: 10px;
    border-radius: var(--st-radius-sm);
    flex-shrink: 0;
}

.chart-legend-label {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.chart-legend-value {
    color: var(--st-text-primary);
    font-family: var(--st-font-num);
    font-variant-numeric: tabular-nums;
}

/* ── Form field (COMPONENT-SPECS.md §2) ────────────────────────────────────────────────────── */

.st-fields {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(272px, 1fr));
    gap: var(--st-space-4) var(--st-space-5);
}

/* 0-VISUAL-PARITY finding 2 (2026-08-05), COMPONENT-SPECS.md §1a "Filter bar": one shared
   implementation instead of TransactionSearch/SubAccountSearch each carrying their own near-
   identical copy. A horizontal row of .kernel-field controls (each already capped to its own
   content-appropriate width -- date/number inputs narrower still, see .kernel-input[type] below)
   that wraps to additional rows before it ever stacks to a single narrow column. */
.kernel-filter-bar {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    gap: var(--st-space-4);
}

@media (max-width: 767px) {
    .kernel-filter-bar {
        flex-direction: column;
        align-items: stretch;
    }
}

.kernel-field {
    display: flex;
    flex-direction: column;
    gap: var(--st-space-1);
    /* 0-LAYOUT (2026-08-04), rule 2: "Inputs size to content, never to viewport." .kernel-field is a
       flex item everywhere it's used, and flexbox's default align-items: stretch was silently
       stretching every field to its ANCESTOR's full width -- with no width cap anywhere in the
       chain, that ancestor was ultimately the viewport. max-width here (not just on .kernel-input)
       means the label/help text block stays the same width as its control, not just the control. */
    max-width: 400px;
}

.kernel-field > span:first-child {
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 500;
    color: var(--st-text-secondary);
}

.kernel-field-hint {
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    color: var(--st-text-muted);
}

.kernel-field-hint a {
    color: var(--st-text-muted);
    text-decoration: underline;
}

.kernel-field-hint a:hover {
    color: var(--st-text-secondary);
}

/* A183 (Mazhar 08-20 late): "Show all items" under a vendor-scoped dropdown -- a real <button> (not an
   <a href>, there is nowhere to navigate) styled to read as a text link, same size/color family as
   .kernel-field-hint so it sits quietly under the control it belongs to. */
.kernel-link-btn {
    display: block;
    margin-top: 4px;
    padding: 0;
    border: none;
    background: none;
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    color: var(--st-text-muted);
    text-decoration: underline;
    cursor: pointer;
}

.kernel-link-btn:hover {
    color: var(--st-text-secondary);
}

.kernel-input {
    width: 100%;
    height: var(--st-control-h);
    border: 1px solid var(--st-border-input);
    border-radius: var(--st-radius-sm);
    padding: 0 10px;
    background: var(--st-surface);
    color: var(--st-text-primary);
    font-family: var(--st-font-text);
    font-size: var(--st-text-sm);
    transition: border-color var(--st-duration-fast) var(--st-ease);
}

.kernel-input::placeholder {
    color: var(--st-text-muted);
}

.kernel-input:hover {
    border-color: var(--st-text-secondary);
}

.kernel-input:focus-visible {
    outline: 2px solid var(--st-focus-ring);
    outline-offset: 1px;
    border-color: var(--st-accent);
}

.kernel-input:disabled {
    background: var(--st-surface-alt);
    opacity: var(--st-disabled-opacity);
    cursor: not-allowed;
}

.kernel-input[aria-invalid="true"] {
    border-color: var(--st-danger);
}

textarea.kernel-input {
    height: auto;
    padding: var(--st-space-2) 10px;
}

/* 0-LAYOUT rule 2's own examples: "date fields date-wide, amount fields amount-wide" -- narrower
   than the 400px generic-text cap, sized to what the content actually needs. A .kernel-field
   wrapping one of these overrides its own max-width to match, so the label doesn't sit orphaned
   above a much narrower control. */
.kernel-input[type="date"],
.kernel-field:has(> .kernel-input[type="date"]) {
    max-width: 160px;
}

.kernel-input[type="number"],
.kernel-field:has(> .kernel-input[type="number"]) {
    max-width: 140px;
}

/* COMPONENT-SPECS.md §2 Form field: "--st-font-num right-aligned for amounts" -- every numeric
   control, not just read-only totals. Found unapplied everywhere during the 0-CONSISTENCY sweep
   (2026-08-05): TrnEdit's qty/unit-price/amount cells and Form1099K's monthly inputs were all
   left-aligned in the body text font. */
.kernel-input[type="number"] {
    text-align: right;
    font-family: var(--st-font-num);
    font-variant-numeric: tabular-nums;
}

/* Bug ledger #14 (2026-08-10, Mazhar): "a plain text box is easier to type into" -- the native
   up/down spinner steals horizontal space from a right-aligned amount and invites accidental
   scroll-wheel value changes; removed app-wide (every numeric input, not just .kernel-input --
   the checklist editor's own type="number" cells don't carry that class). Firefox needs the
   standard property, WebKit/Blink (Chrome/Edge/Safari) only respect the pseudo-elements. */
input[type="number"] {
    -moz-appearance: textfield;
}

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

/* R4 B6 (Mazhar, 2026-08-17): "Setup -> Sub Account: Priority field = numeric up/down control." The
   rule directly above removed the spinner from EVERY number input app-wide for bug 14, so simply
   marking the field type="number" — which it already was — could never produce one. This is the
   deliberate opt-back-in, and it is a class rather than a relaxation of the rule above because the
   reason bug 14 exists is unchanged for amounts: a spinner beside a right-aligned amount steals space
   and turns a stray scroll into a silent value change. Priority is neither right-aligned nor an amount,
   it is a small ordinal a user nudges by one, which is the one case a stepper is genuinely better at.

   Both properties are needed for the same reason the rule above needs both: Firefox honours the
   standard property, Chrome/Edge/Safari only the pseudo-elements.

   A80 (Mazhar 08-19, "still no up/down control", SLIPPED TWICE): root-caused via specificity math, not
   a browser (this batch bars one) -- the ORIGINAL selectors here were bare `.kernel-input-stepper`
   (specificity 0,0,1,0) and `.kernel-input-stepper::-webkit-inner-spin-button` (0,0,1,1), both STRICTLY
   LOWER than the suppress rule above (`input[type="number"]` = 0,0,1,1; its own `::-webkit-...` pair =
   0,0,1,2) -- the type selector `input` and the attribute selector `[type="number"]` both count, so the
   suppress rule always won regardless of source order, and the "outranks" claim in this comment's
   earlier version was never actually tested (browser use has been off this batch since before B6
   shipped). The opt-in class was applied correctly on the input (SubAccountEdit.razor's Priority field)
   and did nothing, which is exactly what a lost/slipped fix looks like from the outside. Fix: prefix
   every override selector with `input[type="number"]` too, so it matches the suppress rule's own
   specificity plus the extra class -- strictly higher, no reliance on source-order tie-breaking. */
input[type="number"].kernel-input-stepper {
    -moz-appearance: number-input;
}

input[type="number"].kernel-input-stepper::-webkit-inner-spin-button,
input[type="number"].kernel-input-stepper::-webkit-outer-spin-button {
    -webkit-appearance: inner-spin-button;
    opacity: 1;
    margin: 0;
}

/* ── Shared amount formatting (COMPONENT-SPECS.md §0 type scale: "every amount" gets
   --st-font-num with tabular-nums so digits align down a column). 0-CONSISTENCY (2026-08-05):
   this rule existed in the spec since day one but no shared class carried it -- each screen that
   rendered a total either applied tabular-nums alone (CoaTree) or nothing at all (TrnEdit,
   TransactionSearch, Form1099K). One class now, applied everywhere a monetary amount renders. */
.st-amount {
    font-family: var(--st-font-num);
    font-variant-numeric: tabular-nums;
}

/* R4 B19 (Mazhar, 2026-08-17): "'Amount' header must sit right-aligned over its numbers." The header
   half landed in DataGrid's RightAlignedColumns, and it looked like the whole job because .st-amount
   was already on the numeric cells -- but .st-amount above is FONT ONLY. It never carried
   text-align, so the header moved right and its own figures stayed left, which is the same
   misalignment he reported, one column narrower. Scoped to `td` deliberately: .st-amount is also on
   inline spans and summary pills (TrnEdit's totals bar, the dashboard tiles) where a right-align
   would be wrong, and those are not table cells. Any grid column whose cells carry .st-amount is a
   numeric column by definition, so this is the one place the rule belongs rather than per screen.
   Cells holding a narrow control (TrnEdit's 100px line inputs) get the same treatment -- the input
   is pushed to the cell's right edge, so its own right-aligned digits land under the header instead
   of at whatever x the control's width happens to end. */
td.st-amount {
    text-align: right;
}

/* ── Buttons and the placement grammar (COMPONENT-SPECS.md §3) ────────────────────────────────
   Secondary is the unmarked default (.kernel-btn alone) -- matches the existing convention of every
   screen already reaching for it as the base class. */

.kernel-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: var(--st-control-h);
    padding: 0 var(--st-space-4);
    min-width: 132px;
    border: 1px solid var(--st-border-input);
    border-radius: var(--st-radius-sm);
    background: var(--st-surface);
    color: var(--st-text-primary);
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    font-weight: 500;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--st-duration-fast) var(--st-ease), border-color var(--st-duration-fast) var(--st-ease);
}

/* SPRINT-QA 2026-08-14 #26 -- THIS RULE WAS THE WHOLE BUG, and it is worth spelling out because it
   looked like three unrelated complaints. Mazhar: "Add turns black in dark mode; Save goes black in
   dark mode (same as Cancel) while distinct in light."
   `[data-theme="dark"] .kernel-btn` has specificity (0,2,0) -- an attribute selector counts as a
   class -- while `.kernel-btn-primary` has (0,1,0). Every primary button also carries .kernel-btn,
   so in DARK MODE ONLY this rule outranked the primary fill and blanked it to transparent: Save and
   Add rendered identical to Cancel, while light mode (which has no such rule) kept them blue. The
   fix is to exclude the buttons that own their own background, not to raise their specificity --
   raising it would leave the next variant to rediscover this. */
[data-theme="dark"] .kernel-btn:not(.kernel-btn-primary):not(.kernel-btn-destructive) {
    background: transparent;
}

.kernel-btn:hover:not(:disabled) {
    background: var(--st-surface-hover);
    border-color: var(--st-text-secondary);
}

.kernel-btn:active:not(:disabled) {
    background: var(--st-surface-active);
}

.kernel-btn-primary {
    border-color: var(--st-accent-fill);
    background: var(--st-accent-fill);
    color: var(--st-text-on-fill);
}

.kernel-btn-primary:hover:not(:disabled) {
    background: var(--st-accent-fill-hover);
    border-color: var(--st-accent-fill-hover);
}

.kernel-btn-primary:active:not(:disabled) {
    background: var(--st-accent-fill-active);
    border-color: var(--st-accent-fill-active);
}

.kernel-btn-destructive {
    border-color: var(--st-danger-fill);
    background: var(--st-danger-fill);
    color: var(--st-text-on-fill);
}

.kernel-btn-destructive:hover:not(:disabled) {
    background: var(--st-danger-fill-hover);
    border-color: var(--st-danger-fill-hover);
}

.kernel-btn:disabled {
    opacity: var(--st-disabled-opacity);
    cursor: not-allowed;
    pointer-events: none;
}

.kernel-btn-sm {
    height: var(--st-control-h-sm);
    min-width: 0;
}

.kernel-btn-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--st-icon-btn);
    height: var(--st-icon-btn);
    min-width: 0;
    padding: 0;
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-sm);
    background: transparent;
    color: var(--st-text-secondary);
    cursor: pointer;
    transition: background var(--st-duration-fast) var(--st-ease);
}

.kernel-btn-icon:hover:not(:disabled) {
    background: var(--st-surface-hover);
}

/* ROUND 2 (2026-08-15), Mazhar on the transaction search grid: "delete button red (or per Claude
   Design palette) - dull gray now". The gray is .kernel-btn-icon's own --st-text-secondary, which is
   correct for a neutral icon button and wrong for the one action on the row that destroys something.

   Colour only -- geometry stays on the base class, because his OTHER item in the same list is that the
   edit screen's delete icon must be CONSISTENT with this one. A danger variant that also changed size
   would make those two harder to reconcile, not easier. --st-danger is themed (it lightens to #f2707c
   in dark mode), so this stays legible on the dark chrome instead of going muddy. */
.kernel-btn-icon-danger {
    color: var(--st-danger);
    border-color: var(--st-danger-subtle);
}

.kernel-btn-icon-danger:hover:not(:disabled) {
    background: var(--st-danger-subtle);
}

/* Placement grammar -- primary last in DOM order (tab order = visual order, never `order:`),
   cancel immediately to its left, constructive secondary actions in a separate left-hand group. */
.st-actionbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--st-space-4);
}

.st-actionbar__primary {
    display: flex;
    gap: var(--st-space-4);
    margin-left: auto;
}

@media (max-width: 640px) {
    .st-actionbar {
        position: sticky;
        bottom: 0;
        background: var(--st-surface);
        border-top: 1px solid var(--st-border);
        padding: var(--st-space-3);
    }

    .st-actionbar__primary {
        width: 100%;
    }

    .st-actionbar__primary .kernel-btn {
        flex: 1;
        min-width: 0;
        height: var(--st-tap-min);
    }

    .st-actionbar__primary .kernel-btn-primary {
        flex: 1.4;
    }
}

/* ── Banner / alert (COMPONENT-SPECS.md §6) ────────────────────────────────────────────────── */

.kernel-error {
    display: flex;
    align-items: center;
    gap: var(--st-space-3);
    padding: var(--st-space-3);
    border-radius: var(--st-radius-sm);
    background: var(--st-danger-subtle);
    box-shadow: inset 3px 0 0 var(--st-danger);
    color: var(--st-danger-on-subtle);
    font-size: var(--st-text-sm);
}

.kernel-banner {
    display: flex;
    align-items: center;
    gap: var(--st-space-3);
    padding: var(--st-space-3);
    border-radius: var(--st-radius-sm);
    font-size: var(--st-text-sm);
}

.kernel-banner-message {
    flex: 1;
}

.kernel-banner-info {
    background: var(--st-accent-subtle);
    box-shadow: inset 3px 0 0 var(--st-accent);
    color: var(--st-accent-on-subtle);
}

.kernel-banner-success {
    background: var(--st-success-subtle);
    box-shadow: inset 3px 0 0 var(--st-success);
    color: var(--st-success-on-subtle);
}

.kernel-banner-warning {
    background: var(--st-warning-subtle);
    box-shadow: inset 3px 0 0 var(--st-warning);
    color: var(--st-warning-on-subtle);
}

.kernel-banner-danger {
    background: var(--st-danger-subtle);
    box-shadow: inset 3px 0 0 var(--st-danger);
    color: var(--st-danger-on-subtle);
}

/* A labeled card grouping a few related fields -- introduced for AccountSettings, promoted here
   once Form1099K needed the identical pattern: Blazor's CSS isolation means a component-scoped
   class never applies outside its own .razor file, so a second screen needing the same look
   belongs in the shared kernel stylesheet, not copy-pasted into a second .razor.css. */
.kernel-card {
    display: flex;
    flex-direction: column;
    gap: var(--st-space-2);
    padding: var(--st-space-4);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-md);
    background: var(--st-surface);
}

.kernel-card-title {
    margin: 0 0 var(--st-space-1);
    font-family: var(--st-font-ui);
    font-size: var(--st-text-base);
    font-weight: 600;
    color: var(--st-text-primary);
}

/* TreeView (Kernel/Components/TreeView.razor) -- generic expand/collapse tree. */

.tree-node-row {
    display: flex;
    align-items: center;
    width: 100%;
    padding: var(--st-space-2);
    border: none;
    background: none;
    color: var(--st-text-primary);
    font-family: var(--st-font-text);
    font-size: var(--st-text-sm);
    text-align: left;
    cursor: default;
    border-bottom: 1px solid var(--st-border);
}

.tree-node-row.tree-clickable {
    cursor: pointer;
}

.tree-node-row.tree-clickable:hover {
    background: var(--st-surface-hover);
}

.tree-node-label {
    display: flex;
    align-items: center;
    gap: var(--st-space-2);
    flex: 1;
    min-width: 0;
}

.tree-toggle,
.tree-toggle-spacer {
    width: 0.75rem;
    flex-shrink: 0;
}

.tree-toggle::before {
    content: "▸";
    color: var(--st-text-secondary);
    display: inline-block;
    transition: transform var(--st-duration-fast) var(--st-ease);
}

.tree-toggle-expanded::before {
    transform: rotate(90deg);
}

.tree-children {
    margin-left: var(--st-space-4);
}

/* Pager (Kernel/Components/Pager.razor) -- secondary buttons at control-h-sm, per grid footer §1. */

.kernel-pager {
    display: flex;
    align-items: center;
    gap: var(--st-space-2);
    font-size: var(--st-text-xs);
    color: var(--st-text-muted);
    font-variant-numeric: tabular-nums;
}

.kernel-pager button {
    height: var(--st-control-h-sm);
    border: 1px solid var(--st-border-input);
    background: var(--st-surface);
    color: var(--st-text-primary);
    border-radius: var(--st-radius-sm);
    padding: 0 var(--st-space-3);
    font-family: var(--st-font-ui);
    font-size: var(--st-text-xs);
    cursor: pointer;
}

.kernel-pager button:hover:not(:disabled) {
    background: var(--st-surface-hover);
}

.kernel-pager button:disabled {
    opacity: var(--st-disabled-opacity);
    cursor: not-allowed;
}

/* CheckListEditor (Kernel/Components/CheckListEditor.razor) -- shared by the recipe and vendor
   pickers on SubAccountEdit. */

.checklist-editor {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--st-text-sm);
}

.checklist-editor th {
    text-align: left;
    color: var(--st-text-secondary);
    font-family: var(--st-font-ui);
    font-weight: 600;
    padding: var(--st-space-2);
    border-bottom: 1px solid var(--st-border-strong);
}

.checklist-editor td {
    padding: var(--st-space-2);
    border-bottom: 1px solid var(--st-border);
    color: var(--st-text-primary);
    vertical-align: middle;
}

.checklist-editor-check-col {
    width: 2.5rem;
}

.checklist-editor input[type="number"],
.checklist-editor input[type="text"] {
    width: 100%;
    max-width: 10rem;
    height: var(--st-control-h-sm);
    border: 1px solid var(--st-border-input);
    border-radius: var(--st-radius-sm);
    padding: 0 var(--st-space-2);
    background: var(--st-surface);
    color: var(--st-text-primary);
    font-family: var(--st-font-text);
}

.checklist-editor input:disabled {
    opacity: var(--st-disabled-opacity);
    background: var(--st-surface-alt);
}

.checklist-editor-empty {
    color: var(--st-text-muted);
    font-size: var(--st-text-sm);
}

/* ── Modal (COMPONENT-SPECS.md §7, Kernel/Components/Modal.razor) ─────────────────────────────── */

.st-modal-scrim {
    position: fixed;
    inset: 0;
    background: var(--st-scrim);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
}

.st-modal {
    background: var(--st-surface-overlay);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-md);
    box-shadow: var(--st-shadow-md);
    display: flex;
    flex-direction: column;
    max-height: 90vh;
    width: 100%;
}

.st-modal-sm { max-width: 440px; }
.st-modal-md { max-width: 720px; }
.st-modal-lg { max-width: 960px; }

.st-modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--st-space-4) var(--st-space-5);
    border-bottom: 1px solid var(--st-border);
    flex: 0 0 auto;
}

.st-modal-title {
    margin: 0;
    font-family: var(--st-font-ui);
    font-size: var(--st-text-base);
    font-weight: 600;
    color: var(--st-text-primary);
}

.st-modal-body {
    padding: var(--st-space-5);
    font-size: var(--st-text-sm);
    color: var(--st-text-secondary);
    max-height: 70vh;
    overflow-y: auto;
    flex: 1 1 auto;
}

.st-modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: var(--st-space-4);
    padding: var(--st-space-4) var(--st-space-5);
    background: var(--st-surface-alt);
    border-top: 1px solid var(--st-border);
    flex: 0 0 auto;
}

@media (max-width: 640px) {
    .st-modal {
        max-width: 100%;
        width: 100%;
        height: 100%;
        max-height: 100%;
        border-radius: 0;
    }

    .st-modal-footer {
        position: sticky;
        bottom: 0;
    }

    .st-modal-footer .kernel-btn {
        flex: 1;
        min-width: 0;
    }
}

/* NIGHT-QUEUE full-unpark ruling (2026-08-06): Billing.razor's plan/usage summary. */
.billing-summary {
    display: flex;
    flex-direction: column;
    gap: var(--st-space-2);
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-md);
    padding: var(--st-space-4);
}

.billing-summary__row {
    display: flex;
    justify-content: space-between;
    gap: var(--st-space-4);
    font-size: var(--st-text-sm);
}

.billing-summary__label {
    color: var(--st-text-secondary);
}

.billing-summary__value {
    color: var(--st-text-primary);
    font-weight: 600;
}

.billing-recent__title {
    margin: 0;
    font-size: var(--st-text-sm);
    font-weight: 600;
    color: var(--st-text-primary);
}

.billing-recent__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--st-space-1);
}

.billing-recent__list li {
    display: flex;
    justify-content: space-between;
    gap: var(--st-space-4);
    font-size: var(--st-text-sm);
    color: var(--st-text-secondary);
    padding: var(--st-space-1) 0;
    border-bottom: 1px solid var(--st-border);
}

/* A212 item 4: cancel/resume subscription action block on Billing.razor. */
.billing-cancel {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--st-space-2);
}

.billing-cancel__note {
    margin: 0;
    font-size: var(--st-text-sm);
    color: var(--st-text-secondary);
}

.billing-cancel__error {
    margin: 0;
    font-size: var(--st-text-sm);
    color: var(--st-danger);
}

/* NIGHT-QUEUE full-unpark ruling (2026-08-06): SuperAdmin trio (Analytics/SupportAccess/Helpdesk). */
.analytics-section-title {
    margin: var(--st-space-5) 0 0;
    font-size: var(--st-text-sm);
    font-weight: 600;
    color: var(--st-text-primary);
}

.supportaccess-account-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--st-space-2);
}

.supportaccess-account-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: var(--st-space-4);
    width: 100%;
    border: 1px solid var(--st-border);
    border-radius: var(--st-radius-md);
    padding: var(--st-space-3);
    background: var(--st-surface);
    color: var(--st-text-primary);
    font-size: var(--st-text-sm);
    text-align: left;
    cursor: pointer;
}

.supportaccess-account-row:hover {
    border-color: var(--st-accent);
}

/* Persistent banner AppShell shows while browsing a borrowed customer session -- same visual
   family as .kernel-banner-warning (impersonation is a genuinely higher-stakes state than the
   Demo badge, worth a stronger color), but full-width under the topbar rather than inline with it,
   since it needs room for the account name + the End button without cramping the topbar itself. */
.shell-support-access-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--st-space-4);
    background: var(--st-warning-subtle);
    color: var(--st-warning-on-subtle);
    padding: var(--st-space-2) var(--st-space-5);
    font-size: var(--st-text-sm);
    border-bottom: 1px solid var(--st-warning);
}

.shell-support-access-banner .kernel-btn {
    flex-shrink: 0;
}

/* A176 (2026-08-20): "Resume setup" banner AppShell shows on every screen while the onboarding
   wizard has been left unfinished -- same full-width-under-topbar shape as the support-access
   banner above, but the calmer --st-accent family (informational nudge, not an impersonation
   warning) since Mazhar's own spec calls this a nudge the customer can dismiss by just finishing
   or ignoring, never a lock. */
.shell-resume-setup-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--st-space-4);
    background: var(--st-accent-subtle, var(--st-warning-subtle));
    color: var(--st-accent-on-subtle, var(--st-warning-on-subtle));
    padding: var(--st-space-2) var(--st-space-5);
    font-size: var(--st-text-sm);
    border-bottom: 1px solid var(--st-accent, var(--st-warning));
}

.shell-resume-setup-banner .kernel-btn {
    flex-shrink: 0;
}

/* A214 (2026-08-21): scheduled-maintenance notice, same full-width-under-topbar shape as its two
   siblings above, informational (no action button -- there is nothing for the customer to do but
   note the window) so the calmer --st-info family fits better than --st-accent/--st-warning. */
.shell-maintenance-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--st-space-4);
    background: var(--st-info-subtle, var(--st-accent-subtle, var(--st-warning-subtle)));
    color: var(--st-info-on-subtle, var(--st-accent-on-subtle, var(--st-warning-on-subtle)));
    padding: var(--st-space-2) var(--st-space-5);
    font-size: var(--st-text-sm);
    text-align: center;
    border-bottom: 1px solid var(--st-info, var(--st-accent, var(--st-warning)));
}

/* A213 (5) (2026-08-21): the "which tier am I in" banner -- deliberately the --st-warning family
   (stronger than the maintenance banner's calmer --st-info above) and a persistent every-page presence
   rather than a dismissible/timed notice, since the whole point is that nobody can miss it on ANY
   non-prod hostname (EnvironmentBannerResolver), for as long as they're there. */
.shell-stage-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--st-space-4);
    background: var(--st-warning-subtle, var(--st-accent-subtle));
    color: var(--st-warning-on-subtle, var(--st-accent-on-subtle));
    padding: var(--st-space-2) var(--st-space-5);
    font-size: var(--st-text-sm);
    font-weight: 600;
    text-align: center;
    border-bottom: 1px solid var(--st-warning, var(--st-accent));
}

/* ---- Report breadcrumb -------------------------------------------------------------------
   ROUND 2 (Mazhar, 2026-08-15): "dynamic-report navigation missing on Chart-of-Account tree (no
   path back to top)." Moved here VERBATIM from Features/Reports/DynamicReport.razor.css. It had to
   move rather than be copied: scoped CSS is per-component, so the same markup added to CoaTree
   would have rendered as unstyled buttons and slashes -- a "fix" that looks like a new bug. One
   definition means the two report screens cannot drift apart on what a breadcrumb looks like.
   Values are unchanged, so this is a move, not a restyle. ---------------------------------- */
.report-breadcrumb {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1);
    font-size: var(--text-sm);
}

.report-breadcrumb-link {
    border: none;
    background: transparent;
    padding: 0;
    color: var(--accent);
    font-size: var(--text-sm);
    cursor: pointer;
}

.report-breadcrumb-link:hover {
    text-decoration: underline;
}

.report-breadcrumb-current {
    color: var(--text-secondary);
}

.report-breadcrumb-sep {
    color: var(--text-secondary);
}



/* W527/W536 (2026-08-15): the plan-flow banner's timing line ("starts when your trial ends",
   "next billing cycle", proration). Block-level and quieter than the headline on purpose -- it is
   the sentence that stops a customer clicking through to a payment screen unsure of when they get
   charged, so it must be readable without competing with the headline it qualifies. */
.billing-state__note {
    display: block;
    margin-top: 0.25rem;
    font-size: 0.8125rem;
    opacity: 0.85;
}
