/*! frb-compat.css — the Feerebu design system, layer 3 of 3.
 *
 * WHAT THIS IS FOR
 * The site has 131 distinct button class names. Migrating every one of them to
 * .frb-btn in the same change would be thousands of markup edits landing at
 * once, unreviewable and unrevertable. This file makes the names that already
 * exist render as the design system, so the whole product looks unified
 * immediately and the markup migrates afterwards at a survivable pace.
 *
 * IT IS MEANT TO SHRINK. Every rule is a debt with the wave that opened it
 * written next to it. When a wave migrates a page's markup, its entries come
 * out. A compat file that only grows means the migration stopped happening.
 *
 * OPT-OUT
 * Some pages are deliberately not the product: the entry screen's scenography,
 * the baking animation on coming_soon, the three games' own visual language.
 * They set `data-frb-optout="btn"` on <body> and every rule here is skipped for
 * them. This is why the selectors are written the way they are — the :not()
 * has to sit on an ancestor, so it is on <body>.
 */

/* ═══ `hidden` HAS TO MEAN HIDDEN ══════════════════════════════════════════
   The browser's own `[hidden] { display: none }` lives in the USER AGENT
   origin, which loses to every author declaration regardless of specificity.
   So the moment this file says `.nav-item { display: flex }` — and it does,
   for all 75 rails — `<a class="nav-item" hidden>` renders anyway.

   That silently undid the whole "hide, never remove" rule. Three out-of-app
   entries on Chroma's rail were marked `hidden` by the reconstruction and
   still showed Home / Portals / Profile, because the rail rule was speaking
   over them; the same was true of every `hidden` the transform sets on a
   folded-away nav, a deduped button or a dead toggle. Removing that markup
   instead is not the answer — it is what broke Media Optimizer three times,
   because the page's own script addresses those nodes by id.

   `!important` is the correct tool here and not a shortcut: the attribute is
   the platform's way of saying "this is not part of the page right now", and
   nothing styling a `.nav-item` has an opinion that should outrank it. */
[hidden] { display: none !important; }

/* ═══ AN ICON IS NOT SPARE WIDTH ═══════════════════════════════════════════
   Meta Tagger's four head actions, measured at 1440 and again at 390:

       1440   Presets w105   icon w16      390   Presets w88   icon w0
              Import  w100   icon w16            Import  w85   icon w1
              Copy as w107   icon w16            Copy as w90   icon w0

   The buttons are flex rows, so the icon is a flex ITEM, and a flex item
   shrinks by default. Squeeze the row and the `<i>` is the part that gives:
   its box goes to zero — but the glyph is drawn by a webfont on ::before and
   keeps its 16px whatever the box says, so it paints straight over the first
   letter of the label. Reported as the buttons and icons overlapping, and
   that is exactly what it is: an icon standing in a box with no width.

   A label can be squeezed and still be read. A 16px glyph cannot be squeezed
   at all — it is 16px or it is on top of something. So it does not shrink,
   here and everywhere, and what has to give is the row, which wraps. */
:is(button, .btn, .btn-action, .action-btn, [class*="btn"], [class*="button"])
    > :is(i, svg, .fa, [class*="fa-"]) {
    flex: none;
}

/* ═══ WAVE 1 — BUTTONS ═════════════════════════════════════════════════════
   Every selector below is scoped under body:not([data-frb-optout~="btn"]) so a
   page can decline. `~=` matches one word in a space-separated list, so
   data-frb-optout="btn card" opts out of both.

   These carry ONE extra class of specificity over a bare .btn in a page's own
   <style>, and this file loads after it, so they win — which is the point. */

body:not([data-frb-optout~="btn"]) .btn,
body:not([data-frb-optout~="btn"]) .btn-action,
body:not([data-frb-optout~="btn"]) .action-btn,
body:not([data-frb-optout~="btn"]) .cta-btn,
body:not([data-frb-optout~="btn"]) .tool-btn,
body:not([data-frb-optout~="btn"]) .custom-btn,
body:not([data-frb-optout~="btn"]) .toolbar-btn,
body:not([data-frb-optout~="btn"]) .category-btn,
body:not([data-frb-optout~="btn"]) .mini-btn,
body:not([data-frb-optout~="btn"]) .btn-mini,
body:not([data-frb-optout~="btn"]) .frb-btn-legacy,
body:not([data-frb-optout~="btn"]) .task-action-btn,
body:not([data-frb-optout~="btn"]) .element-control-btn,
body:not([data-frb-optout~="btn"]) .timer-btn,
body:not([data-frb-optout~="btn"]) .btn-export,
body:not([data-frb-optout~="btn"]) .download-btn,
body:not([data-frb-optout~="btn"]) .back-btn,
body:not([data-frb-optout~="btn"]) .btn-back,
/* Bootstrap-shaped names that are used WITHOUT .btn far more often than with
   it — .btn-outline appears 662 times standalone and only 3 times alongside
   .btn, .btn-secondary 128 of 164, .btn-primary 42 of 111. Mapping only their
   colours left their SHAPE to whatever each page happened to say, which is the
   fragmentation this wave exists to end. They are listed here so the geometry
   lands on them too. */
body:not([data-frb-optout~="btn"]) .btn-primary,
body:not([data-frb-optout~="btn"]) .btn-secondary,
body:not([data-frb-optout~="btn"]) .btn-outline,
body:not([data-frb-optout~="btn"]) .btn-outline-light,
body:not([data-frb-optout~="btn"]) .btn-outline-secondary,
body:not([data-frb-optout~="btn"]) .btn-danger,
body:not([data-frb-optout~="btn"]) .btn-success,
body:not([data-frb-optout~="btn"]) .btn-login,
body:not([data-frb-optout~="btn"]) .btn-register,
body:not([data-frb-optout~="btn"]) .btn-reset,
body:not([data-frb-optout~="btn"]) .btn-update,
body:not([data-frb-optout~="btn"]) .start-btn {
    display: inline-flex; align-items: center; justify-content: center;
    gap: var(--frb-space-2); box-sizing: border-box;
    /* THE SECOND COPY OF THE BUTTON RECIPE. This sheet carries two — this one
       for the .btn/.btn-primary family and another further down for the
       .timer-btn-* family — and they have to say the same thing or a legacy
       .btn-primary and a modern .frb-btn-primary are different buttons, which
       is precisely what check_wave asserts they are not. Converting only the
       other one left this block on the old 14px/600/8px and the gate caught
       it. Both now read the same --frb-btn-* tokens. */
    font-family: var(--frb-btn-font, var(--frb-font));
    font-size: var(--frb-btn-fs, var(--frb-text-lg));
    font-weight: var(--frb-btn-weight, var(--frb-weight-semibold));
    line-height: 1;
    letter-spacing: var(--frb-btn-tracking, .2px);
    text-transform: var(--frb-btn-transform, none);
    padding: 0 var(--frb-space-4); min-height: var(--frb-btn-min-h, 38px);
    border-radius: var(--frb-btn-radius, var(--frb-radius-md)); border: 1px solid transparent;
    text-decoration: none; white-space: nowrap; cursor: pointer;
    -webkit-appearance: none; appearance: none;
    transition: background var(--frb-dur-base) var(--frb-ease),
                border-color var(--frb-dur-base) var(--frb-ease),
                color var(--frb-dur-base) var(--frb-ease),
                box-shadow var(--frb-dur-base) var(--frb-ease),
                transform var(--frb-dur-fast) var(--frb-ease);
}

/* The keyboard ring, granted to every legacy button at once. Almost no page
   defined one; this is the single biggest accessibility gain in the wave. */
body:not([data-frb-optout~="btn"]) .btn:focus-visible,
body:not([data-frb-optout~="btn"]) .btn-action:focus-visible,
body:not([data-frb-optout~="btn"]) .action-btn:focus-visible,
body:not([data-frb-optout~="btn"]) .cta-btn:focus-visible,
body:not([data-frb-optout~="btn"]) .tool-btn:focus-visible,
body:not([data-frb-optout~="btn"]) .custom-btn:focus-visible,
body:not([data-frb-optout~="btn"]) .toolbar-btn:focus-visible,
body:not([data-frb-optout~="btn"]) .category-btn:focus-visible,
body:not([data-frb-optout~="btn"]) .icon-btn:focus-visible,
body:not([data-frb-optout~="btn"]) .btn-icon:focus-visible,
body:not([data-frb-optout~="btn"]) .mini-btn:focus-visible,
body:not([data-frb-optout~="btn"]) button:focus-visible {
    outline-width: var(--frb-focus-width, 2px);
    outline-style: solid;
    outline-color: var(--accent-primary, #d4af37);
    outline-offset: var(--frb-focus-offset, 2px);
}

/* ── THE BASELINE, for every button there is ──────────────────────────────
   The wave above works by class name, and rendering the product says that
   leaves a quarter of its buttons drawing whatever their own page happens to
   say: 497 of 1,983, on 134 pages. 56 of them carry no class at all, which a
   class-based rule cannot reach however long the list gets.

   The obvious repair — keep adding names — is the wrong one, and the evidence
   is in this file already. 335 of those 497 are on a SINGLE page, so there is
   no disagreement to settle; and almost all of those are members of a toolbar,
   a tab strip or a palette, where the geometry belongs to the group and the
   selected state IS the group's state. Mapping .view-btn cost portals its
   layout switcher. Mapping .rail-btn made one app's sidebar the odd one out.
   Doing that forty more times would be forty more of the same.

   So this rule takes the OTHER half of Wave 2's approach: element-level, like
   `input[type="text"]`, but carrying only what is true of a button whatever
   kind of button it is. Nothing here decides what a control IS. A tab stays a
   tab, a palette swatch stays a swatch, and both get the product's font, its
   pointer, its focus ring and its disabled state.

   Deliberately NOT here: background, color, border-color, padding, radius,
   font-size, font-weight. Those are intent, and intent is what the name-mapped
   list above is for. A blanket rule that set them would be the .view-btn
   mistake applied to all 1,983 at once.

   font-family is the one that reads as cosmetic and is not: a <button> does not
   inherit the page's font in any browser. It takes the system UI font unless
   somebody says otherwise, which is why an unstyled button has always looked
   like it came from a different product. */
body:not([data-frb-optout~="btn"]) button,
body:not([data-frb-optout~="btn"]) input[type="button"],
body:not([data-frb-optout~="btn"]) input[type="submit"],
body:not([data-frb-optout~="btn"]) input[type="reset"],
body:not([data-frb-optout~="btn"]) [role="button"] {
    box-sizing: border-box;
    /* `inherit` was right when the product's button was simply the page's font
       and the job was stopping the browser's own. Now the button has a face of
       its own, and this rule matches every <button> element — so on any page
       where the primary button is also a <button> tag it landed after the
       class rule and quietly put the page font back. That is why the style
       guide's own .btn-primary stayed Outfit while its size, weight and radius
       all moved. Same token, same fallback: a page with no token layer still
       inherits, exactly as before. */
    font-family: var(--frb-btn-font, inherit);
    cursor: pointer;
    transition: background-color var(--frb-dur-base, .22s) var(--frb-ease, ease),
                border-color var(--frb-dur-base, .22s) var(--frb-ease, ease),
                color var(--frb-dur-base, .22s) var(--frb-ease, ease),
                box-shadow var(--frb-dur-base, .22s) var(--frb-ease, ease);
}
/* A control that cannot be used should say so, and say it the same way
   everywhere. `:disabled` does not match a div with role=button, so the ARIA
   spelling is listed beside it. */
body:not([data-frb-optout~="btn"]) button:disabled,
body:not([data-frb-optout~="btn"]) input[type="button"]:disabled,
body:not([data-frb-optout~="btn"]) input[type="submit"]:disabled,
body:not([data-frb-optout~="btn"]) [role="button"][aria-disabled="true"] {
    opacity: var(--frb-disabled-opacity, .5);
    cursor: not-allowed;
}
body:not([data-frb-optout~="btn"]) input[type="button"]:focus-visible,
body:not([data-frb-optout~="btn"]) input[type="submit"]:focus-visible,
body:not([data-frb-optout~="btn"]) input[type="reset"]:focus-visible,
body:not([data-frb-optout~="btn"]) [role="button"]:focus-visible {
    outline-width: var(--frb-focus-width, 2px);
    outline-style: solid;
    outline-color: var(--accent-primary, #d4af37);
    outline-offset: var(--frb-focus-offset, 2px);
}

/* ── intents, mapped ─────────────────────────────────────────────────────
   Bootstrap's .btn-primary and the site's .btn-action / .cta-btn all mean
   "the main action here", so they all become the primary intent. */
body:not([data-frb-optout~="btn"]) .btn-primary,
body:not([data-frb-optout~="btn"]) .btn-action,
body:not([data-frb-optout~="btn"]) .cta-btn,
body:not([data-frb-optout~="btn"]) .btn-login,
body:not([data-frb-optout~="btn"]) .btn-register,
body:not([data-frb-optout~="btn"]) .btn-reset,
body:not([data-frb-optout~="btn"]) .btn-update,
body:not([data-frb-optout~="btn"]) .start-btn {
    background: var(--frb-btn-tint, var(--accent-fill, var(--accent-primary)));
    color: var(--btn-text);
    border-color: transparent;
    box-shadow: 0 0 18px var(--accent-glow);
}
body:not([data-frb-optout~="btn"]) .btn-primary:hover:not(:disabled),
body:not([data-frb-optout~="btn"]) .btn-action:hover:not(:disabled),
body:not([data-frb-optout~="btn"]) .cta-btn:hover:not(:disabled) {
    filter: brightness(1.08); box-shadow: 0 0 28px var(--accent-glow);
}

body:not([data-frb-optout~="btn"]) .btn-secondary,
body:not([data-frb-optout~="btn"]) .tool-btn,
body:not([data-frb-optout~="btn"]) .custom-btn,
body:not([data-frb-optout~="btn"]) .toolbar-btn,
body:not([data-frb-optout~="btn"]) .btn-export,
body:not([data-frb-optout~="btn"]) .download-btn {
    background: var(--inset-bg);
    border-color: var(--inset-border);
    color: var(--text-primary);
}
body:not([data-frb-optout~="btn"]) .btn-secondary:hover:not(:disabled),
body:not([data-frb-optout~="btn"]) .tool-btn:hover:not(:disabled),
body:not([data-frb-optout~="btn"]) .toolbar-btn:hover:not(:disabled) {
    background: var(--frb-surface-hover); border-color: var(--accent-primary);
}

body:not([data-frb-optout~="btn"]) .btn-outline,
body:not([data-frb-optout~="btn"]) .btn-outline-secondary,
body:not([data-frb-optout~="btn"]) .btn-outline-light,
body:not([data-frb-optout~="btn"]) .btn-back,
body:not([data-frb-optout~="btn"]) .back-btn {
    background: transparent;
    border-color: var(--frb-btn-tint, var(--accent-fill, var(--accent-primary)));
    color: var(--frb-btn-tint, var(--accent-text, var(--accent-primary)));
}
body:not([data-frb-optout~="btn"]) .btn-outline:hover:not(:disabled),
body:not([data-frb-optout~="btn"]) .btn-outline-secondary:hover:not(:disabled),
body:not([data-frb-optout~="btn"]) .btn-outline-light:hover:not(:disabled) {
    background: var(--accent-glow); color: var(--text-primary);
}

/* `color: #fff` was here, on a fill the token file tuned as TYPE for a dark
   ground: 2.77:1 on the danger red and 1.92:1 on the success green. The ink is
   a token now and flips with the mode — see --danger-ink in frb-tokens.css. */
body:not([data-frb-optout~="btn"]) .btn-danger,
body:not([data-frb-optout~="btn"]) .btn-delete {
    background: var(--danger); border-color: var(--danger);
    color: var(--danger-ink, #fff);
}
body:not([data-frb-optout~="btn"]) .btn-success {
    background: var(--success); border-color: var(--success);
    color: var(--success-ink, #fff);
}

/* ── sizes ─────────────────────────────────────────────────────────────────
   A SIZE IS NOT A SHAPE. These kept --frb-radius-sm and -lg after the product's
   button became square, so every small and large button in the product stayed
   rounded while the default next to it did not — a 6px .btn-sm beside a 0px
   .btn, on the same toolbar. Found by measuring a hub's "Back to Portals",
   which is a .btn.btn-sm: it came back 6px when everything else came back 0.
   The radius now comes from the same --frb-btn-radius the default reads; the
   size variants set size. */
body:not([data-frb-optout~="btn"]) .btn-sm,
body:not([data-frb-optout~="btn"]) .mini-btn,
body:not([data-frb-optout~="btn"]) .btn-mini {
    min-height: var(--frb-btn-sm-h, 32px); padding: 0 var(--frb-space-3);
    font-size: var(--frb-text-md);
    border-radius: var(--frb-btn-radius, var(--frb-radius-sm));
}
body:not([data-frb-optout~="btn"]) .btn-lg {
    min-height: var(--frb-btn-lg-h, 46px); padding: 0 var(--frb-space-6);
    font-size: var(--frb-text-xl);
    border-radius: var(--frb-btn-radius, var(--frb-radius-lg));
}

/* Seven names meant "small square icon button". */
body:not([data-frb-optout~="btn"]) .btn-icon,
body:not([data-frb-optout~="btn"]) .icon-btn,
body:not([data-frb-optout~="btn"]) .btn-action-icon,
body:not([data-frb-optout~="btn"]) .hs-icon-btn,
body:not([data-frb-optout~="btn"]) .da-icon-btn {
    display: inline-flex; align-items: center; justify-content: center;
    padding: 0; width: 34px; min-height: 34px;
    /* The line above says SQUARE and this used to say --frb-radius-sm, which
       is 6px. The button token is the one place that decides button shape and
       it reads 0 — so these were the only icon buttons in the product with
       corners on them, beside the square ones they sit next to in the same
       toolbars. Kept as a fallback rather than a literal 0 so a page that
       means its own shape still sets --frb-btn-radius and keeps it. */
    border-radius: var(--frb-btn-radius, var(--frb-radius-sm));
    border: 1px solid var(--inset-border);
    background: var(--inset-bg); color: var(--text-secondary);
    cursor: pointer; transition: all var(--frb-dur-base) var(--frb-ease);
}
body:not([data-frb-optout~="btn"]) .btn-icon:hover:not(:disabled),
body:not([data-frb-optout~="btn"]) .icon-btn:hover:not(:disabled) {
    color: var(--accent-text, var(--accent-primary)); border-color: var(--accent-primary);
}

/* ── disabled, everywhere, however it was expressed ────────────────────── */
body:not([data-frb-optout~="btn"]) .btn:disabled,
body:not([data-frb-optout~="btn"]) .btn-action:disabled,
body:not([data-frb-optout~="btn"]) .cta-btn:disabled,
body:not([data-frb-optout~="btn"]) button:disabled,
body:not([data-frb-optout~="btn"]) .btn[aria-disabled="true"],
body:not([data-frb-optout~="btn"]) .btn.disabled {
    opacity: .45; cursor: not-allowed; transform: none;
    box-shadow: none; pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    body:not([data-frb-optout~="btn"]) .btn,
    body:not([data-frb-optout~="btn"]) .btn-action,
    body:not([data-frb-optout~="btn"]) .cta-btn { transition: none; }
}


/* ═══ WAVE 1 (completed) — THE REST OF THE BUTTONS ═════════════════════════
   The first pass mapped 36 names. A count against the markup found 160 in use,
   so a quarter of the product's buttons — 119 names, 1,049 uses — were still
   drawing whatever their own page said. That is not a rounding error: .btn-b
   had SIX distinct visual definitions across seven pages, .btn-a six, .btn-c
   five, .export-btn three in three rules. The same class name meaning a
   different button depending on which page you were on is the exact drift this
   wave exists to end, and it was still true for most button names.

   Intent was derived from what each name currently PAINTS, not from what it is
   called — a name is a guess about meaning, a background is evidence of it.
   Hand-corrected only where the name carries meaning the paint cannot show
   (.btn-outline-danger paints nothing, and is danger).

   NOT mapped, on purpose:
     · frb-btn-* — already the design system's own names, in correct use
     · btn-block, btn-xl, btn-small, btn-group-sm, cta-section, cta-buttons —
       size, width and layout modifiers. Button geometry on a container turns a
       section into a button.
     · btn-github (#24292e), btn-slack (#4a154b) — a brand button is recognised
       BY its brand colour; the accent would be the wrong answer.
     · btn-prestige, btn-game, btn-danger-sim, theme-btn — game currency, a
       simulation affordance and a theme swatch. Not product buttons.
     · restore-editor-btn — an overlay control on a dark scrim the page paints
       and does not lighten in light mode. The classifier read its background
       and could not read what is BEHIND it: mapped as secondary it took
       --text-primary, near-black, onto black at 1.18:1 on three pages. A
       component that stands on a ground somebody else owns cannot take half its
       colour pair from a token — the same lesson as the account chip and the
       plan badge — and the intent that fits a scrim control is not one of the
       six. It keeps its own styling.
     · rail-btn — a NAV RAIL ICON, not a content button. The classifier read a
       transparent background and an accent active state and called it outline,
       which is what a rail item looks like and is not what one is. Mapped, it
       took the content-button scale: 10px corners and a 13px icon where every
       other app's rail runs 12px and 16px, plus an accent border around each
       icon. Sixty-four pages draw the same rail through .nav-item, which was
       never mapped — so mapping this one name made ONE app the odd one out.
       The tell is structural, not visual: it lives inside the nav that switches
       views, so its shape belongs to the rail's scale, not the button scale.
     · view-btn — the five icon toggles that pick the layout on portals.html,
       inside a .view-toggles segmented pill. One page uses the name, so there
       was NO drift to converge and nothing to gain; what it cost was the
       control. The wave's geometry blew 1px 6px of padding out to 10px 16px
       and shrank the icons from 16px to 13px, and the outline intent, at one
       class more specificity than `.view-btn.active`, painted over the selected
       item's background — so the segmented control stopped showing which view
       you were in. A segment inside a segmented control is not a button: its
       geometry belongs to the group, and its selected state IS the group's
       state. Same shape of error as rail-btn, and the same rule catches both.
*/
/* The geometry, once, for every name below. */
body:not([data-frb-optout~="btn"]) .ac-btn,
body:not([data-frb-optout~="btn"]) .add-card-btn,
body:not([data-frb-optout~="btn"]) .add-color-stop-btn,
body:not([data-frb-optout~="btn"]) .add-course-btn,
body:not([data-frb-optout~="btn"]) .add-deal-btn,
body:not([data-frb-optout~="btn"]) .add-task-btn,
body:not([data-frb-optout~="btn"]) .ai-btn,
body:not([data-frb-optout~="btn"]) .back-button,
body:not([data-frb-optout~="btn"]) .bb-cap-btn,
body:not([data-frb-optout~="btn"]) .blocked-btn,
body:not([data-frb-optout~="btn"]) .btn-a,
body:not([data-frb-optout~="btn"]) .btn-app,
body:not([data-frb-optout~="btn"]) .btn-b,
body:not([data-frb-optout~="btn"]) .btn-c,
body:not([data-frb-optout~="btn"]) .btn-close,
body:not([data-frb-optout~="btn"]) .btn-close-white,
body:not([data-frb-optout~="btn"]) .btn-d,
body:not([data-frb-optout~="btn"]) .btn-dark,
body:not([data-frb-optout~="btn"]) .btn-enter,
body:not([data-frb-optout~="btn"]) .btn-ghost,
body:not([data-frb-optout~="btn"]) .btn-ghost-frb,
body:not([data-frb-optout~="btn"]) .btn-home,
body:not([data-frb-optout~="btn"]) .btn-light,
body:not([data-frb-optout~="btn"]) .btn-ops,
body:not([data-frb-optout~="btn"]) .btn-org,
body:not([data-frb-optout~="btn"]) .btn-outline-danger,
body:not([data-frb-optout~="btn"]) .btn-outline-info,
body:not([data-frb-optout~="btn"]) .btn-outline-primary,
body:not([data-frb-optout~="btn"]) .btn-outline-success,
body:not([data-frb-optout~="btn"]) .btn-primary-frb,
body:not([data-frb-optout~="btn"]) .btn-primary-glow,
body:not([data-frb-optout~="btn"]) .btn-redirect,
body:not([data-frb-optout~="btn"]) .btn-sm-ghost,
body:not([data-frb-optout~="btn"]) .btn-tour,
body:not([data-frb-optout~="btn"]) .btn-tour-skip,
body:not([data-frb-optout~="btn"]) .btn-warning,
body:not([data-frb-optout~="btn"]) .calculate-btn,
body:not([data-frb-optout~="btn"]) .canvas-btn,
body:not([data-frb-optout~="btn"]) .card-action-btn,
body:not([data-frb-optout~="btn"]) .chip-btn,
body:not([data-frb-optout~="btn"]) .chore-btn,
body:not([data-frb-optout~="btn"]) .clear-agreement-btn,
body:not([data-frb-optout~="btn"]) .color-stop-btn,
body:not([data-frb-optout~="btn"]) .compass-btn,
body:not([data-frb-optout~="btn"]) .copy-btn,
body:not([data-frb-optout~="btn"]) .course-action-btn,
body:not([data-frb-optout~="btn"]) .ctrl-btn,
body:not([data-frb-optout~="btn"]) .equipment-btn,
body:not([data-frb-optout~="btn"]) .export-btn,
body:not([data-frb-optout~="btn"]) .fav-btn-card,
body:not([data-frb-optout~="btn"]) .fd-mini-btn,
body:not([data-frb-optout~="btn"]) .fda-link-btn,
body:not([data-frb-optout~="btn"]) .frequency-btn,
body:not([data-frb-optout~="btn"]) .generate-btn,
body:not([data-frb-optout~="btn"]) .glass-btn,
body:not([data-frb-optout~="btn"]) .gradient-type-btn,
body:not([data-frb-optout~="btn"]) .history-btn,
body:not([data-frb-optout~="btn"]) .hs-seg-btn,
body:not([data-frb-optout~="btn"]) .inc-mini-btn,
body:not([data-frb-optout~="btn"]) .item-btn,
body:not([data-frb-optout~="btn"]) .lib-btn,
body:not([data-frb-optout~="btn"]) .mark-done-btn,
body:not([data-frb-optout~="btn"]) .mic-btn,
body:not([data-frb-optout~="btn"]) .mock-btn,
body:not([data-frb-optout~="btn"]) .mode-toggle-btn,
body:not([data-frb-optout~="btn"]) .pagination-btn,
body:not([data-frb-optout~="btn"]) .paint-btn,
body:not([data-frb-optout~="btn"]) .pattern-btn,
body:not([data-frb-optout~="btn"]) .preset-btn,
body:not([data-frb-optout~="btn"]) .qty-btn,
body:not([data-frb-optout~="btn"]) .quick-action-btn,
body:not([data-frb-optout~="btn"]) .regenerate-btn,
body:not([data-frb-optout~="btn"]) .release-btn,
body:not([data-frb-optout~="btn"]) .report-btn,
body:not([data-frb-optout~="btn"]) .row-btn,
body:not([data-frb-optout~="btn"]) .scaler-btn,
body:not([data-frb-optout~="btn"]) .semester-btn,
body:not([data-frb-optout~="btn"]) .send-preview-btn,
body:not([data-frb-optout~="btn"]) .sidebar-close-btn,
body:not([data-frb-optout~="btn"]) .snip-btn,
body:not([data-frb-optout~="btn"]) .stats-btn,
body:not([data-frb-optout~="btn"]) .ta-mini-btn,
body:not([data-frb-optout~="btn"]) .tab-btn,
body:not([data-frb-optout~="btn"]) .target-btn,
body:not([data-frb-optout~="btn"]) .timer-btn-pause,
body:not([data-frb-optout~="btn"]) .timer-btn-primary,
body:not([data-frb-optout~="btn"]) .timer-btn-reset,
body:not([data-frb-optout~="btn"]) .timer-btn-secondary,
body:not([data-frb-optout~="btn"]) .timer-btn-start,
body:not([data-frb-optout~="btn"]) .urge-btn,
body:not([data-frb-optout~="btn"]) .video-play-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--frb-btn-gap, 8px);
  /* The same --frb-btn-* tokens .frb-btn reads. These names were the older
     half of the split this layer exists to close: `inherit` and 13px here
     against Outfit and 14px there meant a legacy .btn-outline and a modern
     .frb-btn-primary were never the same button even when both were "right".
     One set of tokens, one button. */
  font-family: var(--frb-btn-font, inherit);
  font-size: var(--frb-btn-fs, 13px);
  font-weight: var(--frb-btn-weight, 600);
  letter-spacing: var(--frb-btn-tracking, normal);
  text-transform: var(--frb-btn-transform, none);
  line-height: 1;
  padding: var(--frb-btn-py, 10px) var(--frb-btn-px, 16px);
  min-height: var(--frb-btn-min-h, auto);
  border: 1px solid transparent; border-radius: var(--frb-btn-radius, 10px);
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease,
              box-shadow .25s cubic-bezier(.16,1,.3,1),
              transform .25s cubic-bezier(.16,1,.3,1);
}
/* primary — 21 name(s) */
body:not([data-frb-optout~="btn"]) .add-card-btn,
body:not([data-frb-optout~="btn"]) .add-color-stop-btn,
body:not([data-frb-optout~="btn"]) .add-course-btn,
body:not([data-frb-optout~="btn"]) .add-deal-btn,
body:not([data-frb-optout~="btn"]) .add-task-btn,
body:not([data-frb-optout~="btn"]) .ai-btn,
body:not([data-frb-optout~="btn"]) .btn-a,
body:not([data-frb-optout~="btn"]) .btn-enter,
body:not([data-frb-optout~="btn"]) .btn-primary-frb,
body:not([data-frb-optout~="btn"]) .btn-primary-glow,
body:not([data-frb-optout~="btn"]) .btn-redirect,
body:not([data-frb-optout~="btn"]) .btn-tour,
body:not([data-frb-optout~="btn"]) .calculate-btn,
body:not([data-frb-optout~="btn"]) .generate-btn,
body:not([data-frb-optout~="btn"]) .mock-btn,
body:not([data-frb-optout~="btn"]) .regenerate-btn,
body:not([data-frb-optout~="btn"]) .send-preview-btn,
body:not([data-frb-optout~="btn"]) .target-btn,
body:not([data-frb-optout~="btn"]) .timer-btn-primary,
body:not([data-frb-optout~="btn"]) .timer-btn-start,
body:not([data-frb-optout~="btn"]) .video-play-btn {
  background: var(--accent-fill, var(--accent-primary, #d4af37));
  color: var(--accent-ink, var(--btn-text, #000));
  border-color: transparent;
}
/* secondary — 41 name(s) */
body:not([data-frb-optout~="btn"]) .ac-btn,
body:not([data-frb-optout~="btn"]) .blocked-btn,
body:not([data-frb-optout~="btn"]) .btn-dark,
body:not([data-frb-optout~="btn"]) .btn-ghost-frb,
body:not([data-frb-optout~="btn"]) .btn-light,
body:not([data-frb-optout~="btn"]) .btn-ops,
body:not([data-frb-optout~="btn"]) .btn-warning,
body:not([data-frb-optout~="btn"]) .canvas-btn,
body:not([data-frb-optout~="btn"]) .card-action-btn,
body:not([data-frb-optout~="btn"]) .chip-btn,
body:not([data-frb-optout~="btn"]) .color-stop-btn,
body:not([data-frb-optout~="btn"]) .copy-btn,
body:not([data-frb-optout~="btn"]) .ctrl-btn,
body:not([data-frb-optout~="btn"]) .export-btn,
body:not([data-frb-optout~="btn"]) .fav-btn-card,
body:not([data-frb-optout~="btn"]) .fda-link-btn,
body:not([data-frb-optout~="btn"]) .frequency-btn,
body:not([data-frb-optout~="btn"]) .glass-btn,
body:not([data-frb-optout~="btn"]) .gradient-type-btn,
body:not([data-frb-optout~="btn"]) .history-btn,
body:not([data-frb-optout~="btn"]) .hs-seg-btn,
body:not([data-frb-optout~="btn"]) .item-btn,
body:not([data-frb-optout~="btn"]) .lib-btn,
body:not([data-frb-optout~="btn"]) .mark-done-btn,
body:not([data-frb-optout~="btn"]) .mode-toggle-btn,
body:not([data-frb-optout~="btn"]) .pagination-btn,
body:not([data-frb-optout~="btn"]) .paint-btn,
body:not([data-frb-optout~="btn"]) .pattern-btn,
body:not([data-frb-optout~="btn"]) .preset-btn,
body:not([data-frb-optout~="btn"]) .qty-btn,
body:not([data-frb-optout~="btn"]) .quick-action-btn,
body:not([data-frb-optout~="btn"]) .release-btn,
body:not([data-frb-optout~="btn"]) .report-btn,
body:not([data-frb-optout~="btn"]) .scaler-btn,
body:not([data-frb-optout~="btn"]) .semester-btn,
body:not([data-frb-optout~="btn"]) .snip-btn,
body:not([data-frb-optout~="btn"]) .stats-btn,
body:not([data-frb-optout~="btn"]) .timer-btn-pause,
body:not([data-frb-optout~="btn"]) .timer-btn-reset,
body:not([data-frb-optout~="btn"]) .timer-btn-secondary {
  background: var(--inset-bg, rgba(127,127,127,.08));
  color: var(--text-primary, inherit);
  border-color: var(--inset-border, rgba(127,127,127,.18));
}
/* outline — 19 name(s) */
body:not([data-frb-optout~="btn"]) .back-button,
body:not([data-frb-optout~="btn"]) .bb-cap-btn,
body:not([data-frb-optout~="btn"]) .btn-app,
body:not([data-frb-optout~="btn"]) .btn-b,
body:not([data-frb-optout~="btn"]) .btn-c,
body:not([data-frb-optout~="btn"]) .btn-home,
body:not([data-frb-optout~="btn"]) .btn-outline-info,
body:not([data-frb-optout~="btn"]) .btn-outline-primary,
body:not([data-frb-optout~="btn"]) .btn-outline-success,
body:not([data-frb-optout~="btn"]) .btn-tour-skip,
body:not([data-frb-optout~="btn"]) .chore-btn,
body:not([data-frb-optout~="btn"]) .course-action-btn,
body:not([data-frb-optout~="btn"]) .fd-mini-btn,
body:not([data-frb-optout~="btn"]) .inc-mini-btn,
body:not([data-frb-optout~="btn"]) .row-btn,
body:not([data-frb-optout~="btn"]) .ta-mini-btn,
body:not([data-frb-optout~="btn"]) .tab-btn {
  background: transparent;
  color: var(--accent-text, var(--accent-primary, #d4af37));
  border-color: var(--accent-primary, #d4af37);
}
/* ghost — 5 name(s) */
body:not([data-frb-optout~="btn"]) .btn-ghost,
body:not([data-frb-optout~="btn"]) .btn-org,
body:not([data-frb-optout~="btn"]) .btn-sm-ghost,
body:not([data-frb-optout~="btn"]) .equipment-btn,
body:not([data-frb-optout~="btn"]) .urge-btn {
  background: transparent;
  color: var(--text-secondary, inherit);
  border-color: transparent;
}
/* danger — 3 name(s) */
body:not([data-frb-optout~="btn"]) .btn-d,
body:not([data-frb-optout~="btn"]) .btn-outline-danger,
body:not([data-frb-optout~="btn"]) .clear-agreement-btn {
  background: transparent;
  color: var(--danger, #dc2626);
  border-color: var(--danger, #dc2626);
}
/* icon — 5 name(s) */
body:not([data-frb-optout~="btn"]) .btn-close,
body:not([data-frb-optout~="btn"]) .btn-close-white,
body:not([data-frb-optout~="btn"]) .compass-btn,
body:not([data-frb-optout~="btn"]) .mic-btn,
body:not([data-frb-optout~="btn"]) .sidebar-close-btn {
  background: transparent;
  color: var(--text-secondary, inherit);
  border-color: transparent;
  padding: 0;
  width: var(--frb-btn-icon-size, 34px);
  height: var(--frb-btn-icon-size, 34px);
}

/* ═══ WAVE 2 — FORM CONTROLS ═══════════════════════════════════════════════
   The button wave could work by class name because buttons carry one. Form
   controls mostly do not: 1,063 of the 1,625 in this tree have NO class and are
   reached only by bare `input {}` rules — 192 of them across 58 pages, each
   page having decided its own padding, radius and border. So this wave styles
   the ELEMENTS, and the same load-order trick that lets a class rule win lets an
   element rule win: `input` here and `input` in a page's <style> are both
   (0,0,1), and this file is linked last.

   Deliberately NOT width. A page's inputs sit in layouts this file cannot see —
   inline filters, grids, toolbars — and forcing width:100% at element level
   would rearrange 58 pages to fix a colour. The .frb-input class sets width for
   markup that opts in; the element rules set only how a control LOOKS.

   Type-by-type, because `input` is not one control. A checkbox given a text
   field's padding becomes a rectangle; a range given a border grows a box around
   the track. Only the text-like types take the field treatment; checkbox, radio
   and range take accent-color and nothing else; file, color, submit and button
   are left alone entirely.

   Pages decline with data-frb-optout="input" on <body>.

   SHAPE. This rule owns border-radius, and it has to: 1,063 of the fields here
   carry no class at all and would otherwise sit at the browser default. But
   owning it flattened 614 fields that their pages HAD shaped, and measuring the
   614 shows two different things wearing one number. 608 were drift — 6, 9, 10,
   12, 14 and 16px all landing on 8 — which is exactly the disagreement this
   wave exists to settle, and nobody can see the difference. Six were pills: 20,
   30 and 50px search fields on five pages, where a rectangle is not a smaller
   radius but a different control. --frb-field-radius is the escape hatch for
   those six, matching --frb-btn-radius, which is how a page keeps its own
   button shape. Custom properties inherit, so a page sets it on the wrapper and
   the field inside picks it up without touching anything else. */

body:not([data-frb-optout~="input"]) input[type="text"],
body:not([data-frb-optout~="input"]) input[type="email"],
body:not([data-frb-optout~="input"]) input[type="password"],
body:not([data-frb-optout~="input"]) input[type="search"],
body:not([data-frb-optout~="input"]) input[type="url"],
body:not([data-frb-optout~="input"]) input[type="tel"],
body:not([data-frb-optout~="input"]) input[type="number"],
body:not([data-frb-optout~="input"]) input[type="date"],
body:not([data-frb-optout~="input"]) input[type="time"],
body:not([data-frb-optout~="input"]) input[type="datetime-local"],
body:not([data-frb-optout~="input"]) input[type="month"],
body:not([data-frb-optout~="input"]) input[type="week"],
body:not([data-frb-optout~="input"]) input:not([type]),
body:not([data-frb-optout~="input"]) textarea,
body:not([data-frb-optout~="input"]) select {
    font-family: inherit;
    color: var(--text-primary);
    /* background-COLOR, not the shorthand. `background: var(--input-bg)` sets
       every background property, and the one it was quietly resetting is
       `background-image` — which on a <select> is where the drop-down arrow
       lives. Bootstrap draws `.form-select`'s chevron there; several pages
       draw their own; this rule wiped all of them. Measured across the 131:
       63 selects on 17 pages rendered with `appearance: none` and no image at
       all, which is a drop-down that looks exactly like a text box, on
       Gradient Lab (10 of 10), Ranchie onboarding (8 of 8), Secure QR (4 of
       4), Canvas Pro, Codefolio Sync, Incorporation and the five editors.
       A shorthand takes properties you did not name with it. */
    background-color: var(--input-bg);
    border: 1px solid var(--input-border);
    border-radius: var(--frb-field-radius, var(--frb-radius-md, 10px));
    transition: border-color .15s ease, box-shadow .15s ease;
    /* INHERIT, not a value. color-scheme is inherited, but a declaration ON the
       element beats inheritance whatever the specificity — and eleven pages
       write `color-scheme: dark` straight onto `input, select, textarea`, which
       pinned their date pickers and open <select> lists to dark on a light page.
       Handing the property back to the body is what lets one token in the theme
       file decide it for all of them. */
    color-scheme: inherit;
}

/* ── AND EVERY DROP-DOWN LOOKS LIKE ONE ───────────────────────────────────
 * Restoring the shorthand above gives back the arrows the pages already drew,
 * and leaves the ones that never had any — the pages that set `appearance:
 * none` themselves and supplied nothing in its place. Rather than leave the
 * product with two kinds of drop-down, every select is drawn the same way:
 * native furniture off, one chevron, same size and inset everywhere.
 *
 * ONE ARROW, NOT TWO, whichever the page does. `appearance: none` here removes
 * the engine's own, so a page that draws its own image simply replaces this
 * one — the count is one either way, which is what a page-by-page rule could
 * not promise.
 *
 * The chevron is a mid grey rather than a token, because a data URI cannot
 * read a custom property and cannot use currentColor. #8a8a8a is the one value
 * that clears 3:1 against both --input-bg grounds, dark and light.
 *
 * NOT list boxes: `multiple` and `size > 1` render as a scrolling list with no
 * arrow position to speak of, and a chevron floating over the first row is
 * furniture describing a control that is not there. */
/* ONLY THE ONES THAT HAVE NONE, and the marking is done in script because the
 * question cannot be asked in CSS — there is no selector for "this element's
 * computed appearance is none and it has no background image". frb-app-shell.js
 * asks it of each select at boot and sets `.frb-select-bare` on the ones where
 * the answer is yes.
 *
 * The first version of this rule skipped that and simply styled every select.
 * It put the chevron on all 386 — and 32px of right padding with it, onto
 * controls the pages had sized to their contents: Menu Genie Lite's rounding
 * mode is a 40px box holding "%", so the padding left one pixel for the text.
 * Measured afterwards, seven pages had a select whose own selected value no
 * longer fitted. A control that already works does not need this rule, and the
 * padding is exactly the part that cannot be applied speculatively.
 *
 * The inset is tighter than a native arrow's for the same reason: these are
 * the narrow ones, or they would have been given an arrow already. */
body:not([data-frb-optout~="input"]) select.frb-select-bare {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.6 6 6.4 11 1.6' fill='none' stroke='%238a8a8a' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 7px center;
    background-size: 10px 6px;
    padding-right: 23px;
    /* And it grows rather than clipping. A select's intrinsic width is its
       widest option; `fit-content` makes that a floor which now includes the
       room the arrow needs, so a box the page sized by hand gets wider instead
       of printing its value underneath the chevron. */
    min-width: fit-content;
}

/* The three states that were defined once each across 143 pages. */
body:not([data-frb-optout~="input"]) input::placeholder,
body:not([data-frb-optout~="input"]) textarea::placeholder {
    color: var(--text-secondary);
    opacity: 1;
}
body:not([data-frb-optout~="input"]) input:focus-visible,
body:not([data-frb-optout~="input"]) textarea:focus-visible,
body:not([data-frb-optout~="input"]) select:focus-visible {
    outline: 2px solid var(--accent-text, var(--accent-primary));
    outline-offset: 1px;
}
body:not([data-frb-optout~="input"]) input:disabled,
body:not([data-frb-optout~="input"]) textarea:disabled,
body:not([data-frb-optout~="input"]) select:disabled {
    opacity: .55;
    cursor: not-allowed;
}

/* accent-color and nothing else: these controls are drawn by the engine, and
   the engine gets both modes right once it knows the hue. */
body:not([data-frb-optout~="input"]) input[type="checkbox"],
body:not([data-frb-optout~="input"]) input[type="radio"],
body:not([data-frb-optout~="input"]) input[type="range"] {
    accent-color: var(--accent-fill, var(--accent-primary));
}

/* ── the class names that DO exist ────────────────────────────────────────
   .search-input is NOT among them, and the reason is structural rather than
   stylistic. Seven pages use it and all seven put an icon inside the field,
   positioned absolutely against the wrapper, with the field's own left padding
   as the only thing keeping the text clear of it — portals.html writes
   `padding: 10px 15px 10px 40px` against a `.search-icon` at `left: 15px`.
   This rule's `padding: 10px 12px` is not a different look, it is the icon
   landing on top of the placeholder, on every page that uses the name. Padding
   that is load-bearing for a sibling this file cannot see is the same lesson as
   the scrim button and the account chip: a component cannot take half of a
   relationship from a token. The element rules below still give these fields
   their colour, border and focus ring. */
body:not([data-frb-optout~="input"]) .form-control,
body:not([data-frb-optout~="input"]) .form-control-sm,
body:not([data-frb-optout~="input"]) .form-control-custom,
body:not([data-frb-optout~="input"]) .form-input,
body:not([data-frb-optout~="input"]) .form-input-small,
body:not([data-frb-optout~="input"]) .form-select,
body:not([data-frb-optout~="input"]) .glass-input,
body:not([data-frb-optout~="input"]) .tool-input,
body:not([data-frb-optout~="input"]) .property-input,
body:not([data-frb-optout~="input"]) .currency-input,
body:not([data-frb-optout~="input"]) .frb-in {
    box-sizing: border-box;
    font-family: inherit;
    font-size: 14px;
    line-height: 1.4;
    color: var(--text-primary);
    background: var(--input-bg);
    border: 1px solid var(--input-border);
    border-radius: var(--frb-field-radius, var(--frb-radius-md, 10px));
    padding: 10px 12px;
}
/* .form-control-sm and .form-input-small say small in their names and mean it. */
body:not([data-frb-optout~="input"]) .form-control-sm,
body:not([data-frb-optout~="input"]) .form-input-small {
    font-size: 13px; padding: 7px 10px;
}

/* Labels. .form-label is 227 uses over 25 pages at three different sizes; this
   is the one it should have been. `.fld`, `.f`, `.lbl` and `.chk` are left
   alone — a two-letter class is as likely to be a layout hook as a label, and
   guessing wrong here restyles somebody's grid. */
body:not([data-frb-optout~="input"]) .form-label,
body:not([data-frb-optout~="input"]) .field-label,
body:not([data-frb-optout~="input"]) .control-label,
body:not([data-frb-optout~="input"]) .property-label,
body:not([data-frb-optout~="input"]) .form-check-label {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: .02em;
    color: var(--text-secondary);
    line-height: 1.4;
}

/* ═══ WAVE 2 (completed) — THE FURNITURE AROUND THE CONTROL ════════════════
   The element rules reach every control. They do not reach what surrounds one:
   the wrapper that stacks a label over a field, the label itself, the hint under
   it, the error, the toggle, the search box. 877 uses of that furniture across
   36 names, and the same drift — .form-group means one gap on twenty-one pages
   and something else on each of them.

   Not mapped, and why:
     · .field, .slider — exact matches are rare and the names are ambiguous;
       `field` is as likely to be a data field as a form one, and every `slider`
       in this tree turned out to be `toggle-slider`. Guessing restyles the wrong
       thing, and a wrong guess here is a broken layout rather than a wrong hue.
     · .input-group — Bootstrap's JOINED control group. Its whole point is that
       the borders touch; a gap would un-join it, and it is layout rather than
       a field. Left alone deliberately.
     · .chip-toggle — reads like a toggle and is a <button> filter chip. That is
       Wave 4's family (badge / chip / pill), not a form control, and styling it
       as one here would mean unstyling it there.

   FOUR NAMES CAME BACK OFF THIS LIST after being looked at properly rather than
   grepped at. `.slider` was written off as a phantom because a search for it
   kept returning `toggle-slider` — but there are seven real ones, and every
   single one is the track span of a toggle: `label > input + span.slider`, the
   same shape as `.toggle-slider` under a different name. `.switch` was called
   three shapes across three pages; it is two, and :has() tells them apart. And
   `.field` and `.fld-row` are plain column wrappers — one of them literally
   declares `flex-direction: column`.
*/

/* ── the wrapper that stacks a label over a field ───────────────────────── */
body:not([data-frb-optout~="input"]) .form-group,
body:not([data-frb-optout~="input"]) .control-group,
body:not([data-frb-optout~="input"]) .fldwrap,
body:not([data-frb-optout~="input"]) .wb-fld,
body:not([data-frb-optout~="input"]) .cs-fld,
body:not([data-frb-optout~="input"]) .fa-fld,
body:not([data-frb-optout~="input"]) .fv-fld,
body:not([data-frb-optout~="input"]) .lo-fld,
body:not([data-frb-optout~="input"]) .field,
body:not([data-frb-optout~="input"]) .fld-row {
    display: flex;
    flex-direction: column;
    gap: var(--frb-space-2, 8px);
    min-width: 0;
}

/* ── labels, hints and errors are type, and type is the cheap half ─────── */
body:not([data-frb-optout~="input"]) .micro-label,
body:not([data-frb-optout~="input"]) .input-label,
body:not([data-frb-optout~="input"]) .toggle-label {
    font-size: 12px; font-weight: 600; letter-spacing: .02em;
    color: var(--text-secondary); line-height: 1.4;
}
body:not([data-frb-optout~="input"]) .hint,
body:not([data-frb-optout~="input"]) .form-hint {
    font-size: 12px; color: var(--text-secondary); line-height: 1.4;
}
body:not([data-frb-optout~="input"]) .field-error,
body:not([data-frb-optout~="input"]) .form-error,
body:not([data-frb-optout~="input"]) .is-error,
body:not([data-frb-optout~="input"]) .error-state {
    font-size: 12px; color: var(--danger, #dc2626); line-height: 1.4; font-weight: 500;
}

/* ── the label > input + span toggle, which is the shape this tree uses ── */
/* Different markup from .frb-switch — there the checkbox IS the track, here a
   sibling <span> is, and the input is hidden. Both are legitimate; a compat
   layer meets the markup that exists rather than asking 6 pages to change. */
body:not([data-frb-optout~="input"]) .toggle-switch {
    position: relative; display: inline-flex; align-items: center;
    width: 38px; height: 22px; flex: none; cursor: pointer;
}
body:not([data-frb-optout~="input"]) .toggle-switch input[type="checkbox"] {
    position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer;
}
body:not([data-frb-optout~="input"]) .toggle-slider {
    position: absolute; inset: 0; border-radius: var(--frb-radius-pill, 999px);
    background: var(--inset-bg); border: 1px solid var(--input-border);
    transition: background-color .15s ease, border-color .15s ease;
}
body:not([data-frb-optout~="input"]) .toggle-slider::before {
    content: ""; position: absolute; top: 50%; left: 3px; transform: translateY(-50%);
    width: 14px; height: 14px; border-radius: 50%;
    background: var(--text-secondary); transition: left .15s ease, background-color .15s ease;
}
body:not([data-frb-optout~="input"]) .toggle-switch input:checked + .toggle-slider {
    background: var(--accent-fill, var(--accent-primary));
    border-color: var(--accent-fill, var(--accent-primary));
}
body:not([data-frb-optout~="input"]) .toggle-switch input:checked + .toggle-slider::before {
    left: 19px; background: var(--btn-text, #000);
}
body:not([data-frb-optout~="input"]) .toggle-switch input:focus-visible + .toggle-slider {
    outline: 2px solid var(--accent-text, var(--accent-primary)); outline-offset: 2px;
}

/* ── range ─────────────────────────────────────────────────────────────── */
body:not([data-frb-optout~="input"]) .form-range,
body:not([data-frb-optout~="input"]) .volume-slider {
    accent-color: var(--accent-fill, var(--accent-primary));
    width: 100%; height: 20px; background: transparent; cursor: pointer;
}

/* ── search: an icon sitting inside the field it belongs to ────────────── */
body:not([data-frb-optout~="input"]) .searchbox,
body:not([data-frb-optout~="input"]) .searchbar,
body:not([data-frb-optout~="input"]) .search-container,
body:not([data-frb-optout~="input"]) .search-box,
body:not([data-frb-optout~="input"]) .search-wrap,
body:not([data-frb-optout~="input"]) .toolbar-search {
    position: relative; display: flex; align-items: center;
}
body:not([data-frb-optout~="input"]) .searchbox > i,
body:not([data-frb-optout~="input"]) .searchbar > i,
body:not([data-frb-optout~="input"]) .search-container > i,
body:not([data-frb-optout~="input"]) .search-box > i,
body:not([data-frb-optout~="input"]) .search-wrap > i,
body:not([data-frb-optout~="input"]) .toolbar-search > i {
    position: absolute; left: 11px; pointer-events: none;
    color: var(--text-secondary); font-size: 13px; line-height: 1;
}
body:not([data-frb-optout~="input"]) .searchbox > input,
body:not([data-frb-optout~="input"]) .searchbar > input,
body:not([data-frb-optout~="input"]) .search-container > input,
body:not([data-frb-optout~="input"]) .search-box > input,
body:not([data-frb-optout~="input"]) .search-wrap > input,
body:not([data-frb-optout~="input"]) .toolbar-search > input {
    padding-left: 34px; width: 100%;
}

/* ── a place to drop a file ────────────────────────────────────────────── */
body:not([data-frb-optout~="input"]) .upload-area,
body:not([data-frb-optout~="input"]) .drop-zone,
body:not([data-frb-optout~="input"]) .dropzone {
    border: 1px dashed var(--input-border);
    border-radius: var(--frb-radius-lg, 12px);
    background: var(--input-bg);
    color: var(--text-secondary);
    transition: border-color .15s ease, background-color .15s ease;
}
body:not([data-frb-optout~="input"]) .upload-area:hover,
body:not([data-frb-optout~="input"]) .drop-zone:hover,
body:not([data-frb-optout~="input"]) .dropzone:hover {
    border-color: var(--accent-text, var(--accent-primary));
}

/* ── the row wrappers, as rows ─────────────────────────────────────────── */
/* .form-row is a GRID in every page that defines it, so it keeps its columns
   and gains only the gap it was setting by hand. Changing its direction would be
   a rewrite; making its spacing agree with every other field is the wave's job. */
body:not([data-frb-optout~="input"]) .form-row {
    gap: var(--frb-space-3, 12px);
}

/* ── the toggle, under all three wrapper names this tree uses ──────────── */
/* One shape, three names — .toggle-switch, .switch and .setting-control — and
   two track names, .toggle-slider and .slider. The track rules are scoped to a
   wrapper child on purpose: `.slider` is a generic enough word that an unrelated
   element could carry it, and a compat layer should not reach past what it can
   see. */
body:not([data-frb-optout~="input"]) .switch:has(> .slider),
body:not([data-frb-optout~="input"]) .switch:has(> .toggle-slider),
body:not([data-frb-optout~="input"]) .setting-control:has(> .slider),
body:not([data-frb-optout~="input"]) .setting-control:has(> .toggle-slider) {
    position: relative; display: inline-flex; align-items: center;
    width: 38px; height: 22px; flex: none; cursor: pointer;
}
body:not([data-frb-optout~="input"]) .switch:has(> .slider) > input[type="checkbox"],
body:not([data-frb-optout~="input"]) .switch:has(> .toggle-slider) > input[type="checkbox"],
body:not([data-frb-optout~="input"]) .setting-control:has(> .slider) > input[type="checkbox"],
body:not([data-frb-optout~="input"]) .setting-control:has(> .toggle-slider) > input[type="checkbox"] {
    position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer;
}
body:not([data-frb-optout~="input"]) .switch > .slider,
body:not([data-frb-optout~="input"]) .setting-control > .slider,
body:not([data-frb-optout~="input"]) .toggle-switch > .slider {
    position: absolute; inset: 0; border-radius: var(--frb-radius-pill, 999px);
    background: var(--inset-bg); border: 1px solid var(--input-border);
    transition: background-color .15s ease, border-color .15s ease;
}
body:not([data-frb-optout~="input"]) .switch > .slider::before,
body:not([data-frb-optout~="input"]) .setting-control > .slider::before,
body:not([data-frb-optout~="input"]) .toggle-switch > .slider::before {
    content: ""; position: absolute; top: 50%; left: 3px; transform: translateY(-50%);
    width: 14px; height: 14px; border-radius: 50%;
    background: var(--text-secondary); transition: left .15s ease, background-color .15s ease;
}
body:not([data-frb-optout~="input"]) .switch > input:checked + .slider,
body:not([data-frb-optout~="input"]) .setting-control > input:checked + .slider,
body:not([data-frb-optout~="input"]) .toggle-switch > input:checked + .slider {
    background: var(--accent-fill, var(--accent-primary));
    border-color: var(--accent-fill, var(--accent-primary));
}
body:not([data-frb-optout~="input"]) .switch > input:checked + .slider::before,
body:not([data-frb-optout~="input"]) .setting-control > input:checked + .slider::before,
body:not([data-frb-optout~="input"]) .toggle-switch > input:checked + .slider::before {
    left: 19px; background: var(--btn-text, #000);
}
body:not([data-frb-optout~="input"]) .switch > input:focus-visible + .slider,
body:not([data-frb-optout~="input"]) .setting-control > input:focus-visible + .slider,
body:not([data-frb-optout~="input"]) .toggle-switch > input:focus-visible + .slider {
    outline: 2px solid var(--accent-text, var(--accent-primary)); outline-offset: 2px;
}

/* And the OTHER .switch — a label wrapping a checkbox and its own text, which is
   a choice, not a track. :has() is what tells the two apart; without it one rule
   would have to guess and would silently break whichever page it guessed wrong. */
body:not([data-frb-optout~="input"]) .switch:not(:has(> .slider)):not(:has(> .toggle-slider)) {
    display: inline-flex; align-items: center; gap: var(--frb-space-2, 8px);
    cursor: pointer; font-size: 14px; color: var(--text-primary);
}

/* ═══ WAVE 4 — TYPE ════════════════════════════════════════════════════════
   VENDOR TYPE, overridden rather than edited.

   Rendering the product found eight typefaces where there should be two. Six of
   the strays were our own stacks written without a lead face --
   `ui-monospace, SFMono-Regular, Menlo, monospace` names four fallbacks and
   never mentions JetBrains Mono -- and those were fixed at the source.

   The rest belong to Leaflet and Bootstrap. Editing a vendor stylesheet is a
   change that disappears at the next upgrade and takes a while to notice, so
   the system overrides them here instead. A map popup rendering in Helvetica
   inside a product that is otherwise Outfit is not Leaflet's mistake; it is
   ours for never saying otherwise. */
/* Bootstrap ships its own font tokens and its <code>, <pre> and <kbd> rules
   read them, so eight code samples were still rendering in SFMono while every
   other line of code in the product was JetBrains Mono. Redefining the token is
   the whole fix -- Bootstrap's own rules then resolve to ours. */
body:not([data-frb-optout~="type"]) {
    --bs-font-monospace: var(--frb-mono, 'JetBrains Mono', ui-monospace, monospace);
    --bs-body-font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
}
body:not([data-frb-optout~="type"]) .leaflet-container,
body:not([data-frb-optout~="type"]) .leaflet-container .leaflet-control,
body:not([data-frb-optout~="type"]) .leaflet-popup-content,
body:not([data-frb-optout~="type"]) .leaflet-tooltip {
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
}

/* ═══ WAVE 5 — THE PHONE ═══════════════════════════════════════════════════
   78 of 101 apps scrolled sideways at 390px. 75 of them for one reason: the
   shared header's three tubes are sized with `clamp(200px,18vw,400px)` and the
   existing collapse was written at a specificity the app-page selector beats,
   so the collapse never fired where it was needed. That is fixed in
   frb-header.js. What remains is here, and it is four shapes rather than ten
   pages.

   TWO TIERS, and the difference between them is the whole design.

   Tier A is PERMISSION, written inside :where() so it carries no specificity at
   all. `min-width: 0` on everything sounds violent and is not: min-width's
   initial value already computes to 0 for a block box, so the declaration
   changes nothing except for flex and grid ITEMS, where the initial `auto`
   means "never shrink below your own content" -- which is the mechanism behind
   nearly every sideways scroll in this product. A page that genuinely needs a
   floor still has it, because :where() puts this at (0,0,0) and the page's own
   `min-width: 820px` on a data table is (0,1,0). The system asks; the page
   decides.

   Tier B is OVERRIDE, at real specificity, for the four layouts that are
   measurably broken on a phone and cannot be fixed by permission alone. Each
   one is named from a render, not from a guess, and each is a shape the
   product repeats rather than a page: 53 pages mount <nav class="sidebar">,
   34 use .module-header, 6 build the same split-pane editor. Overriding an
   author rule is a real cost and it is paid here only under 620px, where the
   alternative is a page the reader has to drag sideways to finish reading. */

@media (max-width: 620px) {

/* ── Tier A — permission to shrink ─────────────────────────────────────── */
:where(body:not([data-frb-optout~="mobile"]) *) { min-width: 0; }

/* Media that was sized for a desktop column. `height: auto` is deliberately
   NOT applied to canvas: a canvas's height attribute is its drawing buffer and
   several apps read it back. */
:where(body:not([data-frb-optout~="mobile"]) img,
       body:not([data-frb-optout~="mobile"]) video) { max-width: 100%; height: auto; }
:where(body:not([data-frb-optout~="mobile"]) svg,
       body:not([data-frb-optout~="mobile"]) canvas,
       body:not([data-frb-optout~="mobile"]) iframe,
       body:not([data-frb-optout~="mobile"]) table) { max-width: 100%; }

/* A long unbroken token -- a URL, a hash, a base64 blob -- is the one thing a
   text block cannot wrap out of on its own. */
:where(body:not([data-frb-optout~="mobile"]) *) { overflow-wrap: break-word; }

/* ── Tier B — the four shapes that break ───────────────────────────────── */

/* 1. THE ICON RAIL. 53 pages mount <nav class="sidebar"> as a 70px column of
      icons, and several of them already collapse it to a 56px ROW at their own
      breakpoint -- which turns a vertical list into a horizontal one without
      telling it to stop growing sideways. job_tracker's rail was 446px wide
      inside a 390px viewport for exactly that reason: it is `flex-direction:
      row; flex-wrap: nowrap`, so its min-content became the grid column's
      width and every sibling inherited the overflow. A rail that has become a
      strip scrolls in the direction it now runs. */
body:not([data-frb-optout~="mobile"]) nav.sidebar,
body:not([data-frb-optout~="mobile"]) .sidebar.nav-rail {
    flex-direction: row; flex-wrap: nowrap;
    overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
    width: 100%; max-width: 100%; height: auto;
}
body:not([data-frb-optout~="mobile"]) nav.sidebar::-webkit-scrollbar { display: none; }
/* The rail's own children must keep their size while the rail scrolls -- a
   40px icon that shrinks to 12px is not a target anyone can hit. Tier A gave
   every element permission to shrink; this takes it back where shrinking is
   the bug rather than the fix. */
body:not([data-frb-optout~="mobile"]) nav.sidebar > * { flex: 0 0 auto; }
/* `margin-top: auto` pushes the theme dots to the bottom of a COLUMN. In a row
   it pushes them off the end, past everything. */
body:not([data-frb-optout~="mobile"]) nav.sidebar > [style*="margin-top:auto"],
body:not([data-frb-optout~="mobile"]) nav.sidebar > [style*="margin-top: auto"] {
    margin-top: 0 !important; margin-left: auto; flex-direction: row !important;
}

/* 2. THE SPLIT-PANE EDITOR. Six apps -- both menu genies, both resume
      architects, codefolio and the cover letter architect -- lay out as
      `grid-template-columns: 70px var(--editor-width) 12px 1fr` with
      `.editor-panel { min-width: 420px }`. That is 502px of chrome before the
      preview is given a pixel, inside a 390px screen, and it is why these six
      were the worst overflows in the product at 492 to 692px over.

      A phone does not have room for two panes side by side, so it gets them in
      sequence. The drag handle goes with them: a 12px grip exists to be
      dragged with a mouse and there is no mouse here. */
body:not([data-frb-optout~="mobile"]) .app-container {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto auto;
    height: auto; min-height: 100vh;
}
body:not([data-frb-optout~="mobile"]) .resizer-bar { display: none; }
body:not([data-frb-optout~="mobile"]) .editor-panel,
body:not([data-frb-optout~="mobile"]) .preview-panel {
    min-width: 0; width: 100%; max-width: 100%;
    height: auto; overflow-x: hidden;
}

/* 3. THE NOWRAP TITLE. A section divider set in 25px uppercase with 5px of
      letter-spacing and `white-space: nowrap` measures 693px for the words
      "Ranchie DevOps - Recruitment Phase". Letter-spacing is what makes it a
      divider and it is also what makes it 693px, so the phone keeps the look
      and loses the tracking, and the line is allowed to break.

      Written at (0,2,1) on purpose. The page's own `white-space: nowrap` is
      (0,1,0) and a zero-specificity rule would lose to it -- which is the
      difference between a layer that asks and a layer that fixes. */
body:not([data-frb-optout~="mobile"]) .section-divider-text,
body:not([data-frb-optout~="mobile"]) .subsection-divider-text,
body:not([data-frb-optout~="mobile"]) .header-title,
body:not([data-frb-optout~="mobile"]) .page-title,
body:not([data-frb-optout~="mobile"]) .app-title {
    white-space: normal; letter-spacing: normal;
    max-width: 100%; overflow-wrap: break-word;
}
body:not([data-frb-optout~="mobile"]) .section-divider,
body:not([data-frb-optout~="mobile"]) .subsection-divider { flex-wrap: wrap; }

/* 4. THE HEADER ROW. 34 pages open a module with .module-header -- a flex row
      carrying a title on the left and a cluster of pill buttons on the right,
      `flex-wrap` never set, so it is nowrap by initial value. On a desktop
      there is room; at 390px events_mgmt's row measured 405. Every one of
      these rows is a title and some controls, and a title above its controls
      is the ordinary phone answer. */
body:not([data-frb-optout~="mobile"]) .module-header,
body:not([data-frb-optout~="mobile"]) .header-section,
body:not([data-frb-optout~="mobile"]) .page-head,
body:not([data-frb-optout~="mobile"]) .card-header-bar,
body:not([data-frb-optout~="mobile"]) .toolbar,
body:not([data-frb-optout~="mobile"]) .section-header {
    flex-wrap: wrap; row-gap: var(--frb-space-2, 8px);
}

}

/* ═══ WAVE 5 — THE APP NAME ════════════════════════════════════════════════
   Mapping 101 apps' own title markup onto the one component.

   Measured across every app page at 1440px: 13 sizes from 16px to 80px, under
   eight class names, plus nine pages that used a bare <h1> and got whatever
   the browser felt like. Two of those nine render at 80px and one at 72,
   directly under a site header that already says the product's name.

   The mapping is by CLASS NAME, not by position, and that is a decision worth
   defending. A rule scoped to "the first heading in the header block" would be
   more surgical and would also be a guess about DOM shape on 101 pages that do
   not share one. The class name is the page's own statement that this element
   is a page title -- eight pages put `.page-title` on more than one element
   (pharma_watch on 26, one per view) and every one of those is genuinely a
   view's title. Sizing them all alike is the point rather than a side effect.

   None of this edits a single page's HTML. An app that wants the real
   component writes `.frb-app-title` and skips the whole layer. */

body:not([data-frb-optout~="title"]) h1.display-5,
body:not([data-frb-optout~="title"]) .app-title,
body:not([data-frb-optout~="title"]) .page-title,
body:not([data-frb-optout~="title"]) .header-title,
body:not([data-frb-optout~="title"]) .dashboard-title,
/* Ranchie's three document pages name their heading after the document --
   .guidelines-title, .policy-title -- and were the last two off-scale titles
   in the product at 32px and 40px. A document inside an app is still that
   app. */
body:not([data-frb-optout~="title"]) .guidelines-title,
body:not([data-frb-optout~="title"]) .policy-title,
/* The nine bare <h1>s. All nine sit inside one of these five blocks -- six in
   .header-section, one in .module-header, one in .page-head, and two more in
   an unclassed div inside .header-section -- so a descendant combinator reaches
   every one of them without a rule that says `h1` on its own. `h1 { }` in a
   layer that loads last on 134 pages is not a rule, it is an accident waiting
   for the first modal with a heading in it. */
/* `.section-title` JOINS THE TWO EXCLUSIONS ALREADY HERE. A head that has been
   demoted to a section heading keeps the container class it was written with —
   `.header-section` on Ops Command, `.module-header` elsewhere — so this rule
   still reached its <h1> and drew it at 28px, which is the one thing the
   demotion exists to stop. "Operations Overview" came out the same size as
   "Ops Command" directly above it. Same mechanism as .frb-view-title: the
   element says what it is, and this rule steps back. */
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .dashboard-header-tile, .top-bar)
    h1:not(.frb-view-title):not(.frb-app-title):not(.section-title) {
    /* THE TOKEN, NOT THE FAMILY — and this rule is why the app heads did not
       move when the token was pointed at Syne. The branch above reaches
       `.page-head h1` at (0,5,2); frb-shell's own `.page-title` rule is
       (0,2,1), so on every app whose head is an <h1> this hardcoded
       `--frb-font` won and the title came out Outfit while its description
       came out mono. Measured, not guessed: Quick Notes reported Outfit 28px
       under a JetBrains Mono sub.

       Exactly the same mistake the shell's own rule carried for plans.html,
       and the same fix: read the token, keep the old value as its default, so
       nothing moves except where the token says to. */
    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);
    letter-spacing: var(--frb-app-title-tracking, -0.01em);
    overflow-wrap: break-word;
}

/* THE ONE !important IN THE APP-SHELL LAYER, and it is here because Bootstrap
   got there first. 32 of the 33 `display-5` titles also carry `.fw-bold`,
   which Bootstrap ships as `font-weight: 700 !important` -- a utility class
   declares itself unarguable, so no amount of specificity reaches it and the
   product converged on 28px at two different weights. The alternatives were
   editing `fw-bold` out of 32 pages' HTML, or moving the system's title weight
   to 700 because a vendor utility said so. Answering one !important with one
   !important, on one property, in one rule, is the smallest of the three.

   `.fw-bold` is left alone everywhere else -- this rule reaches it only where
   it sits on a display-5 heading, which is only ever the app's name. */
body:not([data-frb-optout~="title"]) h1.display-5.fw-bold {
    font-weight: var(--frb-app-title-weight, 800) !important;
}

/* The compact variant's mapping. The six split-pane editors all carry
   `.editor-title-text` and all already render 20px/700, so this rule changes
   nothing today -- it exists so the value lives in a token with the other
   twelve instead of being re-typed in six page stylesheets, and so the next
   editor inherits it. media_kit_one's `.canvas-title` is the same role at
   18px and joins them. */
body:not([data-frb-optout~="title"]) .editor-title-text,
body:not([data-frb-optout~="title"]) .canvas-title {
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: var(--frb-app-title-sm-size, 20px);
    font-weight: var(--frb-app-title-sm-weight, 700);
    line-height: 1.2;
    overflow-wrap: break-word;
}

/* The line under it. 88 of 90 app heads have one and they were set in seven
   sizes: 15px on 33 pages, 14 on 22, 13 on 14, then 16, 11, 18 and 20.

   SCOPED TO THE HEAD BLOCK, and this is the whole reason the selector is long.
   `.app-desc` is the head's subtitle on 42 pages and is ALSO the description
   line on every card in every hub grid -- 466 elements in total. An unscoped
   rule would have resized a few dozen headers and several hundred cards, and
   the cards were never the problem. */
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .dashboard-header-tile, .top-bar,
    .header-main) :is(.app-desc, .app-subtitle, .header-subtitle,
    .page-subtitle, .module-subtitle, .subtitle) {
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: var(--frb-app-sub-size, 14px);
    line-height: 1.5;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    overflow-wrap: break-word;
}

/* The head block's LAYOUT, and nothing else about its box.

   34 pages give `.module-header` `padding: 40px 40px 20px`, and on those pages
   that padding is the left gutter the whole view is aligned to -- the cards
   below inherit their inset from it. Standardising padding here would pull 34
   titles flush against the viewport while everything under them stayed where
   it was. So the compat layer takes the two properties that were inconsistent
   for no reason (a flex row that never set flex-wrap, so it was nowrap by
   default on all 34) and leaves the box alone. */
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .header-main) {
    flex-wrap: wrap;
    gap: var(--frb-app-head-gap, 16px);
    row-gap: var(--frb-space-3, 12px);
}

/* ═══ WAVE 5 — THE RAIL ════════════════════════════════════════════════════
   73 of 101 apps have a vertical strip of navigation down the left edge. 59 of
   them are icons only at 70px. 14 are not, and 7 of those 14 are the same
   family -- business_ops, business_incubation and events_mgmt -- which put a
   word under every icon and widened the rail to 90, 110 or 120px to fit it.
   Those seven are also the seven that read as a different product.

   ONE TOKEN MOVES ALL OF IT. Every one of the 21 pages that sizes its rail
   does it through `--sidebar-w`, and every one of them also uses that same
   token for the main container's `margin-left`. So the rail and the content
   beside it move together, from one declaration, on 21 pages -- which is the
   only reason this is a three-line change rather than 21 careful ones. It is
   set on <body> rather than :root because a page's own `:root{--sidebar-w}` is
   (0,1,0) and body:not([...]) is (0,1,1); a custom property set on an ancestor
   of the rail is what the rail resolves. */
body:not([data-frb-optout~="rail"]) { --sidebar-w: 70px; }

/* THE LABEL BECOMES THE TOOLTIP, which is the whole trick and needs no
   JavaScript and no HTML edit. `display: none` would have been simpler and
   would also have deleted the only text naming these five views for anyone who
   does not recognise a toolbox icon. Taking it out of flow instead keeps the
   word in the document -- a screen reader still reads it, a find-in-page still
   finds it -- and paints it beside the rail on hover, which is exactly the
   affordance the other 59 rails already have.

   Scoped to a rail that is actually a rail: `.sidebar .nav-item .nav-label`.
   `.nav-label` on its own appears in a few page footers too. */
/* THE LABEL IS THE `title` ATTRIBUTE NOW, NOT A CSS TOOLTIP, and the reason
   is the rail's own overflow. This was an absolutely-positioned box that
   escaped the rail on hover — which only works while the rail's overflow is
   `visible`, and the rail has to scroll: on four apps the icon list is taller
   than the window, so the theme swatches and the light/dark switch at its foot
   sat below the fold and could not be reached at all. A scrolling box clips
   what escapes it, and a half-clipped tooltip is worse than none.

   Nothing is lost: frb-header.js sets `title` on every rail item from the same
   text, so hovering still names the view — through the browser rather than
   through this stylesheet — and the word stays the item's accessible name. */
body:not([data-frb-optout~="rail"]) .sidebar .nav-item { position: relative; }
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) .nav-label {
    display: none;
}

/* THE RAIL SCROLLS, AND ITS FOOT STAYS PUT.
   `max-height` alone stopped the rail overrunning the window but did nothing
   about a rail whose CONTENT overruns it: on codefolio_sync, chroma,
   menu_genie and fda the icons ran past the bottom and took the theme
   swatches and the light/dark switch with them. Every app in this product has
   a theme picker; on those four it could not be reached.

   `position: sticky; bottom: 0` on the foot is what makes scrolling safe — the
   settings stay pinned to the bottom of the rail while the icons above them
   scroll, so they are reachable whatever the list length. The foot needs its
   own ground or the icons scroll through it. */
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) {
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
}
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav)::-webkit-scrollbar { display: none; }
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) .theme-footer {
    position: sticky;
    bottom: 0;
    background: var(--sidebar-bg, var(--bg-secondary, Canvas));
    padding-bottom: var(--frb-space-2, 8px);
}
/* A rail that clips its own tooltip has not gained one. Six of the seven set
   `overflow: hidden` on the rail for the active-item highlight.

   ABOVE THE PHONE BREAKPOINT ONLY. Below it the rail has become a horizontal
   strip that has to scroll, and `overflow: visible` here beat the strip's own
   `overflow-x: auto` -- so travel_planning's fourteen icons pushed 70px past
   the edge of the page instead of scrolling inside their own strip. The
   tooltip is hidden on a phone anyway; there is nothing left to clip. */
/* (An `overflow: visible` exception lived here so the old CSS tooltip could
   escape the rail. The tooltip is the `title` attribute now and the rail has
   to scroll, so the exception is gone — it was the thing keeping four apps'
   settings below the fold.) */
/* The icons were sized to sit above a word. Without the word they are the
   whole item — at the rail component's 16px, which is what 58 of the 75 rails
   were already drawing before any of this. (This said 20px when it was
   written, before the glyph had been measured across the product.) */
body:not([data-frb-optout~="rail"]) .sidebar .nav-item .nav-icon { font-size: 16px; }

/* ═══ THE STEP RAIL'S TOOLTIP, WHICH NEVER ONCE APPEARED ═══════════════════
   Seven apps draw their rail with `.step-item` rather than `.nav-item`, and
   each writes its own tooltip:

       .step-item:hover::after { content: attr(data-title); position: absolute;
                                 left: 80px; ... }

   80px, out of a rail that is 70px wide and, since the fix above, scrolls.
   An absolutely-positioned box is clipped by the nearest scrolling ancestor,
   so the tooltip was painted and immediately cut off at the rail's edge —
   thirty-five names, on seven apps, that no hover has ever shown. The rule
   looked right in the stylesheet, which is why it survived: nothing about
   reading it says "this never renders".

   `fixed` rather than `visible`. Making the rail overflow-visible is what the
   note above spent four apps' settings to undo, so the escape has to happen
   from the other side: a fixed box is positioned against the viewport and no
   ancestor's overflow can clip it. The rail is flush to the left edge at 70px
   wide, so a viewport `left` of 78px lands 8px clear of it — the same gap the
   author was asking for. `top` stays `auto`, which resolves to the item's own
   static position, so each name still sits beside its own icon.

   ABOVE THE PHONE BREAKPOINT ONLY, for the reason the note above gives: below
   it the rail is a horizontal strip across the top, where "78px from the left
   of the window" is not beside anything. There the `title` attribute is the
   tooltip, as it is on every `.nav-item` rail. */
@media (min-width: 621px) {
    body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
        .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) .step-item:hover::after {
        position: fixed;
        left: 78px;
        /* AND IT HAS TO BE READABLE ONCE IT IS VISIBLE. `--panel-bg` is
           `rgba(10,10,10,.8)` — a fifth of whatever is behind it comes
           through, which was invisible while the tooltip was clipped and is a
           column of body text running under the word now that it is not. The
           blur is what the rest of this product's panels already do
           (frb-shell.css does the same at 15px), so the glass look is kept and
           the text beneath stops being text. */
        backdrop-filter: blur(10px);
        -webkit-backdrop-filter: blur(10px);
    }
}

/* ═══ WAVE 5 — A NAME IS NOT A LOGO ════════════════════════════════════════
   Eight apps paint their own name with a linear-gradient and a transparent
   text fill. 88 do not. On a page whose header bar already carries the
   product's mark, an app name in a two-colour gradient reads as a second
   brand rather than a title -- and four of the eight chose a gradient that
   is not even in the theme they are running.

   Both halves have to be undone or the text disappears: killing the gradient
   without restoring the fill leaves transparent glyphs over a plain
   background, which is a blank header. */
body:not([data-frb-optout~="title"]) h1.display-5,
body:not([data-frb-optout~="title"]) .app-title,
body:not([data-frb-optout~="title"]) .page-title,
body:not([data-frb-optout~="title"]) .header-title,
body:not([data-frb-optout~="title"]) .dashboard-title,
body:not([data-frb-optout~="title"]) .guidelines-title,
body:not([data-frb-optout~="title"]) .policy-title,
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .dashboard-header-tile, .top-bar) h1 {
    background-image: none;
    background-clip: border-box;
    -webkit-background-clip: border-box;
    -webkit-text-fill-color: currentColor;
    color: var(--text-primary, CanvasText);
}

/* ═══ WAVE 5 — THE APP BAR'S NAME ══════════════════════════════════════════
   Two shells exist in this product and both are fine. Most apps put their name
   in a header block at 28px. The rest carry a slim bar under the shared header
   -- the six split-pane editors, research_command, ops_command, accounting_core
   -- and put the name in there instead, where 28px would not fit beside the
   controls.

   Every one of those bars renders the name at 20px/700 sentence case, except
   two: ops_command and accounting_core set 12px/800 UPPERCASE with 2px of
   tracking, which is the size and treatment of a LABEL. Their app bar did not
   look like a header because the name in it was not being set as one. */
body:not([data-frb-optout~="title"]) .oc-topbar-name,
body:not([data-frb-optout~="title"]) .ac-topbar-name {
    /* Set in JetBrains Mono, which is the product's CODE face. An app's name
       is not code. */
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: var(--frb-app-title-sm-size, 20px);
    font-weight: var(--frb-app-title-sm-weight, 700);
    text-transform: none;
    letter-spacing: normal;
    line-height: 1.2;
    color: var(--text-primary, CanvasText);
}

/* ═══ WAVE 5 — THE APP HEAD'S CHROME ═══════════════════════════════════════
   28 pages open with `.header-section`. 18 of them draw nothing but the title
   and a line under it. The other ten decorate: a 4px gradient rule down the
   left edge in a ::before, a 150px gradient underline beneath the title in an
   ::after, the description set in JetBrains Mono at 18px with a `>>` glyph
   hung in its own ::before, and 30px of padding to clear the left rule --
   which leaves the title indented 30px further than every card below it.

   None of that is a font size, which is why converging type alone left these
   pages still looking like a different product. It is structure, and it is
   structure inside a SHARED class name, so it belongs in the shared layer.

   `content: none` rather than `display: none`: a generated box that is not
   generated cannot be laid out, cannot leave a gap, and cannot be re-shown by
   a page rule that only sets display. */
body:not([data-frb-optout~="title"]) .header-section::before,
body:not([data-frb-optout~="title"]) .header-section::after,
body:not([data-frb-optout~="title"]) .header-section > h1::after,
body:not([data-frb-optout~="title"]) .header-section > h1::before,
body:not([data-frb-optout~="title"]) .header-section > p::before,
body:not([data-frb-optout~="title"]) .header-section > p::after {
    content: none;
}
/* The 30px was there to clear the rule that is now gone. Without this the
   title floats 30px right of the content it introduces. The 60px below it was
   sized to balance a 72px title; under a 28px one it is a hole. 24 to the
   content and 4 from the title to its own description are the spacing steps
   the rest of the product already uses. */
body:not([data-frb-optout~="title"]) .header-section {
    padding-left: 0;
    margin-bottom: var(--frb-space-6, 24px);
}
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .header-main) > p {
    margin-top: var(--frb-space-1, 4px);
    margin-bottom: 0;
}

/* THE DESCRIPTION, FOUND BY POSITION. A direct <p> child of the app head is
   its description -- there is nothing else it could be -- and finding it that
   way reaches the pages that never gave it a class, which is where the 18px
   monospace lives. The classed ones are handled above; this catches the rest
   without a rule that says `p` on its own. */
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .header-main) > p {
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: var(--frb-app-sub-size, 14px);
    font-weight: 400;
    line-height: 1.5;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    opacity: 1;
    /* A description is read, so it stops at a readable measure instead of
       running the full width of a 1440px dashboard. */
    max-width: 74ch;
    overflow-wrap: break-word;
}

/* The 100px hole. `.view-section` is on four pages and two of them open with
   `padding: 100px ...`, which was sized to balance a 72px and an 80px title.
   Under a 28px one it is 100px of nothing between the app bar and the first
   word — measured at 162 and 166px from the shared bar to the title, against
   20-59px on the 55 pages that set the pace. Only the top is touched; the side
   padding on these two is 60 and 120px and is the gutter the content aligns
   to, which is the same reason .module-header's padding is left alone above. */
body:not([data-frb-optout~="title"]) .view-section { padding-top: var(--frb-space-10, 40px); }

/* ═══ WAVE 5 — THE RAIL RUNS OFF THE BOTTOM ════════════════════════════════
   38 of the 73 rails are exactly 47px too tall, and 47px is the height of the
   shared header. They were written as `height: 100vh` back when the rail
   started at the top of the window; the header now sits above them, so the
   rail begins at y=47 and ends at y=947 on a 900px screen. What falls off the
   end is `.theme-footer` -- the colour swatches and the light/dark switch --
   because a rail puts its settings at the bottom with `margin-top: auto`, and
   the bottom is the part that left the screen. Every app in the product has a
   theme picker; on 38 of them it could not be reached.

   `max-height`, NOT `height`, and the difference matters. `height` would fix
   the 38 that say 100vh and would also stretch the five rails that are 245 to
   408px tall because their content is short and they meant it. A ceiling
   constrains what is too tall and says nothing about what is not.

   The header publishes its own measured height as --frb-header-h on
   <html> (frb-header.js), so this tracks a header that changes size rather
   than hard-coding today's 47px. The fallback is there for the instant before
   the script runs. */
body:not([data-frb-optout~="rail"]) .sidebar,
body:not([data-frb-optout~="rail"]) .sidebar-primary,
body:not([data-frb-optout~="rail"]) .ranchie-sidebar,
body:not([data-frb-optout~="rail"]) .nav-rail,
body:not([data-frb-optout~="rail"]) .rail-nav,
body:not([data-frb-optout~="rail"]) .tp-rail,
body:not([data-frb-optout~="rail"]) .rm-rail,
body:not([data-frb-optout~="rail"]) nav.rail,
body:not([data-frb-optout~="rail"]) .app-sidebar,
/* AND THE SELF-MAINTAINING ONE. Six class names is a list that goes stale the
   first time someone writes a seventh. Every rail in this product ends in a
   .theme-footer -- that is the product's own marker for "this strip is the app
   rail" -- so an element that directly contains one IS the rail, whatever it
   calls itself, including the one nobody has written yet. */
body:not([data-frb-optout~="rail"]) :has(> .theme-footer) {
    max-height: calc(100vh - var(--frb-header-h, 47px));
}

/* ═══ A GRID ITEM MAY NOT DECIDE THE GRID'S WIDTH ══════════════════════════
   `.grid-2`, `.grid-3` and `.grid-4` are a shared vocabulary — 20 apps use
   those names and every one of them defines the rule itself — so this is the
   file for the one thing they all get wrong.

   A `1fr` track is `minmax(auto, 1fr)`, and `auto` there is the item's
   min-content width, which a grid item may not go under. So a single wide
   thing inside one cell stops being laid out by the grid and starts laying out
   the grid: Habit Streak's two columns resolved to 331px and 1027px inside a
   1370px column and the right-hand card ran off the screen — reported as the
   Current Streak container not stopping at the right padding.

   `min-width: 0` is the standard release for that, and it says the true thing:
   the column decides how wide the cell is, and the content fits it. */
body:not([data-frb-optout~="grid"]) :is(.grid-2, .grid-3, .grid-4) > * {
    min-width: 0;
}

/* ═══ WAVE 5 — WHERE A MESSAGE APPEARS ═════════════════════════════════════
   The pages that hand-rolled a toast host, moved onto the component's place.
   Only the placement properties are set: each page keeps its own toast
   styling, its own animation and its own JavaScript, and simply stops raising
   the message where it cannot be read.

   BOTTOM CENTRE, AND THE TOP-RIGHT IS WHY. Under the header is exactly where
   the hub tree runs — the one strip that says where you are and how to get
   back — so on Kanban Lite "Sample board loaded" sat on top of the trail for
   as long as it showed, and every other confirmation did the same. The bottom
   of the screen carries nothing but the plan badge, which this clears.

   Centred with auto margins rather than a transform, deliberately: several of
   these pages animate their toast in on `transform`, and a translate set here
   would be overwritten by the animation the moment it ran. */
/* Arriving from below, since below is where it now lives. Eight pages built
   their toast with `slideIn` — `translateX(100%)` — which read as a notice
   flying in from the right and stopping in the middle. Defined once here and
   named in those eight inline styles, because an inline `animation` is not
   something a stylesheet can talk over. */
@keyframes frbRise {
    from { transform: translateY(14px); opacity: 0; }
    to   { transform: translateY(0);    opacity: 1; }
}

body:not([data-frb-optout~="toast"]) .frb-toast-wrap,
body:not([data-frb-optout~="toast"]) .toast-wrap,
body:not([data-frb-optout~="toast"]) .toast-container,
body:not([data-frb-optout~="toast"]) .toast-host,
body:not([data-frb-optout~="toast"]) .toast-stack,
body:not([data-frb-optout~="toast"]) .notification-center {
    top: auto;
    bottom: calc(var(--frb-space-4, 16px) + 34px);
    left: 0;
    right: 0;
    margin-left: auto;
    margin-right: auto;
    width: max-content;
    max-width: min(90vw, 380px);
    z-index: 2147483100;
}
@media (max-width: 620px) {
    body:not([data-frb-optout~="toast"]) .frb-toast-wrap,
    body:not([data-frb-optout~="toast"]) .toast-wrap,
    body:not([data-frb-optout~="toast"]) .toast-container,
    body:not([data-frb-optout~="toast"]) .toast-host,
    body:not([data-frb-optout~="toast"]) .toast-stack,
    body:not([data-frb-optout~="toast"]) .notification-center {
        left: var(--frb-space-3, 12px);
        right: var(--frb-space-3, 12px);
        max-width: none;
    }
}

/* ═══ WAVE 5 — EVERY RAIL IS THE RAIL ══════════════════════════════════════
   The 75 existing rails, mapped onto .frb-rail without touching a page.

   The measured spread this closes: eight item boxes (44×44 r12 on 36 pages,
   46×46, 48×48 r14, 69×50 r0, 69×70, 69×90, 69×100, and a 191×42 row on the
   seven-page wide-nav family), five icon sizes, and eight active treatments.
   44×44 at radius 12 and a 16px glyph are the plurality on 36 and 58 pages
   respectively; the active state is industry_pipelines', because it is the
   only one of the eight that is legible in light mode.

   THE WIDE NAV JOINS THEM. industry_pipelines and its six siblings run a 216px
   rail of labelled rows. Their markup is already identical to everyone else's
   -- .nav-item, an <i>, a .nav-label -- so squaring the item and parking the
   label turns them into the same rail the other 68 have, with no loss: every
   one of those rows also carries a `title`, and the label itself is still
   there on hover. */

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) {
    width: 44px; height: 44px; min-height: 44px; min-width: 44px;
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center;
    padding: 0;
    margin-left: auto; margin-right: auto;
    border-radius: var(--frb-space-3, 12px);
    position: relative;
    /* A rail item is a glyph. Any label inside it is the tooltip, and a
       tooltip must not set the box's width. */
    white-space: nowrap;
    gap: 0;
    /* 44 plus a 1px border on each side is 46, which is what the six editors
       were measuring. A width is only a width once the box knows what it
       includes. */
    box-sizing: border-box;
}
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: 16px; width: auto; margin: 0;
}

/* ONE ACTIVE STATE. Three of the eight in use are white at 3 to 5 percent,
   which is a highlight you cannot see on a white panel — the light mode of
   those apps had no way of showing you which view you were in. A neutral grey
   wash reads on both grounds, and the accent does the actual signalling. */
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):hover {
    background: rgba(127, 127, 127, .14);
    color: var(--text-primary, CanvasText);
}
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(.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);
}

/* The rail's own box: centred column, one gap, settings at the foot. The
   wide-nav family stretches its items (`align-items: stretch`) because they
   were rows; they are squares now. */
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) {
    align-items: center;
    gap: var(--frb-space-1, 4px);
}
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) .theme-footer {
    margin-top: auto;
    display: flex; flex-direction: column; align-items: center;
    gap: var(--frb-space-2, 8px);
    padding-top: var(--frb-space-3, 12px);
    flex: 0 0 auto;
}

/* THE RAIL'S COLUMN. Seven pages lay out as `grid-template-columns: 216px 1fr`
   (or 236px) because their rail used to be a list of labelled rows. The items
   are 44px squares now, so the track is 170px of nothing beside them. 37 pages
   already write `70px 1fr` here, which is the same number --sidebar-w holds.

   `:has(> ...)` is what makes this safe to write once. The rule fires only
   where a rail is a DIRECT child of the grid — so it reaches the seven wide
   ones and cannot touch the split-pane editors, whose grid is on
   .app-container and runs four tracks. A rule that just matched "a grid with a
   px first column" would have flattened those to two. */
body:not([data-frb-optout~="rail"]):has(> :is(.sidebar, .sidebar-primary,
    nav.rail, .nav-rail, .rail-nav)) {
    grid-template-columns: var(--sidebar-w, 70px) 1fr;
}

/* ═══ WAVE 5 — A SCRIM IS NOT A GRID CELL ══════════════════════════════════
   scamshield lays its body out as `grid-template-columns: 224px 1fr` and puts
   an empty <div class="nav-scrim"> in front of the rail — the backdrop behind
   the mobile menu. Its `position: fixed; display: none` was written INSIDE the
   page's max-width media query, so above that breakpoint the scrim has no
   styles at all: a visible, in-flow, 18px-tall div that quietly takes a grid
   cell. Everything after it shifts by one. The rail landed in column 2 at
   1370px, the whole app landed in column 1 at 70px, and the page rendered as a
   thin ribbon of content beside an empty screen.

   This predates the design system and it is worth naming precisely, because
   the shape recurs: a decorative element that is only positioned inside a
   media query is in-flow everywhere else, and inside a grid, in-flow means it
   consumes a track.

   A scrim covers the viewport or it is not there. Neither state is a cell. */
body:not([data-frb-optout~="rail"]) > :is(.nav-scrim, .scrim, .nav-backdrop,
    .sidebar-scrim) {
    position: fixed;
    inset: 0;
    display: none;
}
body:not([data-frb-optout~="rail"]) > :is(.nav-scrim, .scrim, .nav-backdrop,
    .sidebar-scrim):is(.on, .open, .is-open, .active, [aria-hidden="false"]) {
    display: block;
}

/* ONE ITEM SIZE, INCLUDING WHILE IT IS ACTIVE. The six split-pane editors
   scale their active rail item to 1.05, which measures 46×46 against everyone
   else's 44 and makes the rail's rhythm jump as you move between views. The
   active state is already carried by colour, background and the accent's glow;
   a size change on top of that is a fourth signal for one fact. */
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(.active, .is-active, :hover) {
    transform: none;
}

/* WHAT ELSE IS IN A RAIL. Three rails carry text that is not an item: a
   `.brand-sub` naming the app, and `.nav-sep` headings grouping the icons
   ("Read something", "Learn it", "Act"). Neither fits a 70px column and both
   were being clipped mid-word.

   The brand goes: the app's name is in the sub-header now, and saying it twice
   was the thing that made Ops Command read as two products stacked.

   The separator STAYS, as the line it was describing. A group heading in a
   labelled sidebar becomes a rule in an icon rail — the grouping is real
   information and the reader still gets it, just without the word. */
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) :is(.brand-sub,
    .brand-logo, .rail-brand) {
    display: none;
}
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) :is(.nav-sep, .rail-sep) {
    /* The text is not hidden, it is unset: zeroing the size keeps the element
       in the accessibility tree as the group label it always was, while the
       box becomes the divider. */
    font-size: 0;
    line-height: 0;
    width: 24px; height: 1px;
    margin: var(--frb-space-2, 8px) auto;
    padding: 0;
    background: var(--glass-border-color, rgba(127, 127, 127, .3));
    border: 0;
    overflow: hidden;
    flex: 0 0 auto;
}

/* ═══ WAVE 5 — THE ICON BESIDE THE NAME ════════════════════════════════════
   54 of the 96 app heads put a glyph before the app's name, and all 54 draw it
   at 16px — the one thing about these headers that never drifted. What did
   drift is the colour: 33 render it in the theme's accent and 21 in
   --text-secondary, which is the difference between an app that looks branded
   and an app that looks unfinished, on two pages sitting side by side in the
   same product.

   Every one of the 54 writes the icon as the FIRST CHILD of the title element
   itself, so `> :is(i, svg):first-child` reaches all of them and nothing else.
   An icon inside a button in the same header is not a first child of a title
   and is left alone. */
/* `.frb-app-glyph` USED TO BE IN THIS LIST and has been taken out of it. It is
   the design system's own class, written into the markup of every app head by
   the shell migration; sizing it here meant the component in frb-components.css
   declared one size and this layer silently overrode it from two files away.
   The glyph beside a 28px name is not the same object as an icon beside an h5
   in a card header, and it should not be reading its size from the same rule.
   frb-components.css owns it now; this rule keeps the legacy heads. */
body:not([data-frb-optout~="title"]) :is(.app-title, .page-title, .header-title,
    .dashboard-title, .guidelines-title, .policy-title, .editor-title-text,
    .oc-topbar-name, .ac-topbar-name) > :is(i, svg):first-child,
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .dashboard-header-tile, .top-bar,
    .header-main) :is(h1, h2, h5) > :is(i, svg):first-child {
    color: var(--accent-text, var(--accent-primary));
    font-size: 16px;
    /* Carbon writes the accent inline, which outranks any stylesheet — it is
       already right, so this changes nothing there and everything on the 21
       that were grey. */
}

/* ═══ WAVE 5 — EVERY FIELD LABEL IS A FIELD LABEL ══════════════════════════
   209 elements say `form-label`, 169 say `fld`, 31 `lbl`, 29 `label`, 24
   `field-label`, 22 `property-label`, and 321 <label>s say nothing at all.
   Between them: four sizes (12px on 115, 11 on 46, 10 on 17, 16 on 2), five
   weights, eight combinations of case and tracking, and four colours — two of
   which, at 10px in a dim grey, are below the floor this product set for body
   text in Wave 4.

   THE HARD PART IS NOT THE TYPE, IT IS WHICH ELEMENTS ARE FIELD LABELS.
   A <label> around a checkbox is a different thing wearing the same tag: it
   reads as a sentence beside the control, in sentence case at 14px, and
   setting it in 12px uppercase turns "Remember this device" into a heading for
   a box. `:has()` is what tells them apart, and without it this rule would
   have had to guess — the same distinction frb-compat already draws between a
   .switch that wraps a track and a .switch that wraps a choice. */

body:not([data-frb-optout~="input"]) :is(label.form-label, label.field-label,
    label.property-label, label.fld, label.lbl, label.label, label.f, .form-label,
    .field-label, .property-label, .control-label, .control-label-sm,
    .control-header, .fld, .lbl,
    /* and the 321 bare ones, minus every shape that is not a field label */
    label:not([class])):not(:has(input[type="checkbox"])):not(:has(input[type="radio"])):not(:has(.slider)):not(:has(.toggle-slider)):not(.toggle-label):not(.switch):not(.chk) {
    display: block;
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: var(--frb-label-size, 12px);
    font-weight: var(--frb-label-weight, 600);
    text-transform: uppercase;
    letter-spacing: var(--frb-label-tracking, 0.06em);
    line-height: 1.4;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    margin-bottom: var(--frb-field-gap, 6px);
}

/* The other shape, said once rather than left to the exclusions above. */
body:not([data-frb-optout~="input"]) :is(label.toggle-label, label.chk,
    label.switch, label:has(> input[type="checkbox"]), label:has(> input[type="radio"])) {
    display: inline-flex;
    align-items: center;
    gap: var(--frb-space-2, 8px);
    font-size: 14px;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--text-primary, CanvasText);
    cursor: pointer;
}

/* The gap under a field group. 148 pages call it .form-group, 91
   .control-group, 41 .field, 18 .input-group and 18 .form-row; between them
   they left 15px, 14, 12, 20, 16, 10 and 6 below a field. One cluster, one
   step. Only the bottom is set — a group's padding, border and background are
   the page's business, as they were for .module-header. */
body:not([data-frb-optout~="input"]) :is(.form-group, .control-group, .field,
    .form-field, .input-group, .form-row) {
    margin-bottom: var(--frb-field-gap-y, 16px);
}
/* A group nested inside another group would double the space. */
body:not([data-frb-optout~="input"]) :is(.form-group, .control-group, .field,
    .form-field, .input-group, .form-row)
    :is(.form-group, .control-group, .field, .form-field, .input-group, .form-row):last-child {
    margin-bottom: 0;
}

/* ═══ WAVE 5 — THE APP HEAD'S BOX ══════════════════════════════════════════
   Wave 5's first pass unified what the app head SAYS — the name at 28px/800, a
   16px accent glyph, the description at 14px — and left the box it says it in
   to each page. So 96 heads now say their name identically inside 96 frames
   that did not match. Measured at 1440px:

       padding-top      0 on 50 pages, 40 on 33, then 24, 12, 26, 18, 16, 22, 35
       padding-bottom   0 on 44, 20 on 34, 10 on 6, 70 on 5, then five more
       margin below     0 on 57, 24 on 26, then 18, 20, 10, 30, 40, 6
       rule under it    none on 51, 1px on 45

   VERTICAL ONLY, and the horizontal is left exactly as each page wrote it.
   That is not caution, it is a measurement: the title lines up with the content
   below it on 74 of 89 pages, and on the 33 that inset the head by 40px the
   cards below are inset by the same 40. Standardising left padding would break
   74 alignments to fix 15. (The first version of this survey compared the
   title's text position against the content CONTAINER'S border edge and
   reported 43 pages misaligned by exactly 40px — the container's own padding,
   every time. A text position against a box edge measures padding, not
   alignment.)

   THE RULE STAYS, on every head rather than half of them. 45 already draw one
   and the hub family reads correctly with it: a hairline is what makes this a
   sub-HEADER rather than a title floating above a grid. Adding one to the
   other 51 is the direction that cannot cost a page anything — it is 1px and
   it sits in space the head already owns. */

body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .header-main, .dashboard-header-tile,
    /* Six pages named the band after themselves rather than after what it is.
       They are heads — same shape, same contents, same job — and they were the
       last group still at `0 0 10px`. `.wrap`, `.d-flex` and `.view` are NOT
       in this list on purpose: the survey walks up from the title to the
       nearest ancestor carrying a class, and on 13 pages that walk falls
       through the head entirely and lands on a layout wrapper. Drawing a rule
       under one of those would put a hairline across the middle of the page. */
    .gpa-header, .focus-header, .capital-header, .pipeline-header,
    .kitchen-header, .family-header) {
    padding-bottom: var(--frb-space-4, 16px);
    margin-bottom: var(--frb-space-6, 24px);
    border-bottom: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
}

/* THE CONTROLS GO RIGHT. Carbon Snippet Studio and .gitignore Builder ship the
   SAME markup — `.header-section > div(title + description) + div(controls)` —
   and Carbon's controls sit hard right while gitignore's float in the middle
   of the row, because only one of the two pages wrote `justify-content` on the
   class they share. Six pages are wrong this way, by as much as 541px.

   `margin-left: auto` on the last child rather than `justify-content:
   space-between` on the parent, and the `:has()` is what makes it safe: a head
   whose last child is the DESCRIPTION would have had that description shoved
   to the right edge. Requiring the last child to contain something operable
   tells a control cluster from a paragraph, and a paragraph that happens to
   contain a link is not caught, because a link is not in the list. */
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .header-main, .dashboard-header-tile,
    .gpa-header, .focus-header, .capital-header, .pipeline-header,
    .kitchen-header, .family-header)
    > :last-child:has(:is(button, .btn, [role="button"], input, select, textarea)) {
    margin-left: auto;
}

/* ═══ WAVE 5 — ONE SELECTED STATE ══════════════════════════════════════════
   Across the 73 rails, the item you are on is drawn `fill + glow` on 63 pages,
   `fill + edge bar + glow` on 9, and `fill + edge bar` on 1. The edge bar is a
   4px rule down the left of the active item, drawn either as a border or as a
   ::before — a fourth way of saying the same thing on ten pages out of
   seventy-three, and the reason two rails side by side still did not match
   after the fill, the radius and the glow were all converged.

   Removed rather than adopted: 63 pages already do without it, a 44px square
   loses real room to a 4px rule, and the fill plus the accent on the glyph
   already say which item is selected twice over. */
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(.active, .is-active, [aria-current]) {
    border-left-width: 0;
    border-right-width: 0;
}
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(.active, .is-active, [aria-current])::before {
    content: none;
}

/* The rail's own light/dark button, mounted by frb-app-shell.js where a page
   had none — which was 92 of 101. It is a rail item and takes the rail item's
   shape from the rules above; this only gives it the muted weight the theme
   swatches beside it have, so the foot of the rail reads as settings rather
   than as another view to switch to. */
body:not([data-frb-optout~="rail"]) .frb-rail-mode {
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    background: none;
    border: 0;
}
body:not([data-frb-optout~="rail"]) .frb-rail-mode:hover {
    color: var(--text-primary, CanvasText);
    background: rgba(127, 127, 127, .14);
}

/* ═══ WAVE 5 — THE APP BAR'S GROUND ════════════════════════════════════════
   91 of the 96 app heads paint no background at all; they sit on the page.
   Five paint one, and three of those five read it from --panel-bg, which is
   the token every other surface in the product uses and which follows both the
   theme and the mode.

   Two do not. ops_command and accounting_core hard-code `rgba(5,5,5,.9)` and
   `rgba(5,5,5,.88)` on a sticky bar — a near-black slab under the app's name
   that no other app has, in a colour that belongs to no theme. It is also
   still that colour in LIGHT mode, because a literal cannot flip.

   THE FIRST ATTEMPT AT THIS ONLY MOVED THE PROBLEM. Putting the bars on
   --panel-bg made them `rgba(10,10,10,.8)` instead of `rgba(5,5,5,.9)` — a
   different dark slab, still a slab, still nothing like the 91 heads that
   paint `rgba(0,0,0,0)` and simply sit on the page. Token or literal, a filled
   bar next to an unfilled one is the thing that does not match.

   So the fill goes, and the stickiness goes with it — they were one decision.
   A sticky bar needs a ground or the page scrolls through it; a bar with no
   ground cannot be sticky. 91 of 96 app heads are static and transparent, and
   that is the shape the other five join. What stays is the rule underneath,
   which is what separates the head from the content on all 96.

   `.editor-header` is deliberately NOT here. It is a toolbar inside a
   scrolling editor pane, not the band the app names itself in, and it has to
   stay pinned to the top of that pane to be reachable. */
body:not([data-frb-optout~="title"]) :is(.oc-topbar, .ac-topbar, .top-bar,
    .navbar-glass) {
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    position: static;
    border-bottom: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
}

/* ranchie/dashboard draws its head as a tinted card — `rgba(255,255,255,.03)`
   with 35px of padding — while the other 95 heads sit on the page unfilled.
   A card is a container for content; the band naming the app is not one. */
body:not([data-frb-optout~="title"]) .dashboard-header-tile {
    background: none;
    box-shadow: none;
}

/* ═══ WAVE 5 — THE RAIL ON A PHONE ═════════════════════════════════════════
   25 apps still showed the rail as a 70px COLUMN at 390px — 18% of the screen
   given over to navigation before the app gets a pixel — and five showed it as
   a column 94% of the screen tall. Both were mine.

   The phone rules written earlier named `nav.sidebar` and `.sidebar.nav-rail`,
   which is two of the six class names in use. And `grid-template-columns:
   var(--sidebar-w) 1fr` was written OUTSIDE any media query, so on a phone the
   body still reserved a 70px track for a rail that was supposed to have become
   a strip. A rule that collapses the rail but not the column it sits in
   collapses nothing.

   Everything the rail does vertically it does horizontally here: the strip
   scrolls sideways rather than down, and the theme footer stops being a foot
   and becomes the far end of the row. `max-height` is released because the
   ceiling that keeps a column inside the window makes no sense for a strip. */
@media (max-width: 620px) {

/* WHATEVER HOLDS THE RAIL, not only <body>. travel_planning and area_logic
   nest their shell one level down, so a rule anchored to `body:has(> rail)`
   left their rail stretched to the full height of a flex row — 402% and 100%
   of the screen. The parent of a rail IS the shell, wherever it sits. */
body:not([data-frb-optout~="rail"]) :has(> :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav)),
body:not([data-frb-optout~="rail"]):has(> :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav)) {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto 1fr;
    /* AND THE FLEX BODIES, which the grid rule alone did nothing for. Five
       apps lay their shell out as `display: flex` in a row; the rail is a flex
       ITEM there, so it stretched to the height of the row — the whole page.
       The strip was correctly a row of 44px icons and correctly nowrap, and
       measured 2468px tall, because its own height was never the thing setting
       it. `flex-direction: column` is inert on the grid bodies, so one rule
       covers both shells. */
    flex-direction: column;
    align-items: stretch;
}

body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) {
    flex-direction: row;
    /* NOWRAP, and leaving it out was a real regression: the rail became a
       full-width row whose items wrapped into eleven of them, 400% of the
       screen tall, on four apps — and pushed two others sideways. A strip
       scrolls in the direction it runs; it does not wrap. */
    flex-wrap: nowrap;
    align-items: center;
    width: 100%;
    max-width: 100%;
    height: auto;
    max-height: none;
    position: static;
    overflow-x: auto;
    overflow-y: hidden;
    border-right: 0;
    border-bottom: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
    /* 12px, TOP AND BOTTOM, and stated rather than inherited from whatever the
       tallest item in a given rail happens to be. At 8 the inset was a
       consequence: the country page's flex line is 41px, so its 40px icons
       centre at 12 and look right; Kitchen OS's line is exactly 40, so its
       icons sit at 8 and read as touching the header. Same rule, same rails,
       two different results — which is the definition of a number that should
       have been declared.
       THIS FILE, not frb-shell.css, and that is worth a line: the phone rail is
       described in both, and it was the compat rule that was winning. A value
       changed in the file that loses is a change that measures as no change. */
    padding: var(--frb-space-3, 12px) var(--frb-space-3, 12px);
    gap: var(--frb-space-2, 8px);
}
/* Every child keeps its size while the strip scrolls. A 44px icon squeezed to
   12px by a flex row that ran out of room is not a target anyone can hit.

   AND ITS CHILDREN RUN SIDEWAYS TOO. Turning the rail into a row is not enough
   when the icons are one level down: five rails wrap theirs in a
   `.sidebar-steps` column, so the strip became a row containing a very tall
   column — 292%, 302%, 344% and 402% of the screen. The wrapper has to turn
   with it. */
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) > * {
    flex: 0 0 auto;
    flex-direction: row;
    align-items: center;
    max-height: none;
    height: auto;
    margin-top: 0;
    gap: var(--frb-space-2, 8px);
}

/* THE LABEL THAT COMES BACK AS A PSEUDO-ELEMENT. Eleven apps have their own
   `@media (max-width: 900px)` block that turns each rail item into a labelled
   pill — `width: auto; padding: 0 12px; ::after { content: attr(data-label) }`.
   Against the shared 44px square that label has nowhere to go: on Campus OS it
   spilled straight across the next four icons, so the strip read
   "ashbo…ss Sc…ule& Acti…ies…mpus Li…ances…ttings".

   Generated content cannot be hidden by hiding an element, because there is no
   element — `content: none` is the only thing that removes it. The word is not
   lost: every one of these items carries the same text in `title`. */
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)::after {
    content: none;
}

/* The settings were pinned to the BOTTOM of a column with `margin-top: auto`
   and `position: sticky`. In a row that pins them to nothing and leaves them
   mid-strip; `margin-left: auto` is the same instruction turned ninety
   degrees. */
body:not([data-frb-optout~="rail"]) :is(.sidebar, .sidebar-primary,
    .ranchie-sidebar, nav.rail, .nav-rail, .rail-nav) .theme-footer {
    position: static;
    flex-direction: row;
    margin-top: 0;
    margin-left: auto;
    padding-top: 0;
    padding-bottom: 0;
    background: none;
}

}

/* ═══ WAVE 5 — WHAT A FINGER CAN HIT ═══════════════════════════════════════
   Measured at 390px on a touch context: every one of the 100 apps had at least
   one standalone control under 32px, and the smallest were 8px. The list is
   not per-app oddities — it is the same handful of things everywhere. 99 plan
   chips, 58 account chips, 33 theme swatches, and then each app's own row of
   icon buttons: blk-tool, bb-tool, ea-tool, card-action-btn, chore-btn,
   element-control-btn.

   A STANDALONE CONTROL, not every link. A link inside a sentence is not a tap
   target and must not be grown to 32px — that would push the sentence apart.
   The floor applies to buttons, to things with a button role, and to the named
   controls above; prose is left alone.

   32px rather than Apple's 44 or Android's 48: the rail item is 44px and the
   field is 38px, so 32 is the floor beneath the product's own sizes rather
   than a number imported from a guideline. Raising it further would relayout
   toolbars that currently fit. */
@media (max-width: 620px), (pointer: coarse) {

body:not([data-frb-optout~="tap"]) :is(button, [role="button"], .btn,
    .theme-dot, .frb-plan-chip, .frb-acct, .frb-select, select,
    input[type="checkbox"], input[type="radio"]) {
    min-width: 32px;
    min-height: 32px;
}
/* The swatch stays a small dot; the BOX around it is what grew. Without this
   the four theme colours become four 32px slabs and the foot of the rail
   stops reading as a colour picker. */
body:not([data-frb-optout~="tap"]) .theme-dot {
    background-clip: content-box;
    padding: 9px;
    border-radius: 50%;
}

/* THE BREADCRUMB'S OWN LINKS, which were 256 of the remaining small controls —
   three per trail across 93 apps. They are text links, so a `min-height` does
   nothing to them: an inline box takes its height from the line, not from a
   declared one. Vertical PADDING on an inline box grows the hit area without
   growing the line, so the row keeps its height and the target doubles. */
body:not([data-frb-optout~="tap"]) .frb-appcrumb a {
    padding: 8px 2px;
    margin: 0 -2px;
}

/* ═══ WAVE 5 — WHAT THE PLAN BADGE COVERS ══════════════════════════════════
   The Student Boost badge is fixed to the bottom-right corner on 97 pages. At
   390px it lands on top of the last thing in the page on 38 of them — 25 of
   those a hub's final app card. The badge cannot move (bottom-left is the
   theme swatches, and the top corners are the header), so the page ends above
   it instead: one badge's height of clearance at the foot of the scroll. */
body:not([data-frb-optout~="tap"]) :is(.app-grid, .main-content, .main-container,
    .main-stage, .page-content, .content-wrapper, main) {
    padding-bottom: 72px;
}

/* ═══ WAVE 5 — TEXT SMALL ENOUGH TO GIVE UP ON ═════════════════════════════
   20 apps set blocks of reading text below 11px on a phone, and 150 of those
   are one class: `.stat-m` on the geo pages, at 10px. The rest are a
   disclaimer at 9px and a scatter of 10px notes.

   Only elements carrying REAL text are named here — the earlier count of "56
   apps with text under 10px" swept up every badge, count and superscript,
   which is why it never turned into a fix. A badge at 9px is a badge; a
   paragraph at 9px is a paragraph nobody reads. */
body:not([data-frb-optout~="type"]) :is(.stat-m, .stat-sub, .disclaimer,
    .metric-label, .meal-note, .frb-count, .rc-tool-group, .gpa-tool-group,
    .ea-tool-group) {
    font-size: 11px;
    line-height: 1.5;
}

}

/* ═══ WAVE 5 — GRADIENTS IN THE HEAD ═══════════════════════════════════════
   The head was made unfilled everywhere by clearing `background-color`, and a
   gradient is not a background COLOUR — it is a background IMAGE, so it went
   straight through that rule and through the check that verified it. Three
   shapes survived:

     .module-header      a top-to-bottom white wash on 23 hub pages
     .campus-header      a 120px blue-to-green band behind the head of
     .hr-header          Campus OS and Onboarding OS, at 10% opacity and
                         z-index -1 — invisible in a diff, obvious on screen,
                         and in two colours that belong to no theme here
     .status-bar         a 4px gradient rule above Ops Command's title, at
                         z-index 1000, which no other app draws

   All three are decoration on the one band that is supposed to look identical
   on 101 apps. The rule underneath the head is what separates it from the
   content; nothing else in that band should be painted. */
body:not([data-frb-optout~="title"]) :is(.header-section, .module-header,
    .page-head, .dashboard-header, .header-main, .dashboard-header-tile,
    .gpa-header, .focus-header, .capital-header, .pipeline-header,
    .kitchen-header, .family-header, .oc-topbar, .ac-topbar, .top-bar,
    .topbar, .navbar-glass) {
    background-image: none;
}
body:not([data-frb-optout~="title"]) :is(.campus-header, .hr-header,
    .status-bar, .app-header-glow, .header-glow) {
    display: none;
}

/* ═══ WAVE 5 — A THEME IS THE PRODUCT'S, NOT THE PAGE'S ════════════════════
   Set `feerebu_theme` to ocean and load all 101 apps: 71 render the accent at
   #4fa3d1, which is what the token says, and six do not. Tech Stack Architect,
   Accounting Core, Revenue Model and Financial Suite paint #00f2fe; Terra View
   #4fc3f7; area_logic #38bdf8. Each redefines `--accent-primary` inside its own
   `body.theme-ocean` block, so the reader picks Ocean and gets that page's idea
   of Ocean — which is why Tech Stack Architect "opens in blue every time" no
   matter what was chosen.

   Re-asserted here at (0,3,1), which beats a page's own `body.theme-ocean` at
   (0,1,1). The values are copied from frb-tokens.css rather than re-picked.

   `:not(.light-mode)` MATTERS. The light palette is a different set of
   accents, chosen to clear 4.5:1 on a pale ground — #866d1d rather than
   #d4af37, and so on. Re-asserting the dark values unconditionally would drop
   every theme back below AA in light mode, undoing Wave 3. */
body.theme-midnight:not(.light-mode):not([data-frb-optout~="theme"]) {
    --accent-primary: #d4af37;
    --accent-primary-rgb: 212, 175, 55;
    --accent-glow: rgba(212, 175, 55, 0.3);
}
body.theme-ocean:not(.light-mode):not([data-frb-optout~="theme"]) {
    --accent-primary: #4fa3d1;
    --accent-primary-rgb: 79, 163, 209;
    --accent-glow: rgba(79, 163, 209, 0.25);
}
body.theme-sunset:not(.light-mode):not([data-frb-optout~="theme"]) {
    --accent-primary: #ff9f43;
    --accent-primary-rgb: 255, 159, 67;
    --accent-glow: rgba(255, 159, 67, 0.25);
}
body.theme-forest:not(.light-mode):not([data-frb-optout~="theme"]) {
    --accent-primary: #66ff66;
    --accent-primary-rgb: 102, 255, 102;
    --accent-glow: rgba(102, 255, 102, 0.2);
}

/* ═══ WAVE 5 — ONE GUTTER, COUNTED ONCE ════════════════════════════════════
   Two different things read as "useless padding at the sides".

   ONE GUTTER, DIFFERENT WIDTHS. Ops Command's `.view-section` insets its
   content 60px each side where the product's own head inset is 40, so its
   cards start 20px further in than the title above them and 20px further in
   than every other app's.

   THE SAME GUTTER TWICE. Compensation Policy nests `.policy-container`
   (padding 40) inside `.page-content` (padding 40) — 80px of inset each side,
   160px of the screen spent on nothing, and neither container is wrong on its
   own. This is the shape worth naming: a gutter is a property of the COLUMN,
   not of every box in it, and two boxes that each know the gutter produce two
   gutters. The inner one gives it up. */
body:not([data-frb-optout~="gutter"]) :is(.view-section, .page-content,
    .main-stage, .content-wrapper, .policy-container, .doc-container) {
    padding-left: var(--frb-gutter, 40px);
    padding-right: var(--frb-gutter, 40px);
}
body:not([data-frb-optout~="gutter"]) :is(.view-section, .page-content,
    .main-stage, .content-wrapper, .policy-container, .doc-container)
    :is(.view-section, .page-content, .main-stage, .content-wrapper,
    .policy-container, .doc-container) {
    padding-left: 0;
    padding-right: 0;
}

@media (max-width: 620px) {
    /* A 40px gutter is a tenth of a 390px screen on each side. */
    body:not([data-frb-optout~="gutter"]) :is(.view-section, .page-content,
        .main-stage, .content-wrapper, .policy-container, .doc-container) {
        padding-left: var(--frb-space-3, 12px);
        padding-right: var(--frb-space-3, 12px);
    }
}

/* ═══ WAVE 5 — THE LAST OF THE PHONE ═══════════════════════════════════════
   What survived the first tap-target pass, named by where it sits rather than
   by what it is called: 18 links in a page's own `.crumbs`, 12 in a
   `.breadcrumbs`, 10 in ours, 11 in a flex row, 8 in a table cell, 7 in a list
   item, 4 in a footer. All the same shape — a text link standing on its own as
   a control, 17px tall because that is how tall a line of 12px text is.

   Vertical padding, not `min-height`: an inline box takes its height from the
   line, so a declared height does nothing to it, while padding grows the hit
   area and leaves the line where it was. The negative margin keeps the
   horizontal rhythm of a trail unchanged. */
@media (max-width: 620px), (pointer: coarse) {

body:not([data-frb-optout~="tap"]) :is(.crumbs, .breadcrumbs, .frb-appcrumb,
    .footer-links, .contact-item, .prov, td, li) > a {
    padding-top: 8px;
    padding-bottom: 8px;
}
body:not([data-frb-optout~="tap"]) :is(.crumbs, .breadcrumbs, .frb-appcrumb) > a {
    padding-left: 2px;
    padding-right: 2px;
    margin-left: -2px;
    margin-right: -2px;
}
/* Two named controls the generic floor did not reach, because each is sized by
   a rule of its own further down its page. */
body:not([data-frb-optout~="tap"]) :is(.element-control-btn, .text-decoration-none,
    .footer-link, .plan-badge) {
    min-width: 32px;
    min-height: 32px;
}

/* The last blocks of body copy under 11px: a bare <small> on three pages, and
   three named classes. `small` is only floored where it is carrying real text
   — a <small> inside a badge or a count keeps its size, because those are not
   read, they are glanced at. */
body:not([data-frb-optout~="type"]) :is(.alert-desc, .mono-label, .note-sm,
    .hint-sm) {
    font-size: 11px;
    line-height: 1.5;
}
body:not([data-frb-optout~="type"]) p > small,
body:not([data-frb-optout~="type"]) li > small,
body:not([data-frb-optout~="type"]) td > small {
    font-size: 11px;
}

/* area_logic pins its rail with `height: 100vh`, which survived the strip
   rules because a declared height is not something `height: auto` in a
   shorthand can undo when the element is also `position: sticky` — the sticky
   box keeps its own block size. Released explicitly here. */
body:not([data-frb-optout~="rail"]) nav.rail {
    height: auto;
    min-height: 0;
    max-height: none;
    position: static;
    align-self: flex-start;
}

/* THE PAPER IS 794px BECAUSE A4 IS 794px. The resume and menu editors render a
   real sheet at 96dpi, and that is the last thing in the product that should
   be squeezed to fit a phone — a preview that lies about the page it is
   previewing is worse than one you have to scroll. So the sheet keeps its
   width and its FRAME scrolls, which is what a document viewer does. It was
   pushing the whole page sideways by 123 and 208px instead. */
body:not([data-frb-optout~="mobile"]) :is(.paper-wrapper, .sheet-wrap,
    .preview-paper, .page-sheet) {
    max-width: 100%;
    overflow-x: auto;
    overscroll-behavior-x: contain;
}

/* ANYTHING THAT FLOATS OVER THE PAGE STAYS INSIDE IT. A toast host built by a
   page's own script carries inline widths this layer never sees, and on two
   editors it measured 489px inside a 390px screen. A fixed box normally cannot
   scroll the document — but inside a transformed ancestor it is no longer
   fixed to the viewport, it is absolute to that ancestor, and then it can.
   Capped rather than repositioned, so a page that places its own overlay
   somewhere deliberate keeps that placement. */
body:not([data-frb-optout~="mobile"]) :is(.frb-toast-wrap, .toast-wrap,
    .toast-container, .toast-host, .toast-stack, .notification-center,
    .frb-plan-float, .frb-pop) {
    max-width: calc(100vw - 24px);
}

}

/* ═══════════════════════════════════════════════════════════════════════════
   THE HUB AND PORTAL TILES ON A PHONE
   ───────────────────────────────────────────────────────────────────────────
   28 pages carry their own inline copy of the same `.app-grid` block —
   `padding: 40px`, four columns, a 240px row floor — and each copy drops to
   one column at 768px WITHOUT touching the padding. So on a 390px screen the
   grid spent 80 of its 390 pixels on side gutters and gave the remaining 310
   to a single tile 500px tall: one tile, most of a screen, and a wide margin
   of nothing either side of it. That is the whole of "too much space on the
   sides and only one tile that big".

   `body .app-grid`, not `.app-grid`: twenty-eight pages, and this has to win
   on all of them without depending on whether that page happens to link this
   sheet after its own <style>. One element selector is enough to make it
   (0,1,1) against their (0,1,0) and stop the question being asked.

   The gutter is the same token the app shell uses, so a hub page and an app
   page have one left edge between them.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 620px) {
    body .app-grid {
        padding: var(--frb-space-4, 16px) var(--frb-shell-gutter, 14px)
                 var(--frb-space-10, 48px);
        gap: var(--frb-space-3, 12px);
        /* A 240px floor under every row is a floor under nothing here: the
           tiles are 500px of their own content. It only ever padded out the
           short ones. */
        grid-auto-rows: auto;
    }
    /* TWO UP, WHICH IS WHAT THE PAGE IS ON A DESKTOP. One column was still one
       tile filling the screen, only a wider one. The desktop lays four columns
       with the hero spanning two; a phone lays two with the hero spanning two,
       which is the same page at a smaller size rather than a different one. */
    body .app-grid { grid-template-columns: repeat(2, 1fr); }
    /* .tile-wide and .tile-large are profile.html's and dev_profile.html's
       names for exactly what .card-wide means here, and leaving them out of
       this list is not a cosmetic miss. Those two pages put FORMS in their
       wide tiles, so at two up each one got a 175px column: "Personal
       Information" rendered its full name, email and bio one character per
       line down a 20px-wide strip. The two pages already say span 2 at their
       own 800px breakpoint; this rule is (0,1,1) against their (0,1,0) and was
       silently taking it back on phones.

       Only these two files use the names — checked, not assumed — so this
       list is complete rather than a first instalment. */
    body .app-grid > :is(.card-hero, .card-wide, .tile-wide, .tile-large) { grid-column: span 2; }

    body .app-grid > :is(.app-card, .card-hero, .card-wide, .tile, .app-tile) {
        padding: var(--frb-space-3, 12px);
        border-radius: 14px;
    }

    /* THE TYPE INSIDE THE TILE IS NOT THE PAGE'S TITLE. `.app-title` is in the
       shared app-title rule's selector list, so a tile's name was being set at
       --frb-app-title-size — the size meant for the one heading that names the
       whole screen. In a 175px tile that is 23px type on a 151px line: two or
       three words to the line, and it is why the tiles were 400px tall with
       almost nothing in them.
       `body .app-grid .app-title` is (0,2,1) against that rule's (0,2,0), which
       is what it takes to say a tile heading is a tile heading. */
    body .app-grid :is(.app-title, .card-title, h3) {
        font-size: 15px;
        line-height: 1.25;
        letter-spacing: 0;
    }
    body .app-grid :is(.app-desc, .card-desc, p) { font-size: 11.5px; line-height: 1.45; }
    body .app-grid .app-icon {
        width: 38px; height: 38px; min-width: 38px; min-height: 38px;
        font-size: 17px; border-radius: 10px;
    }
    body .app-grid :is(.card-hero, .card-wide) .app-icon {
        width: 44px; height: 44px; min-width: 44px; min-height: 44px; font-size: 19px;
    }

    /* AND THE BADGE COMES OUT OF THE CORNER. It is `position: absolute` at the
       tile's top right, which is safe while a tile is 362px wide and is not at
       175: measured, the 99px "Operational" pill ran from x=69 and the 50px
       icon from x=31, so the pill was printed across the icon. An absolute box
       does not know how wide its tile is; a flow one cannot help but know.
       Top of the card and right-aligned keeps the desktop's reading order —
       status first, then what it is the status of. */
    body .app-grid .status-badge {
        position: static;
        align-self: flex-end;
        margin-bottom: 6px;
        max-width: 100%;
    }
    /* The hub head is on the same gutter as the grid under it. Left at 40px it
       was the one thing on the page still inset a desktop's worth, so the
       title sat 26px to the right of the tiles it names. */
    body :is(.module-header, .hub-header, .portal-header) {
        padding: var(--frb-space-5, 20px) var(--frb-shell-gutter, 14px)
                 var(--frb-space-3, 12px);
    }
}
