/* Alchemys UI — Design Token Layer
 *
 * Loaded FIRST, before shared.css. Scheme convention (matches shared.css):
 * :root holds dark-scheme defaults, html:not(.dark):not(.dark-mode) overrides
 * for light.
 *
 * WHY THIS FILE EXISTS
 * --------------------
 * An audit (2026-09-26) measured the platform's colour system and found the
 * design system itself is healthy — the custom select, focus ring, tabs and
 * dialogs all carry correct ARIA — but the APPS do not use it:
 *
 *   - 1033 hardcoded hex in CSS + 1145 in HTML.
 *   - 1111 Tailwind arbitrary colour classes (text-[#E8623D] and friends).
 *     These live inside class names, so NO token can ever restyle them.
 *   - Five competing accent families: MD3 indigo (#5657AC/#BEC2FF) for most
 *     apps, Studio terracotta (#E8623D/#FF7A4D), expense ledger-green
 *     (#2E7D5B/#6FCF9B), mt tea-amber (#C68642/#E8B074), db-viewer blue
 *     (#5b8def/#2563eb).
 *   - expense and mt each define a full accent token family (--ex-accent*,
 *     --mt-accent*) that shared components do not read: expense uses 3 of its
 *     5 and mt 4 of its 5, all of them container roles, so the accent FILL
 *     itself still never renders and both apps' primary actions ship as
 *     plain indigo while their CSS headers claim a brand identity. The
 *     terracotta family below is the same defect one step further along:
 *     expense, mt and studio all mount the .file-preview-* and
 *     .decision-feedback-btn components, and all three get Studio terracotta
 *     because shared.css writes it as raw hex. Consolidating the five
 *     families needs edits under each app, not just here.
 *   - No z-index scale (20+ ad-hoc values, 400..9999), no scrim token, no
 *     skeleton token, no shared disabled state.
 *
 * This file is the fix. It is ADDITIVE and fully backward compatible: every
 * pre-existing token keeps its name and value, so nothing that links it today
 * changes appearance. What it adds is the missing ROLE vocabulary plus one
 * brand contract that all five accent families must pass through.
 *
 * THE BRAND CONTRACT
 * ------------------
 * An app sets its identity by overriding the roles below — never by writing
 * hex at a callsite. Six roles, because a single accent colour cannot do six
 * jobs at once:
 *
 *   --brand              fills: buttons, chips, active nav, progress. Paired
 *                        with --brand-on. Must reach 3:1 against the app
 *                        background (WCAG 1.4.11 non-text).
 *   --brand-on           text/icons ON --brand. Must reach 4.5:1 against
 *                        --brand.
 *   --brand-ink          text/icons/links ON the app background. THIS is the
 *                        role that replaces the per-button .btn-brand-a11y
 *                        patch: a mid-lightness accent reads fine as a fill
 *                        but fails as text. Measured: Studio terracotta
 *                        #E8623D on white is 3.36:1 (FAILS AA body text);
 *                        #B54522 is 5.47:1 (passes). mt tea-amber #C68642 is
 *                        3.05:1 and db-viewer #5b8def is 3.23:1 — both fail.
 *                        So --brand-ink is a separate, darker step of the
 *                        same hue, and it is the only role text may use.
 *   --brand-container    a soft fill for selected/active rows and chips.
 *   --brand-on-container text ON --brand-container.
 *   --brand-scrim        the press/dim layer behind a modal over brand.
 *
 * RECIPE FOR A NEW ACCENT
 * -----------------------
 * Pick a hue, then fill every role — the darkening in --brand-ink is what
 * keeps text readable, so it must be chosen deliberately, not derived:
 *
 * NOTE the scheme convention (see lines 3-5): `:root` IS the DARK scheme and
 * light lives in `html:not(.dark):not(.dark-mode)`. An earlier draft of this
 * comment had the two blocks swapped, so copying it shipped a light-mode accent
 * as the default slot and dark-mode users got light colours.
 *
 *   :root  —  the DARK scheme (the default)
 *   :root {
 *       --brand:            #A9B4FF;   // fill
 *       --brand-hover:      #BAC3FF;
 *       --brand-press:      #CBD2FF;
 *       --brand-on:         #1B2450;   // >= 4.5:1 on --brand
 *       --brand-ink:        #B4BEFF;   // >= 4.5:1 on the app background;
 *                                         //   dark ink goes LIGHTER, not darker
 *       --brand-ink-hover:  #C9D0FF;
 *       --brand-container:      #2C3570;
 *       --brand-on-container:   #DCE1FF;
 *   }
 *   html:not(.dark):not(.dark-mode)  —  the LIGHT scheme
 *   html:not(.dark):not(.dark-mode) {
 *       --brand:            #5A6CF0;   // fill
 *       --brand-hover:      #4A5CE0;
 *       --brand-press:      #3B4BC9;
 *       --brand-on:         #FFFFFF;   // >= 4.5:1 on --brand
 *       --brand-ink:        #3546C4;   // >= 4.5:1 on the light app background
 *       --brand-ink-hover:  #2634A0;
 *       --brand-container:      #E3E7FF;
 *       --brand-on-container:   #141C4D;
 *   }
 */

:root {
    /* === Brand — defaults to the shared MD3 indigo.
       Both schemes verified: #5657AC on the light surface is 6.26:1 and
       #BEC2FF on the dark surface is 10.93:1, so --brand-ink may safely
       alias --brand here. An app whose accent is lighter than ~#8A8A8A
       MUST declare its own --brand-ink (see the contract comment above). */
    --brand: #BEC2FF;
    --brand-on: #262479;
    --brand-hover: #C9CEFF;
    --brand-press: #D6DAFF;
    --brand-ink: #BEC2FF;
    --brand-ink-hover: #D6DAFF;
    --brand-container: #3D3F90;
    --brand-on-container: #E0E0FF;
    --brand-scrim: color-mix(in srgb, #BEC2FF 32%, transparent);

    /* === Terracotta (Studio) accent family ================================
       shared.css still paints .file-preview-* and .decision-feedback-btn in
       Studio terracotta, and expense + mt + studio all mount those two
       components. The accent was raw hex at every callsite, so no scheme
       could ever correct the text-on-accent steps - and none was:
       white on #E8623D is 3.36:1, under the 4.5:1 floor for the 16px label.
       Same role split as the brand contract above:
         --accent-terracotta       the hue itself. NON-TEXT ONLY - borders,
                                   focus rings, spinner tints. Never a label.
         --accent-terracotta-fill  the fill step, dark enough that
                                   --accent-terracotta-on clears 4.5:1.
         --accent-terracotta-on    the label drawn on that fill.
         --accent-terracotta-ink   text on the app background or on a 10-14%
                                   tint of the hue. DARKENS in the light
                                   scheme, LIGHTENS in the dark one.
       All four are literals on purpose: aliasing them to --md-* would make
       the two families circular, because those apps already set
       --md-primary: var(--brand). */
    --accent-terracotta: #E8623D;
    --accent-terracotta-fill: #C6502F;   /* white on it = 4.58:1 */
    --accent-terracotta-on: #FFFFFF;     /* 4.58:1 on --accent-terracotta-fill */
    /* 5.86:1 on a 14% tint of itself over the dark surface #141218 */
    --accent-terracotta-ink: #FF7A4D;

    /* === Gradient flourish ================================================
       .shell-title-accent is the only place a second hue lives in the
       shared layer. Held as tokens so an app can retint the title word
       without editing a callsite; both stops clear the 3:1 large-text floor
       in BOTH schemes (4.25:1 and 3.76:1 on light, 4.16:1 and 4.70:1 on
       dark). */
    --brand-gradient-from: #6366f1;
    --brand-gradient-to: #a855f7;

    /* === Neutral surface ramp (aliases MD3 surface scale) === */
    --surface-0: var(--md-surface-container-lowest);
    --surface-1: var(--md-surface-container-low);
    --surface-2: var(--md-surface-container);
    --surface-3: var(--md-surface-container-high);
    --surface-4: var(--md-surface-container-highest);
    --surface-ink: var(--md-on-surface);
    --surface-ink-soft: var(--md-on-surface-variant);
    /* Muted ICON/label grey for a control that is present but not primary
       (a 2rem icon button, a hint row). The Tailwind gray-400 it replaces
       measured 2.41:1 on the light surface. */
    --icon-muted: #9CA3AF;               /* 7.32:1 on the dark surface #141218 */
    --surface-line: var(--md-outline-variant);
    --surface-line-strong: var(--md-outline);
    /* Elevation overlay used by menus, popovers and sheets that must read as
       floating above content without a shadow. */
    --surface-scrim: color-mix(in srgb, var(--md-scrim) 100%, transparent);

    /* === Semantic ramp (aliases existing error/success/warning roles) === */
    --signal-error: var(--md-error);
    --signal-on-error: var(--md-on-error);
    --signal-error-container: var(--md-error-container);
    --signal-on-error-container: var(--md-on-error-container);
    /* THE text role for an error. --md-error is a container TONE in both
       schemes (a light #F2B8B5 tint in the dark scheme), so a label written
       in --md-error / #FFB4AB is invisible on a light surface - 1.61:1, which
       is what .modal-btn-destructive shipped. This role is the deep step of
       the same hue that stays readable on the page. */
    --signal-error-ink: #FFB4AB;         /* 10.95:1 on the dark surface #141218 */
    --signal-success: var(--md-success, #4CC38A);
    --signal-on-success-container: var(--md-on-success-container, #00210B);
    --signal-warning: var(--md-warning, #F0B429);
    --signal-on-warning-container: var(--md-on-warning-container, #261A00);
    --signal-info: var(--brand);
    /* Non-text carrier for each signal — a status may NEVER be signalled by
       hue alone (WCAG 1.4.1), so every status needs an icon or text too.
       These are the fills for that badge/strip. */
    --signal-error-fill: var(--md-error-container);
    --signal-success-fill: var(--md-success-container);
    --signal-warning-fill: var(--md-warning-container);

    /* === Type system — one display + one body + one mono.
       Keeps shared.css stacks (with CJK fallbacks) via alias. === */
    --type-display: var(--font-display);
    --type-body: var(--font-sans);
    --type-mono: var(--font-mono);
    /* Fluid scale */
    --type-xs: clamp(0.72rem, 0.68rem + 0.2vw, 0.78rem);
    --type-sm: clamp(0.82rem, 0.78rem + 0.2vw, 0.875rem);
    --type-md: clamp(0.95rem, 0.9rem + 0.25vw, 1rem);
    --type-lg: clamp(1.12rem, 1.02rem + 0.5vw, 1.25rem);
    --type-xl: clamp(1.35rem, 1.15rem + 1vw, 1.75rem);
    --type-2xl: clamp(1.7rem, 1.35rem + 1.75vw, 2.5rem);

    /* === Layout — unified page grid (replaces per-app wrappers) === */
    --layout-max: 1280px;
    --layout-narrow: 640px;
    --layout-medium: 1024px;
    --layout-pad: clamp(16px, 4vw, 32px);
    --layout-gap: clamp(12px, 2.5vw, 24px);
    --layout-nav-w: 264px;
    --layout-rail-w: 72px;

    /* === Unified breakpoints (replaces 640/768/1000/1024/1180/1280 sprawl) === */
    --bp-sm: 480px;
    --bp-md: 768px;
    --bp-lg: 1024px;
    --bp-xl: 1280px;

    /* === Shape + touch (aliases MD3 shape) === */
    --radius-sm: var(--md-shape-sm);
    --radius-md: var(--md-shape-md);
    --radius-lg: var(--md-shape-lg);
    --radius-xl: var(--md-shape-xl);
    --radius-full: var(--md-shape-full);
    --touch-min: 44px;
    --border-width: 1px;
    --border-width-strong: 2px;

    /* === Motion (aliases MD3 durations/easing) === */
    --motion-fast: var(--md-duration-s);
    --motion-base: var(--md-duration-ms);
    --motion-slow: var(--md-duration-m);
    --motion-ease: var(--md-easing-standard);

    /* === Elevation (aliases MD3) === */
    --shadow-1: var(--md-elevation-1);
    --shadow-2: var(--md-elevation-2);
    --shadow-3: var(--md-elevation-3);

    /* === Z-INDEX SCALE (new)
       The audit found 20+ ad-hoc values from 0 to 9999 with no scale, which
       is how a launcher flyout ends up under a toast or a dialog ends up
       under the skip link. Every layer in the product now names a step.

       These values are NOT a tidy 0/100/200 progression — they are pinned to
       the stacking this codebase already ships, because a custom-select opened
       INSIDE a dialog must render above that dialog's scrim. An earlier draft
       here used dropdown:300 / modal:600, which is backwards for this repo and
       would have hidden every select inside a dialog:
         modal.css:7     1000  (scrim + dialog + bottom sheet)
         dropdown.css:181 2500  (custom-select listbox, deliberately above it)
         shared.css:752  9999  (#spcy-shared-toast)
         shared.css:836  9999  (.skip-link — TIED with the toast today)
       Steps are spaced 100 apart above 1000 so a layer can be inserted without
       renumbering. The sub-1000 ad-hoc values (50/100/300) predate this and keep
       working until they are migrated. */
    --z-base: 0;            /* page content */
    --z-raised: 100;        /* cards lifted on hover, sticky table headers */
    --z-sticky: 200;        /* sticky headers, pinned sub-nav */
    --z-overlay: 900;       /* scrim behind a dialog or bottom sheet */
    --z-modal: 1000;        /* dialog + bottom sheet (matches modal.css) */
    --z-dropdown: 2500;     /* custom-select listbox — MUST stay above --z-modal */
    --z-tooltip: 9600;      /* tooltips and popovers over content, under toasts */
    --z-toast: 9999;        /* matches the shipped #spcy-shared-toast */
    --z-skip-link: 10000;   /* must BEAT the toast; the shipped .skip-link is 9999,
                              a tie that lets a toast cover the skip link */

    /* === FOCUS RING (new names, --md-focus-ring-* stays canonical) === */
    --focus-ring-width: var(--md-focus-ring-width);
    --focus-ring-color: var(--md-focus-ring-color);
    --focus-ring-danger: var(--md-focus-ring-danger);
    --focus-ring-offset: 0px;

    /* === SHARED CONTROL STATE (new)
       Before this, only .md-btn:disabled, .decision-feedback-btn:disabled and
       .file-preview-btn:disabled had rules, so a disabled native <button> in
       any app just looked enabled. One opacity + one cursor for everything. */
    --state-disabled-opacity: 0.45;
    --state-disabled-cursor: not-allowed;
    --state-hover-tint: color-mix(in srgb, var(--md-on-surface) 8%, transparent);
    --state-press-tint: color-mix(in srgb, var(--md-on-surface) 12%, transparent);
    --state-selected-bg: var(--brand-container);
    --state-selected-ink: var(--brand-on-container);

    /* === SKELETON (new)
       web/js/shared/skeleton.js exists but had no colour tokens, so every
       caller hardcoded its own grey. */
    --skeleton-base: color-mix(in srgb, var(--md-on-surface) 10%, transparent);
    --skeleton-highlight: color-mix(in srgb, var(--md-on-surface) 18%, transparent);

    /* === SCRIM (new)
       No scrim token existed; dialogs each spelled their own rgba, which is
       why light and dark modals do not dim by the same amount. */
    /* Explicit rgb()/alpha, NOT color-mix(): --md-scrim is ITSELF translucent
       (rgba(0,0,0,.5) dark / .4 light, shared.css:125/401), so mixing it toward
       transparent premultiplies - a nominal "32%" scrim really rendered at 16%.
       Scheme-independent on purpose: deriving it from --md-scrim is exactly what
       made light and dark dialogs dim by different amounts. */
    --scrim: rgb(0 0 0 / 0.42);
    --scrim-strong: rgb(0 0 0 / 0.62);
}

html:not(.dark):not(.dark-mode) {
    /* The light-scheme half of the brand contract. These are the same values
       shared.css assigns to --md-primary / --md-on-primary in its own light
       block, written here as literals so the two files agree by VALUE rather
       than by reference.

       That value-equality is load-bearing, so the rule is worth stating: the
       direction of the alias is ONE WAY and must stay that way.
         tokens.css  --brand: <literal>
         shared.css  --md-primary: <literal>
       and the apps that retint go the other way but still only inward:
         studio/expense/mt/score/notebook/transcribe
                      --md-primary: var(--brand)
       So --md-primary depends on --brand and --brand depends on nothing. A
       cycle only appears if some file ever writes --brand: var(--md-primary),
       which would make the custom property unresolvable and silently fall
       back to its unset value - a whole accent family vanishing, not a
       visible error. Nothing in the tree does this today; grep for
       "--brand: *var(" if you add a sixth accent family. */
    --brand: #5657AC;
    --brand-on: #FFFFFF;
    --brand-hover: #4A4B96;
    --brand-press: #3D3E7E;
    --brand-ink: #5657AC;
    --brand-ink-hover: #3D3E7E;
    --brand-container: #E0E0FF;
    --brand-on-container: #09006E;
    --brand-scrim: color-mix(in srgb, #5657AC 32%, transparent);

    /* Terracotta: only the INK step flips per scheme. The fill/on pair is the
       same step in both - #C6502F carries white at 4.58:1 on a white card as
       well as on #141218, so one value serves both. The ink darkens here and
       lightens in :root, which is the direction the contract asks for. */
    --accent-terracotta-ink: #9C3B1C;    /* 5.27:1 on a 14% tint of itself
                                            over the light surface #FEF7FF,
                                            5.53:1 over a white card */

    /* Error as TEXT. The dark scheme's #FFB4AB is 1.61:1 here - invisible. */
    --signal-error-ink: #B3261E;         /* 6.21:1 on #FEF7FF, 6.54:1 on a card */

    --icon-muted: #6B7280;               /* 4.60:1 on #FEF7FF, 4.83:1 on a card */

    --signal-success: var(--md-success);
    --signal-warning: var(--md-warning);
}
