/* ---------------------------------------------------------
   GLOBAL VARIABLES
   --------------------------------------------------------- */
:root {
}

/* ---------------------------------------------------------
   GLOBAL RESET / BASE
   --------------------------------------------------------- */
html, body, form {
    height: 100%;
    margin: 0;
    padding: 0;
    background: var(--bg);
    font-family: 'Inter', sans-serif;
    color: var(--ink);
}

    /* Left nav disabled: never reserve left-nav space */
    body.ln-disabled {
        --layout-leftnav-col: 0px
    }

    /* Left nav enabled: open reserves full width */
    body.ln-enabled.show-lnav {
        --layout-leftnav-col: var(--leftnav-width);
    }

    /* Left nav enabled, closed + reclaim collapses column to collapsed width */
    body.ln-enabled.close-lnav.ln-reclaim {
        --layout-leftnav-col: var(--leftnav-collapsed-width);
    }

    /* Left nav enabled, closed + preserve keeps the width */
    body.ln-enabled.close-lnav:not(.ln-reclaim) {
        --layout-leftnav-col: var(--leftnav-width);
    }

    /* Left nav: during closing with reclaim, collapse DURING animation */
    body.ln-enabled.ln-reclaim.ln-closing {
        --layout-leftnav-col: var(--leftnav-collapsed-width);
    }


    /* CBS-34 2026-09-19: --layout-rail-col-static is what the APPBAR and the FOOTER use.
     *
     * It holds the column the rail occupies when CLOSED, in every rail state, so opening the
     * rail moves the content track and nothing else. The three rules below are deliberately
     * state-independent - no show-rail, no close-rail - which is the whole point: they cannot
     * change when the rail opens. Each sits beside the --layout-rail-col rule it mirrors so the
     * pair cannot drift apart unnoticed. cbs.css adds the fourth, for the 48px strip.
     *
     * Why the appbar and footer need this at all: .appbar-right and .footer-right are EMPTY
     * spacer divs (Site.master:175 and :280) - the logo, the menu, the user chip and the footer
     * content all live in the centre region. So the rail track in those two grids holds nothing
     * and its only effect was to squeeze the centre when the rail opened, which is exactly what
     * the developer saw: at 1920 on Commission Plans the appbar centre went from 280/1600 to
     * 280/1368 on opening the rail. Using the closed value keeps the appbar and footer
     * pixel-identical to the closed-state body - which is the CBS-24 alignment - and immune to
     * the rail. */

    /* Disabled pages: never reserve rail space */
    body.rr-disabled {
        --layout-rail-col: 0px;
        --layout-rail-col-static: 0px;
    }

    /* Reclaim pages give the column back when closed, so closed is 0 whatever the state is now. */
    body.rr-enabled.rr-reclaim {
        --layout-rail-col-static: 0px;
    }

    /* Preserve pages keep the width when closed, so the static column is the full width. */
    body.rr-enabled:not(.rr-reclaim) {
        --layout-rail-col-static: var(--rail-width);
    }

    /* Enabled pages: when open, reserve full rail width */
    body.rr-enabled.show-rail {
        --layout-rail-col: var(--rail-width);
    }

    /* Enabled pages: when closed AND reclaim is on, collapse the column */
    body.rr-enabled.close-rail.rr-reclaim {
        --layout-rail-col: 0px;
    }

    /* Enabled pages: when closed AND preserve is desired, keep the width */
    body.rr-enabled.close-rail:not(.rr-reclaim) {
        --layout-rail-col: var(--rail-width);
    }

    /* RR: while closing with reclaim, collapse the rail column DURING the animation */
    body.rr-enabled.rr-reclaim.rr-closing {
        --layout-rail-col: 0px;
    }


/* Header right group vs the right rail: ONE mechanism, JavaScript/appbarAlign.js. It MEASURES the rail card and writes
   --appbar-rr-ext and --appbar-rr-w on .user-panel (both 0 when the rail is collapsed or under 1101px); the rule on
   .appbar-center-grid > .user-panel below and the bell-room rule in updates.css read them. The old arithmetic
   (--appbar-rr-shift, --appbar-rr-minw, --rr-header-slot) is gone: it was reasoned from variables and was off on TEST. */

/* ---------------------------------------------------------
 APP SHELL (HEADER / BODY / FOOTER)
 --------------------------------------------------------- */
.layout {
    height: 100vh; /* 🚨 min-height is NOT sufficient */
    /* iOS Safari: 100vh is the viewport with the address bar collapsed, so the layout was taller than what is visible and
       .layout-body (overflow: hidden) cut the bottom of every page off behind the toolbar: the Passkeys card on Preferences
       could not be scrolled to. 100dvh follows the bar. Browsers without dvh keep the 100vh above. */
    height: 100dvh;
    display: flex;
    flex-direction: column;
    background: var(--bg);
    min-height: 0; /* allow children to shrink */
}

/* ---------------------------------------------------------
   APP BAR (HEADER)
   --------------------------------------------------------- */


.appbar-left {
    grid-column: 2;
}

.appbar-center {
    grid-column: 3;
    display: block;
}

/* TL-71 2026-09-20: the chrome does NOT follow the content width any more.
 *
 * The appbar and footer used to cap their centre region at --content-max-width, so they changed
 * shape with the page: 1600 on a grid-wide page, 1000 on a page that sets no body class, 620 on
 * a page-narrow one. Two consequences, both wrong. The chrome visibly jumped inward when a user
 * opened a sparse page - Security/Entity, QuickBooksOnline and the others that take the 1000px
 * default - and the menu's track shrank with it: measured at 1920, the track is 1174 on a
 * grid-wide page, 574 on a default page and 194 on a narrow one, against an Administrator menu
 * that needs 914. That is why the same menu fits on one page and collides on the next.
 *
 * The developer's decision: the appbar and footer are app chrome, identical on every page. The
 * narrow content column belongs to the page, not to the shell.
 *
 * The consequence to know about: on a grid-wide page the appbar centre is now slightly wider
 * than the content column - the shell gutters rather than the content cap decide it - so the
 * logo and the user chip no longer sit exactly on the content's edges. CBS-24 and CBS-34 were
 * about the chrome not COLLAPSING and not moving when the rail opens, and both still hold. */
.appbar-center,
.footer-center {
    width: 100%;
    margin: 0;
    box-sizing: border-box;
}

.appbar-right {
    grid-column: 4;
}


.appbar-center-grid {
    display: grid;
    /* TL-71: the menu's own track follows the appbar, not the page. Capping it at
       --content-max-width here would have undone the full-width change one level down - on a
       page that takes the 1000px default the menu would still have been squeezed to 574. */
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    height: var(--appbar-height);
    /* Designer 2026-10-14: with the bell outside the chip, the logo's left edge and the bell's right edge line up with the main content
       cards at 32px; this also keeps the bar stable when the right cluster changes width. */
    margin-right: 32px;
    margin-left: 32px;
}

    /* TL-72: every child is placed EXPLICITLY.
     *
     * This grid had three columns and three children, so auto-placement happened to be right.
     * TL-70 added the hamburger as a fourth child and auto-placement did what it is supposed to:
     * it wrapped the fourth item onto a second row - which is how the user chip ended up beneath
     * the logo instead of at the right of the appbar, and why its flyout, which is anchored to a
     * right-hand chip, opened off screen. Explicit columns cannot wrap, and a display: none item
     * is not placed at all, so the hamburger simply takes no column above the breakpoint. */
    .appbar-center-grid > .brand-logo {
        grid-column: 1;
    }

    .appbar-center-grid > .appbar-user-container {
        grid-column: 2;
    }

    .appbar-center-grid > .user-panel {
        grid-column: 3;
        justify-self: end;
        /* Header right group follows the right rail (2026-10-14). Collapsed: both values are 0 and the group sits
         * at the right edge of the content cards (the 32px margin on the grid). Expanded: appbarAlign.js has
         * measured the rail card, so the group's right edge moves out by --appbar-rr-ext to the card's right
         * edge and the chip is held at least the card's width less the bell's room (updates.css), which puts
         * the chip's left edge on the card's left edge. No transition here: the script eases the values
         * itself, at the rail's own duration, so the header moves with the rail. */
        box-sizing: border-box;
        margin-right: calc(-1 * var(--appbar-rr-ext, 0px));
        min-width: var(--appbar-rr-w, 0px);
    }

    .appbar-center-grid #menuMain {
        float: none !important;
        display: flex !important;
    }

.brand-logo img {
    height: 40px;
    width: auto;
}

/* Narrow widths: the logo, the chip (initials only from 640, nav.css) and the bell never touch.
 *
 * At 320 the centre track is only what is left between the 16px gutters, the 56px nav icon rail and, on a page
 * that keeps the closed right-rail column in the appbar (CBS Invoices' 48px strip), that column - and the 32px
 * margins take 64 of it, which is less than the logo (119), the avatar chip (58) and the bell's room (37) need
 * (measured in docs/design/harness/apprail.html: the chip overlapped the logo by 38px at 320). Three things give
 * it back, neither at desktop widths:
 *   - up to 640 the centre spans the (empty) right column as well, so the strip's width is usable;
 *   - the grid's side margins are 16, not 32 (the desktop 32 is the content cards' margin; at this width the
 *     cards themselves are edge to edge).
 * Measured after: gap between logo and chip 41px at 320, 81 at 360, 111 at 390, 201 at 480.
 */
@media (max-width: 640px) {
    .appbar-center {
        grid-column: 3 / -2;
    }

    .appbar-center-grid {
        margin-left: 16px;
        margin-right: 16px;
    }
}



/* Smooth rail/leftnav column changes */
.appbar-inner,
.layout-body,
.footer-inner {
    transition: grid-template-columns var(--rails-collapse-expand-time) ease;
}

.appbar-inner {
    display: grid;
    /*grid-template-columns: 1fr var(--layout-leftnav-col) minmax(0, var(--content-max-width)) var(--layout-rail-col) 1fr;*/
    /* TL-71: full width. The centre track takes everything between the gutters instead of
       being capped at the page-s content width, and the gutters are fixed rather than flexible
       so they cannot grow and pull the chrome inward. */
    grid-template-columns: var(--shell-gutter) var(--layout-leftnav-col) minmax(0, 1fr) var(--layout-rail-col-static) var(--shell-gutter);
    width: 100%;
    height: var(--appbar-height);
    box-sizing: border-box;
}


/* ---------------------------------------------------------
 MAIN LAYOUT BODY (SCROLL PARTICIPANT)
 --------------------------------------------------------- */
.layout-body {
    flex: 1; /* ✅ take remaining vertical space */
    min-height: 0; /* ✅ ALLOW SCROLL CONTAINERS */

    display: grid;
    /*grid-template-columns: 1fr var(--layout-leftnav-col) minmax(0, var(--content-max-width)) var(--layout-rail-col) 1fr;*/
    /* TL-71 2026-09-20: the LEFT gutter is fixed, like the appbar's and the footer's.
     *
     * It used to be minmax(--shell-gutter, 1fr), and so did the appbar's, and both grids had the
     * same capped content track - so the two flexible gutters always computed to the same value
     * and everything lined up by coincidence of symmetry. Making the chrome full width broke
     * that coincidence: the appbar's gutter became a fixed 16 while this one still grew to
     * absorb the slack left by a narrow content column, so on a 1000px page at 1920 the left nav
     * started at x=340 while the logo started at x=256 - the nav sitting well to the right of
     * the logo, which is what the developer photographed.
     *
     * With this gutter fixed too, all three grids start their left-nav column at the same x, so
     * the nav, the logo and the content column share one left edge on every page type.
     *
     * The CONTENT track keeps its cap. That is the difference between this grid and the chrome:
     * the narrow column belongs to the page, so a sparse page still gets a narrow content column
     * - the slack now falls on the RIGHT, in the last track, instead of being split evenly and
     * pulling the whole page inward. */
    grid-template-columns: var(--shell-gutter) var(--layout-leftnav-col) minmax(0, 1fr) var(--layout-rail-col) var(--shell-gutter);
    box-sizing: border-box;
    overflow: hidden;
}

/* ---------------------------------------------------------
   LEFT NAV
   --------------------------------------------------------- */
.left-rail {
    grid-column: 2;
    position: sticky;
    top: 0;
    width: var(--layout-leftnav-col);
    min-width: var(--layout-leftnav-col);
    background: var(--bg);
    border-right: 1px solid var(--border);
    box-shadow: 1px 0 0 rgba(var(--shadow-rgb), 0.02);
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    padding-top: var(--page-vertical-gap);
    padding-bottom: var(--page-vertical-gap);
    max-height: calc(100vh - var(--header-footer-total));
    max-height: calc(100dvh - var(--header-footer-total));
    min-height: 0;
    opacity: 1;
    pointer-events: auto;
    transform: none;
    transition: width var(--rails-collapse-expand-time) ease, min-width var(--rails-collapse-expand-time) ease;
}



/* Left nav container: search stays visible; menu list scrolls */
.left-nav {
    width: 100%;
    padding: 0px 16px 0px 16px;
    /* critical: allow inner scroll region to size correctly */
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* prevent the entire aside from scrolling */
    overflow: hidden;
    flex: 1;
    box-sizing: border-box;
    background: var(--bg);
    transition: opacity var(--rails-collapse-expand-time) ease, transform var(--rails-collapse-expand-time) ease;
    will-change: opacity, transform;
    opacity: 0;
    pointer-events: none;
    transform: translateX(-20px);
}

/* =================================================================================================
   TL-114: THE LINK IN THE FLEX CHAIN THAT WAS MISSING, AND WHY THE NAV CLIPPED INSTEAD OF SCROLLING.

   The rules here were written for this DOM:

       .left-rail              max-height: calc(100vh - var(--header-footer-total)), flex column
         .left-nav             flex: 1, flex column, min-height: 0, overflow: hidden
           .left-nav-search    flex: 0 0 auto      <- pinned
           .AspNet-TreeView    flex: 1 1 auto, min-height: 0, overflow-y: auto   <- scroll owner

   THE REAL DOM HAS AN asp:Panel BETWEEN .left-nav AND THOSE TWO. The selectors below are descendant
   selectors, so they still MATCHED - but flex: 1 1 auto on an element that is not a flex item does
   nothing at all. The panel sized itself to its content, .left-nav's overflow: hidden clipped the
   excess, and the tree's overflow-y: auto never had a bounded height to scroll within. The bottom
   of a long menu was therefore unreachable rather than scrollable, which is exactly the report:
   the appended Support, Help centre, Contact support and Logout nodes go last and are what a
   clipped nav hides first.

   THIS IS WHY IT LOOKED LIKE THE CSS WAS ALREADY CORRECT. Every rule was right and the chain was
   broken by markup none of them mentioned.

   This is also the reason TL-116 could not be built alone: showing every top-level node makes the
   menu taller, so without this it would have pushed more items out of reach. */
.left-nav-panel {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

/* Search box area stays pinned (not sticky—just outside the scroll owner) */
.left-nav-search {
    flex: 0 0 auto;
    margin-bottom: 16px;
    background: var(--bg);
}

    .left-nav-search input[type="search"] {
        width: 100%;
        height: 32px;
        padding: 0 8px;
        border-radius: 6px;
        border: 1px solid var(--border-strong);
        box-sizing: border-box;
        box-shadow: 0 1px 0 rgba(var(--shadow-rgb), 0.03);
    }

/* TL-115: the row the user actually searched for. The search opens every ancestor of a match, so
   without this the match is simply one more row in a newly opened tree - and a search matching
   several branches gives no clue which rows were the hits. Applied to the li, so it marks the node
   itself rather than the whole branch. */
/* TL-150: THE MATCH HIGHLIGHT REACHES SPANS AS WELL AS ANCHORS - WHICH IS WHY SEARCHING "fleet"
   CAME BACK HALF INDIGO AND HALF BLACK.

   This selector was `> a` only. TL-138 made a node with children and no Url of its own render as
   .AspNet-TreeView-ClickableNonLink - a SPAN, not an anchor - so a matched GROUP node got the
   nav-search-match class on its li but no colour from this rule, and kept the ordinary ink colour.
   A matched LEAF, which is a real anchor, went brand-primary. Two colours in one result set, and
   nothing to do with visited-link state.

   THIS IS THE FOURTH TIME `> a` ALONE HAS BEEN WRONG in this nav, after the top-level emphasis
   rule, the collapsed-rail centring rule and the icon colour. The adapter renders a label as an
   anchor OR one of two span classes depending on the node, so any rule that styles a nav label must
   list all three. */
/* TL-168 2026-09-30: THE COLOUR IS GONE. The developer rejected colouring search results outright -
   no colour changes for search results at all. WEIGHT ONLY, which still marks the row the user was
   looking for without introducing a second meaning for the brand colour inside the nav.

   The three-selector list stays, and must: the adapter renders a label as an anchor or as one of two
   span classes, so a rule listing only `> a` reaches some matches and not others. That was TL-150,
   and it is the reason a search came back half indigo and half black. Removing the colour does not
   remove that requirement - it would come straight back the next time anything styles a match. */
.left-nav li.nav-search-match > a,
.left-nav li.nav-search-match > .AspNet-TreeView-ClickableNonLink,
.left-nav li.nav-search-match > .AspNet-TreeView-NonLink {
    font-weight: 600;
}

/* TL-168 2026-09-30: THE nav-search-match-desc RULE IS DELETED, not kept dormant.

   It styled a description match in italic muted text. The developer rejected that idea in place:
   people would never learn what the italic meant, so it communicated nothing and cost a second
   visual language inside the nav. The "Also mentions ..." section is the direction that survives,
   because it SAYS why those entries appeared instead of hinting at it with a style.

   The class was already unapplied since TL-153 moved description matches out of the tree. Keeping
   a rule for a class nothing sets is how a dead style gets reinstated by accident later, so it is
   removed rather than left behind a comment. */

/* =================================================================================================
   TL-153: THE "Also mentions ..." SECTION. Description matches live here rather than in the tree,
   because a tree cannot be re-sorted without destroying the hierarchy that gives node positions
   their meaning - see the note in site.core.js.

   Styled as clearly secondary to the tree above it: a muted heading carrying the query, then each
   item with its path underneath so the user can see where it sits without the tree having to move.
   ================================================================================================= */
/* TL-176: BOUNDED, AND WITH ITS OWN SCROLL.

   flex-shrink is 1 and there is a ceiling, because the failure this guards against is the mirror of
   the one above: a query matching many descriptions would make this list tall enough to take the
   space the menu needs, and with flex: 0 0 auto it would have won outright. 40% leaves the majority
   of the rail to the menu, which is the primary thing; the list scrolls inside whatever it gets.

   It stays AFTER the tree in the flex order rather than being positioned, so the menu pushes it down
   naturally and neither element needs to know the other's height. */
.nav-search-also {
    flex: 0 1 auto;
    max-height: 40%;
    overflow-y: auto;
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
}

.nav-search-also-heading {
    font-size: 12px;
    font-weight: 600;
    color: var(--muted);
    margin-bottom: 6px;
}

.nav-search-also-item {
    margin: 0 0 8px 0;
    line-height: 1.25;
}

    .nav-search-also-item > a,
    .nav-search-also-item > span {
        font-size: 13px;
        font-weight: 500;
        color: var(--ink);
        text-decoration: none;
    }

        .nav-search-also-item > a:hover {
            color: var(--brand-ink);
            font-weight: 600;
        }

.nav-search-also-path {
    font-size: 11px;
    color: var(--muted);
}

/* The section is text, not icons - it has no place in the 56px rail. */
body.close-lnav .nav-search-also {
    display: none;
}

/* =================================================================================================
   TL-140: ROOT NAV ICONS, AND THE COLLAPSED ICON RAIL THEY EXIST FOR.

   Ten roots only, never the 188 nodes below them. The markup is added server side by
   MT.Utils.ApplyRootNavIcon, which emits
       <svg class="icon nav-icon" ...>...</svg><span class="nav-label">Title</span>
   into the root node's Text. The label span is not decoration: it is what the collapsed rail
   hides, and without it the title is a bare text node that cannot be hidden separately.

   THE MASK IS WHY THE BRAND MARK IS NOT RED. Every icon here follows the pattern already used by
   .ph--clock in ucKPIDetailDrawer.css: the SVG is a MASK and the colour comes from
   background-color: currentColor. A mask uses only the alpha channel, so the fills inside the
   source SVG are discarded entirely. The Franchisees mark therefore picks up the nav's own colour
   with no edited copy of the artwork and no chance of the Jiffy Lube red leaking into the rail -
   the requirement is satisfied by construction rather than by remembering to change a fill.

   THE FRANCHISEES ARTWORK is the same geometry as brandStoreIcon in CBS/CBSInvoices.aspx.cs. Its
   reasoning, recorded because it is not obvious: CBS users see Franchisees and run reports about
   franchisees, so the brand mark fits and avoids inventing a second "reports" icon. That icon is
   defined inline in the code-behind rather than as a file so the brand can be swapped if future
   versions support brands other than Jiffy Lube - if that happens, this copy needs swapping too.

   BILLING AND CBS INVOICES CURRENTLY SHARE A SHAPE. Accounts and Account sharing one is safe -
   they are shown to different roles and never appear together - but Billing and CBS Invoices CAN
   both be visible to a CBS user, and two identical icons in a ten-item rail is where an icon stops
   helping. Flagged to the developer; built as specified pending his call.
   ================================================================================================= */

/* TL-140: THE ROOT ICON'S OWN APPEARANCE, MATCHING .left-nav-toggle's MECHANISM EXACTLY.

   That control (controls.css:992) is the reference the developer named: it sets
   color: var(--brand-ink) with opacity .6 and transition: opacity .25s ease, and lifts on
   hover with transform: translateY(-1px) and opacity 1. The same three ideas are here - dim
   brand-primary at rest, full opacity plus the lift on hover.

   EVERY ICON IS AN INLINE <svg> CARRYING fill="currentColor", so colour comes from `color` here
   and needs nothing done to the artwork. There is no mask plumbing left in this file. */
.nav-icon {
    flex: 0 0 auto;
    vertical-align: -0.2em;
    margin-right: 10px;
    opacity: .6;
    transition: opacity .25s ease, transform .25s ease;
}

/* THE SPECIFICITY TRAP THAT MAKES THIS SELECTOR NECESSARY, and it is why an !important appeared
   here: theme.css:372 is

       svg.icon { color: currentColor; min-width: 20px; width: 24px; height: 24px }

   at (0,1,1), which BEATS `.nav-icon` at (0,1,0). SO BOTH THE COLOUR AND THE SIZE WERE BEING
   OVERRIDDEN, not just the size. `color: currentColor` on the svg means "take the anchor's colour",
   which is exactly why the icons came out the link colour instead of brand-primary however the
   class was written - the class could never win.

   The developer worked around it with !important, which was the right instinct and the wrong lever:
   an !important here would have to be matched by the next person who needs to override anything on
   these icons. THIS SELECTOR IS ALSO (0,1,1) AND layout.css LOADS AFTER theme.css, so it wins on
   source order with no !important at all. Colour therefore lives HERE, not on .nav-icon above.

   The markup keeps its `icon` class because that is the app's convention for an inline SVG; this
   rule is what makes keeping it safe. */
svg.nav-icon {
    color: var(--brand-ink);
    width: 1.15em;
    height: 1.15em;
    min-width: 0;
}

/* Hover: full opacity and the vertical lift used by .left-nav-toggle. Keyed on the row rather than
   the icon so the whole target lifts together, which is how the toggle behaves. */
.left-nav a:hover .nav-icon,
.left-nav span:hover > .nav-icon,
.left-nav li:hover > a .nav-icon {
    opacity: 1;
    transform: translateY(-1px);
}

/* TL-140: NO PER-ICON CLASSES HERE ANY MORE. All ten root icons are emitted as INLINE SVG by
   MT.Utils.ApplyRootNavIcon, straight from docs/design/SVGs.txt, which is this app's convention:
   source markup as-is with fill="currentColor". The nine data-URI mask classes that used to sit
   here are gone, along with the mask plumbing on .nav-icon above.

   WHY MASKS WERE WRONG AND NOT JUST DIFFERENT: a mask reads only the alpha channel, so it can be
   recoloured wholesale but cannot respond to colour per path. These icons carry currentColor
   themselves and need nothing done to them. */

/* Menu list becomes the ONLY vertical scroll owner */
.left-nav .AspNet-TreeView,
/* TL-176 2026-09-30: THE TREE MUST NOT GROW. IT MAY ONLY SHRINK.

   WHAT WAS WRONG, AND IT IS ONE CHARACTER: flex-grow was 1. The rail is clamped to roughly the
   viewport (.left-rail max-height: calc(100vh - var(--header-footer-total))) and .left-nav and
   .left-nav-panel both fill it, so a tree with flex-grow: 1 EXPANDS TO FILL THE WHOLE RAIL even
   when the menu is short. .nav-search-also is a flex: 0 0 auto sibling AFTER the tree, so it was
   pushed to the very bottom of the rail - separated from the menu it belongs to by however much
   empty rail there was, and on a tall window that is most of the screen. Hence the report: the
   "Also mentions" section sits at the very bottom, below the whole nav tree, and on a tall viewport
   she never sees it.

   THE SCROLL DOES NOT COME FROM flex-grow, WHICH IS WHY THIS IS SAFE. TL-114's fix was to make this
   element a real flex item so it could be BOUNDED and own the scroll. That comes from min-height: 0
   plus overflow-y: auto plus being SHRINKABLE - flex-shrink, which stays 1. Growing was never part
   of it and was doing nothing except claiming space the search results needed.

   WHAT THE CHAIN NOW DOES, at both ends of the range:
     - short menu, tall window: the tree is its content height, the results sit directly under it,
       and the empty rail is below them both.
     - long menu or short window: the tree shrinks to the space left after the search box and the
       results, scrolls internally, and the results stay on screen.
   The menu can push the results down and can never push them off, which is the requirement. */
.left-nav .MenuLeftTree.AspNet-TreeView {
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
}

/* =================================================================================================
   TL-140: THE COLLAPSED STATE IS NOW AN ICON RAIL RATHER THAN AN EMPTY COLUMN.

   WHAT IT USED TO BE: .left-nav went to opacity 0 and pointer-events none, so the collapsed state
   showed nothing at all and only the toggle button remained - the "more than a chevron" in the
   request. The width token in theme.css has been named --leftnav-collapsed-width: 56px, commented
   "icon rail width", since it was written - so the intent was always an icon rail and only the
   icons were missing.

   WHAT IS SHOWN AND WHAT IS NOT. Root nodes only, as icons. Everything else is hidden: the search
   box (unusable at 56px), the label spans, the expand/collapse toggles, and every nested ul. The
   roots' own tooltips still work, which is what makes an icon-only rail navigable.

   THIS APPLIES TO BOTH COLLAPSED VARIANTS, deliberately. ln-reclaim shrinks the column to 56px and
   is the true rail; :not(.ln-reclaim) keeps the full width but still hid everything, which was an
   empty 240px column. Neither was useful, so both now show the rail; the reclaim one simply looks
   like a rail and the other has room to spare.

   TL-114 INTERACTION, which has to hold: the flex chain through .left-nav-panel is untouched here,
   so the rail still scrolls when there are more roots than fit. Ten roots at 56px wide will fit on
   almost any screen, but the scroll must not be removed on the assumption that they always do.
   ================================================================================================= */

/* Closed: visible, and interactive, as an icon rail. */
body.close-lnav .left-nav {
    opacity: 1;
    pointer-events: auto;
    transform: translateX(0);
    overflow: hidden;
    padding-left: 0;
    padding-right: 0;
}

/* The search box cannot be used at 56px, and it is the tallest thing in the column. */
body.close-lnav .left-nav-search {
    display: none;
}

/* Only the roots. Nested levels, labels and the +/- toggles all go. */
body.close-lnav .left-nav .AspNet-TreeView ul ul {
    display: none;
}

body.close-lnav .left-nav .nav-label {
    display: none;
}

body.close-lnav .left-nav .AspNet-TreeView-Expand,
body.close-lnav .left-nav .AspNet-TreeView-Collapse {
    display: none;
}

/* Centre each root's icon in the rail and give it a comfortable target. */
body.close-lnav .left-nav .AspNet-TreeView > ul > li,
body.close-lnav .left-nav .AspNet-TreeView > div > ul > li {
    text-align: center;
    padding: 2px 0;
}

    /* NAMED CLASSES, NOT A BARE `span` - AND THAT IS THE WHOLE FIX FOR THE CARETS REAPPEARING IN
       THE RAIL.

       This used to end in `> li > span`, which was meant to catch the label element on a root that
       renders as a non-link span rather than an anchor. But THE CARET IS ALSO A SPAN AND ALSO A
       DIRECT CHILD OF THE ROOT li, so it matched that too - and at (0,3,3) it beat the
       `display: none` above at (0,3,1) and turned the caret back on with `display: flex`.

       That is why the root carets vanished correctly in the EXPANDED nav and came back in the
       collapsed rail: the expanded hide lives in controls.css and nothing overrode it, while here
       my own centring rule was re-showing what the rule three blocks up had just hidden.

       Listing the two adapter label classes explicitly cannot reach the toggles, because a toggle
       carries AspNet-TreeView-Expand or -Collapse and never these. Same approach as the top-level
       emphasis rule in controls.css, and for the same reason: assuming the label is an <a> - or
       that a bare `span` means the label - is the recurring mistake with this adapter. */
    body.close-lnav .left-nav .AspNet-TreeView > ul > li > a,
    body.close-lnav .left-nav .AspNet-TreeView > div > ul > li > a,
    body.close-lnav .left-nav .AspNet-TreeView > ul > li > .AspNet-TreeView-ClickableNonLink,
    body.close-lnav .left-nav .AspNet-TreeView > div > ul > li > .AspNet-TreeView-ClickableNonLink,
    body.close-lnav .left-nav .AspNet-TreeView > ul > li > .AspNet-TreeView-NonLink,
    body.close-lnav .left-nav .AspNet-TreeView > div > ul > li > .AspNet-TreeView-NonLink {
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 40px;
        border-radius: 8px;
    }

/* Larger than inline, and no right margin - there is no label beside it now. Both selectors are
   needed: the second is (0,3,1) so it also clears theme.css's svg.icon sizing for the inline
   brand mark, which the class-only selector would not on its own. */
body.close-lnav .left-nav .nav-icon,
body.close-lnav .left-nav svg.nav-icon {
    width: 22px;
    height: 22px;
    min-width: 0;
    margin-right: 0;
    vertical-align: middle;
}

/* Open */
body.show-lnav .left-nav {
    opacity: 1;
    pointer-events: auto;
    transform: translateX(0);
}

body.ln-disabled .left-rail {
    display: none;
}




/* ---------------------------------------------------------
 MAIN CONTENT (CENTER COLUMN)
 --------------------------------------------------------- */

/* Page fade in so content is ready */
.app-loading .main-content {
    opacity: 0;
    transform: translateY(4px);
}

.main-content {
    grid-column: 3;

    /* TL-72 2026-09-20: the content column carries its own cap and centres itself.
     *
     * The cap used to live on the shell's track - minmax(0, --content-max-width) on .layout-body
     * - which is why fixing the left gutter for TL-71 threw every narrow page hard left: with
     * one gutter fixed and the cap still on the track, all the slack had nowhere to go but the
     * right-hand track. The two requirements are only in conflict while the content column and
     * the chrome share a gutter.
     *
     * So they no longer share one. The shell track is now a plain 1fr - it spans everything
     * between the fixed gutter and the rail, so the left nav starts where the appbar's left nav
     * column starts and the alignment from TL-71 is untouched - and the CAP moved here, onto the
     * item in that track, with auto inline margins. The content is therefore centred in the
     * space available to it: the whole width on an unauthenticated page, the width beside the
     * nav on an authenticated one, and it moves when the nav collapses, which is what centred in
     * the space available means. */
    width: 100%;
    max-width: var(--content-max-width);
    margin-inline: auto;

    /* layout */
    display: flex;
    flex-direction: column;
    /* ✅ RESTORED VIEWPORT CLAMP */
    /*max-height: calc(100vh - var(--header-footer-total));*/
    max-height: none;
    /* critical for nested scroll containers */
    min-height: 0;
    /* visuals */
    background: var(--bg);
    box-sizing: border-box;
    min-width: 0;
    /* spacing */
    padding-top: var(--page-vertical-gap);
    padding-bottom: var(--page-vertical-gap);
    opacity: 1; /*Page fade in so content is ready*/
    transform: none;
    transition: opacity 180ms ease-out, transform 180ms ease-out; /*Page fade in so content is ready - this is the fade timing*/
}

.main-inner {
    width: 100%;
    max-width: var(--content-max-width);
    margin: 0 auto;
    box-sizing: border-box;
    min-width: 0;
    display: flex;
    flex-direction: column;
    height: 100%;
    min-height: 0;
}
/* ---------------------------------------------------------
   FOOTER (mirrors appbar structure)
   --------------------------------------------------------- */
.site-footer {
    background: var(--bg);
    border-top: 1px solid var(--border);
    padding: 7px 0;
    flex: 0 0 auto;
    font-size: 13px;
    line-height: 1.25;
    position: sticky;
    bottom: 0;
    z-index: 900;
}

/* TL-79 2026-09-20: the footer goes at phone width, on EVERY page.
 *
 * It holds a logo, a support link, a copyright line and a website link. The support link is
 * also in the user flyout, so nothing here is only here, and 63px of sticky chrome is a lot to
 * give up on a phone.
 *
 * 768 rather than the 1100 the compact chrome uses, and a plain width query rather than
 * body.nav-compact, for two reasons. On a laptop at a narrow window the footer is still
 * ordinary chrome and worth keeping. And since TL-75 body.nav-compact can be on at 1440 for an
 * Administrator, whose menu bar does not fit - hanging the footer off that class would take the
 * footer away on a desktop, which is not what anyone asked for.
 *
 * THIS REPLACES A RULE THAT NEVER WORKED. nav.css carried ".footer { display: none }" inside
 * its 1100 query and "body.nav-compact .footer" beside it, and the element is
 * <footer class="site-footer"> - there is no .footer in this application at all. So the footer
 * has been showing at every width since TL-70, on every browser, including the iPhone capture
 * that was otherwise correct. */
@media (max-width: 768px) {
    .site-footer {
        display: none;
    }

    /* and the height maths has to agree, or the page reserves 63px for something that is not
       there: --header-footer-total is appbar + footer (theme.css:102) and is what .left-nav and
       the right rail size themselves against. */
    body {
        --header-footer-total: var(--appbar-height);
    }
}

/* U2-002 (UX standards, 2026-10-12): THE SAME DECISION ON THE HEIGHT AXIS. A short window cannot
 * afford 63px of sticky footer: appbar 86 + report actions 53 + footer 63 is 52% of an 844x390
 * landscape phone and 25% of 1280x600, and landscape tablets (680-830 tall) are the real small
 * screen here. 700px is the threshold the dialog summary digest already uses. Width does not
 * come into it, so this is its own query and the 768 one above is left alone. Same two parts as
 * that rule: the footer goes, and the height maths agrees. Nothing is only in the footer (the
 * support link is in the user flyout). Chrome budget: UI_STANDARDS.md 12.2 rule 5. */
@media (max-height: 700px) {
    .site-footer {
        display: none;
    }

    body {
        --header-footer-total: var(--appbar-height);
    }
}

.footer-inner {
    display: grid;
    /*grid-template-columns: 1fr var(--layout-leftnav-col) minmax(0, var(--content-max-width)) var(--layout-rail-col) 1fr;*/
    /* TL-71: full width, same as the appbar. */
    grid-template-columns: var(--shell-gutter) var(--layout-leftnav-col) minmax(0, 1fr) var(--layout-rail-col-static) var(--shell-gutter);
    width: 100%;
    box-sizing: border-box;
}

.footer-left {
    grid-column: 2;
    display: flex;
    align-items: center
}

.footer-center {
    grid-column: 3;
    display: block;
}

.foot-right {
    grid-column: 4;
    text-align: right;
}

.footer-center-grid {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    height: 100%;
    box-sizing: border-box;
}

.footer-copy-container {
    display: flex;
    justify-content: flex-end;
    min-width: 0;
}

.mg-logo {
    height: 38px;
    width: auto;
    opacity: .9;
}

/* Dark mode: the grey MG logo is unreadable on the dark footer, so the dark-theme file (same artwork, grey made white, orange kept,
   transparent) replaces it. One switch, the html[data-theme="dark"] that theme-init.js and Site.master already write, so no markup,
   no second img and no code-behind. content: url() on an img swaps the picture and keeps the element, its alt text and the link.
   Relative to this file, like the mask urls in updates.css. This file's ?v= stamp changes with every edit. See docs/design/DARK_MODE_2026-10-16.md. */
html[data-theme="dark"] .mg-logo {
    content: url("../../Images/MG_Logo_final_Dark_transparent_horz.svg");
}

.copy-top {
    font-weight: 500;
    display: block;
    opacity: .6;
}

.copy-bottom {
    font-weight: 600;
    display: block;
    opacity: .7;
}

.foot-right .copy-bottom.footer-link {
    color: inherit;
    text-decoration: none;
    opacity: .6;
}

    .foot-right .copy-bottom.footer-link:visited {
        color: inherit;
    }

    /* Footer links never underline (UI_STANDARDS 3.1): hover goes to brand-ink only. Dark below uses the dark token equivalents of the same behaviour.
       Keyboard focus keeps the shared focus ring from theme.css. */
    .foot-right .copy-bottom.footer-link:hover {
        color: var(--brand-ink);
        opacity: .9;
    }
/* Dark mode, footer text (2026-10-17, typography review): light text on a dark ground looks heavy and blurry (halation) at weight 500 or 600 in near-white
   at reduced opacity. In dark only: regular weight, the muted text token at full opacity (opacity blended the text into a fuzzy grey), a touch of letter
   spacing, 12px (the light footer is 13px and is unchanged). Links are one step brighter than the plain text (a mix of the muted and ink tokens), never
   underlined; on hover they go to brand-ink (the same behaviour as light, dark token values). Contrast is in docs/design/DARK_MODE_2026-10-16.md section 9. */
html[data-theme="dark"] .foot-right {
    font-size: 12px;
    letter-spacing: 0.02em;
}

html[data-theme="dark"] .copy-top,
html[data-theme="dark"] .copy-bottom {
    font-weight: 400;
    color: var(--muted);
    opacity: 1;
}

html[data-theme="dark"] .foot-right .copy-bottom.footer-link,
html[data-theme="dark"] .foot-right .copy-bottom.footer-link:visited {
    color: color-mix(in srgb, var(--muted) 65%, var(--ink));
    text-decoration: none;
    opacity: 1;
}

/* the dark rest rule above out-ranks the shared hover rule (it has the html[data-theme] prefix), so hover repeats the same behaviour here: brand-ink, no underline */
html[data-theme="dark"] .foot-right .copy-bottom.footer-link:hover {
    color: var(--brand-ink);
    text-decoration: none;
    opacity: 1;
}

/* ---------------------------------------------------------
   RESPONSIVE BEHAVIOR
   --------------------------------------------------------- */
@media (max-width: 1100px) {

    /* CBS-32 2026-09-19: DO NOT COLLAPSE THE SHELL TO ONE COLUMN HERE.
     *
     * This block used to set grid-template-columns: 1fr on all three shells and move the
     * centre regions to column 1. That threw away the whole column contract below 1100,
     * and the consequences were only ever fixed one page at a time: CBS Invoices had to
     * put the five-column template back for .layout-body (CBS-23) and then again for the
     * appbar and footer (CBS-24), while Commission Plans, Store Groups and Default were
     * left with a rail column that reappeared out of nowhere and an appbar whose logo and
     * user chip sat flush against the page edges.
     *
     * The template does not need collapsing, because both variable columns already go to
     * zero on their own. --layout-rail-col is opt-in (CBS-31): a page that has not asked
     * for a rail resolves it to 0, and one that has keeps its column or its 48px strip. The
     * left nav is hidden here, so its column is zeroed below. Everything else - the shell
     * gutters, the content cap, the centre regions staying in column 3 - then behaves at
     * 1099 exactly as it does at 1101, which is what makes this a breakpoint rather than a
     * cliff. */
    /* The left nav is hidden here, so its column is zero whatever state it is in. The
       list mirrors the five selectors that set it above, because a plain "body" is (0,0,1)
       and loses to their (0,2,1) - measured: it did, and Commission Plans came back at
       1099 with a 240px empty column. The bare "body" is a catch-all for a body that
       somehow carries no ln- class at all. */
    body,
    body.ln-enabled.show-lnav,
    body.ln-enabled.close-lnav.ln-reclaim,
    body.ln-enabled.close-lnav:not(.ln-reclaim),
    body.ln-enabled.ln-reclaim.ln-closing,
    body.ln-disabled {
        --layout-leftnav-col: 0px;
    }

    /* .right-rail was in this list and never did anything: rightrail.css loads after this
       file and its base ".right-rail { display: flex }" has the same specificity, so it won
       on source order on every page. Removed 2026-09-19 (CBS-23) because it READ as "the
       rail is hidden below 1100", which is the wrong mental model and cost several rounds
       of looking in the wrong place.
       The side regions stay hidden: .appbar-left, .appbar-right, .footer-left and
       .footer-right are empty spacer divs, and .left-rail is the left nav itself. Hiding
       them costs nothing now that their columns are zero rather than gone. */
    /* TL-148: .left-rail IS DELIBERATELY NO LONGER IN THIS LIST. Everything else here is an empty
       spacer div; the rail is the left nav itself, and hiding it below 1100 took SEARCH away at
       exactly the width where fast navigation matters most. It floats instead - see below. Its grid
       column stays at 0px above, which is what keeps CBS-32's column contract intact: a floating
       rail must contribute no layout width. */
    .appbar-left,
    .appbar-right,
    .footer-left,
    .footer-right {
        display: none;
    }

    /* =============================================================================================
       TL-148: BELOW 1100 THE LEFT NAV BECOMES A FLOATING ICON RAIL INSTEAD OF DISAPPEARING.

       WHY, in the tester's own scenario: a district manager with his boss on WhatsApp taking a third
       of the screen and the site in the rest, being asked to look something up live. Hiding the nav
       took the SEARCH box with it - so the narrowest width, where finding things fastest matters
       most, was the one width with no search at all.

       THE MECHANISM IS THE ONE ALREADY BUILT FOR THE PEEK, not a new one. The rail shows icons, a
       click opens the nav over the content, an outside click collapses it again. That is TL-141
       unchanged; the only new parts are making it happen at this width and floating rather than
       sitting in a grid column.

       position: fixed, NOT sticky. Above 1100 .left-rail is `grid-column: 2; position: sticky` in a
       real column. Here the column is 0px - and must stay 0px - so there is no track to be sticky
       within and it has to leave the flow entirely. Offset by --appbar-height and bounded by
       --header-footer-total so it cannot run under the footer.

       IT TAKES NO SPACE FROM THE CONTENT, which is what makes it affordable. The width comes out of
       the rail's OWN padding: .left-nav carries `padding: 0 16px`, so 32 of the 56px rail was
       padding around a 22px icon. At 6px a side the rail is about 34px and sits over the 16px shell
       gutter and the 16px .page-body margin that already exist there - the content column is not
       narrowed at all.

       z-index 901 is one above the appbar's 900, so the rail stays reachable instead of sliding
       under the header as the page scrolls.
       ============================================================================================= */
    body.ln-enabled .left-rail {
        display: flex;
        position: fixed;
        top: var(--appbar-height);
        left: 0;
        bottom: auto;
        max-height: calc(100vh - var(--header-footer-total));
        max-height: calc(100dvh - var(--header-footer-total));
        width: auto;
        min-width: 0;
        z-index: 901;
        padding-top: 8px;
        border-right: 1px solid var(--border);
        box-shadow: 2px 0 6px rgba(var(--shadow-rgb), 0.06);
    }

    /* The rail's own padding is where the width is. 6px a side keeps a comfortable target around a
       22px icon without borrowing anything from the content. */
    body.ln-enabled .left-nav {
        padding-left: 6px;
        padding-right: 4px;
    }

    /* NO TOGGLE AT THIS WIDTH, AND IT IS DELIBERATE TWICE OVER - DO NOT RESTORE IT.
       TL-148's reason was mechanical: the rail is about 34px and a second control beside ten icons
       does not fit.
       TL-201's ruling, 2026-10-02, is the stronger one and it would hold even with room: on a WIDE
       screen the user has space and the choice to keep the nav open is genuinely theirs, so a
       persistent toggle earns its place. Here there is NO CHOICE TO OFFER - the nav cannot persist
       without covering the page, so every open is temporary - and a toggle would be a control for a
       decision the user does not get to make. A click on any icon opens it; an outside click or a
       menu selection closes it. */
    body.ln-enabled .left-nav-toggle {
        display: none;
    }

    /* OPEN AT THIS WIDTH the full nav OVERLAYS the content rather than widening the shell - the
       same trade the hamburger drawer already makes. Search comes back with it, which is the
       entire point of the change.

       TL-201: KEYED ON show-lnav, NOT ON ln-peek. The class ln-peek recorded that the nav had been
       opened by a root icon rather than by the toggle, and persistence is now decided by the WIDTH
       instead, so the class has gone from the JS entirely. show-lnav is the open state and is
       already what the rule above keys off to zero this column at this width - which is what makes
       the overlay possible in the first place - so these two rules now agree with it rather than
       describing a parallel state.
       NOTHING VISIBLE CHANGED HERE: ln-peek was the ONLY way the nav could be open below 1100px,
       because MT_EnforceNarrowNav collapsed any other open nav at this width. The selector widens
       to a case that could not occur. */
    body.ln-enabled.show-lnav .left-rail {
        width: var(--leftnav-width);
        min-width: var(--leftnav-width);
        background: var(--bg);
        box-shadow: 4px 0 16px rgba(var(--shadow-rgb), 0.18);
    }

    body.ln-enabled.show-lnav .left-nav {
        padding-left: 16px;
        padding-right: 16px;
    }

    /* THE GUTTER, ONCE FOR EVERY PAGE. The floating rail takes no layout width (its grid column is 0), so a page whose
       content starts at the usual 32px (16px shell + 16px .page-body margin) runs under the rail, which is about 49px wide.
       Reserve the room here, on .page-body, instead of padding each page: left edge 16 + 40 = 56px, clear of the rail with a
       small gap. Pages that set their own .page-body margin (the Notifications page) keep their own rule. */
    body.ln-enabled .page-body {
        margin-left: 40px;
    }
}
/* ---------------------------------------------------------
 PAGE LAYER — CORE PAGE STRUCTURE
 --------------------------------------------------------- */
/* Page container: declares page intent and width only */
.page {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}
    /* Collapse truly empty page-bodies (no elements or text) */
    .page > .page-body:empty {
        display: none;
    }
/* Page body: owns vertical space, neutral by default */
.page-body {
    flex: 1;
    min-height: 0;
    margin: 0 16px;
}
    /* Page-level scrolling (static pages) */
    .page-body.page-scroll {
        overflow-y: auto;
    }
    /* Everything below the header UC must fill remaining height */
    .page-body.page-form > .page-centered {
        flex: 1;
        min-height: 0;
        width: 100%;
    }
    /* If your centered wrapper is also centering horizontally, keep it;
   but it must NOT steal height from descendants */
    .page-body.page-form > .page-centered {
        align-items: center; /* ok */
        justify-content: flex-start; /* ok */
    }
    /* ---------------------------------------------------------
    PAGE WITH HEADER
    --------------------------------------------------------- */
    /* Form pages: header + scroll-card share vertical space */ /* Form pages: header + */
    .page-body.page-form > .cbs-topband.card {
        flex: 0 0 auto;
    }
    /* Scroll-card consumes remaining height (so it can’t run behind sticky footer) */
    .page-body.page-form > .card.card-scroll {
        flex: 1 1 auto;
        min-height: 0;
    }

    .page-body.page-form {
        display: flex;
        flex-direction: column;
        min-height: 0;
        width: 100%; /* ✅ prevents shrink-to-fit */
        max-width: var(--single-focus-card-width); /*  cap */
        align-self: center; /* keep centered within the page rail */
    }

    /* TL-66 2026-09-20: a form page whose content is SEVERAL cards.
     *
     * The two patterns in this file each assume one thing owns the scroll: page-scroll gives it
     * to .page-body (Default, StoreInsights, the Security pages), and page-form gives it to a
     * single > .card.card-scroll, whose .card-content scrolls (CBSBillingContact). CBSAutoPay
     * has neither - it is page-form with a header band and TWO plain cards - so nothing scrolled,
     * and because .layout-body is overflow: hidden the bottom of the form and the Save button
     * were not merely below the fold, they were unreachable.
     *
     * Combining the two classes is the fix, and this rule is what makes the combination behave:
     * in a flex column with overflow-y: auto the children would otherwise SHRINK to fit rather
     * than overflow and scroll, so the cards would compress instead of the page scrolling. Every
     * direct child keeps its natural height and the page body scrolls past them.
     *
     * Scoped to both classes together, so the existing page-form pages and the existing
     * page-scroll pages are untouched. */
    .page-body.page-form.page-scroll > * {
        flex: 0 0 auto;
    }
    /* U2-020 (UX standards, 2026-10-12): A SINGLE SCROLL CARD ON A PAGE THAT SCROLLS ITSELF. Developer ruling 2026-10-12: forms scroll with the page, only dialog bodies scroll inside. Applies to every .page-body.page-scroll with a direct .card.card-scroll (Billing contact, Change password, Entity, Link report subscription).
     *
     * Billing contact is page-form with one .card.card-scroll, so its .card-content was the scroller,
     * squeezed by the account band, appbar and footer: 35px of a 1021px form was visible at 1280x600
     * and 115px at 1366x680 (the screen class real users have). Its markup now also carries
     * page-scroll (the TL-66 combination), so the page body scrolls and the card keeps its natural
     * height: this rule takes the card's own cap and scroller away in that combination only, and keeps
     * the Save/Cancel footer in view at the bottom of the scrolling page. No other page has a single
     * .card-scroll under page-form + page-scroll, so nothing else changes. */
    .page-body.page-scroll > .card.card-scroll {
        max-height: none;
        /* THE DEFECT FOUND ON TEST 2026-10-12: .page-body.page-form > .card.card-scroll (flex: 1 1 auto; min-height: 0) is as
           specific as this and the page-scroll catch-all above, so the card kept shrinking to the page body: at 1280x600 it was
           330px tall with a 114px content area, the fields overflowed it and the sticky footer sat on top of them. In the
           page-scroll combination the card takes its natural height and the page body scrolls. */
        flex: 0 0 auto;
        min-height: auto;
    }

        .page-body.page-scroll > .card.card-scroll .card-content {
            overflow-y: visible;
        }

        .page-body.page-scroll > .card.card-scroll > .card-footer {
            position: sticky;
            bottom: 0;
            z-index: 1;
            background: var(--surface);
            padding: 8px 0;
        }
        /* Ensure the scroll-card uses the full allowed width (up to the single-focus cap)/* Ensure the scroll-card uses the full allowed width ( a flex item inside .page-body.page-form */
        .page-body.page-form > .card.card-scroll {
            width: 100%;
            max-width: var(--single-focus-card-width);
            align-self: center;
        }
/* ---------------------------------------------------------
   SHORT WINDOWS: A GRID CARD KEEPS A USABLE MINIMUM AND THE PAGE SCROLLS (developer ruling 2026-10-13)
   One rule for every grid-in-card page (Edit Users, Delete Users, Store Info, Store Goals, Store Groups, Commission Plans,
   My Scheduled Reports, CBS Invoices, Requests). The card still fills the window and its .card-content still scrolls with a
   sticky header, but it is never shorter than --grid-card-min; when the chrome plus that minimum exceeds the window the PAGE
   scrolls (the same combination the form pages use). 700px is the height threshold the footer and the dialog digest already
   use. The purpose line under the title yields first.
   --------------------------------------------------------- */
:root {
    --grid-card-min: 560px;   /* header and filters (80-170), the 56px grid header, then about 7 rows of 49px */
}

@media (max-height: 700px) {
    body.grid-wide .page-body:not(.page-scroll) {
        overflow-y: auto;
    }

    body.grid-wide .page-body:not(.page-scroll) > .card.card-scroll {
        height: 100%;
        min-height: var(--grid-card-min);
        max-height: none;
    }

    body.grid-wide .card-header .card-sub {
        display: none;
    }

    /* the header block (title, filters, the action bar with Pay on CBS Invoices) stays at the top while the page scrolls
       past the account band: the card's actions are never scrolled out of reach (CI-01) */
    body.grid-wide .page-body:not(.page-scroll) > .card.card-scroll > .card-header {
        position: sticky;
        top: 0;
        z-index: 4;
        background: var(--surface);
    }
}

/* ---------------------------------------------------------
   SHORT WINDOWS: A CARD THAT SCROLLS ITSELF MUST STILL SHOW SOMETHING (developer ruling 2026-10-13)
   A form is ONE card whose .card-content scrolls with the header and footer fixed (Register user, Forgot password,
   QuickBooks connections). At 600px tall that card was squeezed to a sliver, so under 700px tall it gets compact: less
   padding, a tighter field rhythm and no decorative sub-title, so the header, a couple of fields and the footer fit.
   --------------------------------------------------------- */
@media (max-height: 700px) {
    .page-body > .card.card-scroll {
        padding-top: 12px;
        padding-bottom: 12px;
    }

    .page-body > .card.card-scroll .card-sub {
        display: none;
    }

    .page-body > .card.card-scroll .card-header {
        margin-bottom: 8px;
    }

    .page-body > .card.card-scroll .form-row {
        margin-bottom: 8px;
    }

        .page-body > .card.card-scroll .form-row label {
            margin-bottom: 4px;
        }

    .page-body > .card.card-scroll .card-footer {
        margin-top: 8px;
    }
}

/* ---------------------------------------------------------
   PAGE CENTERED
   --------------------------------------------------------- */
.page-centered {
    display: flex;
    flex-direction: column;
    justify-content: flex-start; /* vertical: top */
    align-items: center; /* horizontal: center */

    background: var(--bg);
    gap: 24px;
}
/* ---------------------------------------------------------
   STANDARD FORMS - FOR HEADER AND SUB FIXED, SCROLLING FORM BODY
   --------------------------------------------------------- */
.form {
    overflow-y: auto;
    min-height: 0;
    height: 100%;
    position: relative;
}
/* ---------------------------------------------------------
   AUTH PAGES — GLOBAL, CLEAN, MODERN
   --------------------------------------------------------- */
body.auth-page {
    --content-max-width: var(--content-max-width-auth);
    --bg: var(--surface);
}

    body.auth-page .left-rail {
        border-right: none;
        box-shadow: none;
    }

    body.auth-page .card {
        max-width: none;
        width: 100%;
    }
    /* Keep page-body as the height boundary */
    body.auth-page .page-body {
        display: flex;
        flex-direction: column;
        min-height: 0;
    }
    /* Center the card without stealing height from descendants */
    body.auth-page .auth-center {
        flex: 1;
        min-height: 0;
        display: flex;
        flex-direction: column; /* main axis = vertical */
        justify-content: center; /* vertical centering */
        align-items: stretch; /* keep card full width */
    }
    /* The scroll owner (unchanged, but shown for completeness) */
    body.auth-page .card.card-scroll .card-content {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
    }
    /* Footer remains a natural block at the bottom of the TD */
    body.auth-page .card.card-scroll .card-footer {
        flex: 0 0 auto;
    }
    /* Make confirmation match the form width on anonymous pages */
    body.auth-page .confirm-card {
        width: 100%;
        max-width: none;
        padding: 24px; /* match .card padding for perfect edge alignment */
    }
        /* If you use the success modifier, this keeps the accent and alignment */
        body.auth-page .confirm-card.confirm-success {
            border-top: 4px solid var(--brand-primary);
        }
/* REMOVED, changed confirm-card to var(--shadow-1).... Scoped to anonymous pages so authenticated cards aren’t affected */
/*body.auth-page .confirm-card {*/
/* keep your current background, radius and top accent */
/*border: 1px solid var(--border);*/ /* ✨ crisp edge */
/*background-clip: padding-box;*/ /* prevents border from tinting the fill */
/* keep your existing shadow, or tweak as below */
/*}

    body.auth-page .confirm-card {
        box-shadow: 0 6px 16px rgba(var(--shadow-rgb), .04);
    }*/
.auth-links {
    display: flex;
    justify-content: space-between;
    margin-top: 12px;
}

    .auth-links .link {
        font-size: 0.9rem;
    }

#ctl00_ContentPlaceHolder1_RegistrationForm {
    display: grid;
    grid-template-rows: auto auto 1fr;
    min-height: 0;
}
/* Password wrapper */
.password-wrap {
    position: relative;
}

    .password-wrap .toggle-pw {
        position: absolute;
        right: 8px;
        top: 50%;
        transform: translateY(-50%);
        width: 34px;
        height: 34px;
        border-radius: 10px;
        background: rgba(var(--shadow-rgb), 0.04);
        border: 1px solid var(--icon-btn-border);
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        color: var(--brand-ink);
        opacity: .95;
    }
        /* The eye is a filled glyph: it takes the button colour in both themes. Without this an svg with no fill
           attribute (ForgotPassword, RegisterUser, ChangePassword) painted black, 1.37:1 on the dark input. */
        .password-wrap .toggle-pw svg.icon {
            fill: currentColor;
        }
        /* Hover lift for password toggle (matches icon button behavior) */
        .password-wrap .toggle-pw:hover {
            transform: translateY(calc(-50% - 1px));
        }
            /* Accent stroke on hover: apply to SVG ONLY, and NOT when the tab is active */
            .password-wrap .toggle-pw:hover svg.icon {
                stroke: currentColor;
                stroke-width: var(--icon-accent-stroke-width, 0);
            }

.auth-links {
    display: flex;
    justify-content: space-between;
    margin-top: 12px;
}

    .auth-links .link {
        font-size: 0.9rem;
    }

#ctl00_ContentPlaceHolder1_RegistrationForm {
    display: grid;
    grid-template-rows: auto auto 1fr;
    min-height: 0;
}

/* Validation */
/* Error messages */
.field-error,
.validation-error {
    color: var(--danger);
    font-size: 13px;
    margin-top: 6px;
}

.is-valid {
    color: var(--success);
    font-size: 13px;
    margin-top: 6px;
}

/* VAL-10: .is-valid and .field-error are written for MESSAGE elements - a line of text under a
   field. font-size and margin-top are right there and wrong on the CONTROL itself: an input that
   picks up either one changes size and gains 6px of margin, so the field visibly moves every time
   its validity changes. That is the "shifts up slightly" on the user edit rail, and it happens
   anywhere a control carries these classes. Take the colour, leave the text metrics alone. */
input.is-valid,
input.field-error,
select.is-valid,
select.field-error,
textarea.is-valid,
textarea.field-error {
    font-size: inherit;
    margin-top: 0;
}

/* =================================================================================================
   TL-180 2026-09-30: .field-error HAD NO CONTROL-LEVEL APPEARANCE AT ALL, WHICH IS WHY A FIELD
   COULD BE LISTED IN A VALIDATION SUMMARY AND STILL LOOK PERFECTLY FINE.

   WHAT WAS ACTUALLY THERE. .field-error set a text colour, for a MESSAGE element under a field - and
   the VAL-10 rule immediately above then strips its font-size and margin back off again when it
   lands on a control, correctly, because those metrics move the field. So on an input the class
   ended up applying a colour to text the input does not render, and nothing else. It was doing
   visually nothing. `.field-error` is nonetheless the app's mechanism - it is what
   report.page.validation.js highlightFields applies, and what ucStorePicker.validate already returns
   a `field` id for - so the fix is to give it the appearance it never had, not to invent a class.

   THE APPEARANCE IS DELIBERATELY .input.invalid's (theme.css:683), NOT A NEW ONE. That rule already
   existed for the same purpose and two of the pickers' dropdowns use it, so the app had two invalid
   conventions with only one of them visible. Matching them means a field looks the same however it
   was marked, and nothing that currently uses .invalid changes.

   THE STORE PICKER IS INCLUDED BECAUSE IT IS NOT AN INPUT. It is a tree in a container, so it gets a
   border and ring on the container itself. ucStorePicker.validate already reports
   MT_StorePickerRootId as the failing field for exactly this, with a comment saying so - the id was
   being handed over and then nothing was styling it.
   ================================================================================================= */
input.field-error,
select.field-error,
textarea.field-error,
.input.field-error,
.store-picker.field-error,
.field-error > .store-picker {
    border: 1px solid var(--danger);
    border-radius: 8px;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--danger) 20%, transparent);
}

.input.is-valid {
    border-color: var(--success);
    background-color: var(--surface-success) !important;
    color: var(--success) !important;
}

    .input.is-valid:focus {
        outline: none;
        border-color: var(--success);
        background-color: var(--surface-success) !important;
    }

/* if we see issues we may need a dedicated version for report pages */
.validation-card {
    /*position: fixed; This was overlaying the Run/Schedule toggle on report pages*/
    top: 0;
    left: 0;
    right: 0;
    z-index: 9999;
    margin-bottom: 16px;
    padding: 12px;
    background: var(--surface-danger);
    border-left: 2px solid var(--danger);
    border-radius: var(--radius);
    box-shadow: var(--shadow-1);
    text-align: left;
    color: var(--danger);
    font-size: 13px;
}

    .validation-card ul {
        margin: 0;
        padding-left: 12px;
    }

    .validation-card li {
        margin-bottom: 4px;
    }

    /* TL-103: was ".validation-card warning" - a DESCENDANT selector looking for a <warning>
       ELEMENT, which does not exist in HTML, so this block has never matched anything. The intent
       is the warning VARIANT of the card, which is a second class on the same element, so it needs
       the dot. Compare .qb-validation-card.warning in quickBooksExport.css, which is written
       correctly and is what this was copied from.
       Nothing currently sets the warning class: both MT_ShowValidation implementations
       (JavaScript/storeGroups.js and JavaScript/commissionPlans.js) only write innerHTML. So fixing
       the selector changes nothing on screen today - it makes the variant available to whoever wants
       a non-blocking warning, instead of leaving a rule that silently cannot work. */
    .validation-card.warning {
        background: var(--surface-warning);
        color: var(--warning);
        border-left: 2px solid var(--warning);
    }


/* Small screens */
/*@media (max-width: 480px) {
    .auth-card {
        padding: 16px;
    }

    .password-wrap .toggle-pw {
        right: 6px;
        width: 30px;
        height: 30px;
    }
}*/
/* ---------------------------------------------------------
   GRID PAGES
   --------------------------------------------------------- */
body.grid-wide {
    --content-max-width: var(--content-max-width-grid-wide);
}

/* =================================================================================================
   TL-166: A GRID PAGE HAS NO MAXIMUM CONTENT WIDTH. It fills the track it is given.

   THE CAP WAS COSTING INFORMATION, NOT PROTECTING READABILITY. At 2250px the invoice grid stopped at
   1600 and centred, so the Entity column truncated to "STC Manageme..." while roughly 280px of margin
   sat empty beside it. A cap exists to keep a LINE OF PROSE readable; a table is not prose, gains
   nothing from a short measure, and loses columns to it.

   AND IT IS WHAT DETACHED THE RIGHT RAIL. The rail sits in its own grid track at the right-hand end of
   .layout-body, so it is always against the right edge of the centre track. The CONTENT was the thing
   stopping short. With the cap gone the content's right edge IS the track's right edge, which is where
   the rail begins - so the grid and the rail touch at every width, with no gap to open. One change
   fixes the empty margin and the detached rail together, because they were the same fact seen twice.

   WHAT THE DESIGN SYSTEMS ACTUALLY SAY, since this was checked rather than assumed:
     - Material 3 gives Large and Extra-large window classes from 1200dp and prescribes no maximum
       content width. For a list-detail layout - which is what a grid page with a rail is - its
       guidance is that the list has a fixed width and "the detail pane should be flexible, adjusting
       to the available space by growing or shrinking".
     - Fluent 2 publishes breakpoints up to xxx-large (>1920px) and gives NO maximum-content-width
       guidance at all; its stated approach is container-based rather than viewport-based.
   So neither system supports capping a data view, and M3 arguably contradicts it.

   KEYED ON grid-wide, NOT ON A TENANT. An earlier version of this scoped to body.cbs-invoices and was
   wrong: Edit Users, Delete Users, Store Info, Commission Plans and Store Groups are all grid pages
   and none of them is a CBS page. The class that means "this page shows a data grid" is the one that
   should carry the behaviour.

   PROSE PAGES ARE UNAFFECTED. body.page-narrow and the default width both keep their cap, because
   that is where a measure genuinely helps - Help, About, NoAccess and Errors are reading, not
   scanning. This rule cannot reach them; it only lifts the cap where grid-wide is set.

   .page-centered .card KEEPS ITS CAP (the rule below this one). A centred card on a grid page is a
   form, not a grid, and stretching it to 2250px would be the mistake this change is avoiding in the
   other direction. */
body.grid-wide .main-content,
body.grid-wide .main-inner {
    max-width: none;
}

    body.grid-wide .page-centered .card {
        max-width: var(--content-max-width);
        margin: 0px 24px 0px 24px;
    }
    /* ---------------------------------------------------------
   GRID PAGES — STANDARD
   Card hugs content; grid scrolls inside card-content when constrained
   --------------------------------------------------------- */
    body.grid-wide .page-body {
        display: flex;
        flex-direction: column;
        min-height: 0;
        padding: 0 16px; /* ✅ inset lives here */
        box-sizing: border-box;
    }

        body.grid-wide .page-body > .card {
            width: 100%;
            max-width: none;
            margin: 0; /* ✅ remove margin now */
            min-height: 0;
            max-height: 100%;
        }
    /* Grid pages: card-content owns both vertical + horizontal scrolling.
       Together with theme.css:536 (overflow-y) this is THE horizontal scroll owner for
       CBS Invoices - not .grid-wrapper, whatever datagrid.css used to claim.

       THIS RULE IS LIVE. It is top-level CSS. The commented-out
       "@media (max-width: 480px)" a few rules above starts at line 883 and CLOSES AT
       LINE 893 - well before this one. Checked 2026-09-22 while chasing CBS-57, where it
       was nearly reported as dead code because the nearest comment-open above it was
       assumed to still be open. Before concluding that any rule in this file never
       applies, find the matching close, not just the nearest open. */
    body.grid-wide .card.card-scroll > .card-content {
        overflow-x: auto; /* ✅ horizontal scrollbar stays visible */
        border-radius: var(--radius);
    }
/* TODO: Maybe set breakpoints for larger screens to keep things from getting stretchy */
/* ---------------------------------------------------------
   TECH SUPPORT
   --------------------------------------------------------- */
/* unauthenticated tech support */
body.page-narrow {
    --content-max-width: var(--content-max-width-narrow);
    --bg: var(--surface);
}

    body.page-narrow .main-content {
        background: var(--surface);
    }

    body.page-narrow .left-rail {
        border-right: none;
        box-shadow: none;
    }

    body.page-narrow .main-inner {
        background: var(--surface);
    }

    body.page-narrow .page-centered {
        display: flex;
        justify-content: center; /* vertical */
        align-items: center; /* horizontal */
        height: 100%;
        background: var(--surface);
    }

    body.page-narrow .auth-card {
        display: grid;
        grid-template-rows: auto auto 1fr;
        max-height: 100%;
        min-height: 0;
    }

    body.page-narrow .auth-form {
        overflow-y: auto;
        min-height: 0;
        height: 100%;
    }
    /* Anonymous TechSupport: card fills the column */
    body.page-narrow .page-centered .auth-card {
        width: 100%;
        /* Clarify the cap; the token is already set */
        max-width: var(--content-max-width);
        /* margin-bottom: 24px;??? */
    }
    /* authenticated tech support */
    body.page-narrow.is-auth .layout-body .main-content {
        overflow-y: auto;
    }

    body.page-narrow.is-auth .page-centered #pnlProblem, body.page-narrow.is-auth .page-centered #pnlForm {
        width: 100%;
        max-width: var(--single-focus-card-width);
    }

    body.page-narrow.is-auth {
        --content-max-width: var(--content-max-width-default);
    }

        body.page-narrow.is-auth .main-content {
            background: var(--bg);
        }

        body.page-narrow.is-auth .page-centered {
            justify-content: flex-start;
            background: var(--bg);
            flex-direction: column;
            align-items: center; /* horizontal card centering */
            gap: 24px;
        }

            body.page-narrow.is-auth .page-centered > .auth-card {
                width: 100%;
                max-width: var(--content-max-width);
                margin-bottom: 24px;
            }

        body.page-narrow.is-auth #pnlProblem.auth-card {
            padding: 24px;
            box-sizing: border-box;
            margin-top: 0;
        }
/* ---------------------------------------------------------
   SUCCESS/CONFIRMATION CARD
   --------------------------------------------------------- */
.confirm-card {
    max-width: calc(var(--single-focus-card-width) * .85);
    margin: 0 auto;
    padding: 32px 32px 36px;
    background: var(--surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow-1);
    text-align: center;
}

    .confirm-card h2 {
        margin-top: 0;
    }

    .confirm-card .sub {
        margin-left: auto;
        margin-right: auto;
        max-width: 44ch; /* keeps success copy readable */
    }
    /* Confirmation handoff microcopy */
    .confirm-card .handoff {
        margin-top: 16px;
        margin-bottom: 8px;
        font-size: 0.95rem;
        font-weight: 500;
        line-height: 1.4;
        color: var(--muted);
    }

    .confirm-card.confirm-success {
        border-top: 4px solid var(--brand-primary);
        background-color: color-mix(in srgb, var(--brand-primary) 4%, var(--surface));
    }









/* =================================================================================================
   TL-197: CURRENT-SECTION EMPHASIS IN THE LEFT NAV, MIRRORING THE TOP MENU.

   THE TOP MENU'S TREATMENT IS REUSED, NOT REIMPLEMENTED, which was the whole instruction. Every
   value below is copied from nav.css:106-123 - the ::before bar inset 10px each side, 2px tall, 2px
   radius, sitting 2px off the bottom, scaleX(1) at rest collapsing to scaleX(0) on hover or
   focus-within over 160ms ease-out. ONLY THE COLOUR DIFFERS, because the left nav reverses the
   colours: the top bar is brand-coloured so its bar is white at .45, and here the surface is light
   so the bar is brand-primary at .4.
   IF THE TOP MENU'S ANIMATION EVER CHANGES, CHANGE THIS WITH IT. Two animations that are meant to
   match and are defined separately will drift, and consistency is the point of this row - there is
   no shared custom property to key off, so this comment is the link.

   WHY .4 AND NOT SOMETHING ELSE: the developer's number, and it is relative to the un-hovered icon.
   .nav-icon sits at opacity .6 (layout.css:484) with color var(--brand-primary), so .4 reads as
   slightly lighter than the icon beside it - present without competing with it.
   OPACITY ON THE PSEUDO-ELEMENT rather than an rgba() colour, deliberately: --brand-primary is a
   token whose format is not ours to assume, and rgba() needs channels. The pseudo-element is nothing
   but the bar, so fading the element and fading the colour are the same thing here.

   THE MARKER ALREADY EXISTED AND NOTHING SERVER-SIDE WAS ADDED. TreeViewAdapter writes its node
   class onto the <li> (:272), and GetNodeClass (:535) emits AspNet-TreeView-ChildSelected on any
   node with a selected descendant - which is exactly "the section the user is in". The top menu
   expresses the same idea as :has(a.selected); this is the left nav's equivalent, already in the
   markup.

   BOTH ANCHOR AND SPAN ARE TARGETED because a root section with no Url of its own renders as a
   <span>, not an <a> - that is the whole subject of TL-187. Styling only the anchor would light up
   CBS Invoices and miss Insights, Settings and Accounts, which are the sections this is for.
   ================================================================================================= */
.left-nav li.AspNet-TreeView-ChildSelected > a,
.left-nav li.AspNet-TreeView-ChildSelected > span,
.left-nav li.AspNet-TreeView-Selected > a,
.left-nav li.AspNet-TreeView-Selected > span {
    position: relative;
}

    .left-nav li.AspNet-TreeView-ChildSelected > a::before,
    .left-nav li.AspNet-TreeView-ChildSelected > span::before,
    .left-nav li.AspNet-TreeView-Selected > a::before,
    .left-nav li.AspNet-TreeView-Selected > span::before {
        content: "";
        position: absolute;
        left: 10px;
        right: 10px;
        bottom: 2px;
        height: 2px;
        background: var(--brand-primary);
        opacity: .4;
        border-radius: 2px;
        /* animation setup - the same shrink-inward as the top menu */
        transform: scaleX(1);
        transform-origin: center;
        transition: transform 160ms ease-out;
    }

    /* =============================================================================================
       TL-197 FIX, 2026-10-02: THE HOVER IS ON THE LABEL, NOT ON THE <li>. DO NOT MOVE IT BACK.

       THE BUG: on a ROOT and on a SUBMENU group the bar shrank on hover and then STAYED shrunk -
       it only came back much later, which read as "it never expands again".

       WHY, AND IT IS THE ONE WAY THE LEFT NAV IS NOT LIKE THE TOP MENU: this rule used
       li:hover > a::before, copied from nav.css:121 where it is correct. In the top menu an <li> is
       one horizontal item and its submenu is a flyout. IN THE LEFT NAV AN <li> CONTAINS ITS WHOLE
       EXPANDED SUBTREE - the nested <ul> and every child row sits INSIDE it. So moving the pointer
       off the root label and down onto its own children never leaves the <li>, li:hover stays true,
       and the bar stays collapsed until the pointer leaves the entire section.

       WHY THE CURRENT PAGE LOOKED FINE and these two did not: a selected LEAF has no children, so
       its <li> wraps only that one row and li:hover and a:hover are the same thing. The fault was
       only ever visible where the <li> had a subtree - which is exactly the root and submenu cases
       that were reported.

       :focus-within BECOMES :focus FOR THE SAME REASON - on a group it would stay true while focus
       was anywhere inside the subtree.

       I COPIED THE VALUES FROM THE TOP MENU AND CHECKED THEM ALL, AND COPIED THE SELECTOR SHAPE
       WITHOUT CHECKING THAT IT MEANT THE SAME THING IN A DIFFERENT TREE. The values were the easy
       half.
       ============================================================================================= */
    .left-nav li.AspNet-TreeView-ChildSelected > a:hover::before,
    .left-nav li.AspNet-TreeView-ChildSelected > a:focus::before,
    .left-nav li.AspNet-TreeView-ChildSelected > span:hover::before,
    .left-nav li.AspNet-TreeView-ChildSelected > span:focus::before,
    .left-nav li.AspNet-TreeView-Selected > a:hover::before,
    .left-nav li.AspNet-TreeView-Selected > a:focus::before,
    .left-nav li.AspNet-TreeView-Selected > span:hover::before,
    .left-nav li.AspNet-TreeView-Selected > span:focus::before {
        transform: scaleX(0);
    }

/* The collapsed rail shows icons only, so a bar inset 10px each side would be most of the row and
   would read as an underline on the icon rather than a section marker. Hidden there.

   THE STATE SELECTOR IS THE ONE THIS FILE ALREADY USES, not one invented for this rule. The collapsed
   column is body.ln-enabled.close-lnav.ln-reclaim (see the custom-property block at the top of this
   file); closed WITHOUT reclaim keeps the full width, so the bar should still show in that state and
   this must not match it. I first wrote .left-rail-collapsed here, which exists nowhere in the
   application - it would have been a rule that silently matched nothing. */
body.ln-enabled.close-lnav.ln-reclaim .left-nav li.AspNet-TreeView-ChildSelected > a::before,
body.ln-enabled.close-lnav.ln-reclaim .left-nav li.AspNet-TreeView-ChildSelected > span::before,
body.ln-enabled.close-lnav.ln-reclaim .left-nav li.AspNet-TreeView-Selected > a::before,
body.ln-enabled.close-lnav.ln-reclaim .left-nav li.AspNet-TreeView-Selected > span::before {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    /* The bar still marks the section; it just stops animating. Same concession the rest of this
       file makes. */
    .left-nav li.AspNet-TreeView-ChildSelected > a::before,
    .left-nav li.AspNet-TreeView-ChildSelected > span::before,
    .left-nav li.AspNet-TreeView-Selected > a::before,
    .left-nav li.AspNet-TreeView-Selected > span::before {
        transition: none;
    }
}

/* =============================================================================================
 THE COMPACT APP HEADER (developer ruling 2026-10-14, "mobile is so unusable we cannot break it more").
 86px of header is 14% of a 600px phone and 24% of a 360px-high landscape phone. Guidelines: Material 3 56px (48 landscape), Fluent about 48,
 iOS 44pt. Ours: 48px up to 640 wide or under 500 tall, 44px under 400 tall.
 ONE LEVER: --appbar-height is read by the app bar, the left rail and nav, the right rail, the report shell, the bell panel and the footer maths,
 so changing it here moves all of them. The rest of this block fits the bar content (logo, initials chip, bell) into the smaller bar.
 --page-vertical-gap goes from 24px to 8px (the 24px under the content area that nothing needs on a short or narrow screen).
 The name/date lines of the chip are hidden on short windows as well as narrow ones: three lines cannot sit in a 44px bar.
 Selectors carry .appbar so they beat nav.css (which loads after this file and sets the chip at 44px and 6px padding).
 ============================================================================================= */
@media (max-width: 640px), (max-height: 499.98px) {
    :root {
        --appbar-height: 48px;
        --page-vertical-gap: 8px;
    }
    .appbar .brand-logo img { height: 32px; }
    .appbar .user-trigger { width: 32px; height: 32px; border-radius: 8px; }
    .appbar .user-panel { padding: 3px; gap: 0; }
    .appbar .user-panel .user-meta { display: none; }
    .appbar .user-avatar { font-size: 13px; }
}
@media (max-height: 399.98px) {
    :root { --appbar-height: 44px; }
    .appbar .brand-logo img { height: 30px; }
    .appbar .user-trigger { width: 30px; height: 30px; }
}

/* THE FLOATING RAIL'S SCROLLBAR (found walking 640x360 on TEST, 2026-10-14): on a short window the ten icons do not fit, so the nav tree scrolls and
   a classic 15px scrollbar widened the rail from 49 to 64px, which put it 8px over the page's left edge (the first letters of every line were
   hidden). The scrollbar is hidden while the rail floats (under 1100 wide); wheel, touch and keyboard still scroll it. */
@media (max-width: 1100px) {
    body.ln-enabled .left-rail .AspNet-TreeView { scrollbar-width: none; }
    body.ln-enabled .left-rail .AspNet-TreeView::-webkit-scrollbar { display: none; }
}

/* THE LOGO LINES UP WITH THE RAIL'S ICONS (developer, 2026-10-15): in the compact bar the left edge of the logo's "M" sits on the same vertical line as
   the left edge of the left rail's icon glyphs. The rail's icon box starts at 14px; the glyph is drawn about 2px inside its box, so the logo's box
   starts at 16px: --rail-icon-x. Only while the rail floats at the left (under 1100); a docked rail has its own column. */
@media ((max-width: 640px), (max-height: 499.98px)) and (max-width: 1099.98px) {
    :root { --rail-icon-x: 16px; }
    .appbar .appbar-center-grid { margin-left: calc(var(--rail-icon-x) - var(--shell-gutter)); }
}

/* =============================================================================================
 THE DRAWER UNDER 641 WIDE (developer + designer, 2026-10-15). The floating icon rail is hidden and the nav is an on-demand overlay drawer:
 a button at the far left of the app bar (same icon as the rail uses on larger screens, 44px hit area) opens it; a scrim sits behind; it closes on a
 menu selection, a tap outside, or Escape. The space is given back: the page content is no longer indented for the rail. The open state is the
 existing body.show-lnav (MT_OpenLeftNav / MT_CloseLeftNav), whose overlay rules at under 1100 already position the open nav.
 ============================================================================================= */
.appbar-menu-btn { display: none; }
@media (max-width: 640px) {
    .appbar .appbar-menu-btn {
        display: grid;
        position: absolute;
        left: 4px;
        top: 50%;
        transform: translateY(-50%);
        width: 44px;
        height: 44px;
        place-items: center;
        padding: 0;
        border: 0;
        border-radius: var(--radius);
        background: transparent;
        color: var(--brand-on-primary);
        cursor: pointer;
        z-index: 2;
    }
    .appbar .appbar-menu-btn:hover,
    .appbar .appbar-menu-btn:focus-visible { background: color-mix(in srgb, var(--brand-on-primary) 14%, transparent); }
    /* the logo moves right of the button: 4 + 44 + 4 = 52px from the edge (the grid sits inside a 16px shell gutter) */
    .appbar .appbar-center-grid { margin-left: calc(52px - var(--shell-gutter)); }
    /* closed: no rail at all, and the page is not indented for it */
    body.ln-enabled:not(.show-lnav) .left-rail { display: none; }
    /* the shell's own 16px gutter is the only inset: a card is 328 of 360 wide on every page type (the default .page-body margin made it 296) */
    body.ln-enabled .page-body { margin-left: 0; margin-right: 0; }
    /* open: the drawer, with a scrim behind it */
    body.ln-enabled.show-lnav .left-rail {
        display: flex;
        width: min(280px, 86vw);
        min-width: 0;
        z-index: 1160;
    }
    body.ln-enabled.show-lnav::after {
        content: "";
        position: fixed;
        top: var(--appbar-height);
        right: 0;
        bottom: 0;
        left: 0;
        background: color-mix(in srgb, var(--ink) 40%, transparent);
        z-index: 1150;
    }
}

/* =============================================================================================
 THE MENU BUTTON ON A TABLET, 641 TO 1100 WIDE (iPad finding 8.1, 2026-10-18).
 Under 1101 the top menu is hidden (nav.css) and the left nav is only a floating strip of icons, so on an iPad in portrait (820 wide) a person saw no
 menu and no button, and the first icon, CBS Invoices, is a link that leaves the page. The same button the phone drawer uses now shows here, with the word
 Menu beside the icon (44px target), in the free first column of the app bar. It opens the same navigation (MT_OpenLeftNav). Nothing depends on hover.
 The open nav is lifted above a scrim, as on a phone, so a tap outside closes it. Wider than 1100 the top menu or the docked nav is used, as before.
 ============================================================================================= */
.appbar-menu-label { display: none; }
@media (min-width: 641px) and (max-width: 1100px) {
    .appbar .appbar-menu-btn {
        display: inline-flex;
        grid-column: 1;
        align-items: center;
        gap: 8px;
        min-width: 44px;
        min-height: 44px;
        margin-right: 16px;
        padding: 0 12px;
        border: 0;
        border-radius: var(--radius);
        background: transparent;
        color: var(--brand-on-primary);
        font: inherit;
        font-weight: 600;
        cursor: pointer;
    }
    .appbar .appbar-menu-btn:hover,
    .appbar .appbar-menu-btn:focus-visible { background: color-mix(in srgb, var(--brand-on-primary) 14%, transparent); }
    .appbar .appbar-menu-label { display: inline; }
    body.ln-enabled.show-lnav .left-rail { z-index: 1160; }
    body.ln-enabled.show-lnav::after {
        content: "";
        position: fixed;
        top: var(--appbar-height);
        right: 0;
        bottom: 0;
        left: 0;
        background: color-mix(in srgb, var(--ink) 40%, transparent);
        z-index: 1150;
    }
}

/* The docked nav's own toggle (1101 and wider, where the top menu may step aside) is a 36x26 target: 44x44 on a touch screen. */
@media (pointer: coarse) {
    .left-nav-toggle { min-width: 44px; min-height: 44px; }
}

/* No nav on this page (login, registration, the pages that disable it): no drawer button either. */
body.ln-disabled .appbar .appbar-menu-btn { display: none; }
@media (max-width: 640px) { body.ln-disabled .appbar .appbar-center-grid { margin-left: 16px; } }

/* U2-020 AGAIN, FOR A CARD INSIDE AN UPDATEPANEL (2026-10-15). The single-scroll-card rule above is written with the child combinator
   (.page-body.page-scroll > .card.card-scroll). An UpdatePanel renders a wrapper div (display: contents, set inline) between the page body and the
   card, so the rule never matched on Billing Contact and the form stayed a 98px slit inside a 1001px card on a phone. Same rule, one wrapper level. */
.page-body.page-scroll > div[id] > .card.card-scroll {
    max-height: none;
    flex: 0 0 auto;
    min-height: auto;
}
.page-body.page-scroll > div[id] > .card.card-scroll .card-content {
    overflow-y: visible;
}
.page-body.page-scroll > div[id] > .card.card-scroll > .card-footer {
    position: sticky;
    bottom: 0;
    z-index: 1;
    background: var(--surface);
    padding: 8px 0;
}

/* GRID PAGES ON A PHONE (2026-10-15): the page body carried a 16px margin AND a 16px padding on each side (the padding was written for the rail), so a
   grid card was 264 of 360px wide. Under 641 the shell's own 16px gutter is the only inset: the card gets 328. */
@media (max-width: 640px) {
    body.grid-wide .page-body { margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; }
}

/* COMPACT BAR POLISH (designer review 2026-10-15): under 641 the chip is just its 32px avatar (no outer bordered card around it) and the bar's right
   edge lines up with the card edge below (16px from the screen edge, the same as the left padding of the cards), not 32px. */
@media (max-width: 640px) {
    .appbar .user-panel { background: transparent; border-color: transparent; padding: 0; }
    .appbar .appbar-center-grid { margin-right: 0; }
}

/* =============================================================================================
 THE RIGHT RAIL TAKES NO COLUMN ON A SMALL SCREEN (2026-10-16, overlays lane).
 Under 641 wide, or on a short landscape phone (the same query rightrail.css uses for the overlay), the open rail is a panel over the page
 (rightrail.css, "RIGHT RAIL AS A PHONE OVERLAY"), so the grid keeps no 280px track for it: before this the content column was squeezed to 78px of a
 390px phone. The page's closed-state static track (appbar and footer) goes too. The :not() twice raises the specificity to 0,3,1 so it ties with
 the strongest column rule above (body.rr-enabled.close-rail:not(.rr-reclaim)) and wins on source order. CBS Invoices (cbs-rail-strip) keeps its own
 48px strip and its own overlay rules in tenants/cbs.css.
 ============================================================================================= */
@media (max-width: 640px), (max-width: 900px) and (max-height: 500px) {
    body.rr-enabled:not(.cbs-rail-strip):not(.cbs-rail-strip) {
        --layout-rail-col: 0px;
        --layout-rail-col-static: 0px;
    }
}
