/*! frb-components.css — the Feerebu design system, layer 2 of 3.
 *
 * LOADING ORDER IS LOAD-BEARING. Link this file as the LAST element before
 * </head>, AFTER the page's own <style> block:
 *
 *     <style> ...the page's CSS... </style>
 *     <link rel="stylesheet" href="/assets/css/frb-tokens.css">
 *     <link rel="stylesheet" href="/assets/css/frb-components.css">
 *     <link rel="stylesheet" href="/assets/css/frb-compat.css">
 *     </head>
 *
 * A <style> block always comes after a <link> in normal head order, so at equal
 * specificity the PAGE wins — a design system linked the usual way would have
 * been silently overridden on all 123 themed pages. Putting the links last
 * flips that. It is the whole reason this can be adopted without first deleting
 * 66,000 lines of page CSS.
 *
 * WAVE 1: buttons.
 */

/* ═══════════════════════════════════════════════════════════════════════════
   BUTTON

   131 distinct button class names exist across the app pages. They collapse to
   five intents and three sizes.

   Every state is defined here because almost none of them were defined
   anywhere: :focus-visible (keyboard users got the browser default, differently
   on every page), :disabled (most pages relied on the attribute doing nothing
   visible), and a loading state (pages hand-rolled spinners into labels).
   ═══════════════════════════════════════════════════════════════════════════ */

.frb-btn {
    /* Reset first — these are used on <button>, <a> and <div> across the site,
       and they must not look different depending on which tag was reached for. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--frb-space-2);
    box-sizing: border-box;
    margin: 0;
    text-decoration: none;
    white-space: nowrap;
    vertical-align: middle;
    -webkit-appearance: none;
    appearance: none;

    /* Every one of these reads a --frb-btn-* token from frb-tokens.css, where
       the product's button is defined once. The fallbacks are what this rule
       said before the button was promoted, so a page that loads this sheet
       without the token layer still gets a button rather than a bare <button>. */
    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;
    background: transparent;
    color: var(--text-primary);
    cursor: pointer;

    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);
}

/* One ring for the whole product. :focus-visible, not :focus — a mouse click
   should not leave a ring behind, which is why pages that used :focus turned it
   off again and left keyboard users with nothing. */
.frb-btn: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);
}

.frb-btn:active:not(:disabled):not([aria-disabled="true"]) { transform: translateY(1px); }

/* Disabled is one look regardless of how it was expressed: the attribute, the
   ARIA state, or a class. All three are used across the site. */
.frb-btn:disabled,
.frb-btn[aria-disabled="true"],
.frb-btn.is-disabled {
    opacity: .45;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
    pointer-events: none;
}

/* ── intents ──────────────────────────────────────────────────────────────
   --frb-btn-tint lets a caller retint one button without a new class, which is
   what most of the 131 one-off names were actually doing. */
.frb-btn-primary {
    background: var(--frb-btn-tint, var(--accent-fill, var(--accent-primary)));
    color: var(--btn-text);
    box-shadow: var(--frb-btn-glow, 0 0 18px var(--accent-glow));
}
/* The hard offset block, which is the half of this button that makes the
   square read as deliberate rather than as a missing radius. It moves up and
   left and drops a solid block behind itself; the glow stays lit underneath so
   the press has both. transform is already in the .frb-btn transition list. */
.frb-btn-primary:hover:not(:disabled) {
    filter: brightness(1.08);
    transform: translate(-3px, -3px);
    box-shadow: 6px 6px 0 var(--text-primary),
                var(--frb-btn-glow-hi, 0 0 28px var(--accent-glow));
}
/* :active already sets translateY(1px) further up; without this it would fight
   the hover offset and the button would jump sideways on the way down. */
.frb-btn-primary:active:not(:disabled) {
    transform: translate(-3px, -2px);
    box-shadow: 3px 3px 0 var(--text-primary),
                var(--frb-btn-glow, 0 0 22px var(--accent-glow));
}
@media (prefers-reduced-motion: reduce) {
    .frb-btn-primary:hover:not(:disabled),
    .frb-btn-primary:active:not(:disabled) { transform: none; }
}

.frb-btn-secondary {
    background: var(--inset-bg);
    border-color: var(--inset-border);
    color: var(--text-primary);
}
.frb-btn-secondary:hover:not(:disabled) { background: var(--frb-surface-hover); border-color: var(--accent-primary); }

.frb-btn-outline {
    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)));
}
.frb-btn-outline:hover:not(:disabled) { background: var(--accent-glow); color: var(--text-primary); }

.frb-btn-ghost {
    background: transparent;
    border-color: transparent;
    color: var(--text-secondary);
}
.frb-btn-ghost:hover:not(:disabled) { background: var(--inset-bg); color: var(--text-primary); }

.frb-btn-danger {
    background: var(--danger);
    border-color: var(--danger);
    /* #fff here measured 2.77:1 on the dark-mode danger fill. The ink is a
       token and flips with the mode -- see --danger-ink in frb-tokens.css. */
    color: var(--danger-ink, #fff);
}
.frb-btn-danger:hover:not(:disabled) { filter: brightness(1.1); }

/* ── sizes ─────────────────────────────────────────────────────────────── */
.frb-btn-xs { min-height: 26px; padding: 0 var(--frb-space-2); font-size: var(--frb-text-xs); border-radius: var(--frb-btn-radius, var(--frb-radius-sm)); gap: var(--frb-space-1); }
.frb-btn-sm { 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)); }
.frb-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)); }

/* ── shapes ────────────────────────────────────────────────────────────── */
/* Square icon button. Seven different names existed for exactly this. */
.frb-btn-icon { padding: 0; width: 38px; min-height: 38px; }
.frb-btn-icon.frb-btn-xs { width: 26px; min-height: 26px; }
.frb-btn-icon.frb-btn-sm { width: var(--frb-btn-sm-h, 32px); min-height: var(--frb-btn-sm-h, 32px); }
.frb-btn-icon.frb-btn-lg { width: var(--frb-btn-lg-h, 46px); min-height: var(--frb-btn-lg-h, 46px); }

.frb-btn-pill  { border-radius: var(--frb-radius-pill); }
.frb-btn-block { display: flex; width: 100%; }

/* ── loading ───────────────────────────────────────────────────────────────
   The label stays in the DOM and keeps its width, so the button does not
   resize and the layout does not jump — the reason hand-rolled versions of
   this looked broken. */
.frb-btn.is-loading { color: transparent !important; pointer-events: none; position: relative; }
.frb-btn.is-loading::after {
    content: "";
    position: absolute;
    width: 15px; height: 15px;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: var(--frb-radius-circle);
    color: var(--btn-text);
    animation: frb-spin .6s linear infinite;
}
.frb-btn-secondary.is-loading::after,
.frb-btn-outline.is-loading::after,
.frb-btn-ghost.is-loading::after { color: var(--text-primary); }

@keyframes frb-spin { to { transform: rotate(360deg); } }

/* A group of buttons welded into one control. */
.frb-btn-group { display: inline-flex; }
.frb-btn-group > .frb-btn { border-radius: 0; margin-left: -1px; }
/* The END CAPS take the button token like every other button. They were
   --frb-radius-md, so in a product whose buttons are square a group of three
   was two rounded ends around a square middle — the one place the shape
   changed depending on how many buttons stood next to each other. */
.frb-btn-group > .frb-btn:first-child { border-radius: var(--frb-btn-radius, var(--frb-radius-md)) 0 0 var(--frb-btn-radius, var(--frb-radius-md)); margin-left: 0; }
.frb-btn-group > .frb-btn:last-child  { border-radius: 0 var(--frb-btn-radius, var(--frb-radius-md)) var(--frb-btn-radius, var(--frb-radius-md)) 0; }
.frb-btn-group > .frb-btn:hover { z-index: 1; }

@media (prefers-reduced-motion: reduce) {
    .frb-btn { transition: none; }
    .frb-btn:active:not(:disabled) { transform: none; }
    .frb-btn.is-loading::after { animation-duration: 2s; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   WAVE 2 — FORM CONTROLS

   162 distinct class names sit on a form element across 72 pages. That number
   is the smaller half of the problem: 1,063 of the 1,625 controls in this tree
   carry NO class at all and are styled only by bare `input {}` rules, of which
   there are 192 across 58 pages. A layer that maps class names would have
   missed two thirds of the product's inputs.

   Three states were essentially undefined before this wave, across 143 pages:

     ::placeholder    ONE rule in the entire tree. Everywhere else the browser
                      draws the text colour at roughly 54% alpha, which fails AA
                      on every ground this product uses.
     :disabled        ONE rule. Pages relied on the attribute doing nothing
                      visible, so a dead field looked exactly like a live one.
     :focus-visible   14 rules for 1,625 controls. Keyboard users got whatever
                      the browser felt like, differently on every page.

   The focus ring takes --accent-text rather than --accent-primary. A focus
   indicator is held to 3:1 against what surrounds it (WCAG 2.2 SC 1.4.11), and
   the bright light-mode accent is 2.53:1 on the page ground — a ring you cannot
   find is not an indicator.

   <select> KEEPS ITS NATIVE ARROW. Removing appearance means hand-drawing a
   chevron that then has to be re-coloured per theme and per mode, and it costs
   the native picker on touch devices. Styling the box and letting the engine
   draw the control is fewer moving parts and better behaviour; color-scheme in
   the token file is what makes the arrow and the open list follow the mode.
   ═══════════════════════════════════════════════════════════════════════════ */

.frb-field { display: flex; flex-direction: column; gap: var(--frb-space-2); min-width: 0; }
.frb-field-row { display: flex; align-items: center; gap: var(--frb-space-3); }

.frb-label {
    font-size: 12px; font-weight: 600; letter-spacing: .02em;
    color: var(--text-secondary); line-height: 1.4;
}
/* The asterisk is not the message. A required field says so in the label; this
   is the mark beside it, and it is hidden from screen readers because `required`
   on the control already tells them. */
.frb-label[data-required]::after { content: " *"; color: var(--danger, #dc2626); }

.frb-help  { font-size: 12px; color: var(--text-secondary); line-height: 1.4; }
.frb-error { font-size: 12px; color: var(--danger, #dc2626); line-height: 1.4; font-weight: 500; }

/* ── the shared control ─────────────────────────────────────────────────── */
.frb-input, .frb-textarea, .frb-select, .frb-search-field {
    box-sizing: border-box;
    width: 100%;
    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-radius-md, 10px);
    padding: 10px 12px;
    transition: border-color var(--frb-dur-fast, .15s) ease,
                box-shadow var(--frb-dur-fast, .15s) ease,
                background-color var(--frb-dur-fast, .15s) ease;
}
.frb-textarea { min-height: 92px; resize: vertical; }
.frb-select   { padding-right: 8px; cursor: pointer; }

.frb-input::placeholder, .frb-textarea::placeholder, .frb-search-field::placeholder {
    /* Full opacity on purpose. The browser default multiplies the text colour by
       about .54, which is what puts placeholder text under AA on every one of
       this product's grounds. --text-secondary is already the "quieter than
       body text" value and it is measured to pass. */
    color: var(--text-secondary);
    opacity: 1;
}

.frb-input:hover:not(:disabled), .frb-textarea:hover:not(:disabled),
.frb-select:hover:not(:disabled) { border-color: var(--accent-text, var(--accent-primary)); }

.frb-input:focus-visible, .frb-textarea:focus-visible,
.frb-select:focus-visible, .frb-search-field:focus-visible {
    outline: 2px solid var(--accent-text, var(--accent-primary));
    outline-offset: 1px;
    border-color: var(--accent-text, var(--accent-primary));
}
/* Mouse users get the same affordance without the ring shouting. */
.frb-input:focus:not(:focus-visible), .frb-textarea:focus:not(:focus-visible),
.frb-select:focus:not(:focus-visible) {
    outline: none; border-color: var(--accent-text, var(--accent-primary));
}

.frb-input:disabled, .frb-textarea:disabled, .frb-select:disabled {
    opacity: .55; cursor: not-allowed;
    background: var(--inset-bg);
}
.frb-input[readonly], .frb-textarea[readonly] {
    background: var(--inset-bg); cursor: default;
}
/* Both spellings: aria-invalid is what assistive tech reads, .is-invalid is what
   the markup in this tree already uses. */
.frb-input[aria-invalid="true"], .frb-textarea[aria-invalid="true"],
.frb-select[aria-invalid="true"], .frb-input.is-invalid,
.frb-textarea.is-invalid, .frb-select.is-invalid {
    border-color: var(--danger, #dc2626);
}
.frb-input[aria-invalid="true"]:focus-visible, .frb-input.is-invalid:focus-visible {
    outline-color: var(--danger, #dc2626);
}

/* ── checkbox, radio, range ─────────────────────────────────────────────── */
/* accent-color is one declaration and the engine draws the rest correctly in
   both modes — a hand-built checkbox is markup every page would have to adopt. */
.frb-check, .frb-radio, .frb-range {
    accent-color: var(--accent-fill, var(--accent-primary));
    cursor: pointer;
}
.frb-check, .frb-radio { width: 16px; height: 16px; margin: 0; flex: none; }
.frb-range { width: 100%; height: 20px; background: transparent; }
.frb-check:focus-visible, .frb-radio:focus-visible, .frb-range:focus-visible {
    outline: 2px solid var(--accent-text, var(--accent-primary)); outline-offset: 2px;
}
.frb-check:disabled, .frb-radio:disabled, .frb-range:disabled { opacity: .5; cursor: not-allowed; }

/* A checkbox with a label beside it is one target, not two. */
.frb-choice { display: inline-flex; align-items: center; gap: var(--frb-space-2);
              cursor: pointer; font-size: 14px; color: var(--text-primary); }
.frb-choice:has(:disabled) { opacity: .55; cursor: not-allowed; }

/* ── switch ────────────────────────────────────────────────────────────── */
/* The one control with no native element. The checkbox stays in the markup and
   keeps the semantics; only its painting is replaced. */
.frb-switch { display: inline-flex; align-items: center; gap: var(--frb-space-3);
              cursor: pointer; font-size: 14px; color: var(--text-primary); }
.frb-switch > input[type="checkbox"] {
    appearance: none; -webkit-appearance: none;
    width: 38px; height: 22px; flex: none; margin: 0; cursor: pointer;
    border-radius: var(--frb-radius-pill, 999px);
    background: var(--inset-bg);
    border: 1px solid var(--input-border);
    position: relative;
    transition: background-color var(--frb-dur-fast, .15s) ease,
                border-color var(--frb-dur-fast, .15s) ease;
}
.frb-switch > input[type="checkbox"]::after {
    content: ""; position: absolute; top: 50%; left: 3px; transform: translateY(-50%);
    width: 14px; height: 14px; border-radius: 50%;
    background: var(--text-secondary);
    transition: left var(--frb-dur-fast, .15s) ease, background-color var(--frb-dur-fast, .15s) ease;
}
.frb-switch > input[type="checkbox"]:checked {
    background: var(--accent-fill, var(--accent-primary));
    border-color: var(--accent-fill, var(--accent-primary));
}
.frb-switch > input[type="checkbox"]:checked::after {
    left: 19px; background: var(--btn-text, #000);
}
.frb-switch > input[type="checkbox"]:focus-visible {
    outline: 2px solid var(--accent-text, var(--accent-primary)); outline-offset: 2px;
}
.frb-switch > input[type="checkbox"]:disabled { opacity: .5; cursor: not-allowed; }

/* ── file ──────────────────────────────────────────────────────────────── */
.frb-file { position: relative; display: block; }
.frb-file > input[type="file"] {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; cursor: pointer;
}
.frb-file-drop {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: var(--frb-space-2); text-align: center;
    padding: var(--frb-space-6, 22px);
    border: 1px dashed var(--input-border);
    border-radius: var(--frb-radius-lg, 12px);
    background: var(--input-bg);
    color: var(--text-secondary); font-size: 13px;
    transition: border-color var(--frb-dur-fast, .15s) ease, background-color var(--frb-dur-fast, .15s) ease;
}
.frb-file > input[type="file"]:hover + .frb-file-drop,
.frb-file > input[type="file"]:focus-visible + .frb-file-drop,
.frb-file.is-dragover .frb-file-drop {
    border-color: var(--accent-text, var(--accent-primary));
    background: var(--frb-surface-hover, var(--inset-bg));
}
.frb-file > input[type="file"]:focus-visible + .frb-file-drop {
    outline: 2px solid var(--accent-text, var(--accent-primary)); outline-offset: 2px;
}

/* ── search ────────────────────────────────────────────────────────────── */
.frb-search { position: relative; display: flex; align-items: center; }
.frb-search > .frb-search-field { padding-left: 34px; }
.frb-search > i, .frb-search > svg, .frb-search > .frb-search-icon {
    position: absolute; left: 11px; pointer-events: none;
    color: var(--text-secondary); font-size: 13px; line-height: 1;
}

@media (prefers-reduced-motion: reduce) {
    .frb-input, .frb-textarea, .frb-select, .frb-search-field,
    .frb-switch > input[type="checkbox"], .frb-switch > input[type="checkbox"]::after,
    .frb-file-drop { transition: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   WAVE 3 — SURFACES

   270 distinct class names across 4,326 uses, for what turns out to be three
   things: a raised surface you put content on, a flatter one you inset into it,
   and a tile that shows one number. `app-card` is 541 uses over 28 pages,
   `glass-card` 497 over 28, `card-title` 472 over 29, `stat-card` 128 over 23 —
   the same object, renamed per page.

   THE TOKENS UNDERNEATH ARE THE REAL WORK, and this wave counted them FIRST
   rather than discovering it at the end for the third time:

       --panel-bg      473 declarations   129 pages   54 distinct values
       --sidebar-bg    364                 72          27
       --card-bg       283                 62          33
       --glass-border  237                124          18
       --card-shadow   135                 79          11

   1,492 declarations. A component layer alone gives every page the same RULE and
   leaves it looking different — which is exactly what happened to .form-input in
   Wave 2 until its token was swept.

   BUT THAT SWEEP IS NOT MECHANICAL HERE, and this wave has the measurement to
   prove it. 440 of those declarations are byte-identical to this file, so
   removing them looks like a pure no-op. It is not: deleting a declaration
   changes which rule WINS, and on pages that paint their ground with a gradient
   the surviving translucent value composites over something else entirely. The
   attempt moved home.html from 3 sub-AA nodes to 16 — panels resolving to
   rgb(108,108,108) instead of near-white — and was reverted. The convergence has
   to be done per page with a render behind it, not by a regex over 102 files.

   ── HOW IT ACTUALLY WENT ──────────────────────────────────────────────────

   The count above was of DECLARATIONS. Rendering all 134 pages and reading
   what the surfaces resolve to tells a different story, and it is the one that
   decided the wave:

     · 164 of the 245 surface names appear on ONE page. There is no
       disagreement to settle there, and Wave 1's .view-btn is the standing
       reminder of what mapping a single-page name costs — it converged nothing
       and erased a segmented control's selected state.
     · Of the names that do span pages, the majority already agree. app-card is
       identical on 25 of 28 pages, module-header on 35 of 37, card-wide and
       card-hero on 24 of 25.
     · Four names are excluded outright: status-badge renders eight ways
       because green, amber and red MEAN something. Colour that encodes state
       is not drift.

   So no surface names were added to frb-compat.css. There was no drift for a
   rule layer to settle — the drift was underneath, in the values, exactly as
   the paragraph above predicted, and it was almost entirely in LIGHT MODE:

       --panel-bg        13 resolved values across 134 pages  →  1
       --glass-border    11  →  1
       --sidebar-bg      10  →  1
       --text-secondary   8  →  1
       --card-bg          7  →  1
       --card-shadow      6  →  1

   Two of those were fixed by changing this system's own value rather than the
   pages'. --card-bg was a 2.5% tint that composited to within 1.02:1 of the
   page it sat on; --panel-bg was translucent enough at .7 that what text on a
   panel had to clear depended on where the panel sat over the gradient.

   The mechanical part of the earlier failed sweep was right about one thing
   and wrong about another. Deleting a declaration DOES change which rule wins
   — but ten pages needed exactly that, because `body.theme-midnight` at
   (0,1,1) outranks `.light-mode` at (0,1,0) and their dark sidebar survived
   the mode switch no matter what this file said. What makes it safe now is not
   a better regex, it is tools/styleguide/snapshot.js: every classed node's
   computed style, before and after, across all 134 pages.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── the raised surface ─────────────────────────────────────────────────── */
.frb-card {
    box-sizing: border-box;
    background: var(--panel-bg);
    border: var(--glass-border);
    border-radius: var(--frb-radius-lg, 14px);
    box-shadow: var(--card-shadow);
    padding: var(--frb-space-5, 18px);
    color: var(--text-primary);
    min-width: 0;
}
/* Interactive only when it IS interactive. A card that lifts on hover is making
   a promise about clicking it. */
.frb-card--link {
    display: block; text-decoration: none; cursor: pointer;
    transition: transform var(--frb-dur-fast, .15s) ease,
                border-color var(--frb-dur-fast, .15s) ease,
                box-shadow var(--frb-dur-fast, .15s) ease;
}
.frb-card--link:hover { transform: translateY(-2px); border-color: var(--accent-text, var(--accent-primary)); }
.frb-card--link:focus-visible {
    outline: 2px solid var(--accent-text, var(--accent-primary)); outline-offset: 2px;
}

.frb-card-head {
    display: flex; align-items: center; gap: var(--frb-space-3, 10px);
    margin-bottom: var(--frb-space-4, 14px);
}
.frb-card-title {
    font-size: 13px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
    color: var(--text-secondary); margin: 0; line-height: 1.2;
}
.frb-card-sub  { font-size: 13px; color: var(--text-secondary); margin: 0; line-height: 1.4; }
.frb-card-title + .frb-card-sub { margin-top: 2px; }
.frb-card-body { min-width: 0; }
.frb-card-foot {
    display: flex; align-items: center; gap: var(--frb-space-3, 10px);
    margin-top: var(--frb-space-4, 14px);
    padding-top: var(--frb-space-3, 10px);
    border-top: var(--glass-border);
}

/* ── the inset surface ──────────────────────────────────────────────────── */
/* A panel sits INSIDE a card. It is quieter and it casts no shadow — a shadow
   inside a shadow is how a layout starts to look like sediment. */
.frb-panel {
    box-sizing: border-box;
    background: var(--inset-bg);
    border: 1px solid var(--inset-border);
    border-radius: var(--frb-radius-md, 10px);
    padding: var(--frb-space-4, 14px);
    color: var(--text-primary);
    min-width: 0;
}

/* ── the tile that shows one number ─────────────────────────────────────── */
.frb-tile {
    box-sizing: border-box;
    display: flex; flex-direction: column; gap: 4px;
    background: var(--inset-bg);
    border: 1px solid var(--inset-border);
    border-radius: var(--frb-radius-md, 10px);
    padding: var(--frb-space-4, 14px);
    min-width: 0;
}
.frb-tile-value {
    font-family: var(--frb-mono, ui-monospace, monospace);
    font-size: 26px; font-weight: 700; line-height: 1.05;
    letter-spacing: -.02em; color: var(--text-primary);
    /* Digits in a row of tiles have to line up or the row reads as ragged. */
    font-variant-numeric: tabular-nums;
}
.frb-tile-label {
    font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
    color: var(--text-secondary); line-height: 1.2;
}
/* Direction is not the same as good. A rising cost and a rising signup are both
   "up", so the arrow says which way it moved and the intent class says whether
   that is welcome — the page decides, not the number. */
.frb-tile-delta { font-size: 12px; font-weight: 600; display: inline-flex; align-items: center; gap: 4px; }
.frb-tile-delta--good { color: var(--success, #16a34a); }
.frb-tile-delta--bad  { color: var(--danger, #dc2626); }
.frb-tile-delta--flat { color: var(--text-secondary); }

.frb-tile-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(var(--frb-tile-min, 150px), 1fr));
    gap: var(--frb-space-3, 12px);
}

@media (prefers-reduced-motion: reduce) {
    .frb-card--link { transition: none; }
    .frb-card--link:hover { transform: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   APP HEAD — Wave 5

   The block at the top of an app: its name, a line saying what it does, and
   the controls that act on the whole app. Every one of the 101 apps has one.
   No two of them agreed on anything.

   This is the component a new app writes:

       <div class="frb-app-head">
         <div>
           <h1 class="frb-app-title">Receipt Capture</h1>
           <p class="frb-app-sub">Photograph a receipt, get a line item.</p>
         </div>
         <div class="frb-app-head-actions"> ...buttons... </div>
       </div>

   THEY ARE BEING REWRITTEN TO. The first version of this comment said the 101
   existing apps "are not going to be" rewritten, and that frb-compat.css would
   map their eight class names onto these rules instead. That was the wrong
   call and it cost a wave to learn why: a stylesheet can only restyle what is
   in the markup. It cannot add the sentence saying what an app does to the 53
   apps that never had one, it cannot delete a second name bar, it cannot stop
   a page renaming itself in 28px type every time you click a view, and it
   cannot put the account chip where the other apps keep it. Fifty-four of the
   seventy-seven app pages were built out of different parts, and no amount of
   type-matching makes different parts read as the same product.

   So the markup above is now what every app page actually contains, written
   into the file. The compat layer stays -- it still catches the head of a page
   this migration did not touch -- but it is the fallback, not the mechanism.

   THE BOX IS PART OF THE COMPONENT NOW. The old note here explained that
   padding and margin were deliberately absent, because `.module-header`
   carried the page's left gutter on 34 pages and standardising it would pull
   those titles flush to the viewport. That reasoning held while this class was
   only ever reached through the compat layer, aliased onto a div that already
   had a job. Written directly into a page as the first child of its main
   column, it inherits that column's gutter and owns nothing but its own
   vertical rhythm -- so the rule below sets the one thing every app head has
   to agree on if the pages are to read as siblings: how much room it takes and
   where it ends. */

.frb-app-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--frb-app-head-gap, 16px);
    /* Wraps, because the alternative on a phone is a title and four buttons on
       one line that never fits. This is also why the compat layer can map
       `.module-header` onto it safely: that class was flex and nowrap on 34
       pages, and nowrap is the initial value nobody chose. */
    flex-wrap: wrap;
    padding: 0 0 var(--frb-space-4, 16px);
    margin: 0 0 var(--frb-space-6, 24px);
    /* The hairline is what makes it a BAND rather than a paragraph that
       happens to be first. Two apps side by side with the same type and no
       rule under them still look like two apps; with it they look like two
       views of one. Same token and same value the compat layer already draws
       under the 96 legacy heads, so a migrated page and an un-migrated one
       cannot end up with two different rules. */
    border-bottom: 1px solid var(--glass-border-color, rgba(127, 127, 127, .22));
}

/* The naming half. `min-width: 0` is not decoration: a flex item's floor is
   its content, so without it a long app name refuses to wrap and pushes the
   actions off the page -- which is how eight of the ten worst mobile overflows
   in this product started. `flex: 1 1 320px` lets the actions sit beside the
   name on a laptop and drop under it on a phone, with no media query. */
.frb-app-head-main {
    flex: 1 1 320px;
    min-width: 0;
}

/* The name is a heading, an icon and sometimes a live pill, on one line that
   is allowed to break. The icon is the app's own -- the same glyph the rail,
   the crumb and the portal tile use for it -- and it is what makes the head
   recognisable before a word of it is read. */
.frb-app-title {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--frb-space-3, 12px);
}

.frb-app-glyph {
    color: var(--accent-text, var(--accent-primary, currentColor));
    /* Locked to its own token rather than left at 1em, so an icon FontAwesome
       renders from a different metric cannot shift the baseline of the name
       beside it. Deliberately under the title's 28px: at parity the glyph
       competes with the name for the eye, and the name is what tells you which
       app you are in. */
    font-size: var(--frb-app-glyph-size, 22px);
    line-height: 1;
    flex: 0 0 auto;
}

/* The live pill: a count, a mode, a state. It belongs to the app's content,
   not to the shell, which is why it is optional -- but when a page has one it
   sits here, at this size, in this colour, on all of them. */
.frb-app-badge {
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: var(--frb-label-size, 12px);
    font-weight: 700;
    letter-spacing: var(--frb-label-tracking, .06em);
    text-transform: uppercase;
    line-height: 1;
    padding: 6px 10px;
    border-radius: var(--frb-radius-pill, 999px);
    /* --accent-fill and --btn-text are declared one line apart in the token
       file precisely so a filled control never has to guess its own ink. A
       badge is a filled control. Reaching for --accent-primary here instead
       would read 3.8:1 in light mode on every theme but ocean. */
    background: var(--accent-fill, var(--accent-primary, #d4af37));
    color: var(--btn-text, #0f1115);
    white-space: nowrap;
    flex: 0 0 auto;
}

.frb-app-title {
    font-family: 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);
    /* At 28px and 800 the default tracking reads loose. This is the one place
       in the system that sets negative letter-spacing, and it is the one place
       big enough to need it. */
    letter-spacing: -0.01em;
    color: var(--text-primary, CanvasText);
    margin: 0;
    /* A long app name on a narrow screen breaks rather than pushing the page
       sideways. Eight of the ten worst mobile overflows in the product were a
       title that had been told never to wrap. */
    overflow-wrap: break-word;
    text-wrap: balance;
}

/* The compact variant. Same component, a toolbar's worth of room. */
.frb-app-title-sm {
    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;
    color: var(--text-primary, CanvasText);
    margin: 0;
    overflow-wrap: break-word;
}

.frb-app-sub {
    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));
    margin: var(--frb-space-2, 8px) 0 0;
    overflow-wrap: break-word;
    /* A measure, because the head is the widest thing on the page and a
       sentence set across 1400px is a sentence nobody finishes. */
    max-width: 78ch;
}

/* ── THE VIEW TITLE ──────────────────────────────────────────────────────────
   21 apps carried more than one <h1>: one inside each view, so clicking a rail
   icon renamed the page. On Accounting Core the app's own name was set at 20px
   in a thin bar and the VIEW's name at 28px below it, which is why it read as a
   different product from every app whose name never moves -- the biggest words
   on screen were "Receipt Capture", and nothing on the page said Accounting
   Core louder than a caption.

   A view still deserves a title. It just is not the page's name, so it is set
   below it: same family, same colour, one step down the scale. */
.frb-view-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--frb-space-3, 12px);
    /* No hairline. The app head above already drew one, and a page with a rule
       under its name AND a rule under the name of the view inside it reads as
       two stacked headers -- which is the shape this whole migration exists to
       remove. */
    margin: 0 0 var(--frb-space-4, 16px);
}

.frb-view-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.25;
    letter-spacing: 0;
    color: var(--text-primary, CanvasText);
    margin: 0;
    overflow-wrap: break-word;
}

.frb-view-sub {
    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));
    margin: var(--frb-space-1, 4px) 0 0;
    max-width: 78ch;
}

/* The controls half. `margin-left: auto` rather than `justify-content` on the
   parent, so a head with no actions still puts its title on the left instead
   of centring it. */
.frb-app-head-actions {
    display: flex;
    align-items: center;
    gap: var(--frb-space-2, 8px);
    flex-wrap: wrap;
    margin-left: auto;
}

/* ═══════════════════════════════════════════════════════════════════════════
   TOAST — Wave 5

   48 pages raise a transient message and they raise it in six different
   corners: top-right on 4, bottom-right on 5, bottom-left on 3, and single
   pages at four more offsets. Bottom-left and bottom-right are both taken --
   the theme swatches end the left rail, and the Student Boost badge floats at
   `right: 14px; bottom: 14px` on 97 pages -- so a message in either corner is
   a message the reader has to look behind something to find.

   TOP RIGHT, UNDER THE HEADER, and nothing else in the product lives there.
   The offset is measured from --frb-header-h rather than assumed, so the
   toast follows a header that changes height instead of hiding under it.

       <div class="frb-toast-wrap"><div class="frb-toast">Saved.</div></div> */

/* BOTTOM CENTRE, AND THE TOP-RIGHT IS WHY. Pinned under the header, a toast
   lands squarely on the hub tree — the one strip that says where you are and
   how to get back — so every confirmation covered the app's own navigation for
   as long as it showed. The bottom of the screen has nothing on it but the
   plan badge, and a notice that rises from under the content is the one place
   it can be read without hiding anything. Centred rather than pinned to a
   corner, because the eye is already in the middle of the page when it acts. */
.frb-toast-wrap {
    position: fixed;
    bottom: calc(var(--frb-space-4, 16px) + 34px);   /* clear of the plan badge */
    left: 0;
    right: 0;
    top: auto;
    margin-left: auto;
    margin-right: auto;
    z-index: 2147483100;                    /* above the plan badge */
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--frb-space-2, 8px);
    width: max-content;
    max-width: min(90vw, 380px);
    /* The wrapper is a coat hanger, not a surface — clicks fall through it to
       the app; the toasts themselves take their own. */
    pointer-events: none;
}
.frb-toast-wrap > * { pointer-events: auto; }

.frb-toast {
    display: flex;
    align-items: flex-start;
    gap: var(--frb-space-2, 8px);
    padding: 10px var(--frb-space-3, 12px);
    border-radius: var(--frb-btn-radius, 8px);
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: 13px;
    line-height: 1.5;
    color: var(--text-primary, CanvasText);
    background: var(--panel-bg, var(--card-bg, Canvas));
    border: var(--glass-border, 1px solid rgba(127, 127, 127, .32));
    box-shadow: 0 14px 34px rgba(0, 0, 0, .3);
    backdrop-filter: blur(14px) saturate(1.1);
    overflow-wrap: break-word;
}
/* The three things a toast ever says, marked by a rule down its leading edge
   rather than by a coloured fill — a filled panel at 13px is hard to read in
   either mode and impossible in both. */
.frb-toast.is-ok   { border-left: 3px solid var(--success, #34d399); }
.frb-toast.is-warn { border-left: 3px solid var(--warning, #fbbf24); }
.frb-toast.is-bad  { border-left: 3px solid var(--danger, #ff6b6b); }

@media (max-width: 620px) {
    .frb-toast-wrap {
        left: var(--frb-space-3, 12px);
        right: var(--frb-space-3, 12px);
        width: auto;
        max-width: none;
    }
}
@media print { .frb-toast-wrap { display: none; } }

/* ═══════════════════════════════════════════════════════════════════════════
   RAIL — Wave 5

   The strip of icons down the left of an app. 75 pages have one and they
   already agree on the two things nobody set deliberately: 74 of 75 take their
   background from --sidebar-bg and 75 of 75 draw a 1px right border. What they
   disagree on is everything a hand touched:

       item box     44×44 r12 on 36 pages, then 46×46, 48×48 r14, 69×50 r0,
                    69×70, 69×90, 69×100 and a 191×42 row
       icon         16px on 58, then 18, 20, 22 and 14
       active       eight treatments, three of which are white at 3-5% alpha
                    and therefore invisible in light mode

   The active state below is the one industry_pipelines was already using, and
   it is the only one of the eight that survives both modes: a NEUTRAL grey
   wash rather than a white one, the accent on the glyph, and the accent's own
   glow. rgba(255,255,255,.05) on a white panel is nothing at all.

       <nav class="frb-rail">
         <a class="frb-rail-item is-active"><i class="fa-solid fa-gauge"></i>
            <span class="frb-rail-label">Command</span></a>
         ...
         <div class="theme-footer"> ...swatches, light/dark... </div>
       </nav>

   The label is not decoration and is not optional: it is what names the view
   for someone who does not recognise the glyph. It is taken out of flow and
   shown on hover, so it costs no width and stays in the document. */

.frb-rail {
    width: var(--sidebar-w, 70px);
    flex: 0 0 var(--sidebar-w, 70px);
    background: var(--sidebar-bg, var(--bg-secondary, Canvas));
    border-right: var(--glass-border, 1px solid rgba(127, 127, 127, .18));
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--frb-space-1, 4px);
    padding: var(--frb-space-3, 12px) 0;
    /* The header sits above it, so the rail's room is the window less the
       header — never 100vh, which is how 38 rails pushed their theme picker
       off the bottom of the screen. */
    max-height: calc(100vh - var(--frb-header-h, 47px));
    overflow-y: auto;
    overflow-x: visible;
    scrollbar-width: none;
}
.frb-rail::-webkit-scrollbar { display: none; }

.frb-rail-item {
    width: 44px; height: 44px;
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center;
    position: relative;
    border-radius: var(--frb-space-3, 12px);
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    background: none; border: 0; padding: 0;
    text-decoration: none;
    cursor: pointer;
    transition: background .18s ease, color .18s ease;
}
.frb-rail-item > i, .frb-rail-item > svg { font-size: 16px; }
.frb-rail-item:hover { background: rgba(127, 127, 127, .14); color: var(--text-primary, CanvasText); }
.frb-rail-item:focus-visible {
    outline: 2px solid var(--accent-text, var(--accent-primary));
    outline-offset: 2px;
}
.frb-rail-item.is-active, .frb-rail-item.active {
    background: rgba(127, 127, 127, .1);
    color: var(--accent-text, var(--accent-primary));
    box-shadow: 0 0 15px var(--accent-glow, transparent);
}

/* The name, parked beside the rail until it is wanted. */
.frb-rail-label {
    position: absolute; left: calc(100% + 10px); top: 50%;
    transform: translateY(-50%);
    opacity: 0; pointer-events: none; white-space: nowrap;
    font-size: 11px; font-weight: 600;
    padding: 5px 9px;
    border-radius: var(--frb-btn-radius, 8px);
    background: var(--panel-bg, var(--card-bg, Canvas));
    color: var(--text-primary, CanvasText);
    border: var(--glass-border, 1px solid rgba(127, 127, 127, .32));
    box-shadow: 0 8px 22px rgba(0, 0, 0, .32);
    z-index: 2147482000;
    transition: opacity .16s ease;
}
.frb-rail-item:hover > .frb-rail-label,
.frb-rail-item:focus-visible > .frb-rail-label { opacity: 1; }

/* Settings live at the bottom of a rail — that is the convention and every app
   here already follows it. `margin-top: auto` is what puts them there. */
.frb-rail .theme-footer, .frb-rail-foot {
    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;
}

/* ═══════════════════════════════════════════════════════════════════════════
   FIELD — Wave 5

   The label and the control it names, as one thing. Waves 1 and 2 shaped every
   control in the product and stopped at its edge; this is the edge.

       <div class="frb-field">
         <label class="frb-field-label" for="role">Target role</label>
         <input id="role" type="text" placeholder="Senior developer">
         <small class="frb-field-hint">Shown at the top of the resume.</small>
       </div>

   A column, not a grid, and the gap does the spacing: margins between a label
   and its input are the classic place for a collapse nobody expects. */

.frb-field {
    display: flex;
    flex-direction: column;
    gap: var(--frb-field-gap, 6px);
    margin-bottom: var(--frb-field-gap-y, 16px);
    /* A field in a flex or grid row must be allowed to shrink, or a long
       placeholder sets the width of the form. */
    min-width: 0;
}

.frb-field-label {
    display: block;
    margin: 0;
    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));
}

/* What the reader needs to know that the label has no room for. Sentence case
   on purpose — it is a sentence, and it is the one part of a field that is
   read rather than scanned. */
.frb-field-hint {
    margin: 0;
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: 12px;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    line-height: 1.5;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
}
.frb-field-hint.is-bad { color: var(--danger, #ff6b6b); }

/* Fields side by side, wrapping when they cannot be. `auto-fit` with a floor
   rather than a fixed column count, so the same markup is two columns on a
   laptop and one on a phone with no breakpoint of its own. */
.frb-form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 0 var(--frb-field-gap-y, 16px);
    align-items: start;
}

/* A checkbox or a radio is the other shape a field takes: the control comes
   FIRST and its label is a sentence beside it, not a heading above it. */
.frb-field-check {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: var(--frb-space-2, 8px);
    margin-bottom: var(--frb-space-2, 8px);
    font-size: 14px;
    font-weight: 400;
    text-transform: none;
    letter-spacing: normal;
    color: var(--text-primary, CanvasText);
    cursor: pointer;
}

/* ═══════════════════════════════════════════════════════════════════════════
   BREADCRUMB — Wave 5

   NAMED frb-appcrumb, NOT frb-crumb, and the difference is not cosmetic: the
   shared header already mounts a `.frb-crumb` of its own — "Feerebu / Page
   title" — on every non-app page. Reusing that name would have restyled the
   header's crumb on 42 pages and made this component impossible to tell apart
   from it in a selector.

   Where you are in the product, on every app rather than 19 of them.
   `Portals › Section › Hub › App`, all four parts read from things that
   already exist: the path, the hub page's own <title>, and the heading the app
   already renders.

   It sits as a SIBLING immediately above the app head, not inside it. Inside,
   as a full-width first child, it took a row of its own in a flex container
   built for two children and pushed one app's name and buttons off the right
   edge of the screen. It borrows the head's left inset at mount time so the
   trail still starts where the title starts. */

.frb-appcrumb {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--frb-space-2, 8px);
    margin-bottom: var(--frb-space-2, 8px);
    font-family: var(--frb-font, 'Outfit', system-ui, sans-serif);
    font-size: 12px;
    line-height: 1.4;
    min-width: 0;
}
.frb-appcrumb a {
    color: var(--text-secondary, rgba(127, 127, 127, .95));
    text-decoration: none;
    border-radius: var(--frb-btn-radius, 8px);
}
.frb-appcrumb a:hover { color: var(--text-primary, CanvasText); text-decoration: underline; }
.frb-appcrumb a:focus-visible {
    outline: 2px solid var(--accent-text, var(--accent-primary));
    outline-offset: 2px;
}
.frb-appcrumb-sep {
    font-size: 9px;
    opacity: .45;
    color: var(--text-secondary, rgba(127, 127, 127, .95));
}
/* The page you are on is not a link — it is where the trail stops. */
.frb-appcrumb-here {
    color: var(--accent-text, var(--accent-primary));
    font-weight: 700;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}
