/*! frb-tokens.css — the Feerebu design system, layer 1 of 3.
 *
 * WHAT THIS REPLACES
 * 123 of 139 pages carried their own copy of the four-theme engine, in 65
 * distinct variants, and the palette had already drifted inside them: the
 * midnight accent is #d4af37 in 139 files but #ff9f43 in three and #ff6b6b in
 * one. This is the single definition. A page that links it can delete its own
 * block and the theme it renders stops depending on which copy it happened to
 * inherit.
 *
 * WHY THE TOKEN NAMES ARE NOT NEW
 * --accent-primary alone has ~2,800 var() call sites across the site. Renaming
 * anything here would mean editing all of them for no gain, so the existing
 * vocabulary is adopted verbatim and only EXTENDED. Everything added carries
 * the frb- prefix so it is obvious at a glance which layer a token comes from.
 *
 * THE SCALES ARE MEASURED, NOT INVENTED
 * The site currently uses 111 distinct font sizes, 49 radii and ~1,070 colour
 * literals. The scales below were derived by counting: the ten most-used font
 * sizes cover ~85% of all declarations, so those ten are the scale rather than
 * a tidier set nobody could actually adopt.
 *
 * LOADING ORDER MATTERS — see frb-components.css.
 */

/* ═══ THE LAYER SHIPS THE FAMILY IT NAMES ══════════════════════════════════
 *
 * Every page in the product says `font-family: 'Outfit', system-ui, …` and 57
 * of them get Outfit. Six get DejaVu Sans — Chroma, State Analysis, Country
 * Analysis, README MD, Tag Generator and Area Logic — because the @font-face
 * rules lived in `styles.min.css`, which those six never link. Nothing warns
 * you: the family is simply not there, the stack falls through to the next
 * name, and the page renders in whatever the machine happens to have. Reported
 * as Chroma "using a different font than other apps", and it was — the Linux
 * default.
 *
 * `document.fonts.check('700 28px Outfit')` returns TRUE on all six, which is
 * why this went unnoticed through several passes: with no @font-face rule at
 * all there is nothing pending, so the answer is "nothing to load" rather than
 * "you have it". The only reliable question is what the glyphs were actually
 * drawn with — CSS.getPlatformFontsForNode — and asked that way, six pages
 * answered DejaVu Sans.
 *
 * A page cannot get this wrong once the layer that tells it to use Outfit is
 * also the layer that provides Outfit. The faces are the ones already in
 * /assets/fonts; `swap` because a page whose text is invisible while a local
 * woff2 loads is a worse bug than one frame of fallback.
 */
@font-face { font-family: 'Outfit'; font-style: normal; font-weight: 300; font-display: swap;
    src: url('../fonts/outfit-v15-latin-300.woff2') format('woff2'); }
@font-face { font-family: 'Outfit'; font-style: normal; font-weight: 400; font-display: swap;
    src: url('../fonts/outfit-v15-latin-regular.woff2') format('woff2'); }
@font-face { font-family: 'Outfit'; font-style: normal; font-weight: 500; font-display: swap;
    src: url('../fonts/outfit-v15-latin-500.woff2') format('woff2'); }
@font-face { font-family: 'Outfit'; font-style: normal; font-weight: 600; font-display: swap;
    src: url('../fonts/outfit-v15-latin-600.woff2') format('woff2'); }
@font-face { font-family: 'Outfit'; font-style: normal; font-weight: 700; font-display: swap;
    src: url('../fonts/outfit-v15-latin-700.woff2') format('woff2'); }
@font-face { font-family: 'Outfit'; font-style: normal; font-weight: 800; font-display: swap;
    src: url('../fonts/outfit-v15-latin-800.woff2') format('woff2'); }
/* The mono face goes with it for the same reason: `.mono`, `.sec-num` and every
   figure that lines up in a column name 'JetBrains Mono', and on those same six
   pages it was DejaVu Sans Mono. */
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 400; font-display: swap;
    src: url('../fonts/jetbrains-mono-v24-latin-regular.woff2') format('woff2'); }
@font-face { font-family: 'JetBrains Mono'; font-style: normal; font-weight: 700; font-display: swap;
    src: url('../fonts/jetbrains-mono-v24-latin-700.woff2') format('woff2'); }

:root {
    /* ── TYPE ─────────────────────────────────────────────────────────────
       Derived from usage: 11px (778 uses), 12px (764), 13px (433), 10px (372),
       14px (350), 20px (180), 24px (169), 16px (121), 18px (110), 9px (84). */
    --frb-text-3xs: 9px;
    --frb-text-2xs: 10px;
    --frb-text-xs:  11px;
    --frb-text-sm:  12px;
    --frb-text-md:  13px;    /* body default in this product */
    --frb-text-lg:  14px;
    --frb-text-xl:  16px;
    --frb-text-2xl: 18px;
    --frb-text-3xl: 20px;
    --frb-text-4xl: 24px;
    --frb-text-5xl: 32px;

    --frb-font: 'Outfit', system-ui, -apple-system, sans-serif;
    /* 33 different spellings of two families exist today. These are the two. */
    --frb-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

    --frb-weight-normal: 400;
    --frb-weight-medium: 500;
    --frb-weight-semibold: 600;
    --frb-weight-bold: 700;
    --frb-weight-black: 800;

    --frb-leading-tight: 1.2;
    --frb-leading-normal: 1.5;
    --frb-leading-loose: 1.7;

    /* ── RADIUS ───────────────────────────────────────────────────────────
       49 distinct values today; top ones are 8px (377), 12px (351), 50% (249),
       10px (217), 16px (185), 6px (173), 20px (155), 4px (139). */
    --frb-radius-xs:  4px;
    --frb-radius-sm:  6px;
    --frb-radius-md:  8px;
    --frb-radius-lg:  12px;
    --frb-radius-xl:  16px;
    --frb-radius-2xl: 20px;
    --frb-radius-pill: 999px;
    --frb-radius-circle: 50%;

    /* ── THE BUTTON ───────────────────────────────────────────────────────
       The product's primary button is the one the sign-in screen wears: mono,
       small, uppercase, tracked, square, lit. It started as the editorial
       treatment on six auth pages and was promoted here because it is the
       better button, not because six pages had drifted.

       IT IS A SET OF TOKENS AND NOT A RULE, for the reason frb-compat.css
       gives about field radius: a page that means its own button shape sets
       --frb-btn-radius and keeps it, and `data-frb-optout="btn"` still opts a
       page out of the compat layer wholesale. Promoting the look by writing
       selectors would have taken both of those away.

       Two numbers are deliberately NOT the sign-in button's. It sets its own
       height: 48px, which is right for the one action on a login form and
       wrong for a toolbar; --frb-btn-min-h stays at the 38px every other
       button in the product already has, so tap targets do not move. And the
       tracking is .14em rather than the .18em the LABELS use — a label is
       read once, a button is read at a glance, and at .18em the widest
       button in the product ran past its toolbar. */
    --frb-btn-radius: 0px;
    --frb-btn-font: var(--frb-mono);
    --frb-btn-fs: 12px;
    --frb-btn-weight: 500;
    --frb-btn-tracking: .14em;
    --frb-btn-transform: uppercase;
    --frb-btn-min-h: 38px;
    /* The three sizes, as tokens rather than as numbers inside .frb-btn-sm /
       -lg. The auth card's submit needs the large one and cannot reach a class
       it does not wear; without a token it had `height: 48px` of its own and
       reported a min-height of 0, which is how the one button on the sign-in
       form came to sit outside the product's sizing. */
    --frb-btn-sm-h: 32px;
    --frb-btn-lg-h: 46px;
    /* The lit state and the hard offset block, so both sheets draw one button
       rather than each keeping a copy of the numbers. */
    --frb-btn-glow: 0 0 22px var(--accent-glow, transparent);
    --frb-btn-glow-hi: 0 0 38px var(--accent-glow, transparent);

    /* ── SPACING — a 4px rhythm ───────────────────────────────────────── */
    --frb-space-1: 4px;
    --frb-space-2: 8px;
    --frb-space-3: 12px;
    --frb-space-4: 16px;
    --frb-space-5: 20px;
    --frb-space-6: 24px;
    --frb-space-8: 32px;
    --frb-space-10: 40px;

    /* ── ELEVATION — 192 distinct box-shadows today ─────────────────────── */
    --frb-shadow-1: 0 1px 2px rgba(0,0,0,.28);
    --frb-shadow-2: 0 4px 12px -2px rgba(0,0,0,.4);
    --frb-shadow-3: 0 12px 28px -8px rgba(0,0,0,.5);
    --frb-shadow-4: 0 24px 60px -16px rgba(0,0,0,.65);

    /* ── MOTION ──────────────────────────────────────────────────────────
       One curve, three durations. Everything that moves uses these so the
       whole product accelerates the same way. */
    --frb-dur-fast: .14s;
    --frb-dur-base: .22s;
    --frb-dur-slow: .38s;
    --frb-ease: cubic-bezier(.2,.7,.3,1);

    /* ── FOCUS ───────────────────────────────────────────────────────────
       One ring, used by every interactive element. Keyboard users currently
       get whatever the browser does, page by page. */
    --frb-focus-width: 2px;
    --frb-focus-offset: 2px;
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE FOUR THEMES + LIGHT MODE — one definition, replacing 123 copies.

   Selectors are CLASS-ONLY (.theme-ocean, not body.theme-ocean) so a nested
   element can carry a theme too — a live preview swatch, a style-guide tile.
   `body` still matches; it has the class.

   Values are the ones 136-139 of the copies agreed on. Where copies disagreed,
   the majority won and the outliers are the drift this file exists to end.
   ═══════════════════════════════════════════════════════════════════════════ */

.theme-midnight {
    --bg-solid: #000000;
    --bg-gradient: radial-gradient(circle at 50% 0%, #1a1a1a 0%, #000000 100%);
    --panel-bg: rgba(10, 10, 10, 0.8);
    --sidebar-bg: #0a0a0a;
    --card-bg: rgba(255, 255, 255, 0.03);
    --text-primary: #f8fafc;
    --text-secondary: #9ca3af;
    --accent-primary: #d4af37;
    --accent-primary-rgb: 212, 175, 55;   /* used 58 times site-wide, defined nowhere until now */
    --accent-glow: rgba(212, 175, 55, 0.3);
    --btn-text: #000;
    --bg-pattern-color: rgba(212, 175, 55, 0.05);
}

.theme-ocean {
    --bg-solid: #000814;
    --bg-gradient: radial-gradient(circle at 50% 0%, #0a2540 0%, #000814 100%);
    --panel-bg: rgba(8, 18, 32, 0.85);
    --sidebar-bg: #051b33;
    --card-bg: rgba(255, 255, 255, 0.04);
    --text-primary: #e5effa;
    --text-secondary: #8fa6bf;
    --accent-primary: #4fa3d1;
    --accent-primary-rgb: 79, 163, 209;
    --accent-glow: rgba(79, 163, 209, 0.25);
    --btn-text: #000;
    --bg-pattern-color: rgba(79, 163, 209, 0.05);
}

.theme-sunset {
    --bg-solid: #000000;
    --bg-gradient: radial-gradient(circle at 50% 0%, #434343 0%, #000000 100%);
    --panel-bg: rgba(30, 30, 30, 0.85);
    --sidebar-bg: #1c1c1c;
    --card-bg: rgba(255, 255, 255, 0.04);
    --text-primary: #fff1e6;
    --text-secondary: #dcb3a8;
    --accent-primary: #ff9f43;
    --accent-primary-rgb: 255, 159, 67;
    --accent-glow: rgba(255, 159, 67, 0.25);
    --btn-text: #000;
    --bg-pattern-color: rgba(255, 159, 67, 0.05);
}

.theme-forest {
    --bg-solid: #050f0c;
    --bg-gradient: radial-gradient(circle at 50% 0%, #1a2e28 0%, #050f0c 100%);
    --panel-bg: rgba(13, 27, 30, 0.85);
    --sidebar-bg: #0c1a14;
    --card-bg: rgba(255, 255, 255, 0.04);
    --text-primary: #e8f5e9;
    --text-secondary: #a5d6a7;
    --accent-primary: #66ff66;
    --accent-primary-rgb: 102, 255, 102;
    --accent-glow: rgba(102, 255, 102, 0.2);
    --btn-text: #000;
    --bg-pattern-color: rgba(102, 255, 102, 0.05);
}

/* Shared across all four dark themes. Declared after them so a theme can still
   override any of these, and before .light-mode so light wins. */
.theme-midnight, .theme-ocean, .theme-sunset, .theme-forest {
    --glass-border: 1px solid rgba(255, 255, 255, 0.1);
    /* The same edge as a COLOUR, because the shorthand above cannot be composed
       with. --glass-border carries "1px solid" inside it, so writing a width and
       a style in front of it produces a declaration with two widths and two
       styles in it. That is not a border the browser can parse: it drops the
       whole thing and the element renders with NO border at all, in both modes.
       It had been written that way 116 times across 25 files — every one of
       them an element somebody meant to give an edge and which has never had
       one. Compose against this token instead. */
    --glass-border-color: rgba(255, 255, 255, 0.1);
    --card-shadow: var(--frb-shadow-3);
    /* AN INSET RECESSES. It was rgba(255,255,255,.04) — a WHITE tint, one
       percentage point from --card-bg's .03 — so a control or a panel set into
       a card was the same colour as the card, and the only thing that had ever
       made insets readable was that most of them sat on something else. Put a
       genuine inset on a genuine card and it vanished: 25 of them did, on the
       three Ranchie policy pages that nest cards two deep.

       A recess is darker than what it is cut into, in dark mode as much as in
       light. The light value below has always been a dark tint on white and has
       always worked; this is the same idea, spelled correctly for dark. */
    --inset-bg: rgba(0, 0, 0, 0.28);
    --inset-border: rgba(255, 255, 255, 0.09);
    /* THE MAT BEHIND A DOCUMENT. Six editors show a white sheet floating in a
       frame — the resume pair, the cover letter, codefolio and both menu
       builders — and all six painted that frame #000 flat. In dark mode that is
       right and conventional; in light mode it is a black rectangle in the
       middle of a white page, the only surface in the product that did not
       follow the mode. It is not --panel-bg: a mat is deliberately DARKER than
       the sheet in both modes, which is the whole reason you can see where the
       paper ends. */
    --mat-bg: #000000;
    /* A FIELD IS ITS OWN SURFACE. At .05 and .75 these were translucent enough
       that what a field looked like — and whether its placeholder could be read
       — depended on the card behind it. kanban_lite puts a field on a
       .glass-card that stays dark in light mode, and the placeholder measured
       3.45:1 there while the identical field on a plain panel measured 5.4:1.
       Raised until the field is mostly itself: still glass, no longer a window.
       Same lesson as the account chip, the plan badge and the scrim button — a
       component that borrows half its colour pair from whatever it lands on is
       a component nobody can guarantee. */
    --input-bg: rgba(255, 255, 255, 0.08);
    --input-border: rgba(255, 255, 255, 0.12);
    --frb-surface-hover: rgba(255, 255, 255, 0.07);
    --frb-scrim: rgba(5, 5, 8, 0.82);

    /* SEMANTIC STATUS. Only ~60 of 139 pages define these, so most of the site
       has no agreed colour for "this went wrong" — the rest hardcode a red.
       Tuned for legibility on a dark ground; .light-mode re-tunes them. */
    --success: #34d399;
    --success-bg: rgba(52, 211, 153, 0.12);
    --warning: #fbbf24;
    --warning-bg: rgba(251, 191, 36, 0.12);
    --danger: #ff6b6b;
    --danger-bg: rgba(255, 107, 107, 0.12);

    /* AND THE INK THAT GOES ON THEM WHEN THEY ARE A FILL. The colours above
       are tuned as TYPE on a dark ground, and frb-compat.css turns round and
       paints them behind white text on .btn-danger and .btn-success: #fff on
       #ff6b6b is 2.77:1 and #fff on #34d399 is 1.92:1 — a Stop button and a
       Start button nobody with ordinary eyesight reads comfortably.

       This is the same lesson the accent taught two blocks down, and the same
       answer: no single ink serves a pale fill and a deep one, so the INK has
       to vary with the mode rather than the fill. Dark here, because these
       fills are pale; white under .light-mode, where they are deep. */
    --danger-ink: #0f1115;      /* 6.8:1 on #ff6b6b */
    --success-ink: #0f1115;     /* 9.7:1 on #34d399 */
    --warning-ink: #0f1115;     /* 10.4:1 on #fbbf24 */
    --info: #60a5fa;
    --info-bg: rgba(96, 165, 250, 0.12);
}

/* Light mode. Same specificity as the theme blocks and declared after them, so
   source order decides and light wins.

   THE ACCENT IS RE-TUNED PER THEME, further down. It has to be: the dark
   accents are chosen to glow on black and they disappear on white. Measured
   against this mode's #f3f4f6 ground they score 1.91:1 (midnight), 2.55:1
   (ocean), 1.85:1 (sunset) and 1.19:1 (forest) — forest is the colour of the
   page it is written on. Pages used to paper over this individually and
   disagreed while doing it (midnight resolved to #8a7220 on analytics.html and
   #bfa039 on portals.html), which is precisely the drift this file exists to
   end. The replacements below keep each theme's hue and are darkened only as
   far as WCAG AA for body text, 4.5:1. */
.light-mode {
    --bg-solid: #f3f4f6;
    --bg-gradient: radial-gradient(circle at 50% 0%, #ffffff 0%, #f3f4f6 100%);
    --panel-bg: rgba(255, 255, 255, 0.92);
    --sidebar-bg: #ffffff;
    --card-bg: #ffffff;
    --text-primary: #0f172a;
    /* 5.5:1 on --bg-solid, not 4.3:1. The block above promises these are
       "darkened only as far as WCAG AA for body text, 4.5:1" and this value did
       not keep that promise against the very background this file sets two
       lines up: #64748b on #f3f4f6 is 4.32:1, and secondary text is where a
       product puts its labels, timestamps and captions. Measured across twenty
       pages it was the single largest source of sub-AA text in light mode —
       112 of 312 failing nodes, more than a third, from one value. */
    --text-secondary: #55637a;
    /* THE EDGE OF EVERY CARD, and it was the most drifted value in the product:
       114 of 134 pages resolved it to something other than this file's, across
       rgba(0,0,0,.05), .06, .08, .1 and .12, plus rgba(15,23,42,.1) and
       #e2e8f0. Seven ways of writing "a faint neutral line", none of them
       chosen over the others for a reason.

       It matters more than a border usually would because of what light mode
       does to a card: --card-bg resolves to a near-white or a 2.5% tint, so on
       a #f3f4f6 ground the FILL barely separates the card from the page and the
       border is doing most of the work of saying where the card is. At .06 it
       was not doing it. Slate rather than pure black, matching --inset-border
       and --input-border in this same block, and at .12 — between the two. */
    --glass-border: 1px solid rgba(15, 23, 42, 0.12);
    --glass-border-color: rgba(15, 23, 42, 0.12);
    --card-shadow: 0 20px 40px -10px rgba(0,0,0,0.1);
    /* DARK, like all four theme blocks say — this was #fff and it is the ink ON
       an accent-filled button, not text on the page. "Light mode, therefore
       light text" is the wrong reading: the accent stays a saturated mid-tone
       in light mode, so white on it scored 2.53:1 on midnight, 2.85:1 on sunset
       and 3.77:1 on forest. Sixty-two button labels across the product, the
       header's own avatar initials among them. Same category error as the
       account chip and the plan badge: a colour PAIR split between two owners,
       each half correct on its own. */
    --btn-text: #0f1115;                        /* 7.5:1 on #bfa039, 6.6:1 on #e67e22, 5.0:1 on #059669 */
    --bg-pattern-color: rgba(15, 23, 42, 0.03);
    --inset-bg: rgba(15, 23, 42, 0.035);
    --inset-border: rgba(15, 23, 42, 0.08);
    /* Mid slate, not white and not black. White would lose the sheet's edge on
       a page whose ground is already #f3f4f6; black is what these six editors
       were doing and is what this token exists to stop. 2.2:1 against the sheet
       is enough to read an edge, which is all a mat has to do. */
    --mat-bg: #aeb6c2;
    --input-bg: rgba(255, 255, 255, 0.92);
    --input-border: rgba(15, 23, 42, 0.14);
    --frb-surface-hover: rgba(15, 23, 42, 0.05);
    --frb-scrim: rgba(15, 23, 42, 0.45);

    /* Darker than the dark-mode set: the pale versions fail contrast on white. */
    --success: #047857;
    --success-bg: rgba(4, 120, 87, 0.1);
    --warning: #b45309;
    --warning-bg: rgba(180, 83, 9, 0.1);
    --danger: #c0392b;
    --danger-bg: rgba(192, 57, 43, 0.1);
    /* Deep fills, so the ink flips. */
    --danger-ink: #ffffff;      /* 5.4:1 on #c0392b */
    --success-ink: #ffffff;     /* 5.5:1 on #047857 */
    --warning-ink: #ffffff;     /* 5.0:1 on #b45309 */
    --info: #1d4ed8;
    --info-bg: rgba(29, 78, 216, 0.1);
}

/* NATIVE CONTROLS FOLLOW THE MODE, OR THEY FOLLOW THE BROWSER.
   color-scheme is what tells the engine which way to paint the parts CSS cannot
   reach: an open <select> list, a date picker's calendar, a number input's
   spinners, the scrollbars. It appears nowhere in this tree, so all of them have
   been rendering in the browser's default — a white dropdown list hanging off a
   dark page — and the two pages that noticed hardcoded `color-scheme: dark`,
   which then showed a dark picker on a light page. One declaration per mode
   fixes every one of them. */
:root, body { color-scheme: dark; }
.light-mode { color-scheme: light; }

/* Per-theme light accents. Two classes, so these beat both the theme block
   (one class) and anything a page set on `body.light-mode`. Each is its own
   theme's hue darkened to 4.5:1 on the light ground — the same colour, not a
   different one. */
.theme-midnight.light-mode {
    --accent-primary: #866d1d;                 /* 4.52:1 on #f3f4f6 */
    --accent-primary-rgb: 134, 109, 29;
    --accent-glow: rgba(134, 109, 29, 0.22);
}
.theme-ocean.light-mode {
    --accent-primary: #2a76a0;                 /* 4.55:1 */
    --accent-primary-rgb: 42, 118, 160;
    --accent-glow: rgba(42, 118, 160, 0.22);
}
.theme-sunset.light-mode {
    --accent-primary: #b15600;                 /* 4.54:1 */
    --accent-primary-rgb: 177, 86, 0;
    --accent-glow: rgba(177, 86, 0, 0.22);
}
.theme-forest.light-mode {
    --accent-primary: #008200;                 /* 4.55:1 — was 1.19:1 */
    --accent-primary-rgb: 0, 130, 0;
    --accent-glow: rgba(0, 130, 0, 0.22);
}

/* ── THE GROUND, RESTATED AT TWO CLASSES, AND THIS IS THE LIGHT-MODE BUG ─────
   `.light-mode` above sets --bg-gradient to the white one at (0,1,0). Every app
   in this product opens its stylesheet with its own theme engine:

       body.theme-midnight { --bg-gradient: radial-gradient(… #1a1a1a … #000); }

   which is (0,1,1) — one class AND an element — and therefore beats it. 329 of
   those declarations across 82 pages. So switching to light flipped the INK to
   #0f172a and left the GROUND on the app's black gradient: dark navy text on a
   black page, which is the unreadable state the reader has been reporting.
   Measured on Kanban Lite before this rule, the app's title computed to
   rgb(15, 23, 42) and rendered invisible, because it was correct text on a
   ground that had not moved.

   Two classes beat one class plus an element, so restating the ground here wins
   against all 82 without any of them being edited. Only the tokens a page's
   theme block actually reaches are repeated — the rest are already safe at
   (0,1,0), and copying them would be four more places to keep in step.

   The four selectors are listed rather than folded into `[class*="theme-"]`
   because a page is free to invent its own theme class, and a rule that
   silently claims every one of them is how a shared file starts fighting a
   page it has never seen. */
.theme-midnight.light-mode,
.theme-ocean.light-mode,
.theme-sunset.light-mode,
.theme-forest.light-mode {
    --bg-solid: #f3f4f6;
    --bg-gradient: radial-gradient(circle at 50% 0%, #ffffff 0%, #f3f4f6 100%);
    --bg-body: #f3f4f6;
    --panel-bg: rgba(255, 255, 255, 0.92);
    --sidebar-bg: #ffffff;
    --card-bg: #ffffff;
    --text-primary: #0f172a;
    --text-secondary: #55637a;
}

/* ── THE ACCENT AS TEXT IS A DIFFERENT COLOUR FROM THE ACCENT AS PAINT ───────
   An accent filling a button, drawing a border or throwing a glow has no
   contrast obligation — it is not text. The same value used AS text on a light
   page does, and three of the four themes miss it badly: measured on white,
   midnight's #bfa039 is 2.53:1, sunset's #e67e22 2.85:1, forest's #059669
   3.77:1. Only ocean's #0077b6 (4.87:1) passes.

   Darkening --accent-primary itself would fix the text and dull every fill,
   border and glow in the product's light mode — paying for legibility with the
   identity. So the accent SPLITS: --accent-primary keeps the page's own bright
   value for paint, and --accent-text carries the AA-safe one for type.

   These land at (0,2,0) and (0,1,0). Pages do not declare --accent-text at all —
   which is the point. They compete hard over --accent-primary (a page's
   `body.light-mode.theme-midnight` is (0,2,1) and beats the block above, which
   is why those AA values were being overruled everywhere), and there is nothing
   here for them to overrule.

   In DARK mode --accent-text follows whatever the page chose: a bright accent on
   a dark ground already clears AA, and second-guessing the page's hue there
   would change the product's look for no benefit. */
:root                       { --accent-text: var(--accent-primary, #d4af37); }
.theme-midnight             { --accent-text: var(--accent-primary, #d4af37); }
.theme-ocean                { --accent-text: var(--accent-primary, #4fa3d1); }
.theme-sunset               { --accent-text: var(--accent-primary, #ff9f43); }
.theme-forest               { --accent-text: var(--accent-primary, #66ff66); }
.light-mode                 { --accent-text: #866d1d; }
.theme-midnight.light-mode  { --accent-text: #866d1d; }   /* 4.52:1 on #f3f4f6 */
.theme-ocean.light-mode     { --accent-text: #2a76a0; }   /* 4.55:1 */

/* ── AND THE ACCENT AS A FILL IS A THIRD THING ───────────────────────────────
   Splitting paint from type was not enough, because the ink on an accent-filled
   button has to pair with a value that VARIES: a page that overrides
   --accent-primary at (0,2,1) fills its buttons with #bfa039, and a page that
   does not gets the token file's #866d1d. Dark ink is 7.5:1 on the first and
   3.8:1 on the second; white is 2.5:1 and 5.0:1. No single ink serves both, so
   the fill has to stop varying.

   --accent-fill is the identity colour — the bright, saturated one, the same on
   every page because nothing else declares it — and --btn-text is the ink that
   is known to pair with it. Declared together, one line apart, so they cannot
   drift into two owners the way --accent-primary and --btn-text did. */
.theme-midnight             { --accent-fill: #d4af37; }
.theme-ocean                { --accent-fill: #4fa3d1; }
.theme-sunset               { --accent-fill: #ff9f43; }
.theme-forest               { --accent-fill: #66ff66; }
:root                       { --accent-fill: var(--accent-primary, #d4af37); }
.theme-midnight.light-mode  { --accent-fill: #bfa039; --btn-text: #0f1115; }  /* 7.5:1 */
.theme-ocean.light-mode     { --accent-fill: #0077b6; --btn-text: #ffffff; }  /* 4.9:1 — the one fill dark ink cannot carry */
.theme-sunset.light-mode    { --accent-fill: #e67e22; --btn-text: #0f1115; }  /* 6.6:1 */
.theme-forest.light-mode    { --accent-fill: #059669; --btn-text: #0f1115; }  /* 5.0:1 */
.theme-sunset.light-mode    { --accent-text: #b15600; }   /* 4.54:1 */
.theme-forest.light-mode    { --accent-text: #008200; }   /* 4.55:1 */

/* A page that links this file but sets no theme class still gets a full set of
   tokens rather than rendering unstyled. Lowest specificity, so any real theme
   class beats it. */
:root:not(.theme-midnight):not(.theme-ocean):not(.theme-sunset):not(.theme-forest) {
    --bg-solid: #000000;
    --bg-gradient: radial-gradient(circle at 50% 0%, #1a1a1a 0%, #000000 100%);
    --panel-bg: rgba(10, 10, 10, 0.8);
    --text-primary: #f8fafc;
    --text-secondary: #9ca3af;
    --accent-primary: #d4af37;
    --accent-primary-rgb: 212, 175, 55;
    --accent-glow: rgba(212, 175, 55, 0.3);
    --btn-text: #000;
    --glass-border: 1px solid rgba(255, 255, 255, 0.1);
    /* The semantic set belongs here too. Status badges across the hubs read
       var(--success) and var(--warning); on a page that sets no theme class
       those would be undefined, the declaration invalid at computed-value time,
       and the badge would quietly inherit the surrounding text colour. */
    --success: #34d399;
    --success-bg: rgba(52, 211, 153, 0.12);
    --warning: #fbbf24;
    --warning-bg: rgba(251, 191, 36, 0.12);
    --danger: #ff6b6b;
    --danger-bg: rgba(255, 107, 107, 0.12);
    --info: #60a5fa;
    --info-bg: rgba(96, 165, 250, 0.12);
}

/* ═══ WAVE 5 — THE APP SHELL ═══════════════════════════════════════════════
   How loudly an app says its own name.

   101 apps said it 13 different ways, from 16px to 80px, under eight class
   names. Most of them never chose: 33 pages carry Bootstrap's `display-5`,
   which is 48px because Bootstrap says so, and 9 pages use a bare <h1>, which
   is whatever the browser's default happens to be -- 72px on Ops Command and
   80px on Receipt Capture, both of which shout across a page that already has
   the product's name in the bar above it.

   28px is the middle of what the pages that DID choose landed on. Set the two
   inherited defaults aside and the remaining titles cluster at 22-29 with a
   median of 27; `app-title` and `page-title` had already independently found
   28. It is not on the body type scale and should not be -- Wave 4 left
   display type alone at 20px and up on purpose, because nobody minds that one
   hero is 22px and another 24. What they mind is 80.

   These are tokens rather than literals so the whole product's title size is
   one line to change, and so a page with a real reason can set its own on the
   element rather than fighting a hard-coded rule. */
:root {
    --frb-app-title-size: 28px;
    --frb-app-title-weight: 800;
    --frb-app-title-leading: 1.2;
    /* Secondary text, and the same 14px the fields and buttons settled on in
       Waves 1 and 2. The product had seven sizes for this one role: 15px on 33
       pages, 14 on 22, 13 on 14, then 16, 11, 18 and 20. */
    --frb-app-sub-size: 14px;
    --frb-app-head-gap: 16px;
    /* The compact variant, for apps whose shell is a dense toolbar rather than
       a header block: the six split-pane editors and the canvas builder. All
       six editors already render their name at exactly 20px/700 and agree with
       each other -- this token does not change them, it gives what they were
       already doing a name, so the seventh editor inherits it instead of
       guessing. 28px in a 56px toolbar would crowd out the controls beside it,
       which is a real reason for a second size and the only one. */
    --frb-app-title-sm-size: 20px;
    --frb-app-title-sm-weight: 700;
    /* The icon beside the app's name. 54 of the heads that had one drew it at
       whatever `font-size` the title happened to inherit, which is how the same
       glyph came out at 12px on one app and 35px on another. Under the title's
       28px on purpose: at parity the icon competes with the name, and the name
       is the thing that says which app you are looking at. */
    --frb-app-glyph-size: 22px;
}

/* ═══ WAVE 5 — THE FORM ════════════════════════════════════════════════════
   A field is a label and the control it names, and the product had 188 of
   them in four sizes, five weights, eight combinations of case and tracking,
   four colours, and eight different distances between the label and the thing
   it labels. Waves 1 and 2 governed the CONTROL — its height, radius, focus
   ring, disabled state — and stopped at its edge.

   12px / 600 / uppercase is the plurality on every axis: 115 of 180 labels are
   12px, 97 are 600, and 120 are uppercase against 46 in sentence case. The
   tracking is the one number not taken from a vote — the product used six
   values between 0.24px and 2px, none of them a majority, and 0.06em is the
   step that keeps 12px uppercase readable without spacing it into a heading. */
:root {
    --frb-label-size: 12px;
    --frb-label-weight: 600;
    --frb-label-tracking: 0.06em;
    /* Label to control: 5px on 89 fields, then 13, 16, 14, 4, 20, 2 and 0.
       6px is the plurality rounded to an even number. */
    --frb-field-gap: 6px;
    /* Field to field. 15px on 36, 14 on 22, 12 on 23, then 20, 16, 10, 6 —
       one cluster around 14, and 16 is the space step it belongs to. */
    --frb-field-gap-y: 16px;
}

/* The gutter between the rail and the content. 40px is what `.module-header`
   uses on the 33 pages that set one, and what the cards below it align to. */
:root { --frb-gutter: 40px; }

/* THE SHELL'S GUTTER IS NARROWER, and deliberately not the same number.
   --frb-gutter above is a MEASUREMENT of the product as it was built: 40px is
   what `.module-header` uses on the 33 pages that set one, so it is the value
   that keeps those pages' existing content aligned with their existing heads.
   It is the right answer for the compat layer, whose job is not to move
   anything that already agrees.

   A page reconstructed onto the shell has no existing alignment to preserve,
   and 40px on each side of a 1440px window was the "useless padding to the
   sides" called out on Event Matrix, Financial Suite and Ops Command. 28px is
   Industry Hub's, which is the app this shell is modelled on. */
:root { --frb-shell-gutter: 28px; }
/* HALF OF IT ON A PHONE. 28px is right against 1440 and heavy against 390 —
   and it was not even consistent there, because the trail band carried 12px of
   its own: Kitchen OS drew its breadcrumb at x=12 and every tile under it at
   x=28, two left edges sixteen pixels apart down the whole page. One number,
   read by the column, the head and the band alike.
   Beside the definition rather than in frb-shell.css, and that is the point:
   an override written on `body` in another file loses to this `:root` rule for
   every element that inherits the value rather than matching the selector —
   which is all of them. A token is overridden where it is declared. */
@media (max-width: 620px) { :root { --frb-shell-gutter: 14px; } }

/* AND THE APP TITLE COMES DOWN WITH IT. 28px was measured against a 1440
   column; on a 390px screen it is the single biggest reason a heading takes
   two and three lines, and every attempt to fix it on one page failed for the
   reason written above — the winning rule is
   `body:not([data-frb-optout~="title"]) h1.display-5` at (0,2,1), reading this
   token, so a page-level `h1.display-5` at (0,1,1) never gets a say. One number
   here moves every app title in the product at phone width, which is the only
   place all of them are wrong in the same way.

   23px, not smaller: it is still unmistakably the largest thing on the screen,
   which is the job the title has. */
@media (max-width: 620px) { :root { --frb-app-title-size: 23px; } }
