/* ════════════════════════════════════════════════════════════════════════════
   FRB SHELL — the app page itself, not the parts inside it.

   frb-tokens.css says what the values are. frb-components.css says what a
   button, a field, a head is. frb-compat.css maps 101 pages' own class names
   onto those. This file says what an APP PAGE IS: a rail, a bar carrying the
   trail down from the portals, a head, and sections under it — in that order,
   at those sizes, on every one of them.

   It exists because four waves of mapping could not close the gap. A stylesheet
   can only restyle what the markup already contains; it cannot put the trail
   somewhere it is not, cannot delete a second name bar, and cannot decide that
   an app should have a head. Those are markup, and markup is what the shell
   reconstruction changes. This file is the target that reconstruction aims at.

   TWO HALVES, and the split matters:

     · GLOBAL — the dropdown list and the scrollbar. Every page gets these,
       reconstructed or not, because both are defects rather than differences.

     · THE SHELL — layout, scoped to `body[data-frb-shell]`. A page opts in by
       carrying that attribute, which it gets when its markup is reconstructed
       to the shape below. An un-reconstructed page is untouched, so the
       rollout can be done app by app and verified app by app.

   The shape, which is Industry Hub's and Carbon Snippet Studio's — the two the
   product owner picked out as right:

       <body data-frb-shell>
         <nav class="sidebar">              icon rail, 70px
           <div class="nav-item active" title="…"><i class="fa-…"></i></div>
           …
           <div class="theme-footer">       4 swatches, then ONE light/dark
         </nav>
         <div class="main">
           <div class="topbar">             sticky; the trail, and global actions
             <nav class="breadcrumbs">Portals › Business › Accounting Core</nav>
             <div class="row-flex">…</div>
           </div>
           <div class="view" id="view-x">
             <div class="page-head">        the view's name, its line, its actions
               <div>
                 <h1 class="page-title"><i class="fa-… frb-app-glyph"></i>Name</h1>
                 <p class="page-sub">One sentence.</p>
               </div>
               <div class="row-flex">…</div>
             </div>
             <div class="section">…</div>
           </div>
         </div>
       </body>
   ════════════════════════════════════════════════════════════════════════════ */

/* ═══ GLOBAL 1 — THE DROPDOWN LIST ═════════════════════════════════════════
   Measured across the product: 380 <select> elements on 64 pages, and 77 of
   them would paint their open list at 1.05:1.

   The mechanism, because it is not obvious and it is why `color-scheme: dark`
   in the token file did not already fix it. A native list is painted by the
   engine, not the page, and Chromium takes the list's ground from the SELECT's
   own `background-color`. Every one of those pages writes

       input, select, textarea { background: var(--input-bg); }

   and --input-bg is `rgba(255,255,255,.08)` — a translucent white. Translucent
   over the page is the dark grey you see on the closed control; translucent
   over the engine's own popup ground is near-white, and the near-white ink
   goes with it. The engine then forces black text to keep the list readable,
   which is the black-on-a-dark-app the list looks like.

   AN OPAQUE GROUND IS THE WHOLE FIX. Not a new colour: the same 8% white,
   composited against the theme's own opaque surface here rather than against
   whatever the engine happens to paint behind it. The closed control keeps the
   colour it has today; the open list finally gets the same one.

   (0,1,1) via `body:not(…)`, so it outranks the (0,1,0) class rules — 39 pages
   style their selects through `.form-input` and friends. */

:root,
.theme-midnight, .theme-ocean, .theme-sunset, .theme-forest {
    /* The fallback is a real colour, not a guess: --sidebar-bg is the opaque
       per-theme surface this product already has. Browsers without color-mix
       get the rail's ground, which is legible in every theme. */
    --frb-menu-bg: var(--sidebar-bg, #0a0a0a);
    --frb-menu-bg: color-mix(in srgb, #ffffff 8%, var(--sidebar-bg, #0a0a0a));
    --frb-menu-ink: var(--text-primary, #f8fafc);
}
.light-mode {
    --frb-menu-bg: #ffffff;
    --frb-menu-bg: color-mix(in srgb, #0f172a 4%, #ffffff);
    --frb-menu-ink: var(--text-primary, #0f172a);
}

body:not([data-frb-optout~="menu"]) select {
    background-color: var(--frb-menu-bg);
    color: var(--frb-menu-ink);
}
/* Chromium paints each row from the option's own pair when it has one. Set
   both, or a row inherits half its colours from the select and half from the
   engine — which is the state this rule is here to end. */
body:not([data-frb-optout~="menu"]) select option,
body:not([data-frb-optout~="menu"]) select optgroup {
    background-color: var(--frb-menu-bg);
    color: var(--frb-menu-ink);
}
body:not([data-frb-optout~="menu"]) select option:disabled {
    color: var(--text-secondary, rgba(127, 127, 127, .95));
}

/* ═══ GLOBAL 2 — THE SCROLLBAR ═════════════════════════════════════════════
   A product whose every surface follows a theme had one part still painted by
   the operating system. Set on :root rather than on `*`, for two reasons: both
   properties inherit, so one declaration reaches every scrolling box; and an
   element that deliberately hides its own bar — the rail does, so its icons do
   not scroll under a track — keeps that, because a rule on the element beats an
   inherited value.

   The thumb is the accent at just over half strength: at full strength a
   scrollbar competes with the accent doing real work on the page, and at a
   quarter it disappears on the light ground. */
:root {
    --frb-scroll-thumb: var(--accent-primary, #d4af37);
    --frb-scroll-thumb: color-mix(in srgb, var(--accent-primary, #d4af37) 55%, transparent);
    scrollbar-width: thin;
    scrollbar-color: var(--frb-scroll-thumb) transparent;
}

/* NO ::-webkit-scrollbar RULES HERE, and that is the whole reason this comment
   exists. The first version of this block styled the pseudo-elements —
   `::-webkit-scrollbar { width: 10px }` and a rounded thumb — which looks
   better and costs 10px of viewport on every page. Defining ANY
   `::-webkit-scrollbar` rule opts the scroller out of overlay scrollbars and
   back into classic ones that take layout space; the phone gate caught it
   immediately, with CV Architect scrolling sideways by 35px and Menu Genie by
   177px on a 390px screen for no reason but the gutter that had appeared.

   `scrollbar-width` and `scrollbar-color` are the standard properties, they
   colour the bar in the theme's accent, and they leave it an overlay. A
   slightly plainer scrollbar is worth more than 10px of every phone. */

/* ═══ GLOBAL 3 — THE BAR AT THE TOP SCROLLS, LIKE THE RAIL ═════════════════
   Wave 5 turned the 70px rail into a horizontal strip on a phone and left the
   bar under it wrapping. On an app with a trail four levels deep and two
   controls that is three stacked rows before any content — and on the narrowest
   phones the controls still clipped.

   A strip scrolls in the direction it runs. The rail already does; this is the
   same instruction for the bar beside it, under the same breakpoint, so the two
   behave alike instead of one scrolling and one reflowing.

   `flex-wrap: nowrap` has to be said out loud: every one of these bars sets
   `flex-wrap: wrap` for the desktop layout, and a wrapping row inside a
   scrolling box wraps to the box's width and never scrolls. */
/* ── THE INJECTED TRAIL IS A BAND AT EVERY WIDTH ──────────────────────────
 * `.frb-topbar` is the wrapper frb-app-shell.js puts around the trail on the
 * pages that never wrote a bar of their own. It was given a ground, a floor
 * and a hairline inside the phone block below — and nowhere else, so above
 * 620px it was whatever the box it landed in happened to give it. Measured at
 * 1440: 25px tall on Kitchen OS and 17px on Terra View, against the 46px band
 * every shelled app carries. Seventeen pixels of unstyled text is why the hub
 * tree reads as "messy" on one and "missing" on the other — it is neither, it
 * is a bar nobody drew.
 *
 * A HAIRLINE, NOT A FILL, and that is the same reasoning as the phone rule: a
 * written `.topbar` is a direct child of the content column and paints edge to
 * edge, while this one is inserted inside whatever wrapper the page already
 * had, so a filled band stops short of both edges and reads as a card floating
 * in the column rather than a bar across it.
 *
 * Nothing here reaches a page that wrote its own bar: those use `.topbar` and
 * its five aliases, and this names only the injected wrapper. */
body:not([data-frb-optout~="mobile"]) .frb-topbar {
    display: flex;
    align-items: center;
    /* STATED, NOT INHERITED FROM THE PAGE'S RESET. The band's floor is a
       `min-height`, and under `content-box` a min-height is the CONTENT's, so
       the padding and the rule are added on top of it: 44 + 8 + 8 + 1 = 61.
       Most pages set `box-sizing: border-box` globally and the band came out
       44; privacy, terms and the styleguide do not, and their band was 17px
       taller than the identical band on a section hub. A component that lands
       on 130 pages cannot take the reset on faith. */
    box-sizing: border-box;
    /* The same inset as everything else in the column, read from the same
       token, so the trail and the tiles under it share one left edge. */
    padding-left: var(--frb-shell-gutter, 28px);
    padding-right: var(--frb-shell-gutter, 28px);
    /* THE DATE GOES TO THE FAR END, which is what makes this the hub tree and
       not a row of links. The written `.topbar` gets `space-between` from its
       own rule and puts the trail left and the clock right; the INJECTED band
       had neither, so on the five editors the date rendered immediately after
       the breadcrumb — measured at x=406 against the reference's x=1198. It is
       in the band, it is visible, and it is in the wrong place, which reads as
       not being there at all. */
    justify-content: space-between;
    gap: var(--frb-space-4, 16px);
    min-height: 44px;
    padding-top: var(--frb-space-2, 6px);
    padding-bottom: var(--frb-space-2, 6px);
    border-bottom: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
    margin-bottom: var(--frb-space-3, 12px);
    font-size: 13px;
}

@media (max-width: 620px) {
    body:not([data-frb-optout~="mobile"]) :is(.topbar, .top-bar, .ac-topbar,
        .oc-topbar, .rm-topbar, .frb-topbar) {
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        overscroll-behavior-x: contain;
        scrollbar-width: none;
        gap: var(--frb-space-3, 12px);
        /* THE SAME SQUEEZE THE RAIL HAD, one row down. Measured at 390px: the
           reconstructed page's bar is 61px and Industry Hub's is 29 — for a
           trail whose own height is 36. Same mechanism exactly: the bar sits in
           a `height: 100vh` grid whose rows divide a fixed total, `overflow-x:
           auto` sets a grid item's automatic minimum size to zero, and the
           content row takes the difference. The trail was rendering clipped
           top and bottom inside a bar too short to hold it.

           And the padding is stated rather than left to each page, because the
           three bars measured carried three different values — 12px, 14/28 and
           13/24 — so the same trail sat at three different insets and three
           different heights on three apps. One inset, matching the content
           gutter under it; one floor, tall enough for the tallest thing the bar
           carries.

           60, not 56: the trail measures 36px tall — 13px type plus the 8px of
           vertical padding the tap-target floor gives a breadcrumb link — and
           36 + 12 + 12 is 60. A 56px floor left Industry Hub's trail clipped by
           four pixels, which is the same defect one size smaller.

           52 NOW, AND FOR THE SAME REASON. The arithmetic above is right and
           the input was a desktop's: 12px of air above and below is what a
           1440px column can spend. On a phone the band came to 66px around a
           33px trail, and on the section hubs it then added a 12px bottom
           margin on top of that — 78px of chrome between the header and the
           page, for one line of small grey text. 36 + 8 + 8 is 52, which still
           clears the trail exactly and does not clip it: the floor is derived
           from the trail either way, only the air around it changed. */
        min-height: 52px;
        padding: var(--frb-space-2, 8px) var(--frb-space-3, 12px);
    }

    /* (The injected band's bottom margin is dropped where it is declared,
       further down this same media block — a rule written up here would tie
       with it on specificity and lose on source order.) */

    /* ── A TITLE ROW WRAPS RATHER THAN SHREDDING ITS BADGES ───────────────
       Several heads put status chips beside the app's name in one flex row —
       Kitchen OS carries three: "WIKIBOOKS EDITION", "6 Recipes cached",
       "OFFLINE CACHED". At 390 the row is 240px wide and the chips are
       `flex: 0 1 auto` with `min-width: 0`, which is permission to shrink below
       min-content: they came out 25px wide and 177px TALL, one letter per line,
       three vertical strips of capitals down the side of the title.

       `min-width: 0` is the standard unblocker for a flex child that must
       shrink, and it is the wrong tool on a run of text — the floor it removes
       is the one that says "no narrower than your longest word". Restored as
       min-content, and the row wraps, which is what a title and its chips
       should do on a phone anyway. */
    body:not([data-frb-optout~="mobile"]) :is(.page-title, .page-head, .app-title-row) {
        flex-wrap: wrap;
        row-gap: var(--frb-space-2, 6px);
    }
    body:not([data-frb-optout~="mobile"]) :is(.page-title, .page-head, .app-title-row) > * {
        min-width: min-content;
    }

    /* THE INJECTED BAND LOOKS LIKE THE WRITTEN ONE. `.frb-topbar` is the
       wrapper frb-app-shell.js now puts around the trail on the 40 pages that
       never wrote a bar of their own. It inherits the rule above for its floor,
       inset and scrolling; what it has no page CSS for is the ground and the
       rule under it, which is what makes a row read as a band rather than as
       text that escaped. Taken from the same tokens the written bars use.

       In flow, not sticky: pinned, it landed on top of the app's own title,
       because the header it would have been pinned below scrolls away. */
    body:not([data-frb-optout~="mobile"]) .frb-topbar {
        display: flex;
        align-items: center;
        justify-content: space-between;
        /* A RULE, NOT A GROUND. A written `.topbar` is a direct child of the
           content column and can paint edge to edge; this one is inserted
           inside whatever wrapper the page already had, so a filled band stops
           short of both edges and reads as a card floating in the column
           rather than as a bar across it. The hairline does the same job
           without needing to reach the edge to be right. */
        border-bottom: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
        /* NO MARGIN UNDER IT, and it used to have 12px. The reasoning was that
           the injected band could land against a head with no top padding of
           its own — but everything the band is ever inserted above starts with
           its own inset, so the 12px was only ever added to a gap that already
           existed. The written `.topbar` never had it, which is exactly why a
           section hub read thicker than an app showing the same trail: 78px of
           chrome against 67, for one line of small grey text. */
        margin-bottom: 0;
    }
    body:not([data-frb-optout~="mobile"]) :is(.topbar, .top-bar, .ac-topbar,
        .oc-topbar, .rm-topbar, .frb-topbar)::-webkit-scrollbar { display: none; }
    /* Every child keeps its width while the bar scrolls past it. Without this
       the trail is squeezed to its narrowest word and the controls to their
       icons — the same failure the rail strip had before `flex: 0 0 auto`. */
    body:not([data-frb-optout~="mobile"]) :is(.topbar, .top-bar, .ac-topbar,
        .oc-topbar, .rm-topbar, .frb-topbar) > * {
        flex: 0 0 auto;
    }
    /* The trail itself is one line now rather than a wrapped block: a
       breadcrumb that wraps mid-path reads as two paths. */
    body:not([data-frb-optout~="mobile"]) :is(.topbar, .top-bar, .frb-topbar)
        :is(.breadcrumbs, .crumbs, .frb-appcrumb) {
        flex-wrap: nowrap;
        white-space: nowrap;
    }

    /* ── THE TRAIL COLLAPSES, IT DOES NOT JUST SCROLL ──────────────────────
       Making the bar scroll stopped it stacking into rows and clipping the
       controls, and introduced a worse problem in its place: the trail is
       written shallowest-first, so what a 390px screen shows is
       "Portals › Business › Business Operations › Accounting Co…" — the app's
       own name, which is the ONE step that says where you are, cut off at the
       edge of a bar with no sign that it scrolls.

       So on a phone the middle of the path is dropped rather than pushed off
       the side. The last three children are a link to the parent, a separator
       and the current page, which is the pair worth keeping: where you are,
       and one tap up. Everything before them stays in the document — a screen
       reader still reads the whole path — and the `…` says it was shortened.

       `:has(> :nth-child(4))` is the test for "deeper than parent › here", so
       a two-step trail is left alone and never gets a leading ellipsis. */
    body:not([data-frb-optout~="mobile"]) :is(.breadcrumbs, .crumbs, .frb-appcrumb)
        > *:not(:nth-last-child(-n+3)) {
        display: none;
    }
    body:not([data-frb-optout~="mobile"]) :is(.breadcrumbs, .crumbs, .frb-appcrumb):has(> :nth-child(4))::before {
        content: "…";
        color: var(--text-secondary, rgba(127, 127, 127, .95));
        margin-right: var(--frb-space-1, 4px);
    }
}

/* ── THERE IS NO SECOND PHONE BREAKPOINT, AND REMOVING IT IS THE FIX ───────
   This block used to collapse the parent step into the ellipsis below 430px,
   so the trail read "… › Accounting Core" on a small phone and
   "… › Business Operations › Accounting Core" on a slightly larger one. Two
   appearances, and the line between them falls straight through the middle of
   the widths real phones report: 390, 393, 412, 414, 430. Two people on two
   handsets saw the hub tree differently on the same build, which is exactly
   the "why is it dots here and full there" this work exists to remove.

   The reason it was written is gone. It was there because the bar overflowed —
   the trail at 250px plus a 130px button plus the plan chip did not fit 366px,
   so a control ended up chopped mid-word at the edge. The bar is one row that
   scrolls sideways at every width now, so nothing is chopped: the trail is
   first, the app's name is on screen, and a bar with more in it than fits
   pushes the rest sideways.

   Measured after the change, at 390px: 60 of the 90 trails fit uncollapsed
   even without the last-three rule, and with it every one of them fits. One
   rule, one appearance, from 320px to 620px. */

@media (max-width: 430px) {
    /* ══ A STRIP THAT SCROLLS SAYS SO — and both strips are the same thing ══
       This is the rule the product was missing, and it is written once here for
       every app rather than per page.

       A phone turns two rows of this shell into horizontal strips: the icon
       rail across the top, and the sub-header under it. Both were made to
       scroll, which stopped them stacking into three rows and clipping their
       controls. Neither was made to LOOK like it scrolls, and that is the whole
       of the complaint: an icon sliced in half at the right edge and a button
       reading "Comman" are indistinguishable from a layout that broke.

       Two things fix it together, and neither is enough alone.

       ONE — FIT MORE, so most apps cut nothing at all. A 44px item with an 8px
       gap is the desktop rail's box, and eight of them plus the strip's padding
       is 440px on a 390px screen. 40px with a 6px gap is 392px for eight and
       340px for seven — every app in this product with eight views or fewer
       now shows its whole rail with nothing cut. 40px is still well clear of
       the 32px the tap-target gate enforces, and the glyph steps 16 → 15px so
       it keeps its proportion inside the smaller box.

       TWO — FADE WHAT IS LEFT, so an app with nine views reads as a strip that
       continues rather than one that was chopped. 28px, as a mask rather than
       an overlay, so it works on whatever ground the strip has in either mode
       and on both strips identically.

       Snapping is the third piece and costs nothing: a swipe lands with an
       icon against the edge instead of halfway through one. */
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) :is(.nav-item,
        .rail-btn, .step-item, .sidebar-item) {
        /* `box-sizing` and `padding` are part of saying how big an item is. A
           mode button written for a head carries `padding: 8px 14px`, and with
           the default content-box that 40px became 68 — one item in the strip
           wider than the rest. Stated here so a control's origin stops
           deciding its size. */
        box-sizing: border-box;
        padding: 0;
        /* And no GAP. These buttons were `display: flex; gap: 8px` so the word
           sat away from the glyph; the word is a tooltip now, but the gap is
           still reserved beside an element that is out of flow, which left one
           item in the strip 55px wide against everyone else's 40. */
        gap: 0;
        width: 40px; height: 40px; min-width: 40px; min-height: 40px;
    }
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) :is(.nav-item,
        .rail-btn, .step-item, .sidebar-item) > :is(i, svg) {
        font-size: 15px;
    }
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) {
        /* `display: flex` HAS TO BE SAID, and leaving it unsaid is what made
           the strip a column on five apps. This block sets `flex-direction:
           row` further down and every rail in the product was already a flex
           box — because its own stylesheet said so. The reconstruction deletes
           that rule, and on a rail nested inside a wrapper the shell's own
           `body[data-frb-shell] > .sidebar` cannot reach it either, so nothing
           was left to say it: `display` fell back to `block`, `flex-direction`
           became inert, and the six icons stacked into a 390x350 tower at the
           top of GPA Forecaster and Focus Command with the swatches below them.

           A rule that depends on the page having said half of it is not a
           shared rule. */
        display: flex;
        align-items: center;
        gap: 6px;
        padding-left: var(--frb-space-2, 8px);
        padding-right: var(--frb-space-2, 8px);
        /* A STRIP CANNOT BE SHORTER THAN THE CONTROL INSIDE IT, and saying so
           out loud is not redundant. Industry Hub lays its shell out as a
           `height: 100vh` grid, so the rows have a fixed total to divide; the
           strip carries `overflow-y: hidden`, which sets a grid item's
           automatic minimum size to ZERO; and the content row's base size ate
           the difference. The row came out 17px — one border and two paddings,
           with no room at all for the 40px icons inside it, which rendered
           sliced in half along their top edge.
           40px item + 8px of padding on each side. */
        min-height: 56px;

        /* THE STRIP IS NOT STICKY, and it was for one round on a premise that
           turned out to be false. The reasoning was that the shared header is
           fixed, so a static strip slides under it on a page whose body
           scrolls. Measured: `.frb-header` computes `position: relative` on
           every app at every width — it scrolls away with the page like
           everything else, so there is nothing for a strip to slide under.

           Pinning it at `top: var(--frb-header-h)` therefore parked it 47px
           down from the viewport with a band of bare page showing above it, and
           the trail band pinned under it landed on top of the app's own title.
           Both are visible in one screenshot and neither existed before.

           It scrolls with the page, which is what everything else on a phone
           does. */
        background: var(--sidebar-bg, #0a0a0a);
    }

    /* A GROUP DIVIDER TURNS WITH THE RAIL. Five rails separate their view icons
       from their tool icons with an empty div, styled by a rule the
       reconstruction deletes. Left to itself in a horizontal strip it computes
       40x1 — a hairline lying the wrong way, which reads as a hole in the row
       rather than as a division of it. Empty and childless is the test, so a
       real control is never mistaken for one. */
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) > :is(.frb-rail-sep, div:empty) {
        flex: 0 0 1px;
        width: 1px;
        min-width: 1px;
        height: 24px;
        min-height: 0;
        margin: 0 2px;
        align-self: center;
        background: var(--glass-border-color, rgba(127, 127, 127, .22));
    }
    /* WHAT HAS TO FIT IS THE APP, NOT THE COLOUR PICKER. Measured on the
       rebuilt page: six view icons, the mode switch and their gaps come to
       350px of a 390px screen — the whole of this app's navigation, visible
       without a swipe. The four theme swatches after them are another 152px,
       and they are the right thing to put beyond the fade: a theme is chosen
       once and never again, while a view is chosen constantly.

       An app with nine or more views still scrolls, and that is correct rather
       than a failure — but it now scrolls behind a fade instead of ending in a
       half-drawn icon. */

    body:not([data-frb-optout~="mobile"]) :is(.topbar, .top-bar, .ac-topbar,
        .oc-topbar, .rm-topbar, .frb-topbar),
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) {
        -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
        mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent 100%);
    }

    /* NO SCROLL SNAPPING, and it was in here for one round before being taken
       back out. `scroll-snap-type: x proximity` with `scroll-snap-align: start`
       on the children reads as a free improvement — a swipe lands with an icon
       against the edge instead of halfway through one — and it moved the
       strip's RESTING position. The light/dark switch is `order: -1`, so it is
       laid out first while remaining the last child in the DOM; the snap engine
       resolved to a different item and the strip opened already scrolled, with
       the switch measured at x = -46, off the left edge, on every app.
       Measured, not guessed, and removed rather than tuned: the strip already
       scrolls perfectly well without it. */
}

/* ═══ GLOBAL 3b — THE RAIL IS 70px WHEREVER IT SITS IN THE TREE ════════════
   Nothing shared said how wide a rail is on a desktop. The shell says it, but
   only for a rail that is a DIRECT child of <body>, and Travel Planning keeps
   its rail inside `.app-container` — so when the reconstruction deleted that
   page's own `.sidebar { width: 70px }`, nothing was left to say it. As a flex
   item with no floor it shrank to 35px: half a rail, icons clipped, on a page
   that had just been "fixed".

   Width alone would not have held it. A flex item shrinks below its width
   unless it is told not to, which is what `flex: 0 0 auto` and the matching
   `min-width` are for — the same pairing the bar's children needed.

   Guarded above the phone breakpoint, because below it the rail is a
   full-width strip and this would fight the rules that make it one. That guard
   is a `min-width` query rather than ordering, so it cannot be broken later by
   moving a block. */
@media (min-width: 621px) {
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) {
        /* Same reason the strip below needs it: the column was only ever a flex
           box because each page's own stylesheet said so, and that rule is what
           the reconstruction deletes. It happened to survive on a desktop —
           block-level children stack vertically, which is what a column wants —
           but "correct by accident" is how the phone case broke. */
        display: flex;
        flex-direction: column;
        align-items: center;
        flex: 0 0 auto;
        width: var(--sidebar-w, 70px);
        min-width: var(--sidebar-w, 70px);
        box-sizing: border-box;
    }
    /* The same divider, lying the way a column divides: across the rail. */
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) > :is(.frb-rail-sep, div:empty) {
        flex: 0 0 1px;
        width: 32px;
        height: 1px;
        min-height: 1px;
        margin: 4px 0;
        align-self: center;
        background: var(--glass-border-color, rgba(127, 127, 127, .22));
    }
}

/* ═══ GLOBAL 4a — THE SWITCH AT THE FOOT OF A DESKTOP RAIL ═════════════════
   frb-app-shell.js appends it as a direct child of the rail rather than into
   `.theme-footer`, so that `order` can reach it on a phone (see 4b). On a
   desktop that leaves it as the last item in a column whose foot already has
   `margin-top: auto`, which puts it exactly where it was — under the swatches,
   at the bottom — and it needs the same sticky bottom the foot has, or it
   scrolls away on the four rails whose icon list is taller than the window. */
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) > :is(.frb-rail-mode, #modeToggle, .mode-toggle-btn) {
    position: sticky;
    bottom: 0;
    background: var(--sidebar-bg, #0a0a0a);
    z-index: 1;
}

/* ═══ GLOBAL 4b — THE LIGHT/DARK SWITCH STAYS ON SCREEN ════════════════════
   The rail becomes a strip on a phone and its foot — four theme swatches and
   the one light/dark switch — becomes the far end of that strip. Which means
   that on every app, at every phone width, the control the product owner
   specifically asked never to be missing is the first thing to go off the
   right-hand edge. Present in the markup, unreachable without discovering that
   a bar you cannot tell is scrollable scrolls.

   `position: sticky; right: 0` is the horizontal form of the `sticky; bottom:
   0` that already pins this foot to the bottom of the rail on a desktop: the
   icons scroll underneath it and the switch stays against the edge. It needs
   its own ground for that, or the icons show through it.

   ONLY THE SWITCH IS PINNED. Pinning the whole foot would park four swatches
   and a button — about 124px — permanently across a 390px screen. The swatches
   scroll with the strip; the mode switch does not, because it is the one that
   was asked for by name. */
@media (max-width: 620px) {
    /* IT MOVES TO THE FRONT OF THE STRIP. Not `position: sticky; right: 0`,
       which is what this was and which took two goes to get wrong twice.

       Sticky clamps to the CONTAINING BLOCK, and the switch's parent is
       `.theme-footer` — pushed to the far end of the strip by `margin-left:
       auto`. On Industry Hub the foot's left edge lands at 380px on a 390px
       screen, so `right: 0` could pull the switch back exactly 10px: pinned,
       and pinned off the edge. On Accounting Core the foot starts at 324px and
       it looked perfect. One rule, right on one app and wrong on the next, for
       a reason neither app's markup mentions.

       The second attempt dissolved the foot with `display: contents` so the
       strip became the containing block. That worked on all three apps I
       looked at and broke the two I did not: CV Architect and Menu Genie lay
       their shell out as a split pane, and turning four swatches from one flex
       item into four pushed their rails past the viewport — 35px and 193px of
       sideways scroll on pages that had none.

       A strip always starts at scroll 0, so the FIRST item is the one item
       that is visible without qualification — no containing block, no scroll
       position, no assumption about how the page around it is laid out.
       `order` moves the switch there and changes nothing else: not the DOM,
       not the tab order's meaning, not one box's size. The four swatches stay
       at the far end and scroll, which is right — a theme is chosen once, and
       light/dark is flicked constantly. */
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav)
        :is(.frb-rail-mode, #modeToggle, .mode-toggle-btn) {
        order: -1;
        /* It is a setting, not a view. The rule keeps it from reading as the
           first icon in the app's own list of screens. */
        margin-right: var(--frb-space-2, 8px);
        padding-right: var(--frb-space-2, 8px);
        border-right: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
        width: auto;
        min-width: 44px;
    }
}

/* ════════════════════════════════════════════════════════════════════════════
   THE SHELL — everything below is scoped to a page that has been reconstructed
   to the shape at the top of this file, and reaches nothing else.
   ════════════════════════════════════════════════════════════════════════════ */

/* The page is a rail and a column, and the column is the only thing that
   scrolls. The rail is fixed to the viewport rather than laid out in a grid
   track: a grid column has to be as tall as its row, which is what made four
   rails 2468px tall when the row grew, and it means the content column can be
   the document's scroller — so the browser's own scroll restoration, anchor
   links and find-in-page all work the way they do on an ordinary page. */
body[data-frb-shell] {
    margin: 0;
    /* THE SHELL OWNS THE PAGE'S LAYOUT, not just its insets, and this block is
       what a page opts into. Nearly every app lays its own body out as
       `display: grid; grid-template-columns: 70px 1fr; height: 100vh;
       overflow: hidden` — the rail in column one, the content in column two.
       The shell takes the rail OUT of flow (`position: fixed`), so the content
       column becomes the first grid item and lands in the 70px track: Kanban
       Lite reconstructed to a 40px sliver of text one character wide.

       Declared here rather than deleted from each page, because a page's body
       rule also carries its typeface and its ground, and those are still its
       own to set. */
    display: block;
    grid-template-columns: none;
    grid-template-rows: none;
    height: auto;
    min-height: 100vh;
    overflow: visible;
    padding-left: var(--sidebar-w, 70px);
    box-sizing: border-box;
    /* THE PAGE'S GROUND COMES FROM THE TOKEN FILE, not from the page. Every app
       painted its own: Accounting Core declared `--bg-body` in four theme
       blocks and nowhere else, so switching to light mode turned the TEXT dark
       and left the ground on the dark gradient — black-on-black, the whole page
       unreadable, and no gate caught it because every gate measured dark mode.
       --bg-gradient is declared per theme AND for light mode in frb-tokens.css,
       which is the only place in this product that knows all eight values. */
    background-image: var(--bg-gradient);
    background-attachment: fixed;
    background-color: var(--sidebar-bg, #0a0a0a);
    color: var(--text-primary, CanvasText);
}

/* EVERY NAME THE RAIL GOES BY, not just the one the reference page uses.
   These rules said `> .sidebar`, and thirteen reconstructed pages call theirs
   `.sidebar-primary` — the ten Ranchie documents, Growth Engine, Kitchen Dash,
   Media Optimizer and the vet. On those the fixed-column rule never matched, so
   the rail rendered as a full-width 1030px block at the top of the page while
   the body still held 70px of padding open for a column that was not there.
   They were on the shell and not getting it, which is the worst of both.

   The alias list is the same one the global half of this file already uses,
   minus `nav.rail` — spelled `.rail`, so `:is()` resolves to one class and the
   selector keeps the (0,2,1) it had. Widening what a rule matches should not
   quietly change what it outranks. */
body[data-frb-shell] > :is(.sidebar, .sidebar-primary, .ranchie-sidebar, .rail, .nav-rail, .rail-nav) {
    position: fixed;
    top: var(--frb-header-h, 47px);
    left: 0;
    bottom: 0;
    width: var(--sidebar-w, 70px);
    z-index: 40;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--frb-space-1, 4px);
    /* One inset, top and bottom, so the first icon sits the same distance from
       the header on every app. Seven different values were in use. */
    padding: var(--frb-space-4, 16px) var(--frb-space-2, 8px);
    background: var(--sidebar-bg, #0a0a0a);
    border-right: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
    box-sizing: border-box;
}
body[data-frb-shell] > :is(.sidebar, .sidebar-primary, .ranchie-sidebar, .rail, .nav-rail, .rail-nav)::-webkit-scrollbar { display: none; }

/* AND THE RAIL'S GAP IS THE RAIL'S ONLY SPACING. The column above sets 4px
   between its icons; Area Logic's buttons are `.rail-btn` and bring a
   `margin-bottom: 15px` of their own from when the page drew its own rail, so
   the seam there measured 19px against everyone else's 4 and the six icons
   read as a loose scatter down the side. Margins and a gap are added, and only
   one of the two is visible to whoever set the other.

   Two exceptions, both deliberate. `.theme-footer` is pushed to the bottom by
   `margin-top: auto` and would spring back up beside the icons without it. A
   separator is a margin — a 1px line with space either side is the whole of
   what it draws — so anything named for one keeps what it has. */
body[data-frb-shell] > :is(.sidebar, .sidebar-primary, .ranchie-sidebar, .rail, .nav-rail, .rail-nav)
    > *:not(.theme-footer):not([class*="sep"]) {
    margin-top: 0;
    margin-bottom: 0;
}

/* The foot is the swatches and the one light/dark switch, pinned to the bottom
   of the rail and staying there while the icons above it scroll. */
body[data-frb-shell] > :is(.sidebar, .sidebar-primary, .ranchie-sidebar, .rail, .nav-rail, .rail-nav) > .theme-footer {
    margin-top: auto;
    position: sticky;
    bottom: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--frb-space-2, 8px);
    padding-top: var(--frb-space-3, 12px);
    background: var(--sidebar-bg, #0a0a0a);
}

/* The rail's own parts. A reconstructed page has deleted its private `.sidebar`
   block, so these are no longer defined anywhere else on it — and a swatch with
   no width is a swatch you cannot see, which is what the first rebuild of
   Accounting Core shipped: a theme footer holding four invisible circles and
   one light/dark button. Sizes are the plurality across the 75 rails that were
   measured: a 44×44 item at radius 12 holding a 16px glyph. */
body[data-frb-shell] .nav-item,
body[data-frb-shell] .rail-btn {
    width: 44px; height: 44px;
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center;
    padding: 0;
    border: 0;
    background: none;
    border-radius: var(--frb-space-3, 12px);
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    cursor: pointer;
    position: relative;
    box-sizing: border-box;
}
body[data-frb-shell] :is(.nav-item, .rail-btn) > :is(i, svg) { font-size: 16px; }
body[data-frb-shell] :is(.nav-item, .rail-btn):hover {
    background: rgba(127, 127, 127, .14);
    color: var(--text-primary, CanvasText);
}
body[data-frb-shell] :is(.nav-item, .rail-btn):is(.active, .is-active, [aria-current]) {
    background: rgba(127, 127, 127, .1);
    color: var(--accent-text, var(--accent-primary));
    box-shadow: 0 0 15px var(--accent-glow, transparent);
}
body[data-frb-shell] .theme-dot {
    width: 20px; height: 20px;
    flex: 0 0 auto;
    padding: 0;
    border-radius: 50%;
    border: 2px solid rgba(127, 127, 127, .3);
    cursor: pointer;
    box-sizing: border-box;
    /* A SWATCH IS THE COLOUR IT NAMES, however the page wrote that colour down.
       59 of the 60 rails set it as `style="background:#d4af37"`, which wins here
       as an inline style regardless. Ops Command writes `style="color:#d4af37"`
       and relied on its own `.theme-dot` rule to turn that into a filled circle
       — and that rule is exactly what the reconstruction deletes, so its four
       swatches came out as empty grey rings. Reading currentColor covers both
       spellings without either page having to change. */
    background: currentColor;
}
/* A THIRD SPELLING: THE COLOUR IS IN THE CLASS NAME. The rule above covers a
   page that writes the colour on the element, either way round. Area Logic
   writes neither — its dots are `.theme-dot.dot-midnight`, `.dot-ocean`,
   `.dot-sunset`, `.dot-forest`, and the colour lived in the page's own rules
   for those names, which is exactly the CSS the reconstruction deletes. With
   nothing left to read, `currentColor` fell back to the inherited white and
   all four swatches came out as plain white circles.

   Named for the theme they set, so the name IS the answer, and these are the
   same four values the registry in frb-app-shell.js carries. Given as `color`
   rather than `background` so it feeds the one mechanism above rather than
   opening a second one. */
body[data-frb-shell] .theme-dot.dot-midnight { color: #d4af37; }
body[data-frb-shell] .theme-dot.dot-ocean    { color: #4fa3d1; }
body[data-frb-shell] .theme-dot.dot-sunset   { color: #ff9f43; }
body[data-frb-shell] .theme-dot.dot-forest   { color: #66ff66; }

body[data-frb-shell] .theme-dot:hover {
    transform: scale(1.2);
    border-color: var(--text-primary, CanvasText);
}

/* ── THE TWO BUTTONS AN APP SHELL USES ────────────────────────────────────
   Not a third button system: these are the names Industry Hub and Carbon
   already use for "the action" and "the other actions", given one definition
   here so a reconstructed page does not have to carry its own. The height,
   radius and focus ring are the ones Wave 1 settled on for every control in
   the product. */
body[data-frb-shell] :is(.btn-action, .btn-outline) {
    display: inline-flex; align-items: center; justify-content: center;
    gap: var(--frb-space-2, 8px);
    /* The fourth and last copy of the button recipe in this design system, and
       the one the app pages actually reach — job_tracker and kanban_lite were
       still 13px/700/Outfit after the other three were converted, because this
       rule outranks them all on specificity. It also had --frb-space-2 doing
       duty as a radius, which is how an 8px gap token came to decide a button's
       corners. All of it now reads the --frb-btn-* tokens. */
    min-height: var(--frb-btn-min-h, 38px);
    padding: 0 var(--frb-space-4, 16px);
    border-radius: var(--frb-btn-radius, var(--frb-radius-md, 8px));
    font-family: var(--frb-btn-font, inherit);
    font-size: var(--frb-btn-fs, 13px);
    font-weight: var(--frb-btn-weight, 700);
    letter-spacing: var(--frb-btn-tracking, normal);
    text-transform: var(--frb-btn-transform, none);
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
}
body[data-frb-shell] .btn-action {
    background: var(--accent-fill, var(--accent-primary, #d4af37));
    color: var(--btn-text, #0f1115);
    border: 1px solid transparent;
}
/* `.btn-outline` HAS NO COLOURS HERE ON PURPOSE. frb-compat.css already owns
   them, for the 662 places in this product that use the class — transparent
   fill, accent border, accent ink, and an accent-glow hover. Restating any of
   that here would make a reconstructed page's outline button a different
   colour from every other outline button in the product, which is the exact
   drift this whole exercise exists to remove. The shell contributes geometry
   (above) and nothing else. */
body[data-frb-shell] .btn-outline { border-style: solid; border-width: 1px; }

body[data-frb-shell] > .main {
    display: flex;
    flex-direction: column;
    min-width: 0;
    /* THE COLUMN HAS NO INSET OF ITS OWN, and this is the other half of the
       gutter rule below. Eight apps write `padding: 0 40px` on their content
       column, from when each page set its own page margin. The gutter is given
       to the column's CHILDREN, so those pages got 40 + 28 = 68px on one app
       against 28 on the next — and the head's rule, drawn on the head's own
       box, stopped 40px short of nothing at all, which is the "divider does not
       reach the edges" that was reported. Stated here so the inset is the
       children's, once. */
    padding-left: 0;
    padding-right: 0;
    /* AND NONE AT THE TOP EITHER. The bar is the column's first child and it
       sits directly under the shared header on sixty-two apps; Area Logic pads
       its column 20px, so its bar floated with a strip of ground above it and
       everything below was 20px out of step with every other app. Whatever gap
       there is above the bar is the header's, not the app's. */
    padding-top: 0;
    padding-bottom: 64px;
}

/* THE GUTTER COMES FROM WHERE A BLOCK SITS, NOT FROM A CLASS IT MIGHT NOT HAVE.
   It used to come from `.section`, which is what the hand-built reference page
   wraps its content in — and on the reference that reads fine, because it was
   written that way. Kanban Lite was not: the transform can add a head, but it
   cannot know that `#view-board` and `#frbAppFeatures` and a stats grid were
   meant to be sections, so those three landed as direct children of the column
   with no padding of their own and sat flush against the viewport edge. At
   390px the stat cards touched both sides and the capability notice started at
   x=0. Sixty apps are built from sixty different parts; requiring each to
   opt in to the gutter by wearing the right class is the assumption that made
   the four earlier mapping waves fail.

   So the column gives every block in it the same gutter, whatever the block is
   called. Two exceptions, both deliberate:

   THE BAR keeps the full width of the column, because its rule is what makes
   it read as a band rather than as an underline drawn beneath the trail.

   A `.view` is a screen, not a block — the gutter goes to the blocks INSIDE
   it, so the head's rule still spans the whole column the way it does on the
   reference page. Putting it on the view instead would have inset that rule by
   28px on both sides and quietly restyled the one app already signed off.

   Padding does not accumulate across rules, so a `.page-head` or `.section`
   matched by both this and its own rule lands on 28px either way; the token is
   still the single place the number is written.

   `box-sizing` is set here rather than assumed: there is no universal reset in
   this stylesheet chain, so a child written as `width: 100%` would otherwise
   add the gutter on top of a full-width box and overflow the column. */
body[data-frb-shell] .main > *:not(.topbar, .view),
body[data-frb-shell] .main > .view > * {
    /* NOTHING IN THE COLUMN IS SIZED TO THE LEFTOVER VIEWPORT. Several apps
       were built as a 100vh frame and write `flex: 1` on their content
       container — Budget Burn HUD's `.dashboard-layout` is one — which under
       the shell asks for whatever is left of the screen and then divides THAT
       between its rows. Measured: each of its four metric tubes came out a
       314x42 box around 144px of content, so the label and the amount sat on
       the bottom edge with the rest spilling past it.

       The shell's column is `height: auto` and the document scrolls, so a child
       that grows to fill is asking for a height that no longer means anything.
       Content-sized, and the page scrolls — which is what the other sixty do.

       `max-height` goes with it, and that is the half that actually fixed the
       tubes. `.dashboard-layout` also carries `max-height: calc(100vh - 70px)`
       with `overflow-y: auto` — an inner scroller sized to the screen, from
       when this app was its own 100vh frame. Under the shell that nests a
       scrolling box inside a scrolling page, and it caps the grid's total
       height; the metric tubes are `overflow: hidden`, which sets a grid item's
       automatic minimum size to ZERO, so they were free to shrink below their
       own content and did — 42px of box around 144px of content. Uncapped, the
       grid sizes to its rows, every tube fits, and the document does the
       scrolling. The overflow property is left alone: a pane that means to
       scroll still scrolls, it just is not cut to the viewport first. */
    flex: 0 0 auto;
    max-height: none;
    box-sizing: border-box;
    padding-left: var(--frb-shell-gutter, 28px);
    padding-right: var(--frb-shell-gutter, 28px);
    /* AND NO MARGIN OF ITS OWN SIDEWAYS. Eight apps put a 40px horizontal
       margin on their head or their first card, from when each page set its own
       page inset. On top of the column's gutter that is 68px on one app and 28
       on the next — and because a border-bottom is drawn on the head's box, the
       head's rule stopped 40px short of the cards under it, which is the
       "divider does not reach the edges" that was reported. The inset is the
       column's to give, so it is the column's alone. */
    margin-left: 0;
    margin-right: 0;
}

/* ── THE BAR: WHERE THE HUB TREE LIVES ────────────────────────────────────
   One place, on every app, and it is the top of the content column — not
   floating above the head on one app and inside it on the next. Sticky, so the
   trail stays available while the view scrolls; under the shared header, whose
   height is a token rather than a number repeated here. */
body[data-frb-shell] .main > .topbar {
    position: sticky;
    top: var(--frb-header-h, 47px);
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* ONE ROW AT EVERY WIDTH, SCROLLING IF THE APP OVERFILLED IT.
       This was `wrap`, on the assumption that a bar with too much in it should
       use a second line. Canvas Pro shows what that actually does: trail, app
       name, a project field and six toolbar buttons come to more than 1370px,
       so the buttons dropped onto a row of their own — and because that page's
       editor is sized against the viewport rather than reflowed, the second row
       landed on top of the canvas. A bar that grows is a bar that covers the
       app under it.

       Scrolling is what the same bar already does on a phone, for the same
       reason, and it is what the reader asked for there: the trail keeps its
       place, the height stays 62, and an app that put eight controls in its bar
       pushes them sideways instead of downwards. */
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
    gap: var(--frb-space-4, 16px);
    padding: var(--frb-space-3, 12px) var(--frb-shell-gutter, 28px);
    background: var(--panel-bg, rgba(10, 10, 10, .8));
    backdrop-filter: blur(15px);
    -webkit-backdrop-filter: blur(15px);
    border-bottom: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
    /* THE BAR HAS A HEIGHT OF ITS OWN, not its contents'. Measured at 1440px:
       Accounting Core's bar is 63px because it holds a 38px button beside the
       trail; Kanban Lite's, with nothing in it but the trail, is 50 — and at 50
       it stops reading as a band and reads as a line of text that escaped into
       the gap between the header and the head. The bar is the same object on
       both pages, so it is the same height on both, whether or not the app
       happens to have put a control in it.

       62 = a 38px control plus 12px above and below, which is where the app
       that does have one already lands. */
    min-height: 62px;
}
@media (max-width: 620px) {
    /* AND ON A PHONE IT IS 52, the same number the injected band takes. This
       rule is (0,3,0) — `body[data-frb-shell] .main > .topbar` — so the phone
       block near the top of this file, which names the band family at lower
       weight, never reached the written bar at all: the hubs thinned and the
       apps did not, which would have left the same trail set two different
       heights depending on which page you opened. 36px of trail plus 8 and 8,
       derived the same way 62 was, from a phone's air rather than a desktop's. */
    body[data-frb-shell] .main > .topbar {
        min-height: 52px;
        padding: var(--frb-space-2, 8px) var(--frb-shell-gutter, 14px);
    }
}
body[data-frb-shell] .main > .topbar::-webkit-scrollbar { display: none; }
body[data-frb-shell] .main > .topbar > * { flex: 0 0 auto; min-width: 0; }

/* ── ONE TRAIL PER PAGE ───────────────────────────────────────────────────
   There used to be a rule here hiding the header's `Feerebu / <page>` crumb on
   any page that also had a band. It is gone because the crumb is gone: the
   header stopped drawing one at all, so there is no longer a second trail to
   suppress on the pages that have a band, and nothing left to suppress on the
   pages that do not. See frb-header.js, build(). */

/* THE TRAIL LOOKS THE SAME WHETHER THE PAGE WROTE IT OR THE SHELL DERIVED IT.
   `.breadcrumbs` is what a page writes; `.frb-appcrumb` is what
   frb-app-shell.js builds for the 40 that do not, and frb-components.css sizes
   that one at 12px for use as a bare line above a head. Inside the bar it is
   the same trail doing the same job, so it takes the same 13px — otherwise the
   hub tree is set in two different sizes depending on which app you opened. */
body[data-frb-shell] :is(.breadcrumbs, .frb-appcrumb, .crumbs) {
    display: flex;
    align-items: center;
    gap: var(--frb-space-2, 8px);
    flex-wrap: wrap;
    min-width: 0;
    font-size: 13px;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
}
body[data-frb-shell] :is(.breadcrumbs, .frb-appcrumb, .crumbs) a {
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    text-decoration: none;
}
body[data-frb-shell] :is(.breadcrumbs, .frb-appcrumb, .crumbs) a:hover { color: var(--accent-text, var(--accent-primary)); }
body[data-frb-shell] :is(.breadcrumbs, .frb-appcrumb, .crumbs) :is(.active, .frb-appcrumb-here),
body[data-frb-shell] :is(.breadcrumbs, .frb-appcrumb, .crumbs) [aria-current="page"] {
    color: var(--text-primary, CanvasText);
    font-weight: 600;
}
body[data-frb-shell] :is(.breadcrumbs, .frb-appcrumb, .crumbs) i { font-size: 9px; opacity: .55; }

/* ── THE VIEW AND ITS HEAD ────────────────────────────────────────────────
   A view is a screen of the app. Only one is shown, and the head at the top of
   it names it. The APP's name is not repeated here — it is the last step of
   the trail in the bar above, which is the one place it appears, so clicking
   through the views never changes what the page says it is. */
/* THE SHELL ONLY SHOWS AND HIDES SCREENS ON A PAGE THAT USES `.active`.
   Written unconditionally, this pair blanks two apps outright. Favicon Forge's
   four screens are `<section class="view" hidden>` and its switchView() toggles
   the attribute; Scamshield's eight are `<section class="view on">` and its
   showView() toggles the class. Neither has ever put `active` on anything, so
   `.view { display: none }` hides all twelve screens and `.view.active` matches
   none of them — the app opens to an empty column with a working rail.

   `:has()` makes the takeover conditional on the convention actually being in
   use: a page that marks its current screen with `.active` gets the shell's
   switching, and a page that marks it some other way keeps its own. The word
   `view` means "a screen of this app" on all of them; it does not follow that
   they agree on how a screen is shown, and assuming they did is what would
   have shipped two blank apps. */
body[data-frb-shell]:has(.view.active) .view { display: none; }
body[data-frb-shell]:has(.view.active) .view.active { display: block; }

body[data-frb-shell] .page-head {
    /* IT SPANS WHATEVER IT LANDED IN. Budget Burn HUD and Event Analytics keep
       their content in a `.dashboard-layout` grid of twelve auto columns, and
       the head is one of that grid's items — so it was given the first column,
       147px wide, and "Budget Burn HUD" came out stacked one word per line above
       a description in a 91px ribbon. Reported as the title and description
       being a mess, and it was: a full-width band folded into a twelfth of the
       page.

       `grid-column: 1 / -1` is ignored by a flex or block parent, so it costs
       nothing on the other sixty and rescues the head on any page that still
       lays its content out as a grid around it.

       NO `align-self` HERE, and the reason is that the property means opposite
       things in the two layouts. It was added beside the line above to stop a
       grid row stretching the head vertically; in a grid `align-self` is the
       block axis, but `.main` is a FLEX COLUMN, where it is the inline one — so
       `start` stopped the head stretching sideways and it came out 1185px wide
       against content 1370 wide, with its rule ending 185px short of the cards
       under it. The heads are hoisted out of content grids now, so nothing
       needs saying about the block axis at all. */
    grid-column: 1 / -1;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--frb-app-head-gap, 16px);
    /* THE BOTTOM PADDING IS NOT ZERO, and writing it as zero here was a real
       bug rather than a shorthand tidy. The rule under the head is drawn by
       frb-compat.css on the head's own border-bottom, and that layer also gives
       it `padding-bottom: 16px` to sit away from the text — at (0,1,1). This
       selector is (0,2,2), so a shorthand ending in `0` silently won that
       argument and pulled the line up against the last descender of the
       description. Barely visible on a desktop; on a phone the sentence sits
       directly on the divider. */
    padding: var(--frb-space-6, 24px) var(--frb-shell-gutter, 28px) var(--frb-space-4, 16px);
    /* AND THE MARGIN UNDER IT IS THE SHELL'S, NOT THE COMPAT LAYER'S.
       Measured on the rebuilt page at 1440px: 17px between the description and
       the rule, and 66px between the rule and the first card. A band with four
       times the air below it as above reads as a divider that failed to attach
       to anything.

       The 66 was three rules stacking: frb-compat.css gives every head
       `margin-bottom: 24px`, `.section` adds 20px of its own padding, and this
       page put 22px on top of the note that happens to be first. The head's
       own spacing stops here at 0 and `.section` owns the whole gap, so there
       is one number to read and one place to change it. */
    /* THE GAP UNDER THE RULE IS 20px ON EVERY APP, and it used to be zero on
       most of them. This said `margin-bottom: 0` and left the whole gap to
       `.section`, which is what the hand-built reference wraps its content in —
       so the two apps that have sections got 20px and the other fifty got the
       first card pressed against the divider. Measured across the 52 apps with
       a head: 45 of them at 0px.

       Three lines rather than one, because the gap has to be the same however
       the page is built. The head owns it; a `.section` that follows drops its
       own top padding so the two do not add (the parent is a flex column on the
       pages without views, where margins do not collapse); and a block that
       brought its own 26 or 40px margin gives it up, because "one number in one
       place" is the point and a page-by-page value is what this replaces. */
    /* THE SAME GAP THE SIDES HAVE. The content is inset 28px left and right;
       under the rule it was 20 plus whatever the first box brought with it —
       60px on Revenue Projection Model, which is what "top and bottom padding
       is bad it should be similar to side padding" was describing. One number
       on all four edges, and frb-app-shell.js takes the extra margin off the
       first box so this stays the only one. */
    margin-bottom: var(--frb-shell-gutter, 28px);
}
body[data-frb-shell] .page-head + * { margin-top: 0; }
body[data-frb-shell] .page-head + .section { padding-top: 0; }
/* `1 1 auto`, NOT `1 1 320px`, AND THE DIRECTION IS WHY. A flex-basis is
   measured along the main axis, so 320px is a WIDTH in a row and a HEIGHT in a
   column. The head is a row here, but a page is free to turn its own head into
   a column at its own breakpoint and Font Awesome Library does exactly that
   below 900px — so on a phone its title block was handed a 320px basis in the
   column direction and stood 320px tall around 76px of text, a black band under
   the app's name that nothing accounted for.

   `auto` sizes to the content in whichever direction it is asked, and `grow`
   still gives the text group the leftover width beside the actions, which is
   what the 320 was for. `min-width: 0` stays: without it the group refuses to
   shrink and the actions get pushed off the end. */
body[data-frb-shell] .page-head > :first-child { flex: 1 1 auto; min-width: 0; }
body[data-frb-shell] .page-head > .row-flex { margin-left: auto; }

body[data-frb-shell] .page-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--frb-space-3, 12px);
    margin: 0;
    /* The LAST hardcoded value in this rule, and the one that mattered most:
       at (0,2,1) it beat plans.html's own .page-title at (0,1,0), so that
       page's title was drawn in Outfit while every other page head was
       Syne — measured, not guessed: CSS.getPlatformFontsForNode returned
       "Outfit Thin ExtraBold" there and "Syne" everywhere else. Same shape
       as the four tokens below it, same default, so no app page moves. */
    font-family: var(--frb-app-title-font, var(--frb-font, 'Outfit', system-ui, sans-serif));
    font-size: var(--frb-app-title-size, 28px);
    font-weight: var(--frb-app-title-weight, 800);
    line-height: var(--frb-app-title-leading, 1.2);
    /* A TOKEN, like the three above it. This was the one hardcoded value left
       in the rule, and it is why plans.html — the only page head that is also
       shelled — tracked at -0.01em while every other title tracked at -0.04em.
       The default is exactly what was here, so no shelled app page moves. */
    letter-spacing: var(--frb-app-title-tracking, -0.01em);
    color: var(--text-primary, CanvasText);
    overflow-wrap: break-word;
}
/* NO GLYPH BEFORE THE APP'S NAME. Nine heads write one into their own title
   and frb-app-icon.js used to add one to the rest; the head is a name at 28px
   with a sentence under it, and it says what it is without a bullet in front
   of it. Hidden rather than cut, because a page's script may hold the node —
   `display: none` on a flex item takes it out of the row and out of the gap,
   so the title starts where the description does. Section headings keep
   theirs: those mark a place in the page rather than name the app. */
body[data-frb-shell] .page-head .page-title > :is(i, svg, img, .frb-app-glyph) {
    display: none;
}

body[data-frb-shell] .page-sub {
    margin: var(--frb-space-2, 8px) 0 0;
    font-size: var(--frb-app-sub-size, 14px);
    line-height: 1.5;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    max-width: 78ch;
}

/* ═══ THE APP HEAD JOINS THE EDITORIAL SET ═════════════════════════════════
   The 21 editorial pages and the 24 hubs have carried Syne for the name and
   JetBrains Mono for the sentence under it since the codefolio pass. The 77
   tools were deliberately left out, and the reason still stands for everything
   BELOW the head: an app's dense working surface is what a person came for,
   and the editorial voice has no business overriding it.

   The head is the exception, because the head is the one part of an app page
   that is not the app. It is the app's NAME and its one line — the same two
   things every hub and every editorial page says in this face — and it sits
   above the rule that separates it from the working surface.

   Set as the TOKENS the rules above already read, not as new declarations, so
   a page that wants its old head back overrides one custom property rather
   than fighting a (0,2,1) selector. The comment on --frb-app-title-font used
   to say the default was chosen "so no app page moves"; they move now, and
   deliberately. ════════════════════════════════════════════════════════════ */
body[data-frb-shell] {
    --frb-app-title-font: var(--frb-ed-display, 'Syne', 'Outfit', system-ui, sans-serif);
    --frb-app-title-tracking: -.035em;
}
body[data-frb-shell] .page-sub {
    font-family: var(--frb-ed-mono, 'JetBrains Mono', ui-monospace, monospace);
    font-size: 12.5px;
    line-height: 1.7;
}

/* ═══ THE HEAD FOLDS AWAY ══════════════════════════════════════════════════
   A name and a sentence are worth reading once. On a phone they are worth
   about a fifth of the screen, every visit, above a tool someone opened to
   use — so the band closes upward like a drawer and stays closed, per device,
   until it is asked back.

   The height is animated in script from a measured value rather than from a
   `max-height` guess: a guess that is too small clips the description on a
   narrow screen and a guess that is too large makes the close take a visible
   pause before anything moves. The group is restored to `height: auto` once
   the transition ends, so a later reflow — a longer description, a rotated
   phone — is not pinned to the pixel count it had when it opened.

   The breadcrumb above the head names the app too, so nothing is lost while
   this is shut: it says Portals › Academics › Career Workspace › CV whether
   the band is open or not. ═══════════════════════════════════════════════ */
/* NOT SCOPED TO THE SHELL, because there are two heads. 68 app pages carry
   `.page-head`; the six split-pane editors have no head in their markup and
   get a `.frb-apphead` built by frb-editor.js. The script marks whichever one
   it found with `.frb-head-fold`, so the geometry below is written once
   against that class rather than twice against two containers. */
.frb-head-fold {
    overflow: hidden;
    transition: height .32s var(--frb-ease, cubic-bezier(.4, 0, .2, 1)),
                opacity .2s ease;
}
.frb-head-min > .frb-head-fold { opacity: 0; }
/* Closed, the band is the rule and the control — the padding that framed a
   title has nothing left to frame. */
body[data-frb-shell] .page-head.frb-head-min {
    padding-top: var(--frb-space-3, 12px);
    padding-bottom: var(--frb-space-3, 12px);
}
/* The editor head is a block, not a flex row, so its control is placed rather
   than laid out — the band is already `position: relative`, and the padding
   keeps a long title from running under it. */
body[data-frb-editor] .frb-apphead { padding-right: 62px; }
body[data-frb-editor] .frb-apphead > .frb-head-toggle {
    position: absolute;
    top: var(--frb-space-4, 16px);
    right: var(--frb-space-4, 16px);
    margin-left: 0;
}
/* On a phone the editor head is a title, three lines of description and a tour
   button, and this control floats beside the first line rather than sitting in
   a row with others — so it is the loudest thing in the band unless it is
   handled.

   IT IS NOT MADE SMALLER, and I tried: a 26px override here did nothing,
   because `body:not([data-frb-optout~="tap"]) :is(button, …)` sets
   `min-width: 32px` under `(max-width: 620px), (pointer: coarse)`. That is the
   product's tap-target floor and it is right — a control you are meant to
   press with a thumb does not get to be 26px because it looks tidier. What was
   actually wrong was its WEIGHT: a border plus a 1px spread ring plus a glow
   reads as a warning box at that size. One ring fixed it; the box stays 32.

   It moves in to the tighter gutter, which is the part that was worth doing. */
@media (max-width: 640px) {
    body[data-frb-editor] .frb-apphead > .frb-head-toggle {
        top: var(--frb-space-3, 12px);
        right: var(--frb-space-3, 12px);
    }
}
body[data-frb-editor] .frb-apphead.frb-head-min {
    padding-top: var(--frb-space-3, 12px);
    padding-bottom: var(--frb-space-3, 12px);
}
body[data-frb-editor] .frb-apphead.frb-head-min > .frb-head-toggle { top: var(--frb-space-2, 8px); }

/* ═══ CLOSED, THERE IS NO BAND — ONLY THE CONTROL ═════════════════════════
   Folding the name away but keeping the band it sat in left a bar with
   nothing in it: a rule, a background, its padding, and a chevron floating at
   the end. Two empty bands, where the point of the exercise was to get the
   screen back.

   So closed, the head keeps no chrome at all. The trail bar folds with it
   (animated from script, beside the head's own group), the actions row and
   the tour button go with the band they belong to, and the only thing left on
   screen is the control that brings all of it back — pinned to the corner the
   band used to end at, so it does not move when it is pressed.

   Everything here is reversible in one click, which is the reason the actions
   are allowed to go: an action folded away is one press from being back, and a
   bar that is still there is not minimised. ══════════════════════════════ */
body.frb-head-min[data-frb-shell] .page-head,
body.frb-head-min[data-frb-editor] .frb-apphead {
    padding: 0;
    min-height: 0;
    border-bottom: 0;
    background: transparent;
    overflow: visible;
    position: relative;
}
body.frb-head-min[data-frb-shell] .page-head > .row-flex,
body.frb-head-min[data-frb-editor] .frb-apphead-tour { display: none; }

body.frb-head-min .frb-head-toggle,
body.frb-head-min[data-frb-editor] .frb-apphead > .frb-head-toggle {
    position: absolute;
    top: 6px;
    right: var(--frb-shell-gutter, 28px);
    margin: 0;
    z-index: 6;
    /* it sits over the app now, so it needs its own ground to be legible on
       whatever it lands on */
    background: var(--frb-cell, var(--bg-solid, rgba(8, 8, 10, .92)));
}

/* The trail bar's height is animated inline; these are the parts a height
   cannot carry — the rule under it and its own padding, which would otherwise
   leave a one-pixel line and a sliver of ground where the bar used to be. */
/* THE ATTRIBUTE IS LOAD-BEARING, and leaving it off is why the bar measured
   62px → 62px twice. `body.frb-head-min .frb-topbar` is (0,2,1); so is
   `body[data-frb-editor] .frb-topbar` in frb-editor.css, which sets a 62px
   floor and 12px of padding — and that sheet is linked AFTER frb-shell.css on
   the editor pages, so it took the tie. Adding the attribute here makes this
   (0,3,1), which wins on specificity and no longer depends on link order. */
body.frb-head-min :is(.frb-topbar, .topbar),
body.frb-head-min[data-frb-shell] :is(.frb-topbar, .topbar),
body.frb-head-min[data-frb-editor] :is(.frb-topbar, .topbar) {
    /* MIN-HEIGHT AND MARGIN, not just padding. The bar states its own floor and
       a margin under it, so an inline `height: 0` from the script took it
       from 62px to 62px — the height it was given could not go below the
       height it insisted on. Both have to be released here for the animated
       height to mean anything. */
    min-height: 0;
    margin-bottom: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-bottom-width: 0;
    opacity: 0;
    transition: opacity .2s ease,
                padding .32s var(--frb-ease, cubic-bezier(.4, 0, .2, 1)),
                margin .32s var(--frb-ease, cubic-bezier(.4, 0, .2, 1));
}
:is(.frb-topbar, .topbar) {
    transition: opacity .2s ease, padding .32s var(--frb-ease, cubic-bezier(.4, 0, .2, 1));
}

.frb-head-toggle {
    flex: 0 0 auto;
    align-self: flex-start;
    margin-left: var(--frb-space-3, 12px);
    width: 30px; height: 30px;
    display: grid; place-items: center;
    padding: 0;
    border: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
    border-radius: var(--frb-btn-radius, 0px);
    background: transparent;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    cursor: pointer;
    font-size: 11px; line-height: 1;
    transition: color .2s, border-color .2s, background .2s;
}
/* A RING, SO IT IS FINDABLE. Closed, this is the only control left on screen
   and it sits in the same gutter as whatever the app keeps down the right-hand
   edge — the editors keep an A4/zoom/export column there. A hairline square in
   the dark reads as one more of those buttons. The accent ring says it belongs
   to the page's chrome rather than to the app's toolbar, and it is the one
   thing on screen that is lit. */
/* ONE RING, NOT TWO. The first version had a border AND a 1px spread shadow
   under the glow, which at 32px on a phone stops reading as a control and
   starts reading as a warning box — reported against the editors, where the
   control is placed on its own beside the title rather than sitting in a row
   of other buttons. The border carries the accent; the glow is soft and has no
   hard edge of its own. */
.frb-head-toggle {
    border-color: color-mix(in srgb, var(--accent-primary, #d4af37) 60%, transparent);
    box-shadow: 0 0 16px var(--accent-glow, transparent);
    color: var(--accent-text, var(--accent-primary));
}
.frb-head-toggle:hover {
    color: var(--accent-text, var(--accent-primary));
    border-color: var(--accent-primary);
    box-shadow: 0 0 0 1px var(--accent-primary),
                0 0 22px var(--accent-glow, transparent);
}
/* Brighter once the band is gone, because that is when it has to be found. */
body.frb-head-min .frb-head-toggle {
    border-color: var(--accent-primary, #d4af37);
    box-shadow: 0 0 22px var(--accent-glow, transparent);
}
.frb-head-toggle:focus-visible {
    outline: 2px solid var(--accent-primary);
    outline-offset: 2px;
}
/* The chevron points the way the band will go: up to close it, down to bring
   it back. One glyph, rotated, so there is no second icon to keep in step. */
.frb-head-toggle > * {
    transition: transform .32s var(--frb-ease, cubic-bezier(.4, 0, .2, 1));
    pointer-events: none;
}
.frb-head-min .frb-head-toggle > * {
    transform: rotate(180deg);
}
/* The head's actions row keeps the left-hand slack; the control sits last so
   it is in the same place on every app, whether or not that app has actions. */
.page-head > .row-flex ~ .frb-head-toggle { margin-left: var(--frb-space-3, 12px); }

@media (prefers-reduced-motion: reduce) {
    .frb-head-fold,
    .frb-head-toggle > * { transition: none; }
}

body[data-frb-shell] .section { padding: var(--frb-space-5, 20px) var(--frb-shell-gutter, 28px) 0; }

/* ── A SECTION OF THE APP, NOT A SECOND APP ───────────────────────────────
   What is left when a page's own head is displaced by the app's. Twelve apps
   carried two of these bands and Media Optimizer three, each at 28px with a
   rule under it and 24px of clearance — so "Overview" and "Studio" and
   "Operations Overview" all announced themselves the way the app does.

   A section heading is the same information one step down: the name at the
   size the sections already use, the sentence in the same grey, NO rule,
   because the rule is what says "everything below this belongs to the app"
   and it is only true once per screen. */
body[data-frb-shell] .section-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--frb-space-3, 12px);
    padding: var(--frb-space-5, 20px) var(--frb-shell-gutter, 28px) var(--frb-space-2, 8px);
    border: 0;
    margin: 0;
}
/* THE GUTTER IS THE COLUMN'S, AND IT IS SPENT ONCE. The rule above gives a
   section band the column's inset, which is right when the band IS a child of
   the column. Quick Notes puts its views inside a `.view-scroll` that has
   already taken that inset as a column child of its own, so its "Write" band
   paid it twice and started at 126 while every card under it started at 98 —
   a heading indented 28px past its own content. Charged only to a band the
   column itself holds; one nested deeper is already inside the gutter. */
/* A CANVAS DOES NOT DECIDE HOW WIDE THE PAGE IS. A <canvas> carries an
   intrinsic size from its width attribute, and an intrinsic size is a
   min-content contribution: inside a `1fr` track — which is `minmax(auto, 1fr)`
   — the track cannot go under it, so the grid grows to fit the drawing instead
   of the drawing fitting the grid. Habit Streak's heatmap is the case: its two
   columns resolved to 331px and 1133px in a 1370px column and the right-hand
   card ran 142px off the screen, which is the "cutting out from the right
   side" that was reported.

   Capped, so the contribution collapses and the track goes back to its share.
   Chart libraries redraw to the box they are given; a canvas that is genuinely
   meant to be scrolled sideways still can be, inside a parent that scrolls —
   measured across all 63, none is. */
body[data-frb-shell] .main canvas { max-width: 100%; }

body[data-frb-shell] .section-head {
    padding-left: 0;
    padding-right: 0;
    /* AND NOTHING ABOVE IT EITHER. The band's own 20px sits on top of whatever
       the wrapper holding it already spends, and on Quick Notes that made the
       run from the head's rule down to the word "Write" 70px deep while the
       text beside it was inset 28. A band nested inside a padded box is
       already where it should be, vertically as well as sideways. */
    padding-top: 0;
}
body[data-frb-shell] .main > .section-head,
body[data-frb-shell] .main > .view > .section-head {
    padding-left: var(--frb-shell-gutter, 28px);
    padding-right: var(--frb-shell-gutter, 28px);
    padding-top: var(--frb-space-5, 20px);
}
body[data-frb-shell] .section-head > :first-child { flex: 1 1 auto; min-width: 0; }
body[data-frb-shell] .section-title {
    display: flex; align-items: center; flex-wrap: wrap;
    gap: var(--frb-space-2, 8px);
    margin: 0;
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: var(--frb-sec-title-size, 18px);
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: 0;
    color: var(--text-primary, CanvasText);
}
body[data-frb-shell] .section-sub {
    margin: var(--frb-space-1, 4px) 0 0;
    font-size: var(--frb-sec-sub-size, 13px);
    line-height: 1.5;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    max-width: 78ch;
}
/* It sits on the content it introduces, so the band under it drops the top
   padding it would otherwise add — the same pairing the app head has. */
body[data-frb-shell] .section-head + .section { padding-top: 0; }
body[data-frb-shell] .section-head + * { margin-top: 0; }

/* ── THE BAR SAYS WHEN ────────────────────────────────────────────────────
   The trail on the left, the date on the right, on every app. Mono because
   the seconds change every tick and proportional digits make the whole line
   twitch; `tabular-nums` for the same reason. It is the quietest thing in the
   bar on purpose — it answers a question nobody asked, so it must not compete
   with the one thing there that is navigation. */
body[data-frb-shell] .frb-bar-date {
    margin-left: auto;
    flex: 0 0 auto;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
    font-variant-numeric: tabular-nums;
    font-size: var(--frb-bar-date-size, 12px);
    letter-spacing: .01em;
    white-space: nowrap;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    opacity: .85;
}
@media (max-width: 620px) {
    /* A phone bar is a scrolling strip of trail; the clock would double its
       width for something the phone's own status bar already shows.

       AND THE HUBS ARE NOT SHELLED. `body[data-frb-shell]` alone reached the
       67 apps and none of the section hubs, which carry the same injected band
       without the attribute — so on those the clock stayed, and measured at
       390 its right edge was x=431. Forty-one pixels past the screen: not
       "the date is in the wrong place", just gone. The band is the thing that
       is on a phone, not the body it happens to hang from, so the band is what
       the rule names. */
    body[data-frb-shell] .frb-bar-date,
    :is(.frb-topbar, .topbar, .top-bar) .frb-bar-date { display: none; }
}

/* ── A SCREEN'S CONTROLS SIT ON THE HEAD ──────────────────────────────────
   Twenty-four apps kept theirs in the bar, where they read as navigation and
   on Ops Command collided with the trail outright. The head is a flex row
   with the name on the left and this on the right — which is what its
   `justify-content: space-between` was always for. */
body[data-frb-shell] .page-head > .frb-head-actions {
    margin-left: auto;
    /* `0 1 auto`, NOT `0 0 auto`. The head is a flex row, so a group that
       refuses to shrink is sized at its max-content width — and a group at
       max-content never reaches its own `flex-wrap`, because it is never
       narrow enough to need it. Media Kit One's five controls therefore came
       out as one 768px row on a 390px phone and the page clipped 406px of
       itself. Allowed to shrink, the wrap does what it says. */
    flex: 0 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: var(--frb-space-2, 8px);
}
@media (max-width: 620px) {
    /* On a phone the controls get the row rather than the end of one: there is
       no room beside a title for anything, and a group pushed right with
       `margin-left: auto` reads as five buttons falling off the screen. */
    body[data-frb-shell] .page-head > .frb-head-actions {
        margin-left: 0;
        flex-basis: 100%;
        justify-content: flex-start;
        /* !important, and Bootstrap is why. The group above already asks for
           8px, but eleven of these rows carry `gap-3` from the page's own
           markup and Bootstrap's utilities are all `!important` — so the gap
           stayed 16 and no amount of specificity was going to change it.
           Community Guidelines has 334px of row and 306px of buttons: at 16px
           gaps that is 338 and Print drops to a second line, at 8px it is 322
           and the three sit together, which is what was asked for. */
        gap: var(--frb-space-2, 8px) !important;
    }
    /* AND THE GAP IS THE ONLY SPACING. Community Guidelines' script sets an
       inline `margin-left: 10px` on Export and on Print — written when the
       row had no gap of its own — so each seam was really 18px and the three
       came to 343 in a 334px row. A group that owns a `gap` cannot also let
       its children bring margins; the two are added, and only one of them is
       visible to whoever set the other. Inline styles need `!important` to
       reach. Phone only: at this width the group is a plain left-aligned row,
       so nothing here is using a margin to position itself. */
    body[data-frb-shell] .page-head > .frb-head-actions > * {
        margin-left: 0 !important;
        margin-right: 0 !important;
    }
    /* A GROUP INSIDE THE GROUP WRAPS TOO. Meta Tagger keeps its four actions
       in a bare div, and a div in here is a set of controls, not a control —
       so it may break onto a second line rather than crush what it holds.
       Deliberately not the buttons themselves: a button that wraps puts its
       label under its icon. */
    body[data-frb-shell] .page-head > .frb-head-actions > div {
        flex-wrap: wrap;
    }
}
/* Four icon-only buttons in a row is a 200px strip beside a title; two by two
   is a block the same height as the two lines of text next to it. */
body[data-frb-shell] .page-head > .frb-actions-grid {
    display: grid;
    grid-template-columns: repeat(2, auto);
    justify-content: end;
}

/* ── NOTHING BELOW THE HEAD OUTRANKS IT ───────────────────────────────────
   The scale a content heading takes once frb-app-shell.js has measured it at
   or above the app's own title. Only those are marked, so a page that draws
   its section titles at 14px keeps them: this caps, it does not standardise.
   Written with the tag as well as the class so it is (0,2,2) and beats the
   page rule that set 32px in the first place.

   The sizes are written out rather than taken from --frb-text-*: those are the
   BODY scale, where "2xl" is 18px, and a heading scale borrowed from it puts
   an h3 at 14px against a 28px title. This is its own four steps under 28. */
body[data-frb-shell] h1.frb-capped { font-size: var(--frb-h1, 22px); line-height: 1.25; }
body[data-frb-shell] h2.frb-capped { font-size: var(--frb-h2, 20px); line-height: 1.3; }
body[data-frb-shell] h3.frb-capped { font-size: var(--frb-h3, 18px); line-height: 1.35; }
body[data-frb-shell] h4.frb-capped { font-size: var(--frb-h4, 16px); line-height: 1.4; }
@media (max-width: 620px) {
    /* One step down again on a phone. A capped h2 is 20px, which is fine
       across 1314px and is two wrapped lines across 200 once a button takes
       the rest of the row — "Communication Sequence" on Event Command Centre,
       against "RSVP Auto-Pilot" at 18 right above it. The section heading is
       the size to match, because that is what these are. */
    body[data-frb-shell] h1.frb-capped { font-size: var(--frb-h1-sm, 19px); }
    body[data-frb-shell] h2.frb-capped { font-size: var(--frb-h2-sm, 18px); }
    body[data-frb-shell] h3.frb-capped { font-size: var(--frb-h3-sm, 16px); }
    body[data-frb-shell] h4.frb-capped { font-size: var(--frb-h4-sm, 15px); }
}

/* ── THE VIEW IS A PASS-THROUGH BOX ───────────────────────────────────────
   A screen is not a thing with an inset of its own; the column holds the
   inset and the content inside the screen carries it, which is what
   `> .main > .view > *` says. Five apps also pad the view itself — Ops
   Command, Events and Research Command by 40px, Pipeline Command and Quick
   Notes by 28 — so the inset landed twice and their tiles started 68 or 56px
   in while the head above them started at 28. Reported as the tiles having
   too much side padding, and as 40px of dead space above the first heading.

   Stated here rather than deleted from five files, for the same reason
   `.main`'s own padding is: the shape has to hold for the ones that come
   next, not just for the five that were measured. */
body[data-frb-shell] .main > .view { padding: 0; }

/* ── AN EDITOR IS THE PAGE ────────────────────────────────────────────────
   A grid of tiles wants the column's gutter around it. A split-pane editor
   does not: its panes are the thing you came for, and 28px of ground on each
   side of them is 56px the preview does not get. Marked by
   frb-app-shell.js from the name the page itself gave the layout —
   `editor-layout`, `studio-grid`, `generator-layout` — because nothing
   measurable separates an editor from a two-column dashboard.

   Written against the column and the view so it outranks the gutter it is
   replacing, exactly as `.frb-inset` has to. */
body[data-frb-shell] .main > .frb-bleed,
body[data-frb-shell] .main > .view > .frb-bleed {
    padding-left: 0;
    padding-right: 0;
}
/* And it starts AT the rule. The gap under the head is the gap at the sides,
   which is right when the sides have one; an editor that runs to both edges
   has none, so a band of ground under the divider is the only inset left on
   the screen and reads as something failing to load. */
body[data-frb-shell] .page-head + .frb-bleed,
body[data-frb-shell] .section-head + .frb-bleed { margin-top: calc(-1 * var(--frb-shell-gutter, 28px)); }

/* ── A CONTROL IS A CONTROL ───────────────────────────────────────────────
   The size a button gets back once frb-app-shell.js has measured it well
   outside the system's own scale. `.frb-btn-lg`, not the default: these are
   the primary action on their screen and should still read as one — 16px in a
   46px box rather than 18px in 104. `!important` because the declarations
   being replaced are page rules of the same specificity, and because a slab
   that only shrinks on some pages would be worse than one that never did. */
body[data-frb-shell] .frb-oversize {
    font-size: var(--frb-btn-lg-size, 16px) !important;
    min-height: 46px !important;
    height: auto !important;
    padding: 10px var(--frb-space-6, 24px) !important;
    line-height: 1.2 !important;
}

/* ── A ROW THAT WRAPPED ON A PHONE FILLS IT ───────────────────────────────
   Marked only at 620px and under, and only when the row's own children left
   most of the width empty. Both properties are written because the row can be
   either display: a `grid-template-columns` is inert on a flex box and a
   `flex` on the children is inert in a grid. */
@media (max-width: 620px) {
    /* ── A TITLE AND ITS BUTTON STACK ON A PHONE ──────────────────────────
       Even at 18px, "Communication Sequence" is 200px of type and the button
       beside it is 166 — in a 334px column one of them has to wrap, and the
       one that wraps is the words. The row is a title and an action, so below
       620 it becomes two rows and both get their full width.

       Both `:has()` are required, which is what keeps this narrow: a heading
       AND a control as DIRECT children is the "section title with its button"
       shape and nothing else. */
    body[data-frb-shell] > .main :is(div, header, section):has(> :is(h1, h2, h3, h4)):has(> :is(button, a, .btn)) {
        flex-wrap: wrap;
        /* AND A GAP BETWEEN THE TWO ROWS. These rows were built to hold one
           line, so most of them set `gap` for the horizontal axis only or set
           none at all — wrapped, the button lands against the descenders of
           the title above it. `row-gap` and not `gap`, so whatever spacing the
           row already had across is left alone. */
        row-gap: var(--frb-space-3, 12px);
    }
    body[data-frb-shell] > .main :is(div, header, section):has(> :is(h1, h2, h3, h4)):has(> :is(button, a, .btn)) > :is(h1, h2, h3, h4) {
        flex: 1 1 100%;
        min-width: 0;
    }

    body[data-frb-shell] .frb-fill-phone {
        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    }
    body[data-frb-shell] .frb-fill-phone > * {
        flex: 1 1 140px;
        min-width: 0;
        width: auto;
    }

    /* A ROW OF WORDS WITH NO ROOM FOR WORDS. Marked at runtime by measuring
       the damage — see unpackPhoneRows in frb-app-shell.js.

       40%, not 50%, and deliberately: a basis of exactly half plus a border
       or a stray pixel of padding is 50.5% and drops to one per line, which
       is the failure this rule exists to prevent. Two of anything at 40% fit
       and three never do, whatever the box model, and `grow` then closes the
       line. */
    body[data-frb-shell] .frb-tab-wrap {
        flex-wrap: wrap !important;
    }
    body[data-frb-shell] .frb-tab-wrap > * {
        flex: 1 1 40% !important;
        min-width: 0;
        box-sizing: border-box;
    }

    /* And the other shape: the row itself had no room to give, so the row
       ABOVE it wraps and the branch holding the text is given a floor. The
       controls that were squeezing it take the next line instead. */
    body[data-frb-shell] .frb-row-wrap {
        flex-wrap: wrap !important;
    }
    body[data-frb-shell] .frb-row-wrap > .frb-row-keep {
        flex: 1 1 60% !important;
        min-width: 60%;
    }
}

/* ── CLEARANCE UNDER A DRAWN BOX ──────────────────────────────────────────
   A box that ends where the next one begins reads as one broken box rather
   than two. Marked by frb-app-shell.js on whatever the outermost thing is
   whose whole content is one drawn box — sometimes the box, sometimes the
   bare div wrapping it, which is what took the gutter. */
body[data-frb-shell] .frb-gap-below { margin-bottom: var(--frb-space-5, 20px); }

/* ── A MACHINE READOUT IS NOT THE APP'S SENTENCE ──────────────────────────
   Set dressing in the business_incubation family: a `>`, an ALL_CAPS token,
   a colon. It keeps its place under the title and stops claiming to be the
   description — mono, quiet, and out of the way of the sentence that says
   what the app actually does. */
body[data-frb-shell] .page-note {
    margin: var(--frb-space-2, 8px) 0 0;
    font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: var(--frb-note-size, 12px);
    line-height: 1.5;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    opacity: .8;
}

/* ── A DRAWN BOX STOPS AT THE GUTTER ──────────────────────────────────────
   The column's inset is padding on its own children, which is right for a
   child that draws nothing — the text lands 28px in and the ground behind it
   runs edge to edge, which is what the bar and the head want. A child that
   draws a CARD gets the opposite: its border hard against both edges with the
   text inset inside it, so the app looks wider than the shell it is in.

   frb-app-shell.js marks those by asking the computed style whether the box
   paints anything — the only place that question has a true answer, since
   `.section` is a band on Accounting Core and a card on Chroma, and half the
   painting comes from frb-components.css rather than the page. Here the mark
   is spent: the gutter becomes margin, and the box keeps a card's own padding
   rather than the column's.

   Written against the column and the view because it has to OUTRANK the inset
   it is replacing. `> .main > *:not(.topbar, .view)` is (0,3,1); a plain
   `body[data-frb-shell] .frb-inset` is (0,2,1) and would lose in silence,
   which is the same specificity trap that cost this system the whole of light
   mode. */
body[data-frb-shell] .main > .frb-inset,
body[data-frb-shell] .main > .view > .frb-inset {
    margin-left: var(--frb-shell-gutter, 28px);
    margin-right: var(--frb-shell-gutter, 28px);
    padding-left: var(--frb-space-5, 20px);
    padding-right: var(--frb-space-5, 20px);
    /* A BOX HAS THE SAME ROOM ON ALL FOUR SIDES. The `.section` rule above ends
       in `0` because a band has nothing to clear below it; a box does, and a
       box with 40px above its first line and none under its last reads as
       broken rather than as tight. Pipeline Command's "No deals yet" banner is
       the clearest case — one line of text sitting on the bottom border. */
    padding-top: var(--frb-space-5, 20px);
    padding-bottom: var(--frb-space-5, 20px);
}
@media (max-width: 620px) {
    /* The phone gutter is smaller, and a card inset by the desktop 28 on a
       390px screen loses a seventh of its width to margins. */
    body[data-frb-shell] .main > .frb-inset,
    body[data-frb-shell] .main > .view > .frb-inset {
        margin-left: var(--frb-space-3, 12px);
        margin-right: var(--frb-space-3, 12px);
    }
}
body[data-frb-shell] .row-flex {
    display: flex;
    align-items: center;
    gap: var(--frb-space-2, 8px);
    flex-wrap: wrap;
}

/* ── THE SHELL ON A PHONE ─────────────────────────────────────────────────
   The rail stops being a column and becomes a strip across the top, and the
   bar under it scrolls sideways rather than stacking. Both are already true
   for every app through frb-compat.css; what the shell has to undo here is its
   own desktop layout — the fixed rail, the column inset, and the two sticky
   offsets, which would otherwise pin the strip and the bar on top of each
   other. */
/* AN INSET BOX IS NOT ALSO FULL WIDTH. `.frb-inset` holds a box off the
   column's edges with a margin; a page that also writes `width: 100%` on that
   box gets the two added together and the box runs its own margin past the
   screen. Font Awesome Library's icon count is the case — `width: 100%` under
   its own mobile breakpoint, so on a 390px screen the tile measured 390 wide
   starting at x12 and ran 12px off the right edge. The margin decides the
   width here; the box fills what is left. */
body[data-frb-shell] .frb-inset { width: auto; max-width: 100%; }

@media (max-width: 620px) {
    /* A ROW OF BOXES IS INSET LIKE A BOX, NOT LIKE A HEAD.
       `.frb-inset` above already narrows to 12 on a phone, for the reason
       written there — 28 on a 390px screen costs a card a seventh of its
       width. But that class is only ever put on a column child that PAINTS,
       and SKU Forge and Media Kit One do not paint: each is a bare grid
       holding the panels that do. So the wrapper took the column's ordinary
       28 and passed it to panels that should have had 12.

       `frb-boxrow` is that wrapper, marked at runtime by asking whether the
       thing holds a drawn box — see insetDrawn in frb-app-shell.js. The
       gutter is NOT narrowed on the token, deliberately: the bar, the app's
       name and description, and the section bands are text and keep the
       inset every app gives them. Only what holds boxes moves in. */
    body[data-frb-shell] .main > .frb-boxrow,
    body[data-frb-shell] .main > .view > .frb-boxrow {
        padding-left: var(--frb-space-3, 12px);
        padding-right: var(--frb-space-3, 12px);
    }

    body[data-frb-shell] { padding-left: 0; }
    body[data-frb-shell] > :is(.sidebar, .sidebar-primary, .ranchie-sidebar, .rail, .nav-rail, .rail-nav) {
        position: sticky;
        top: var(--frb-header-h, 47px);
        left: auto;
        bottom: auto;
        width: 100%;
        height: auto;
        z-index: 41;
        flex-direction: row;
        flex-wrap: nowrap;
        align-items: center;
        overflow-x: auto;
        overflow-y: hidden;
        /* The inset itself is set in frb-compat.css, which is the rule that
           wins here; repeating it in two files is how one of them becomes
           wrong without anybody noticing. */
        padding: var(--frb-space-3, 12px) var(--frb-space-3, 12px);
        gap: var(--frb-space-2, 8px);
        border-right: 0;
        border-bottom: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
    }
    body[data-frb-shell] > :is(.sidebar, .sidebar-primary, .ranchie-sidebar, .rail, .nav-rail, .rail-nav) > * { flex: 0 0 auto; }
    body[data-frb-shell] > :is(.sidebar, .sidebar-primary, .ranchie-sidebar, .rail, .nav-rail, .rail-nav) > .theme-footer {
        position: static;
        flex-direction: row;
        margin-top: 0;
        margin-left: auto;
        padding-top: 0;
    }
    /* The bar is no longer under the header alone — it is under the header AND
       the strip. Neither height is a constant, so the bar stops being sticky
       rather than being pinned to a number that is wrong on half the apps.

       IT SCROLLS SIDEWAYS, LIKE THE STRIP ABOVE IT, and this block has to say
       so itself. The global rule further up sets `flex-wrap: nowrap` at
       (0,1,1); the shell's own desktop rule sets `flex-wrap: wrap` at (0,2,2)
       and is not in a media query, so it wins at every width — and the bar
       stacked the trail and the controls into two rows instead of scrolling.
       Specificity does not care which rule is more specific about the SCREEN. */
    body[data-frb-shell] .main > .topbar {
        position: static;
        flex-wrap: nowrap;
        overflow-x: auto;
        overflow-y: hidden;
        overscroll-behavior-x: contain;
        scrollbar-width: none;
        /* THE GUTTER, NOT A SPACING STEP. 12px here against the 14px the head
           and every card below it read from `--frb-shell-gutter` put the trail
           two pixels left of the title it belongs to — one edge on a phone,
           drawn twice. Same token, one edge. */
        padding-left: var(--frb-shell-gutter, 14px);
        padding-right: var(--frb-shell-gutter, 14px);
    }
    body[data-frb-shell] .main > .topbar::-webkit-scrollbar { display: none; }
    body[data-frb-shell] .main > .topbar > * { flex: 0 0 auto; }
    body[data-frb-shell] :is(.breadcrumbs, .frb-appcrumb, .crumbs) { flex-wrap: nowrap; white-space: nowrap; }
    body[data-frb-shell] .page-head,
    body[data-frb-shell] .section {
        padding-left: var(--frb-space-3, 12px);
        padding-right: var(--frb-space-3, 12px);
    }

    /* ── THE VERTICAL RHYTHM IS NOT THE DESKTOP'S ──────────────────────────
       Measured on the rebuilt Accounting Core at 390px: 24px of head padding,
       then the compat layer's 24px margin under the head, then 20px of section
       padding — 68px of nothing between the line under the title and the first
       card, on a screen 390px wide. It reads as a gap where something failed
       to load.

       Halved, not removed. The band still has to separate from the content;
       it just does not need a desktop's worth of room to do it. */
    body[data-frb-shell] .page-head {
        padding-top: var(--frb-space-4, 16px);
        /* Not tightened as far as the top inset: this is the gap between the
           description and the rule under it, and a line of type sitting on a
           border reads as an error rather than as a band. */
        padding-bottom: var(--frb-space-4, 16px);
    }
    /* The gap under the rule matches the one above it, at this width as at
       every other: 16px of description-to-rule, 16px of rule-to-content. */
    body[data-frb-shell] .section { padding-top: var(--frb-space-4, 16px); }
    body[data-frb-shell] .section + .section { padding-top: var(--frb-space-4, 16px); }

    /* A 28px title on a 390px screen is 14 characters a line. The scale steps
       down once — far enough to fit a name, not so far that the head stops
       being the biggest thing on the page. */
    body[data-frb-shell] .page-title { font-size: 24px; }
    body[data-frb-shell] .frb-app-glyph { font-size: 20px; }
}
