/*
 * tokens.css — GENERATED FILE, DO NOT EDIT.
 * 
 * Compiled from core/admin/skin/ by control-plane/scripts/generate-skin.js
 * so the operator console runs the SAME skin engine as every tenant admin
 * rather than a second copy of it. Edit core, then regenerate:
 * 
 *   node control-plane/scripts/generate-skin.js
 * 
 * Drift guard: control-plane/functions/test/skin-parity.test.js
 */
/* =============================================================
   WarHorse admin — the token contract for the "liquid glass" skin
   -------------------------------------------------------------
   ONE file defines every colour, and nothing downstream may hardcode one.
   That rule is not tidiness. Before this file existed the admin's chrome was
   literally `#0f1a2b` and `#ed1937` typed into core/admin/*.html — which meant
   a registered charity's admin was painted in a federal party's campaign red,
   83 occurrences of it, because both tenants are served the same core file.
   Tokens are what stop that from being possible again.

   HOW IT IS SWITCHED
     <html data-skin="classic|glass" data-theme="light|dark">
       data-skin  — BUILD-TIME safety valve, from site.config.json
                    admin.skin. Default "classic" = the admin exactly as it
                    ships today. This is what lets the restyle live on main
                    without changing a single live pixel.
       data-theme — the USER's choice, per member, set by skin/theme.js before
                    first paint. Absent ⇒ follow prefers-color-scheme.

   Every value below is a token. Two of them are per-tenant and arrive as
   double-brace placeholders that scripts/assemble.js substitutes into the
   dist copy from site.config.json → admin.brand:

       --brand-a   primary buttons · top-left blob · non-money bars · avatars
       --brand-b   bottom-right blob · active markers · money · deltas · alerts

   They are declared HERE rather than in a file of their own because this is
   the one stylesheet every admin page links. A separate brand.css is a file
   somebody eventually forgets to link, and an admin with no --brand-a is an
   admin with invisible buttons. Assembly kills the build on a missing key,
   on a malformed hex, and on a colour too light to carry white text.

   A tenant with one brand colour sets both to the same value; the design
   degrades to a single-hue version rather than inventing a second one.

   NOTE FOR ANYONE READING THE SOURCE: in core/ those two declarations are
   still placeholder text, not colours. The source tree is a template — open
   a page from dist/<tenant>/public/admin/, never from core/, or the brand
   is undefined and every button paints as nothing.
   ============================================================= */

:root {
  /* ---- per-tenant, substituted at assembly (see the header) ---- */
  --brand-a: #4f46e5;
  --brand-b: #0f766e;

  /* ---- geometry: identical in both themes, so it lives outside them ---- */
  --r-frame: 22px;
  --r-panel: 16px;
  --r-panel-lg: 18px;
  --r-field: 9px;
  --r-btn: 11px;
  --r-pill: 7px;
  --r-rail: 18px;
  --r-rail-open: 24px;

  --pad-frame: 18px;
  --gap-panel: 15px;
  --pad-panel: 18px;

  --rail-w: 64px;
  --rail-w-open: 194px;

  /* The spring. 520ms and an overshoot — deliberately slower than a normal
     UI transition. A 260ms ease-out was tried first and read mechanical. */
  --ease-spring: cubic-bezier(0.16, 1.24, 0.3, 1);
  --dur-rail: 0.52s;

  /* ---- type ---- */
  --font-chrome: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  --font-content: 'Archivo', system-ui, sans-serif;
  /* Headings AND item titles. Owner re-decided 2026-08-25 after the
     three-tier mockup (docs/design/admin-glass/sora-system-options.html):
     Sora, option D — a strict two-family system. Sora carries headings and
     item titles (petition names, row titles); EVERYTHING else — data,
     labels, numbers, subtitles, helper copy — stays --font-chrome. glass.css
     applies it (headings + [data-ttl]); theme.js loads the face (glass
     only). */
  --font-display: 'Sora', 'Archivo', system-ui, sans-serif;

  --t-label: 9.5px;    /* uppercase section labels, tracked 1.2px, weight 700 */
  --t-th: 9px;         /* table headers, tracked 0.9px */
  --t-body: 11px;      /* the workhorse */
  --t-kpi: 21px;
  --t-hero: 38px;
}


/* WHY EVERY PALETTE SELECTOR CARRIES [data-skin='glass']
   -------------------------------------------------------------
   These names are generic — --ink, --body, --muted — and two of the four
   admin apps declared their own --ink years before this file existed. An
   ungated :root here therefore repaints THEIR variable on a classic page.
   It cost two real defects before the gate went on, both found in a browser
   and neither visible in review:

     · Classic broke for anyone who had chosen Dark. theme.js stamps
       data-theme whatever the skin, so a classic page inherited this file's
       near-white --ink and painted it on its own white panels.
     · A page's own re-point of a legacy name lost the specificity fight:
       :root[data-theme='dark'] is (0,2,0) and a plain :root[data-skin] rule
       is (0,1,0), so the palette won under an explicit theme and lost under
       "match system" — a bug that appears only on one of three settings.

   The gate means classic is not merely styled the same, it is unreachable
   from here. Same reason color-scheme is gated below. */
/* =============================================================
   LIGHT — the handoff's palette, verbatim. This is the reference.
   ============================================================= */
:root[data-skin='glass'],
:root[data-skin='glass'][data-theme='light'] {
  --ink: #101114;
  --body: #2c3138;
  --mid: #555b64;
  /* Handoff value was #7b8087. MEASURED on the composited .gl-work surface it
     is 3.75:1 — a fail for 9.5px section labels, which carry real information
     and are not decorative. Darkened to the first value that clears AA with
     margin (4.77:1) while staying plainly quieter than --mid at 6.46. This is
     the one place the handoff's palette is not verbatim, and the reason is
     legibility rather than taste. */
  --muted: #6a6f76;
  /* --faint CANNOT be made to pass AA and still be faint: on a near-white
     surface anything at 4.5:1 is --muted. It measures 2.34:1. Treat it as
     DECORATIVE — timestamps and placeholders that repeat information already
     available elsewhere. Never the only place something is said. If a screen
     needs quiet text that carries meaning, that is --muted's job. */
  --faint: #a0a5ac;

  --frame-bg: #f4f5f6;
  --frame-border: #dfe0e3;

  --hairline: rgba(16, 17, 20, 0.08);
  --hairline-light: rgba(16, 17, 20, 0.05);
  --field-border: rgba(16, 17, 20, 0.12);
  --row-hover: rgba(16, 17, 20, 0.03);
  --tool-active: rgba(16, 17, 20, 0.06);

  /* Three glass densities. The rule that keeps it legible: the more reading
     or typing a surface hosts, the more opaque it is. */
  /* Each density dropped ~8 points from the handoff, on the owner's note that
     it "needs a bit of transparency". The ORDER is what makes the design
     legible — the more reading or typing a surface hosts, the more opaque —
     and that is untouched; the whole scale simply sits lower, so the colour
     behind shows THROUGH a panel instead of only around it. Re-measured after
     the change rather than assumed. */
  /* The rail alone stays at .66. It is the most transparent surface AND it
     sits directly over the densest part of blob A, so it is the one place
     where opening the glass up costs legibility rather than just contrast
     headroom: at .55 its quiet labels measured 2.96:1. At .66 --mid clears
     AA (4.61) and --muted still does not — so rail text is --mid or darker,
     never --muted. Measured, not guessed. */
  --glass-rail: rgba(255, 255, 255, 0.66);
  --glass-panel: rgba(255, 255, 255, 0.78);
  --glass-work: rgba(255, 255, 255, 0.86);

  --glass-edge: rgba(255, 255, 255, 0.95);
  --glass-edge-solid: #fff;
  --glass-inset: inset 0 1px 0 #fff;

  --blur-rail: blur(32px) saturate(1.7);
  --blur-panel: blur(26px) saturate(1.45);
  --blur-work: blur(30px) saturate(1.4);

  --shadow-rail: 0 18px 46px rgba(16, 17, 20, 0.12);
  --shadow-rail-open: 0 26px 60px rgba(16, 17, 20, 0.18);
  --shadow-panel: 0 13px 35px rgba(16, 17, 20, 0.095);
  --shadow-work: 0 18px 46px rgba(16, 17, 20, 0.11);

  --nav-hover: rgba(255, 255, 255, 0.55);
  --nav-item-hover: rgba(255, 255, 255, 0.6);
  --nav-active: rgba(255, 255, 255, 0.92);
  --shadow-nav-active: 0 4px 12px rgba(16, 17, 20, 0.08);

  /* Background stack. The veil is the contrast safety net — do not remove. */
  /* THE BLOB COLOUR IS NOT THE BRAND COLOUR, and that is the whole fix for
     "a little too dark". Aaron's brand-a is #142f52 — a navy chosen to carry
     white text on a button. Painted full-strength across a quarter of the
     screen the same colour is a dark stain, not a gradient, and raising its
     opacity to make the gradient visible only made it heavier. Mixing toward
     white keeps the hue — it still reads as the tenant's navy — while giving
     it the lightness a wash needs. A tenant whose brand is already pale gets
     a subtler mix and loses nothing.

     Then the opacity can come DOWN again, because a light tint at .62 reads
     more strongly than a dark one at .72 while weighing far less. */
  --blob-a: color-mix(in oklab, var(--brand-a) 52%, #ffffff 48%);
  --blob-b: color-mix(in oklab, var(--brand-b) 56%, #ffffff 44%);
  --blob-a-opacity: 0.62;
  --blob-b-opacity: 0.56;
  --blob-blur: 74px;
  --grain-opacity: 0.5;
  --grain-blend: multiply;
  /* Lightened from .34/.5. Still the contrast safety net — every number below
     is measured through it — but at full strength it flattened the blobs into
     a faint tint on a desktop-sized window. */
  --veil: linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.4));

  /* Brand fills carry white text in both themes; in light they are the raw
     tenant colours. See the dark block for why that cannot hold there. */
  --brand-a-fill: var(--brand-a);
  --brand-b-fill: var(--brand-b);
  /* Primary-button hover — the handoff's "brand-a at 92% lightness". */
  --brand-a-hover: color-mix(in oklab, var(--brand-a) 92%, #000000 8%);
  --brand-a-tint: color-mix(in srgb, var(--brand-a) 5%, transparent);
  --brand-a-chip: color-mix(in srgb, var(--brand-a) 9%, transparent);
  /* Label colour ON a brand fill — COMPUTED PER TENANT by assemble.js, which
     picks whichever of white / near-black actually reads on that tenant's
     colour and warns if neither reaches 4.5:1.

     Hardcoding white here is the platform bug in miniature. White on Aaron's
     #ed1937 is 4.38:1; on a tenant with a cyan brand-b it is nearer 3.2:1.
     In light the fills ARE the raw brand colours, so this is exactly
     computable at build time, and guessing is inexcusable.

     Two tokens rather than one, because the two fills stop agreeing about
     what is readable on them the moment the hues differ — see the dark
     block, where they disagree even for a single tenant. */
  --on-brand: #ffffff;
  --on-brand-b: #ffffff;
  --shadow-primary: 0 10px 24px color-mix(in srgb, var(--brand-a) 28%, transparent);
  --shadow-primary-hover: 0 12px 28px color-mix(in srgb, var(--brand-a) 34%, transparent);

  --focus-ring: var(--brand-a);
  --dropzone-stripe: rgba(16, 17, 20, 0.07);
  --dropzone-bg: rgba(16, 17, 20, 0.04);

  /* Status. Deliberately NOT brand-b: a tenant whose brand-b is green would
     make "saved" and "could not save" the same colour, and a tenant whose
     brand-b is red would make every success message look like an error.
     Both are measured against --glass-work, not against the frame. */
  --ok: #17794a;
  --danger: #c02434;
}

/* =============================================================
   DARK — derived here, not in the handoff, which is light-only.
   Four decisions worth stating, because they are the ones a naive
   inversion gets wrong:

   1. GLASS GETS MORE OPAQUE, NOT LESS. Dark translucent panels over a dark
      ground have almost no edge definition, and text on them smears into
      whatever blob is behind. Each density gains ~6 points.
   2. THE WHITE VEIL BECOMES A DARK SCRIM. Same job — hold contrast between
      the blobs and everything above them — opposite direction.
   3. GRAIN SWITCHES multiply → overlay. Multiply on a near-black ground is
      invisible; overlay keeps the texture without lifting the black.
   4. BRAND FILLS ARE LIFTED. #142f52 navy on #0d0e11 is a button you cannot
      find: white text on it still passes, but the button has no edge against
      the page. Both fills are mixed toward white so the control reads as a
      control. The raw brand colour is kept for blobs and text accents, where
      separation is not the job.
   ============================================================= */
:root[data-skin='glass'][data-theme='dark'] {
  --ink: #f2f3f5;
  --body: #d3d6db;
  --mid: #a2a8b2;
  --muted: #7d838d;
  --faint: #61666f;

  --frame-bg: #0d0e11;
  --frame-border: #23252a;

  --hairline: rgba(255, 255, 255, 0.09);
  --hairline-light: rgba(255, 255, 255, 0.06);
  --field-border: rgba(255, 255, 255, 0.14);
  --row-hover: rgba(255, 255, 255, 0.04);
  --tool-active: rgba(255, 255, 255, 0.09);

  --glass-rail: rgba(26, 28, 33, 0.62);
  --glass-panel: rgba(26, 28, 33, 0.82);
  --glass-work: rgba(24, 26, 30, 0.88);

  --glass-edge: rgba(255, 255, 255, 0.08);
  --glass-edge-solid: rgba(255, 255, 255, 0.1);
  --glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.06);

  --blur-rail: blur(32px) saturate(1.5);
  --blur-panel: blur(26px) saturate(1.3);
  --blur-work: blur(30px) saturate(1.25);

  --shadow-rail: 0 18px 46px rgba(0, 0, 0, 0.55);
  --shadow-rail-open: 0 26px 60px rgba(0, 0, 0, 0.65);
  --shadow-panel: 0 13px 35px rgba(0, 0, 0, 0.45);
  --shadow-work: 0 18px 46px rgba(0, 0, 0, 0.5);

  --nav-hover: rgba(255, 255, 255, 0.06);
  --nav-item-hover: rgba(255, 255, 255, 0.07);
  --nav-active: rgba(255, 255, 255, 0.12);
  --shadow-nav-active: 0 4px 12px rgba(0, 0, 0, 0.4);

  --blob-a: color-mix(in oklab, var(--brand-a) 74%, #ffffff 26%);
  --blob-b: color-mix(in oklab, var(--brand-b) 84%, #ffffff 16%);
  --blob-a-opacity: 0.52;
  --blob-b-opacity: 0.46;
  --blob-blur: 74px;
  --grain-opacity: 0.28;
  --grain-blend: overlay;
  --veil: linear-gradient(180deg, rgba(10, 11, 14, 0.26), rgba(10, 11, 14, 0.48));

  --brand-a-fill: color-mix(in oklab, var(--brand-a) 68%, #ffffff 32%);
  --brand-b-fill: color-mix(in oklab, var(--brand-b) 82%, #ffffff 18%);
  /* Hover goes LIGHTER in dark, not darker — the fill is already lifted. */
  --brand-a-hover: color-mix(in oklab, var(--brand-a) 56%, #ffffff 44%);
  --brand-a-tint: color-mix(in srgb, var(--brand-a) 22%, transparent);
  --brand-a-chip: color-mix(in srgb, var(--brand-a) 30%, transparent);
  /* MEASURED, not assumed. In dark the two fills disagree:
     white on --brand-a-fill  = 5.37:1  (near-black would be 3.59 — a FAIL)
     white on --brand-b-fill  = 3.21:1  (near-black is 6.01 — the other way)
     brand-b is lifted much further toward white than brand-a is, so one
     shared label colour cannot serve both. Hence two tokens. */
  --on-brand: #ffffff;
  --on-brand-b: #0d0e11;
  --shadow-primary: 0 10px 24px rgba(0, 0, 0, 0.5);
  --shadow-primary-hover: 0 12px 28px rgba(0, 0, 0, 0.6);

  /* The raw brand navy is too close to the page to read as a focus ring. */
  --focus-ring: color-mix(in oklab, var(--brand-a) 55%, #ffffff 45%);
  --dropzone-stripe: rgba(255, 255, 255, 0.07);
  --dropzone-bg: rgba(255, 255, 255, 0.04);

  --ok: #52c98a;
  --danger: #ff7d8f;
}

/* Default to the OS when the member has expressed no preference. theme.js
   stamps data-theme only when they have; "match system" removes it. */
@media (prefers-color-scheme: dark) {
  :root[data-skin='glass']:not([data-theme='light']) {
    --ink: #f2f3f5;
    --body: #d3d6db;
    --mid: #a2a8b2;
    --muted: #7d838d;
    --faint: #61666f;

    --frame-bg: #0d0e11;
    --frame-border: #23252a;

    --hairline: rgba(255, 255, 255, 0.09);
    --hairline-light: rgba(255, 255, 255, 0.06);
    --field-border: rgba(255, 255, 255, 0.14);
    --row-hover: rgba(255, 255, 255, 0.04);
    --tool-active: rgba(255, 255, 255, 0.09);

    --glass-rail: rgba(26, 28, 33, 0.62);
    --glass-panel: rgba(26, 28, 33, 0.82);
    --glass-work: rgba(24, 26, 30, 0.88);

    --glass-edge: rgba(255, 255, 255, 0.08);
    --glass-edge-solid: rgba(255, 255, 255, 0.1);
    --glass-inset: inset 0 1px 0 rgba(255, 255, 255, 0.06);

    --blur-rail: blur(32px) saturate(1.5);
    --blur-panel: blur(26px) saturate(1.3);
    --blur-work: blur(30px) saturate(1.25);

    --shadow-rail: 0 18px 46px rgba(0, 0, 0, 0.55);
    --shadow-rail-open: 0 26px 60px rgba(0, 0, 0, 0.65);
    --shadow-panel: 0 13px 35px rgba(0, 0, 0, 0.45);
    --shadow-work: 0 18px 46px rgba(0, 0, 0, 0.5);

    --nav-hover: rgba(255, 255, 255, 0.06);
    --nav-item-hover: rgba(255, 255, 255, 0.07);
    --nav-active: rgba(255, 255, 255, 0.12);
    --shadow-nav-active: 0 4px 12px rgba(0, 0, 0, 0.4);

    --blob-a: color-mix(in oklab, var(--brand-a) 74%, #ffffff 26%);
    --blob-b: color-mix(in oklab, var(--brand-b) 84%, #ffffff 16%);
    --blob-a-opacity: 0.52;
    --blob-b-opacity: 0.46;
    --grain-opacity: 0.28;
    --grain-blend: overlay;
    --veil: linear-gradient(180deg, rgba(10, 11, 14, 0.26), rgba(10, 11, 14, 0.48));

    --brand-a-fill: color-mix(in oklab, var(--brand-a) 68%, #ffffff 32%);
    --brand-b-fill: color-mix(in oklab, var(--brand-b) 82%, #ffffff 18%);
    --brand-a-hover: color-mix(in oklab, var(--brand-a) 56%, #ffffff 44%);
    --brand-a-tint: color-mix(in srgb, var(--brand-a) 22%, transparent);
    --brand-a-chip: color-mix(in srgb, var(--brand-a) 30%, transparent);
    /* MEASURED, not assumed. In dark the two fills disagree:
     white on --brand-a-fill  = 5.37:1  (near-black would be 3.59 — a FAIL)
     white on --brand-b-fill  = 3.21:1  (near-black is 6.01 — the other way)
     brand-b is lifted much further toward white than brand-a is, so one
     shared label colour cannot serve both. Hence two tokens. */
  --on-brand: #ffffff;
  --on-brand-b: #0d0e11;
    --shadow-primary: 0 10px 24px rgba(0, 0, 0, 0.5);
    --shadow-primary-hover: 0 12px 28px rgba(0, 0, 0, 0.6);

    --focus-ring: color-mix(in oklab, var(--brand-a) 55%, #ffffff 45%);
    --dropzone-stripe: rgba(255, 255, 255, 0.07);
    --dropzone-bg: rgba(255, 255, 255, 0.04);

    --ok: #52c98a;
    --danger: #ff7d8f;
  }
}

/* =============================================================
   color-scheme — THE ONE DECLARATION IN THIS FILE THAT PAINTS
   -------------------------------------------------------------
   Everything above is a custom property: declaring it changes nothing until
   something references it, which is why this stylesheet is safe to link from
   a classic page. `color-scheme` is not like that. It repaints the UA's own
   widgets — scrollbars, <select> popups, checkboxes, the canvas behind the
   page — the moment it applies.

   It was originally declared inside the three theme blocks, which meant a
   classic admin opened on a machine set to Dark would get dark scrollbars and
   dark form controls, because `:root:not([data-theme='light'])` matches a
   classic page perfectly well. That is a visible change to an admin that is
   supposed to be byte-for-byte what ships today, so the declarations moved
   down here and gained the skin gate. Removing data-skin now restores today's
   rendering exactly.

   Specificity, deliberately: the explicit-dark rule (0,3,0) and the
   OS-follow rule (0,4,0) both outrank the light default (0,2,0), and the
   OS-follow rule carries :not([data-theme='light']) so an explicit Light
   choice still wins over a dark OS.
   ============================================================= */
:root[data-skin='glass'] { color-scheme: light; }
:root[data-skin='glass'][data-theme='dark'] { color-scheme: dark; }
@media (prefers-color-scheme: dark) {
  :root[data-skin='glass']:not([data-theme='light']) { color-scheme: dark; }
}
