/*
 * Portal app shell — geometry tokens, the floor tray, the content column,
 * and the phone tab bar. (The topbar died in v3.1; the sidebar in v3.1.1 —
 * the system bar's own rules live in portal-theme.css, the Launchpad's in
 * global-search.css.)
 */

:root {
    --portal-safe-top: 0px;
    --portal-safe-bottom: 0px;
    --portal-safe-left: 0px;
    --portal-safe-right: 0px;
    /* ── Floating chrome ──────────────────────────────────────────────
     * The strips are inset slabs rather than edge-to-edge bars: content
     * and the aurora run behind and around them, which is what makes the glass
     * read as glass rather than as a painted band.
     *
     * v3.1 (the fold): the TOPBAR IS GONE — its tenants live in the bottom
     * system bar (#site-footer). --portal-chrome-offset-top therefore changed
     * for real, 56px → 8px + safe: the ~40 consumers (the iOS sheet cap, the
     * dock pin, scroll margins, the reading measure) all read the token and
     * all moved together — that reclaimed strip is the whole point of v3.1.
     * --portal-chrome-offset-bottom was HELD unchanged by construction (the
     * bar is 44px, exactly the old footer), so the ~35 bottom consumers, the
     * kiosk zeroing blocks and the sidebar-footer alignment moved by zero. */
    --portal-chrome-inset: 0.5rem;
    --portal-chrome-gap: 0.5rem;
    /* A slab is a shell, not a card: the dialog tier, 14px. */
    --portal-chrome-radius: var(--bs-border-radius-xl);
    --portal-content-top-gap: var(--portal-chrome-gap);
    --portal-chrome-offset-top: calc(
        var(--portal-safe-top) + var(--portal-content-top-gap)
    );
    /* ── The sidebar's column ──────────────────────────────────────
     * The slab is 240px; the ACTIVE WIDTH is what the rest of the shell has
     * to get out of the way of — the slab plus the inset it floats in plus
     * the gap between it and the content. The content column's margin, the
     * reading measure, the system bar's left edge and the kiosk zero-outs
     * all read the derived one, so there is exactly one place to change if
     * the slab ever moves.
     *
     * ZERO HERE, and lit only by the @media block further down: below 768px,
     * and for any account that turned the sidebar off, the shell is the
     * full-bleed one v3.1.1 shipped — same computed values, byte for byte.
     * That is what makes on-by-default a safe default rather than a new
     * layout everyone has to opt out of. */
    --portal-sidebar-width-expanded: 240px;
    --portal-sidebar-width-collapsed: 56px;
    /* THE LIVE WIDTH, and the reason there is one. The slab's own width and
       the column everything else clears used to be two declarations reading
       two different tokens — which is exactly how the desktop collapse shipped
       broken for four commits in v3: a 240px slab with content sliding under
       it, because narrowing the clearance never narrowed the slab. Both read
       this now, so a mode that moves one moves the other by construction. */
    --portal-sidebar-width: var(--portal-sidebar-width-expanded);
    --portal-sidebar-active-width: 0px;
    --portal-footer-height: 44px;
    /* Phone bottom tab bar — 50px of navigation above any home-indicator safe
       area. The attached bar adds that inset to its total height and keeps it
       below the slots, so their touch geometry never changes as Safari chrome
       expands or collapses. */
    --portal-tab-bar-height: 50px;
    --portal-chrome-offset-bottom: calc(
        var(--portal-safe-bottom) + var(--portal-chrome-inset) + var(--portal-footer-height)
    );
    --portal-chrome-divider: var(--portal-chrome-border-dark, rgba(255, 255, 255, 0.08));
    --portal-shell-transition: var(--portal-dur-2, 200ms) var(--portal-ease-out, ease);
}

/* An installed iOS home-screen app draws under the status bar and home indicator
   (black-translucent in head-meta.twig + viewport-fit=cover in base.twig), so the
   fixed chrome has to reserve those strips itself. env() resolves to 0 anywhere
   without a physical inset, so these stay inert in a normal browser window — but not
   scoped to display-mode: standalone, because viewport-fit=cover applies everywhere
   and Safari in landscape has real left/right insets while uninstalled. @supports
   keeps the calc() above valid on engines that have custom properties but not env().
   Same approach as offline.twig. */
@supports (padding-top: env(safe-area-inset-top)) {
    :root {
        --portal-safe-top: env(safe-area-inset-top, 0px);
        --portal-safe-bottom: env(safe-area-inset-bottom, 0px);
        --portal-safe-left: env(safe-area-inset-left, 0px);
        --portal-safe-right: env(safe-area-inset-right, 0px);
    }
}

/* The status-bar inset is reserved in the layout, but reservation alone leaves
 * it transparent: while a page scrolls, cards and headings can still show
 * through that strip and look smeared into the top of an installed iPhone
 * app. Paint a small, fixed canvas cap over the inset AND the reserved gap
 * immediately below it, so the first page header cannot feather into the
 * clock/Wi-Fi/battery strip. It sits below every real overlay/chrome layer. */
@media (max-width: 767.98px) {
    /* iOS standalone sizes fixed chrome against the document box on short
       pages. A long catalogue naturally reaches the screen edge; Home, Login
       and empty states did not, so the remaining home-indicator strip showed
       the UA's black canvas below an otherwise identical tab bar. Make the
       shared shell own at least one FULL layout viewport on every phone
       route. This is intentionally `vh`, not `dvh`: iOS standalone reports
       the dynamic viewport above its bottom app canvas on short documents,
       which is the exact strip we need the shell to include. */
    body.portal-has-chrome,
    body:has(.portal-shell) {
        min-height: 100vh;
    }

    .portal-shell::after {
        content: "";
        position: fixed;
        z-index: calc(var(--portal-z-tabbar) - 1);
        top: 0;
        right: 0;
        left: 0;
        height: var(--portal-chrome-offset-top);
        background: #151225;
        pointer-events: none;
    }

    html[data-bs-theme="light"] .portal-shell::after {
        background: #f4f4f6;
    }

    html[data-bs-theme="matrix"] .portal-shell::after {
        background: #030503;
    }

    html[data-bs-theme="sport"] .portal-shell::after {
        background: #0b0a16;
    }
}

body.portal-has-chrome {
    overflow-x: hidden;
}

body:has(.portal-shell) {
    overflow-x: hidden;
}

/* The icon RAIL is not the default and never became one. It was the 56px
   compromise v3.1 cut the expanded sidebar down to on the way to deleting it,
   and what came back on 2026-08-28 is the expanded slab itself (Brian's call)
   — a column of glyphs you have to already know is a weaker navigation point
   than the one he asked for. It exists as the COLLAPSED state of that slab,
   opt-in per device, and as the shape a 768-991 window starts in; its rules
   are the rail-mode block further down. The kiosk zero-outs (mission-control,
   live-bracket) keep beating the shell by specificity, exactly as before. */

/* The topbar died with v3.1 (the fold) — its blocks lived here. The system
   bar's geometry is portal-theme.css's #site-footer rules (that file loads
   standalone on check-in, so the bar's fallbacks live with it); the floor
   strip below now renders inside it. */

/* ── Floor tray (Mission Control summary, in the system bar) ────────────
   Theme colours live in portal-theme.css, same split as every other bit of
   chrome here. flex: 0 1 auto — the tray shares the bar's centre cell with
   the POLL tag and gives ground before it clips. */
.portal-floor-strip {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex: 0 1 auto;
    min-width: 0;
    font-size: var(--portal-text-2xs);
    overflow: hidden;
    white-space: nowrap;
}

.portal-floor-strip-group {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.portal-floor-strip-label {
    text-transform: uppercase;
    letter-spacing: var(--portal-label-tracking);
    opacity: 0.65;
}

/* Counts are data — mono + tabular so they don't jitter as they tick. */
.portal-floor-strip-value {
    font-family: var(--portal-font-mono);
    font-variant-numeric: tabular-nums;
    font-weight: var(--portal-weight-label);
}

.portal-floor-strip-dots {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    /* Seeing every cage matters — when the row is tight, scroll the dots
       instead of clipping them. Scrollbar hidden, same as the footer stats. */
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
}

.portal-floor-strip-dots::-webkit-scrollbar {
    display: none;
}

.portal-floor-strip-dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: var(--portal-c-text-muted);
    opacity: 0.3;
}

/* A cage with a fight in it is the one thing worth colour in the chrome. */
.portal-floor-strip-dot--active {
    background: var(--portal-c-badge-success-color, #45d381);
    opacity: 1;
}

.portal-floor-strip-dot--queued {
    background: var(--portal-c-badge-warning-color, #f5b342);
    opacity: 0.9;
}

.portal-floor-strip-quiet {
    opacity: 0.5;
    font-style: italic;
}

/* ── The bar's status chips ─────────────────────────────────────────────
   The shape shared by every "something is true right now" readout in the
   bar: Awaiting Decision for judges, On Deck for competitors. The tray's
   next-door neighbours and deliberately its siblings — same 2xs scale, same
   mono tabular figures, same "gives ground before it clips". They are LINKS,
   not readouts, so they carry the hover the rest of the bar's controls have:
   the number is the answer, the click is the next step.

   Every one of them is ALERT-ONLY, hidden until it has something to say. A
   permanent "0 awaiting" or "no upcoming fight" is a worse status bar than
   no status bar, because it trains people not to look at the corner where
   the answer lives. Their renderers own that.

   It was .portal-judging-badge alone until the second chip needed the same
   shape; the specific classes below now carry only what is specific. */
.portal-bar-status {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    flex: 0 1 auto;
    min-width: 0;
    font-size: var(--portal-text-2xs);
    white-space: nowrap;
    color: inherit;
    text-decoration: none;
    padding: 0.15rem 0.45rem;
    border-radius: var(--bs-border-radius);
    opacity: 0.82;
    transition: background-color var(--portal-dur-2) var(--portal-ease-out),
                opacity var(--portal-dur-2) var(--portal-ease-out);
}

.portal-bar-status:hover,
.portal-bar-status:focus-visible {
    color: inherit;
    opacity: 1;
    background: var(--portal-c-row-hover);
}

/* The figures in any of them. */
.portal-bar-status-value {
    font-family: var(--portal-font-mono);
    font-variant-numeric: tabular-nums;
    font-weight: var(--portal-weight-label);
}

/* The word that makes a chip a status readout rather than a mystery number
   next to a glyph. Hides on a narrow bar; the link's aria-label carries the
   whole sentence either way. */
.portal-bar-status-label {
    text-transform: uppercase;
    letter-spacing: var(--portal-label-tracking);
    opacity: 0.65;
}

/* ── On Deck (competitors) ──────────────────────────────────────────────
   The bar's centre is staff-only real estate — the floor tray is gated on
   MISSION_CONTROL — so for every competitor the middle of the chrome was
   dead space. This is the one question they have all day, answered without
   navigating: bot, position, cage. Accent because being on deck is the one
   state in a builder's day that outranks everything else on screen. */
.portal-on-deck-chip.is-imminent .portal-bar-status-value {
    color: var(--portal-c-accent);
}


/* Fights THIS judge still owes a score on. The one state in the bar worth
   colour besides an active cage — it is the difference between "the event is
   busy" and "you are the hold-up". */
.portal-judging-badge-mine {
    color: var(--portal-c-badge-warning-color, #f5b342);
}

/* A tournament went unreachable mid-sweep, so the count is a floor rather
   than a total. Dimmed instead of hidden: an under-count you can see beats a
   badge that vanishes for a reason nobody can find. */
.portal-judging-badge-partial {
    opacity: 0.6;
}

/* Under 992px the bar's centre cell is fighting for room and the tray is
   already scrolling its dots. The glyph plus the number still says it; the
   full sentence is in the title and the aria-label either way. */
@media (max-width: 991.98px) {
    .portal-bar-status-label {
        display: none;
    }
}

/* ── Sidebar ────────────────────────────────────────────────────────────
 * It died on 2026-08-26 (v3.1.1) and came back on 2026-08-28 as a per-account
 * PREFERENCE (SidebarService). What came back is only the navigation: the user
 * menu stayed in the system bar, the phone Appearance row stayed in the
 * palette's footer, and the brand is the bar's. A slab with a brand above a
 * bar with the same brand is two of something the portal only has one of.
 *
 * The two gates that decide whether any of this paints:
 *   1. body[data-portal-sidebar="on"] — the account's preference, stamped
 *      server-side in base.twig so the first paint is already correct. (The
 *      element only exists at all when the host passed portalNavTree.)
 *   2. >=768px. Below that the Launchpad is still the one door — a phone has
 *      no room for either width, and the tab bar's Menu slot is already there.
 * Absent, it is the full-bleed shell, unchanged.
 *
 * WIDTH is a third question, not a fourth gate, and it has two answers:
 *   768-991  the rail is the DEFAULT — 240px against a 900px viewport eats
 *            the reading measure, so a device that has never chosen starts
 *            here — but it is only the default. It was a construction until
 *            2026-09-06, forced by media query with the toggle hidden beside
 *            it, and that had the band wearing the rail's WIDTH without the
 *            rail's STYLING: every rule below is keyed on the class, the
 *            class was only ever set >=992, so a 56px slab painted full-width
 *            labels and clipped them, kept a row per group heading, and hung
 *            the badge pill off its own edge. One state, one answer, and the
 *            toggle is on every width the slab is drawn at.
 *   >=992    the same choice, defaulting the other way — expanded, or the
 *            rail if this browser has collapsed it.
 * Per device (localStorage), because it is about the screen in front of you:
 * a 13" laptop wants the rail and the control-room monitor does not, on the
 * same account. Both arrive at ONE state, body.portal-sidebar-rail, stamped
 * pre-paint in navigation.twig and re-derived on resize by portal-layout.js.
 *
 * Theme colours live in portal-theme.css with the rest of the chrome's inks;
 * this file owns geometry only. */
.portal-sidebar {
    /* Below 992 it is not a drawer, it is nothing. The old offcanvas is NOT
       restored: the tab bar's Menu slot opens the Launchpad on phones, and a
       second phone navigation surface is not an improvement. Rendering it
       hidden rather than skipping it in Twig keeps ONE answer to "is there a
       sidebar" — the preference — instead of a second one that depends on the
       width at request time, which the server does not know. */
    display: none;
}

@media (min-width: 768px) {
    /* WIDTH is the state class and nothing else, at every width the slab is
       drawn — so the rail's geometry and the rail's styling can no longer
       disagree, which is the bug the band shipped with. Which way a device
       that has never chosen STARTS is portal-layout.js's job (rail below 992,
       expanded above), because a default that a control can overrule has to
       be read from the same place the control writes to. */
    body[data-portal-sidebar="on"] {
        --portal-sidebar-width: var(--portal-sidebar-width-expanded);
    }

    body[data-portal-sidebar="on"].portal-sidebar-rail {
        --portal-sidebar-width: var(--portal-sidebar-width-collapsed);
    }

    body[data-portal-sidebar="on"] {
        --portal-sidebar-active-width: calc(
            max(var(--portal-chrome-inset), var(--portal-safe-left))
            + var(--portal-sidebar-width) + var(--portal-chrome-gap)
        );
    }

    body[data-portal-sidebar="on"] .portal-sidebar {
        display: flex;
        flex-direction: column;
        /* A floating slab, the system bar's vertical twin: inset top, bottom
           and left, with the page and the aurora visible around it. Same
           radius, same insets, same elevation family — it has to read as the
           other end of one piece of chrome, not as a panel that wandered in. */
        position: fixed;
        /* --portal-chrome-offset-top, not safe-top + inset. The two are the
           same 8px today, but only the token knows about the IMPERSONATION
           BANNER: portal-components.css re-points it to the banner's measured
           height, so the slab clears the banner for free. The old sidebar
           needed its own `top` + `height` override for that, and the override
           over-constrained the box — top, bottom AND height all set means the
           browser drops `bottom`, which took the 8px foot inset with it. */
        top: var(--portal-chrome-offset-top);
        bottom: calc(var(--portal-safe-bottom) + var(--portal-chrome-inset));
        left: max(var(--portal-chrome-inset), var(--portal-safe-left));
        width: var(--portal-sidebar-width);
        transition: width var(--portal-shell-transition);
        z-index: var(--portal-z-chrome);
        border-radius: var(--portal-chrome-radius);
        box-sizing: border-box;
        /* The slab clips its scrolling list to its own radius. Nothing inside
           opens outward — the flyouts that needed visible overflow belonged to
           the icon rail and to the user menu, and neither came back. */
        overflow: hidden;
    }

    /* The bar starts where the sidebar ends. `left` alone, deliberately: the
       bar is already right-anchored by its own inset, so tweening the width
       too would land its two edges at different times when the sidebar
       collapses. max() keeps the plain inset when the column is 0. */
    body.portal-has-chrome[data-portal-sidebar="on"] #site-footer.footer {
        left: max(var(--portal-sidebar-active-width), var(--portal-chrome-inset));
        transition: left var(--portal-shell-transition);
    }
}

/* THE NO-SCRIPT BAND, and the only place a media query still decides a width.
   `portal-has-chrome` is stamped by the same pre-paint block that stamps the
   rail class (navigation.twig), so its ABSENCE is exactly "no script ran" —
   and with no script there is no stored choice to read and no default to seed
   from, which would leave 768-991 holding a 240px slab over a 900px viewport.
   It keeps the rail it has always had instead. Above 992 the scriptless page
   keeps the expanded slab it has always had, which is the base rule. */
@media (min-width: 768px) and (max-width: 991.98px) {
    body[data-portal-sidebar="on"]:not(.portal-has-chrome) {
        --portal-sidebar-width: var(--portal-sidebar-width-collapsed);
    }
}

/* ── Skip link ──────────────────────────────────────────────────────────
 * Off-screen until focused, then a real slab in the corner. It is the first
 * thing in the tab order and the only way past the sidebar's thirty-odd links
 * without thirty-odd presses.
 *
 * Moved off-screen rather than `display: none` or `visibility: hidden`: both
 * of those take it out of the tab order, which is the one thing it must stay
 * in. It also must not be inside the slab — the slab clips to its radius, and
 * a focused skip link inside it would be clipped away. */
.portal-skip-link {
    position: fixed;
    top: 0;
    left: 0;
    z-index: var(--portal-z-top);
    padding: 0.5rem 0.875rem;
    border-radius: var(--bs-border-radius);
    background: var(--portal-c-surface-opaque, #1f1d2a);
    color: var(--portal-c-text, #e8e7ee);
    font-size: var(--portal-text-sm);
    font-weight: var(--portal-weight-medium);
    text-decoration: none;
    box-shadow: var(--portal-c-elev-overlay);
    /* Off-screen, not hidden. */
    transform: translateY(-200%);
}

.portal-skip-link:focus-visible {
    /* --portal-chrome-offset-top already carries the safe-area top, so it is
       not added again here; the left is the slab's own inset, so the link
       lands square on the sidebar's top corner. */
    transform: translate(
        max(var(--portal-chrome-inset), var(--portal-safe-left)),
        var(--portal-chrome-offset-top)
    );
    outline: 2px solid var(--portal-c-accent);
    outline-offset: 2px;
}

/* ── The head row and its collapse toggle ───────────────────────────────
 * The slab has no brand and no title, so this row exists for one control. It
 * is still a row rather than a floating button: the toggle has to be findable
 * without hovering, and it has to have somewhere to sit centred once the slab
 * is 56px wide. */
/* EXPANDED: the toggle shares the first row's line rather than owning a row
   of its own. A 28px button alone on a 240px row was ~40px of slab spent on
   one control, at the top, where the list should start. Absolute rather than
   floated so it cannot reflow whatever it sits beside, and positioned against
   the SLAB (which is `fixed`, so it is the containing block) rather than the
   nav — the nav scrolls, and a toggle that scrolls away is a toggle you have
   to hunt for. The first row gives up the width in return; see below. */
.portal-sidebar-head {
    position: absolute;
    /* 0.75rem is the nav's own top padding, so the button's top edge lands on
       the first row's top edge — the same y whichever shape is first. Centring
       on the row instead would need two different values, because a heading
       row is ~21px and a link row ~30px, and only ONE of them can be first:
       today that is the link row for 90 of 91 accounts, since only one person
       has pinned anything. Aligning tops is exact for both and needs no guess. */
    top: 0.75rem;
    right: 0.5rem;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: flex-end;
}

/* The row the toggle now shares. Whatever is first — a group heading when
   there are pins, the ungrouped strip's first link when there are not — gives
   back the button's width, so the two never overlap and no label is truncated
   by a control sitting on top of it. */
.portal-sidebar-nav > .portal-sidebar-group:first-child > .portal-sidebar-group-toggle,
.portal-sidebar-nav > .portal-sidebar-top:first-child > li:first-child .portal-sidebar-link {
    padding-right: 2.25rem;
}

.portal-sidebar-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    padding: 0;
    border: 1px solid transparent;
    border-radius: var(--bs-border-radius);
    background: transparent;
    color: inherit;
    opacity: 0.55;
    cursor: pointer;
    font-size: var(--portal-text-xs);
    transition: opacity var(--portal-dur-1, 120ms) var(--portal-ease-out, ease),
                background-color var(--portal-dur-1, 120ms) var(--portal-ease-out, ease);
}

.portal-sidebar-toggle:hover {
    opacity: 1;
    background: var(--portal-c-chrome-hover);
}

.portal-sidebar-toggle:focus-visible {
    opacity: 1;
    outline: 2px solid var(--portal-c-accent);
    outline-offset: -2px;
}

/* The chevron points the way the slab will move. */
.portal-sidebar-toggle i {
    transition: transform var(--portal-dur-2, 200ms) var(--portal-ease-out, ease);
}

body.portal-sidebar-rail .portal-sidebar-toggle i {
    transform: rotate(180deg);
}

/* The toggle needs no width gate of its own: it lives inside the slab, and
   the slab is display:none below 768. It HAD one — hidden across 768-991,
   where the band's rail was a construction with no choice to offer — and
   removing it is the fix for a control that vanished as a window narrowed
   past 992 and left the rail with no way out. */

/* ── Rail mode (the 56px icon rail) ─────────────────────────────────────
 * Keyed on body.portal-sidebar-rail: the desktop collapse and the 768-991
 * band's default both arrive at this one state, so there is one set of rules
 * and not two that can drift. Set pre-paint (navigation.twig) rather than by
 * a bare media query, deliberately — the kiosk zero-outs beat a body class by
 * specificity exactly as they did before. The band USED to reach the rail by
 * media query without ever reaching this class, which is how it ended up with
 * the rail's width and none of its styling. */
@media (min-width: 768px) {
    /* RAIL: the toggle takes a row back, because at 56px there is no line to
       share — the first row is a centred glyph and the button would sit on
       top of it. So the rail pays the ~36px the expanded slab just saved,
       which is the width where that space is cheapest to give. */
    body.portal-sidebar-rail .portal-sidebar-head {
        position: static;
        justify-content: center;
        flex-shrink: 0;
        padding: 0.5rem 0.25rem 0.25rem;
    }

    /* …and the first row stops reserving width for a button no longer on it. */
    body.portal-sidebar-rail .portal-sidebar-nav > .portal-sidebar-group:first-child > .portal-sidebar-group-toggle,
    body.portal-sidebar-rail .portal-sidebar-nav > .portal-sidebar-top:first-child > li:first-child .portal-sidebar-link {
        padding-right: 0.5rem;
    }

    /* Hidden, NOT removed. width/opacity keeps every label in the
       accessibility tree, so a screen reader still reads "Mission Control"
       off a row that only draws a glyph; display:none would have made the
       rail a column of unnamed links. The title attribute (rendered always)
       is what puts the same word under a pointer. */
    body.portal-sidebar-rail .portal-sidebar-link-label,
    body.portal-sidebar-rail .portal-sidebar-group-label-text,
    body.portal-sidebar-rail .portal-sidebar-group-chevron {
        opacity: 0;
        width: 0;
        max-width: 0;
        overflow: hidden;
        pointer-events: none;
    }

    body.portal-sidebar-rail .portal-sidebar-link {
        justify-content: center;
        gap: 0;
        padding-left: 0.5rem;
        padding-right: 0.5rem;
    }

    /* The group HEADINGS go, so the groups must not stay shut: a collapsed
       group in rail mode is rows you cannot reach and no heading to reopen
       them with. !important because the stored per-group state is a class on
       the group and this outranks it for as long as the labels are gone. */
    body.portal-sidebar-rail .portal-sidebar-group-toggle {
        height: 0;
        padding: 0;
        margin: 0;
        overflow: hidden;
        pointer-events: none;
    }

    body.portal-sidebar-rail .portal-sidebar-group-wrap {
        grid-template-rows: 1fr !important;
    }

    /* The count has nowhere to go at 56px, and the old rail simply dropped it
       — which loses the one thing a badge is for. It becomes a DOT on the
       glyph instead: same "there is something here", no digits to fit.
       margin-left:auto and the pill's own padding both have to go, or the
       zero-width leftover pushes the icon off centre (that bug shipped). */
    body.portal-sidebar-rail .portal-sidebar-badge {
        position: absolute;
        top: 0.3rem;
        right: 0.75rem;
        margin-left: 0;
        padding: 0;
        width: 0.4rem;
        height: 0.4rem;
        min-width: 0;
        border-radius: 50%;
        background: var(--portal-c-accent);
        color: transparent;
        font-size: 0;
        overflow: hidden;
    }

    body.portal-sidebar-rail .portal-sidebar-link {
        position: relative;
    }

    /* The top strip's hairline spans a narrower slab. */
    body.portal-sidebar-rail .portal-sidebar-top {
        margin-left: 0.25rem;
        margin-right: 0.25rem;
    }

    /* GROUP SEPARATION, which is a different job in the rail.
       Expanded, the 0.75rem above each group is where its heading sits — the
       space is not empty, it holds the word. In the rail the heading is
       height:0, so the same rule leaves a 12px hole between clusters of icons
       and nothing in it, which reads as a layout mistake rather than a
       boundary. Worse around the ungrouped strip, which carried 12+8+border
       above and 8+8+border below for a strip with no heading either.

       A hairline does the heading's job at this width: half the gap, and
       something in the middle of it that says "new group". */
    body.portal-sidebar-rail .portal-sidebar-group + .portal-sidebar-group,
    body.portal-sidebar-rail .portal-sidebar-group + .portal-sidebar-top {
        margin-top: 0.375rem;
        padding-top: 0.375rem;
        border-top: 1px solid var(--portal-chrome-divider);
    }

    body.portal-sidebar-rail .portal-sidebar-top {
        padding-bottom: 0.375rem;
        margin-bottom: 0.375rem;
    }

    /* A classic scrollbar consumes 10px of this 56px rail and moves every
       glyph 5px left of the collapse control. The rail itself is already a
       compact icon affordance, so keep wheel/touch scrolling but remove the
       gutter; expanded navigation retains the normal visible scrollbar. */
    body.portal-sidebar-rail .portal-sidebar-nav {
        scrollbar-width: none;
        -ms-overflow-style: none;
    }

    body.portal-sidebar-rail .portal-sidebar-nav::-webkit-scrollbar {
        display: none;
    }
}

.portal-sidebar-nav {
    flex: 1 1 auto;
    overflow-y: auto;
    overflow-x: hidden;
    /* No safe-area inset here, unlike the drawer this descends from: the slab
       itself is already inset past the notch by its own `left`. */
    padding: 0.75rem 0.5rem;
    /* The list's scroll must not chain to the page behind it. */
    overscroll-behavior: contain;
}

/* EXPANDED NAV deliberately declares nothing about its scrollbar and inherits
   the kit's thin themed bar from portal-theme.css like Mission Control's panels
   and every .portal-scroll-region. It was hidden for a few hours on 2026-08-30 and
   the reasoning did not survive contact with the facts, so it is written down
   here rather than rediscovered.

   The bar got reported as ugly, but what made it ugly was macOS: with a mouse
   attached (AppleShowScrollBars defaults to "automatically based on mouse or
   trackpad") the whole machine switches off overlay scrollbars, and this list
   got a permanent classic bar. On a trackpad it was already an overlay that
   consumed zero layout width and faded when idle — measured, not assumed. So
   the complaint was about an OS state, not about this rule.

   Hiding it answered that by removing the affordance for exactly the people who
   need it most. Chrome on Windows draws classic always-visible scrollbars by
   default, so every Windows user had a permanent bar here long before any mouse
   was plugged in — and they are also the users who cannot feel the scroll the
   way a trackpad user can. A nav list long enough to overflow should say so.

   If this ever wants hiding again, hide it for a reason that survives a
   different OS: the tab rails and the cage dots hide theirs because a bar
   inside 20px of strip is noise, which is a claim about the STRIP. There is no
   equivalent claim about a full-width list. The 56px rail is the exception:
   its classic 10px gutter displaces the entire icon column, so rail mode hides
   the scrollbar while preserving scrolling (see the rail block above). */


/* ── Top links — the ungrouped strip above the first group (Home / All Tools
   / Statsbook). Same link styling as group members, with breathing room and a
   hairline before the first group header. */
.portal-sidebar-top {
    padding: 0 0 0.5rem;
    margin-bottom: 0.5rem;
    border-bottom: 1px solid var(--portal-chrome-divider, rgba(255, 255, 255, 0.08));
}

/* The strip has no heading, which is right when it is FIRST — Home and All
   Tools are not a category. Under the Pinned group it read as two more pinned
   pages, because nothing but a 4px gap said otherwise: "All Tools" and
   "Statsbook" appeared to be things the user had starred. A rule and real air
   above it end the sentence the group above started. */
.portal-sidebar-group + .portal-sidebar-top {
    margin-top: 0.75rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--portal-chrome-divider, rgba(255, 255, 255, 0.08));
}

.portal-sidebar-group + .portal-sidebar-group {
    margin-top: 0.75rem;
}

/* ── Collapsible group toggle ──────────────────────────────────────── */
.portal-sidebar-group-toggle {
    display: flex;
    align-items: center;
    width: 100%;
    padding: 0.25rem 0.625rem 0.35rem;
    background: transparent;
    border: 0;
    cursor: pointer;
    text-align: left;
    border-radius: var(--bs-border-radius);
    color: inherit;
    transition: background-color var(--portal-dur-1, 120ms) var(--portal-ease-out, ease);
    gap: 0.375rem;
}

.portal-sidebar-group-toggle:hover {
    background: var(--portal-c-chrome-hover);
}

.portal-sidebar-group-toggle:focus-visible {
    outline: 2px solid var(--portal-c-accent);
    outline-offset: -2px;
}

.portal-sidebar-group-label-text {
    font-family: var(--portal-font-mono);
    font-size: var(--portal-label-size-sm);
    font-weight: var(--portal-weight-medium);
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    opacity: 0.5;
    white-space: nowrap;
    overflow: hidden;
    flex: 1 1 auto;
    min-width: 0;
}

.portal-sidebar-group-chevron {
    font-size: 0.45rem;
    opacity: 0;
    flex-shrink: 0;
    transition: transform var(--portal-dur-2, 200ms) var(--portal-ease-out, ease), opacity var(--portal-dur-1, 120ms) var(--portal-ease-out, ease);
    margin-left: auto;
}

.portal-sidebar-group-toggle:hover .portal-sidebar-group-chevron,
.portal-sidebar-group-toggle:focus-visible .portal-sidebar-group-chevron,
.portal-sidebar-group.is-collapsed .portal-sidebar-group-chevron {
    opacity: 0.45;
}

.portal-sidebar-group.is-collapsed .portal-sidebar-group-chevron {
    transform: rotate(-90deg);
}

/* "Your page is in here" — only while the group is shut, since an open group
   shows the highlighted link itself. Keeps the sidebar from having to open
   groups on its own just to show you where you are, which is what made it
   change shape on every navigation before. */
.portal-sidebar-group.has-active.is-collapsed .portal-sidebar-group-label-text::after {
    content: "";
    display: inline-block;
    width: 0.25rem;
    height: 0.25rem;
    margin-left: 0.4em;
    vertical-align: middle;
    border-radius: 50%;
    background: var(--portal-c-accent, var(--portal-accent-teal));
}

/* ── Animated link list ──────────────────────────────────────────────
   grid-template-rows 1fr -> 0fr is the height transition that does not need a
   measured pixel value, so a group can hold any number of links and the
   collapse still animates. */
.portal-sidebar-group-wrap {
    display: grid;
    grid-template-rows: 1fr;
    transition: grid-template-rows var(--portal-dur-2, 200ms) var(--portal-ease-out, ease);
}

.portal-sidebar-group-wrap > ul {
    overflow: hidden;
    min-height: 0;
}

.portal-sidebar-group.is-collapsed .portal-sidebar-group-wrap {
    grid-template-rows: 0fr;
}

.portal-sidebar-link {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.4375rem 0.625rem;
    border-radius: var(--bs-border-radius);
    font-size: var(--portal-text-sm);
    font-weight: var(--portal-weight-medium);
    line-height: var(--portal-leading-tight);
    text-decoration: none;
    transition: color var(--portal-dur-1, 120ms) var(--portal-ease-out, ease), background-color var(--portal-dur-1, 120ms) var(--portal-ease-out, ease);
}

.portal-sidebar-link:focus-visible {
    outline: 2px solid var(--portal-c-accent);
    outline-offset: -2px;
}

.portal-sidebar-link i {
    width: 0.875rem;
    text-align: center;
    opacity: 0.7;
    flex-shrink: 0;
    font-size: var(--portal-text-xs);
}

/* Current page. Colours live in portal-theme.css (they differ per chrome
   theme); this is just the structural marker. */
.portal-sidebar-link.is-active {
    font-weight: var(--portal-weight-label);
    position: relative;
}

/* The current page is marked by a 3px accent PILL on its leading edge, not by
   a wash. The wash was 10% white against an 8% hover — two percent apart,
   which on a venue panel in a dark room is not a difference. A coloured marker
   is legible at a glance and does not compete with hover for the same channel;
   the wash drops to half strength and becomes the quiet backing it always
   should have been.

   Inset from top and bottom so it reads as a marker beside the row rather
   than a border on it. */
.portal-sidebar-link.is-active::before {
    content: "";
    position: absolute;
    top: 22%;
    bottom: 22%;
    left: 0;
    width: 3px;
    border-radius: 999px;
    background: var(--portal-c-accent, var(--portal-accent-teal));
}

.portal-sidebar-link-label {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.portal-sidebar-badge {
    margin-left: auto;
    padding: 0.05rem 0.35rem;
    border-radius: 999px;
    font-size: var(--portal-label-size-sm);
    font-weight: var(--portal-weight-strong);
    letter-spacing: var(--portal-label-tracking);
    text-transform: uppercase;
    /* The same two tokens .tools-card-badge takes. It is the same badge — one
       registry, one WIP marker, one pending count — rendered on a different
       navigation surface, so it has no business having its own colour. */
    background: var(--portal-c-badge-muted-bg, rgba(255, 255, 255, 0.1));
    color: var(--portal-c-badge-muted-color, inherit);
    flex-shrink: 0;
}

/* ── Content column ──────────────────────────────────────────────────── */
/* The scroll-condense (html.portal-scrolled, the observer in
   portal-layout.js, the flat-canvas anti-condense pin) died with the topbar
   in v3.1 — a bottom bar has no business changing shape as the page scrolls,
   and the ~8px sentinel below has nothing left to measure. */
.portal-content-offset {
    height: var(--portal-chrome-offset-top);
    flex-shrink: 0;
    pointer-events: none;
}

body.portal-has-chrome .portal-shell ~ main.portal-app-main,
body:has(.portal-shell) .portal-shell ~ main.portal-app-main {
    margin-left: 0;
    width: 100%;
    max-width: 100%;
    padding-top: 0;
    padding-bottom: var(--portal-chrome-offset-bottom);
    /* The chrome has always respected the notch; the content column never did,
       so a landscape iPhone clipped full-bleed pages against the sensor
       housing. container-fluid's own gutter is 0.75rem and normally wins —
       the inset only takes over when there genuinely is one. */
    padding-left: max(0.75rem, var(--portal-safe-left));
    padding-right: max(0.75rem, var(--portal-safe-right));
    min-height: calc(100dvh - var(--portal-chrome-offset-top) - var(--portal-chrome-offset-bottom));
    box-sizing: border-box;
}

body.portal-has-chrome .portal-shell ~ main.portal-app-main,
body:has(.portal-shell) .portal-shell ~ main.portal-app-main {
    padding-bottom: calc(var(--portal-chrome-offset-bottom) + 1.5rem);
}

@media (min-width: 768px) {
    body.portal-has-chrome .portal-shell ~ main.portal-app-main,
    body:has(.portal-shell) .portal-shell ~ main.portal-app-main {
        margin-left: var(--portal-sidebar-active-width);
        width: calc(100% - var(--portal-sidebar-active-width));
        max-width: calc(100% - var(--portal-sidebar-active-width));
        transition: margin-left var(--portal-shell-transition), width var(--portal-shell-transition), max-width var(--portal-shell-transition);
    }
}

/* ── Reduced motion ─────────────────────────────────────────────────────
 * Only the GEOMETRY, which is the house idiom here: the reduced-motion blocks
 * in portal-components.css kill sliding thumbs and lifting cards and leave
 * colour fades alone, because a 120ms hover tint is not what anyone means by
 * motion. What is killed is a group's height animating open, the SLAB itself
 * widening and narrowing as it collapses, and the whole content column sliding
 * sideways beside it — the last two happen together and move the entire page.
 *
 * Its own block rather than a line in one of theirs: a component that animates
 * owns its opt-out, which is the only arrangement where deleting the component
 * takes the opt-out with it. */
@media (prefers-reduced-motion: reduce) {
    .portal-sidebar-group-wrap,
    .portal-sidebar-group-chevron,
    .portal-sidebar-toggle i,
    body[data-portal-sidebar="on"] .portal-sidebar,
    body.portal-has-chrome .portal-shell ~ main,
    body:has(.portal-shell) .portal-shell ~ main,
    body.portal-has-chrome[data-portal-sidebar="on"] #site-footer.footer {
        transition: none;
    }
}

/* ── Print ──────────────────────────────────────────────────────────────
 * Chrome is for a screen. On paper a fixed slab prints as a stamp on the page
 * and, worse, the column it reserves narrows the text by a quarter of the
 * sheet — every printed page indented past a navigation menu that is not
 * there.
 *
 * Written here rather than in a page's own print sheet because it is the
 * SHELL's problem: the four print sheets the portal has (documentation,
 * playbook, QA report, on-deck poster) each hide their own page furniture and
 * none of them has ever known about the chrome. The bar and the tab bar join
 * the sidebar because they have the same problem and it is one rule; the
 * sidebar is what made it visible, by being the first piece of chrome to take
 * space out of the text column rather than merely sitting on top of it. */
@media print {
    .portal-shell,
    #site-sidebar,
    #site-footer,
    #portal-tab-bar,
    .portal-skip-link,
    .portal-content-offset {
        display: none !important;
    }

    /* The column goes with the slab. The TOKEN, not just the consumers, so the
       reading measure in portal-page-theme.css — which reads it inside its own
       calc() — comes back too.

       On body as well as :root, and that is the whole fix rather than a
       belt-and-braces flourish: the ON state declares this token on
       `body[data-portal-sidebar="on"]`, and a custom property declared on body
       beats one on :root for every descendant no matter what the media query
       or the source order says. A :root-only override here read 256px and
       printed the indent anyway. Same trap the kiosk blocks call out in
       mission-control.css. The attribute form ties that selector's specificity
       and wins on source order, being later in this file. */
    :root,
    body[data-portal-sidebar] {
        --portal-sidebar-active-width: 0px;
    }

    /* The chrome offsets are deliberately LEFT ALONE. They reserve 8px at the
       top and 52px at the foot, they predate the sidebar, and re-pointing them
       here would quietly change every page's print output to fix nothing worth
       fixing. The 256px indent is the regression; this is its scope. */
    body.portal-has-chrome .portal-shell ~ main,
    body:has(.portal-shell) .portal-shell ~ main {
        margin-left: 0;
        width: 100%;
        max-width: 100%;
        /* The screen rule TWEENS these three (the column slides when the
           preference is toggled). Entering print media is a value change like
           any other, so it starts that 200ms tween — and a paged medium has no
           second frame to finish it in, which leaves the sheet rendered at
           whatever the animation happened to reach. Paper does not animate. */
        transition: none;
    }
}

/* ── Shared assets ───────────────────────────────────────────────────── */
.nhrl-logo-svg {
    height: 20px;
    width: auto;
    display: block;
    flex-shrink: 0;
}

.sparky-avatar {
    width: 20px;
    height: 20px;
    border: 1px solid rgba(255, 255, 255, 0.35);
    border-radius: 50%;
    padding: 2px;
    object-fit: contain;
    flex-shrink: 0;
}

/* The search BAR died with the topbar (v3.1): it only ever opened ⌘K, and a
   200px input that is really a button is chrome pretending to be a control.
   Its replacement — the .portal-search-btn icon in the system bar — is styled
   in portal-theme.css beside the bell and Appearance triggers it sits with. */

/* ── Phone bottom tab bar ─────────────────────────────────────────────
 * Structure only — the glass material lives with the other chrome strips in
 * portal-theme.css, so this bar inherits the tint, the blur, the bevel and
 * all three degrade gates without restating any of them.
 *
 * Phones only (<768px). Above that the system bar carries the launcher and
 * every page is one palette away — a tab bar there would be a second way to
 * reach the same grid. */
.portal-tab-bar {
    display: none;
}

@media (max-width: 767.98px) {
    .portal-tab-bar {
        position: fixed;
        /* A phone tab bar is the bottom edge of the app, not a card floating
           above it. Keeping the home-indicator inset INSIDE the bar also
           makes installed/standalone mode read as one continuous shell. */
        bottom: 0;
        left: 0;
        right: 0;
        border-radius: var(--portal-chrome-radius) var(--portal-chrome-radius) 0 0;
        /* The chrome-raised rung: above the bar (1030), below modals — an
           open palette still covers it. */
        z-index: var(--portal-z-tabbar);
        display: flex;
        align-items: stretch;
        justify-content: space-around;
        min-height: var(--portal-tab-bar-height);
        height: calc(var(--portal-tab-bar-height) + var(--portal-safe-bottom));
        /* Give the glyph rail room below the top keyline. The safe-area pad
           remains independently owned at the bottom of the same surface. */
        padding-top: 0.25rem;
        padding-bottom: var(--portal-safe-bottom);
        padding-left: var(--portal-safe-left);
        padding-right: var(--portal-safe-right);
        transition: transform var(--portal-shell-transition),
                    opacity var(--portal-dur-1) var(--portal-ease-out),
                    visibility 0s linear 0s;
    }

    .portal-tab-link {
        flex: 1 1 0;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 0.15rem;
        /* The whole slot is the target — comfortably past the 44px floor. */
        min-width: 0;
        padding: 0;
        border: 0;
        background: none;
        text-decoration: none;
        font-size: var(--portal-label-size-sm);
        letter-spacing: var(--portal-tracking-caps);
        text-transform: uppercase;
        font-family: var(--portal-font-mono);
        transition: color var(--portal-dur-1) var(--portal-ease-out);
    }

    .portal-tab-icon {
        position: relative;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        font-size: var(--portal-text-lg);
        line-height: 1;
    }

    .portal-tab-label {
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* ── The Account slot ─────────────────────────────────────────────
     * A drop-up, so the FLEX ITEM is the wrapper and the button is inside it.
     * Without these it stops being a slot: the wrapper shrink-wraps its
     * button and the other four slots widen to eat the difference, which
     * reads as the bar mis-centring rather than as a missing rule. The
     * button keeps .portal-tab-link, so it inherits every colour, hover and
     * touch-target rule its neighbours have. */
    .portal-tab-user {
        flex: 1 1 0;
        display: flex;
        min-width: 0;
    }

    /* The menu is a PANEL, not a tab-width column — it carries the identity
       card, the theme rows, their notes and Logout. Both bounds matter: the
       floor stops it collapsing to the ~75px slot (the exact failure the
       palette footer's Appearance menu shipped with — a 113px sliver whose
       swatches sat on top of their own labels), and the ceiling keeps it
       inside a 375px viewport once Bootstrap has shifted it in from the
       right edge. */
    .portal-tab-user .portal-user-dropdown {
        min-width: 15rem;
        max-width: calc(
            100vw - 2 * var(--portal-chrome-inset)
            - var(--portal-safe-left) - var(--portal-safe-right)
        );
        /* Taller than the viewport once a host adds a few menu items, and a
           drop-up that overflows upward has no scrollbar of its own — the
           rows above the fold simply cannot be reached. Cap it against the
           space between the bar and the top of the screen. */
        max-height: calc(
            100dvh - var(--portal-chrome-offset-bottom)
            - var(--portal-safe-top) - var(--portal-chrome-gap) * 2
        );
        overflow-y: auto;
    }

    /* Count sits on the icon, not the slot, so it tracks the glyph rather
       than the (variable-width) label. */
    .portal-tab-badge {
        position: absolute;
        top: -0.35rem;
        left: 60%;
        min-width: 1rem;
        padding: 0 0.25rem;
        border-radius: 999px;
        font-family: var(--portal-font-body);
        font-size: var(--portal-label-size-sm);
        font-weight: var(--portal-weight-strong);
        line-height: 1rem;
        text-align: center;
    }

    /* The bar replaces the footer on phones: the footer carries no navigation,
       and 44px of a 667px-tall viewport is too much to spend on a theme
       toggle and social links. Both relocate into the drawer (below). */
    body.portal-has-chrome #site-footer.footer {
        display: none;
    }

    /* Content clears the bar instead of the footer — every consumer of this
       token (main's padding, the sidebar's own footer offset) follows. */
    body.portal-has-chrome,
    body:has(.portal-shell) {
        /* Everything the bar occupies measured from the viewport's bottom edge:
           the home-indicator strip, the inset it floats in, and the slab. The
           inset used to be missing, so the last row of every page sat 8px under
           the slab's lower edge. Derived from the same three tokens the bar's
           own `bottom` reads, so the two cannot drift. */
        --portal-chrome-offset-bottom: calc(var(--portal-safe-bottom) + var(--portal-tab-bar-height));
    }

    /* A software keyboard is already the phone's temporary bottom surface.
       portal-layout.js adds this only while a text editor owns focus AND the
       visual viewport has materially shrunk, so browser-toolbar motion cannot
       make the navigation disappear. Keep the normal content inset in place:
       changing --portal-chrome-offset-bottom here would reflow a form while
       somebody is typing in it. */
    body.portal-virtual-keyboard .portal-tab-bar,
    body.portal-virtual-keyboard .portal-phone-status {
        opacity: 0;
        visibility: hidden;
        pointer-events: none;
        transform: translateY(calc(100% + var(--portal-chrome-inset)));
        transition-delay: 0s, 0s, var(--portal-dur-1);
    }
}

/* ── The phone's status strip ─────────────────────────────────────────
 * The chips the phone never had. #site-footer is display:none under 768px, so
 * every chip in the bar's centre was desktop-only — and the worst case of that
 * was the On Deck chip, built for competitors, who are the most phone-heavy
 * audience the portal has. The one question a builder asks all day was
 * answered on the one device they were least likely to be holding.
 *
 * AN OVERLAY, NOT A LAYOUT CHANGE. --portal-chrome-offset-bottom is read by
 * ~35 consumers and was held at its v3 value by construction; growing it when
 * a fight queues would reflow every page in the portal at the exact moment its
 * reader least wants that. The strip is alert-only, so it floats above the tab
 * bar and covers a sliver of content only while it has something to say.
 *
 * The tab bar is attached to the bottom edge and is 50px plus its safe-area
 * pad, so the strip's own bottom is that total plus one chrome gap. */
.portal-phone-status {
    display: none;
}

@media (max-width: 767.98px) {
    .portal-phone-status {
        position: fixed;
        bottom: calc(
            var(--portal-safe-bottom) + var(--portal-tab-bar-height)
            + var(--portal-chrome-gap)
        );
        left: calc(var(--portal-chrome-inset) + var(--portal-safe-left));
        right: calc(var(--portal-chrome-inset) + var(--portal-safe-right));
        /* Under the tab bar's rung: the navigation must never be covered by a
           status message, however urgent it thinks it is. */
        z-index: calc(var(--portal-z-tabbar) - 1);
        display: flex;
        flex-direction: column;
        gap: var(--portal-chrome-gap);
        pointer-events: none;
    }

    /* Empty is INVISIBLE, not a transparent box: an alert-only strip that
       always has a layout box is a strip that eventually gets a background by
       accident. Same question the dock learned to ask — not "is it empty" but
       "does it hold anything" — and for the same reason: Twig leaves
       whitespace text nodes behind, so :empty would never match here either. */
    .portal-phone-status:not(:has(> :not([hidden]))) {
        display: none;
    }

    .portal-phone-status > * {
        pointer-events: auto;
    }

    /* The chip becomes a full-width row here. In the bar it is one of several
       things competing for a 44px strip; on a phone it is the only thing in
       its own strip, and it is being read at arm's length by somebody who may
       be holding a robot. */
    .portal-on-deck-chip--phone {
        display: flex;
        width: 100%;
        justify-content: center;
        gap: 0.5rem;
        padding: 0.55rem 0.75rem;
        font-size: var(--portal-text-sm);
        border-radius: var(--portal-chrome-radius);
        background: var(--portal-c-overlay-opaque);
        box-shadow: var(--portal-c-edge-card), var(--portal-c-shadow-raised);
        opacity: 1;
    }

    /* AND IT KEEPS THE WORD THE BAR DROPS. `.portal-bar-status-label` is
       display:none under 992px, which is correct for a chip elbowing a floor
       tray for room in a 44px strip — and exactly backwards here. This chip
       owns a full-width row on the roomiest surface it will ever have, and
       "ON DECK" is the word the entire component exists to say. Without this
       a builder gets a bot name and no answer. */
    .portal-on-deck-chip--phone .portal-bar-status-label {
        display: inline;
        opacity: 0.9;
    }
}

/* ── "Live updates paused" ────────────────────────────────────────────
 * chrome-poll.js stamps <body> after two silent cycles; every mount reveals
 * itself from here. One attribute and a CSS rule rather than a JS handle, so
 * the tag can be mounted in the bar AND the phone strip without the poll
 * knowing either exists.
 *
 * Hidden by default and hidden again the moment one request succeeds — the
 * live-dot rule, which this is a late member of: an indicator that cannot go
 * out is not an indicator. */
.portal-chrome-offline {
    display: none;
    align-items: center;
    gap: 0.35rem;
    font-size: var(--portal-text-2xs);
    color: var(--portal-c-text-muted, inherit);
    white-space: nowrap;
}

body[data-portal-chrome-offline] .portal-chrome-offline {
    display: inline-flex;
}

@media (max-width: 767.98px) {
    /* In the strip it is a row like the chip beside it, not a bar tag. */
    body[data-portal-chrome-offline] .portal-phone-status .portal-chrome-offline {
        display: flex;
        justify-content: center;
        padding: 0.4rem 0.75rem;
        font-size: var(--portal-text-xs);
        border-radius: var(--portal-chrome-radius);
        background: var(--portal-c-overlay-opaque);
        box-shadow: var(--portal-c-edge-card), var(--portal-c-shadow-raised);
    }
}
