/* @eifi1/ui-kit design tokens + semantic utilities.
 *
 * The single source of truth for the shared appearance layer: brand + data
 * colours (theme-aware via `.dark`), the semantic money utilities, the brand
 * accent for checkboxes/radios, and the global keyframes/scrollbar chrome.
 *
 * Consumers import this ONCE (after their own `@import "tailwindcss"`):
 *   @import "@eifi1/ui-kit/tokens.css";
 * and add the package to Tailwind's source scan so its class names are kept:
 *   @source "../node_modules/@eifi1/ui-kit/dist";
 * (relative to the importing CSS file — see README §2. An @source that resolves to
 * nothing is silent, and the app renders unstyled rather than erroring.)
 *
 * NOTE: these :root/.dark values MIRROR DEFAULT_PRESET in the palette theme module,
 * which is what actually paints them — the palette layer writes the active preset
 * tokens inline on <html> at runtime (Tailwind v4 strips a root block that holds only
 * custom properties, so this block is a documentation mirror, not the live source).
 *
 * Because it is inert, it drifts silently, and it did — twice. The surfaces and text
 * colour below still held the pre-#328 palette while DEFAULT_PRESET had moved to Solar
 * Dusk; they were resynced when --text-secondary/--text-muted were added. The money
 * trio and the dark brand accent were left behind that time and written off in this
 * note as "nothing reads them" — nine values, including the accent a consumer copies
 * when they want to know what the brand IS.
 *
 * They are resynced now, and theme/__tests__/tokens-css-mirror.test.ts compares every
 * one of these 46 values against DEFAULT_PRESET on each run, so the next drift fails a
 * test instead of earning a paragraph. The direction is one-way: the presets are the
 * source, this file follows. */

@custom-variant dark (&:where(.dark, .dark *));

:root {
  --bg-page: #ede3cf;          /* warm dim sand — low-glare light page */
  --bg-surface: #f6ecd9;       /* warm off-white card/panel (not pure white) */
  --bg-surface-2: #f3ead6;     /* inset/well surface */
  --border: #e2d6bd;           /* warm hairline border */
  --text-primary: #463f33;     /* warm dark grey — not pure black */
  /* The two quieter text roles. Contrast against the WORST of the three surfaces
     above: 7.0:1 and 4.8:1, so both clear WCAG AA for normal text on all of them —
     which is the reason they are tokens and not `text-slate-500`, a cool grey that
     ignores the palette and sat at 4.4:1 on this ground. */
  --text-secondary: #50493c;
  --text-muted: #696153;
  --brand: #4f46e5;            /* indigo-600 */
  --brand-hover: #4338ca;      /* indigo-700 */
  --brand-contrast: #ffffff;
  --money-income: #0b6650;     /* deep teal — the preset's own, darker than
                                  teal-700 so it clears AA as TEXT on --bg-page */
  --money-expense: #905902;    /* solved: amber-700 (#b45309) reached only 3.88:1 as
                                  TEXT on --bg-page, below WCAG AA. See the preset
                                  audit in theme/__tests__/palette-audit.test.ts. */
  --money-net: #7a4fa3;        /* muted violet, warmed to this palette */
  --money-neutral: #6b6453;    /* warm slate */

  /* ── Field sync status ─────────────────────────────────────────────────────
     The four states a database-backed field can be in. Deliberately NOT part of
     `TokenSet` and NOT per-preset: these are functional signals, like a traffic
     light, and a field that means "saved" must not change meaning because somebody
     picked a different appearance preset. They do flip light/dark, because contrast
     is not optional.

     They mark a field's FRAME and an ICON at its end — non-text graphics, for which
     WCAG 1.4.11 asks 3:1 against the surface — and all four clear it on the worst of
     the three light surfaces. `--status-edited` is amber rather than the orange-800 it
     was: beside red-700 that orange read as a second error colour, and "unsaved" has
     to look like a caution, not an alarm. Amber is below 4.5:1, so it is not a TEXT
     colour: `FieldSyncIndicator` prints its words in `--text-secondary` beside it.

     GREEN AND RED ARE THE CLASSIC CVD-UNSAFE PAIR, and the rest of this file goes out
     of its way to avoid it — the money trio is teal/amber/violet for exactly this
     reason. They are used here because the states were specified in these colours, so
     `FieldSyncIndicator` gives every state its own ICON as well and colour is never
     the only signal. Do not drop the icons.  */
  --status-synced: #166534;    /* green-800  — persisted */
  --status-pending: #1d4ed8;   /* blue-700   — in flight */
  --status-edited: #d97706;    /* amber-600  — dirty, not yet saved */
  --status-error: #b91c1c;     /* red-700    — the save failed */
  --chart-1: #332288;
  --chart-2: #88ccee;
  --chart-3: #44aa99;
  --chart-4: #117733;
  --chart-5: #999933;
  --chart-6: #ddcc77;
  --chart-7: #cc6677;
  --chart-8: #882255;
  --chart-9: #aa4499;
}

.dark {
  --bg-page: #0e0f1c;          /* indigo near-black */
  --bg-surface: #181a2c;
  --bg-surface-2: #21243a;     /* inset/well surface */
  --border: #2e3252;
  --text-primary: #e6e8f4;
  /* As in `:root` — 7.0:1 and 4.8:1 against the worst of the three dark surfaces. */
  --text-secondary: #adafbe;
  --text-muted: #8d90a0;
  --brand: #8b93f8;            /* indigo, lifted off the Tailwind scale to sit at
                                  the same OKLab lightness as the other dark tokens */
  --brand-hover: #aab1fb;
  --brand-contrast: #16182b;   /* the dark page, not indigo-950: a filled brand
                                  button reads as a hole in the surface it sits on */
  --money-income: #22c3b6;     /* teal, pulled down to the dark set's lightness */
  /* Deliberately NOT amber-400 (#fbbf24). Every other colour on a dark surface
     sits around OKLab L 0.68–0.74 (brand 0.68, net 0.70, neutral 0.71, income
     0.74); amber-400 lands at 0.84, so outflows — most rows in a register — shouted
     a tenth of a lightness step louder than inflows carrying the same weight of
     meaning. Same hue (84°) and chroma, pulled to L 0.785: still 8.9:1 on the card
     surface, still far from income in CVD space (Keksdose dev#434). */
  --money-expense: #e4b035;
  --money-net: #b39aef;
  --money-neutral: #9aa1bd;    /* the palette's own cool grey, not zinc-400 */

  /* Field sync status — see the note in `:root`. Against the worst of the three dark
     surfaces these run 8.7 / 9.1 / 6.7 / 8.0:1. */
  --status-synced: #4ade80;    /* green-400 */
  --status-pending: #7dd3fc;   /* sky-300 */
  --status-edited: #fbbf24;    /* amber-400 */
  --status-error: #fca5a5;     /* red-300 */
  --chart-1: #7e72d6;
  --chart-2: #9fd8f2;
  --chart-3: #5fc4b0;
  --chart-4: #3fa45f;
  --chart-5: #bfbf5e;
  --chart-6: #e8dda0;
  --chart-7: #e08c9a;
  --chart-8: #c25a78;
  --chart-9: #cc78be;
}

/* ── Derived surface / text roles ───────────────────────────────────────────
   Everything here is computed FROM the tokens `applyTokenSet` writes inline on
   <html>, so each one follows the active palette preset without being part of
   `TokenSet` and without every preset having to spell it out. That is also why
   they are `color-mix` rather than literals: one formula is correct in both
   themes, because the inputs flip.

   These exist because the components did not have them and reached for Tailwind's
   slate scale instead — 815 hardcoded colour utilities against 85 token
   references, which is the reason a consumer could not re-skin this kit without
   forking it. See docs/module-audit-2026-09-22.md §theming. */
:root,
.dark {
  /* Dimmer than `--text-muted`: placeholders, disabled text, the empty half of a
     value. Mixing TOWARD the surface fades it in light and in dark alike. */
  --text-placeholder: color-mix(in oklab, var(--text-muted) 65%, var(--bg-surface));

  /* The wash under a hovered row, menu item or icon button. Mixing the TEXT colour
     into the surface darkens it on a light theme and lightens it on a dark one,
     which is the behaviour a fixed `slate-100`/`slate-800` pair was faking. */
  --bg-hover: color-mix(in oklab, var(--text-primary) 7%, var(--bg-surface));
  --bg-active: color-mix(in oklab, var(--text-primary) 12%, var(--bg-surface));

  /* A chip that inverts against the page — a selected day, a keyboard hint. These
     are literally the text and surface colours swapped, which is what
     `bg-slate-900 dark:bg-slate-100` was approximating. */
  --bg-inverse: var(--text-primary);
  --text-inverse: var(--bg-surface);

  /* A border that has to be seen rather than merely felt: a focused field, a
     selected card. `--border` pulled toward the text colour. */
  --border-strong: color-mix(in oklab, var(--border) 55%, var(--text-primary));

  /* The SOFT brand: a tinted chip rather than a filled one — an active filter, a
     selected day, a resize handle under the cursor. The kit had `--brand`,
     `--brand-hover` and `--brand-contrast` but nothing for "brand, quietly", so
     components reached for `bg-indigo-100 dark:bg-indigo-500/20` and a consumer
     who changed `--brand` was left with an indigo island. Derived, so it follows
     whatever the brand becomes. */
  --brand-bg: color-mix(in oklab, var(--brand) 14%, var(--bg-surface));
  --brand-bg-hover: color-mix(in oklab, var(--brand) 24%, var(--bg-surface));
  /* Brand text ON `--brand-bg` — pulled away from the surface so it keeps contrast
     against its own tint in both themes. */
  --brand-muted: color-mix(in oklab, var(--brand) 70%, var(--text-primary));
}

/* ── Semantic status ────────────────────────────────────────────────────────
   Destructive and cautionary UI. These are the values the components already
   painted, lifted out of the class names so a consumer can change them — the kit
   previously shipped two different destructive hues with no way to override
   either. Not derived, because a warning is a specific colour, not a tint of the
   page.

   Distinct from the `--status-*` family above: those describe a FIELD's journey
   to the database, these describe an ACTION's consequence. A row can be
   `--status-error` (its last save failed) inside a form whose delete button is
   `--danger` (it will destroy something), and they should not look identical. */
:root {
  --danger: #be123c;              /* rose-700 */
  --danger-hover: #9f1239;        /* rose-800 */
  --danger-contrast: #ffffff;
  --danger-border: #fb7185;       /* rose-400 */
  --danger-bg: #fff1f2;           /* rose-50 */

  --warning: #b45309;             /* amber-700 */
  --warning-border: #fcd34d;      /* amber-300 */
  --warning-bg: #fffbeb;          /* amber-50 */

  /* Informational, and deliberately NOT the brand: a count of active filters is
     not a call to action, and painting it `--brand` would make every table look
     like it had a primary button in the toolbar. */
  --info: #0369a1;                /* sky-700 */
  --info-border: #7dd3fc;         /* sky-300 */
  --info-bg: #e0f2fe;             /* sky-100 */

  /* Done, resolved, accepted. Distinct from `--status-synced` for the same reason
     `--danger` is distinct from `--status-error`: that family describes a FIELD's
     journey to the database, this one an ACTION's outcome. Emerald rather than the
     money teal, so a "resolved" chip cannot be misread as an inflow. */
  --success: #047857;             /* emerald-700 */
  --success-border: #6ee7b7;      /* emerald-300 */
  --success-bg: #d1fae5;          /* emerald-100 */
}

.dark {
  --danger: #fda4af;              /* rose-300 */
  --danger-hover: #fecdd3;        /* rose-200 */
  --danger-contrast: #4c0519;     /* rose-950 */
  --danger-border: #e11d48;       /* rose-600 */
  --danger-bg: rgba(136, 19, 55, 0.2);

  --warning: #fcd34d;             /* amber-300 */
  --warning-border: rgba(180, 83, 9, 0.6);
  --warning-bg: rgba(120, 53, 15, 0.2);

  --info: #7dd3fc;                /* sky-300 */
  --info-border: rgba(3, 105, 161, 0.6);
  --info-bg: rgba(12, 74, 110, 0.25);

  --success: #6ee7b7;             /* emerald-300 */
  --success-border: rgba(4, 120, 87, 0.6);
  --success-bg: rgba(6, 78, 59, 0.25);
}

/* ── Media ─────────────────────────────────────────────────────────────────
   Controls drawn ON a photo or a live camera picture (the camera shutter), whose
   background is the picture, not the page — so they must NOT follow the theme: a
   dark-mode inverse pair would put a dark ring on a dark viewfinder. The same in both
   themes, on purpose. */
:root,
.dark {
  --media-ink: #ffffff;
  --media-scrim: rgb(0 0 0 / 0.35);
  --media-scrim-hover: rgb(0 0 0 / 0.5);
}

/* ── Toasts ─────────────────────────────────────────────────────────────────
   The kit `<Toaster>` (components/toast.tsx) paints sonner with the AlertBanner
   tones. These are ALIASES, not new colours, and they exist for one reason: sonner
   declares its own `--success-bg` / `--success-border` (and info, warning) on the
   toaster element, so that element cannot read the kit's value under the same name
   — `--success-bg: var(--success-bg)` is a cycle. Declared here, they are computed
   on `<html>` and inherited down under a name sonner does not touch.

   The background is the tone's wash layered on the surface, as AlertBanner's
   `elevated` frame does: the dark washes are translucent, and a toast floats over
   content. `--z-toast` is above every kit overlay (modal and sheets z-50, tour
   z-60/61, command palette z-70) and above whatever an app stacks on top of those —
   a toast confirming a dialog's action must not land under the dialog. */
:root,
.dark {
  --z-toast: 2147483647;
  --toast-success-bg: linear-gradient(var(--success-bg), var(--success-bg)) var(--bg-surface);
  --toast-success-border: var(--success-border);
  --toast-info-bg: linear-gradient(var(--info-bg), var(--info-bg)) var(--bg-surface);
  --toast-info-border: var(--info-border);
  --toast-warning-bg: linear-gradient(var(--warning-bg), var(--warning-bg)) var(--bg-surface);
  --toast-warning-border: var(--warning-border);
  --toast-danger-bg: linear-gradient(var(--danger-bg), var(--danger-bg)) var(--bg-surface);
  --toast-danger-border: var(--danger-border);
}

/* The description of an untoned toast in the secondary text colour (sonner hard-codes
   #3f3f3f, unreadable in the dark theme); a toned toast's inherits its tone. */
.kit-toaster [data-sonner-toast][data-styled="true"]:not([data-type="success"], [data-type="info"], [data-type="warning"], [data-type="error"]) [data-description] {
  color: var(--text-secondary);
}
/* The action button in the tone's own colour rather than sonner's black pill. */
.kit-toaster [data-sonner-toast][data-styled="true"] [data-button]:not([data-cancel]) {
  background: var(--toast-action-bg, var(--text-primary));
  color: var(--bg-surface);
}
.kit-toaster [data-sonner-toast][data-type="success"] { --toast-action-bg: var(--success); }
.kit-toaster [data-sonner-toast][data-type="info"] { --toast-action-bg: var(--info); }
.kit-toaster [data-sonner-toast][data-type="warning"] { --toast-action-bg: var(--warning); }
.kit-toaster [data-sonner-toast][data-type="error"] { --toast-action-bg: var(--danger); }
/* The kit's focus ring on the toast's buttons — sonner's is a black shadow that
   vanishes on the dark theme. */
.kit-toaster [data-sonner-toast][data-styled="true"] :is([data-button], [data-close-button]):focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* ── Categorical hues ───────────────────────────────────────────────────────
   Colours that MEAN nothing but "this is a different value": kastlan's status
   enums (status-badge.tsx over enum-meta.ts), where ~15 enums map to 9 Tailwind
   hues. Four of those are semantic and already have tokens — green = --success,
   yellow = --warning, red = --danger, gray = the neutral chip — so only the five
   below are new: blue (open / sent / rented), purple (assigned / accepted),
   orange (terminated / high urgency), teal (invoiced) and indigo (a meeting
   stage). The smallest set that covers every enum; deliberately not a ramp.

   Same triple and same shades as the status family (700 text / 300 border /
   100 surface in light, 300 text / translucent 700 border / 900 wash in dark),
   so a categorical chip sits beside a semantic one at the same weight. Not in
   `TokenSet`: like `--danger`, a hue is a specific colour, not a tint of the
   palette preset, so it lives here and nowhere else. */
:root {
  --hue-blue: #1d4ed8;            /* blue-700 */
  --hue-blue-border: #93c5fd;     /* blue-300 */
  --hue-blue-bg: #dbeafe;         /* blue-100 */
  --hue-indigo: #4338ca;          /* indigo-700 */
  --hue-indigo-border: #a5b4fc;   /* indigo-300 */
  --hue-indigo-bg: #e0e7ff;       /* indigo-100 */
  --hue-purple: #7e22ce;          /* purple-700 */
  --hue-purple-border: #d8b4fe;   /* purple-300 */
  --hue-purple-bg: #f3e8ff;       /* purple-100 */
  --hue-teal: #0f766e;            /* teal-700 */
  --hue-teal-border: #5eead4;     /* teal-300 */
  --hue-teal-bg: #ccfbf1;         /* teal-100 */
  --hue-orange: #c2410c;          /* orange-700 */
  --hue-orange-border: #fdba74;   /* orange-300 */
  --hue-orange-bg: #ffedd5;       /* orange-100 */
}

.dark {
  --hue-blue: #93c5fd;            /* blue-300 */
  --hue-blue-border: rgba(29, 78, 216, 0.6);
  --hue-blue-bg: rgba(30, 58, 138, 0.25);
  --hue-indigo: #a5b4fc;          /* indigo-300 */
  --hue-indigo-border: rgba(67, 56, 202, 0.6);
  --hue-indigo-bg: rgba(49, 46, 129, 0.25);
  --hue-purple: #d8b4fe;          /* purple-300 */
  --hue-purple-border: rgba(126, 34, 206, 0.6);
  --hue-purple-bg: rgba(88, 28, 135, 0.25);
  --hue-teal: #5eead4;            /* teal-300 */
  --hue-teal-border: rgba(15, 118, 110, 0.6);
  --hue-teal-bg: rgba(19, 78, 74, 0.25);
  --hue-orange: #fdba74;          /* orange-300 */
  --hue-orange-border: rgba(194, 65, 12, 0.6);
  --hue-orange-bg: rgba(124, 45, 18, 0.25);
}

/* Semantic colour utilities backed by the tokens above. Unlayered so they win
   over Tailwind's layered colour utilities, and theme-aware for free (the var
   flips under .dark) — no dark: variant needed at the call site. */
.text-money-pos { color: var(--money-income); }
.text-money-neg { color: var(--money-expense); }
.text-money-net { color: var(--money-net); }
.text-money-pos-muted { color: var(--money-income); opacity: 0.72; }
.text-money-neg-muted { color: var(--money-expense); opacity: 0.72; }
.text-brand { color: var(--brand); }
.bg-brand { background-color: var(--brand); }
.bg-money-pos { background-color: var(--money-income); }
.bg-money-neg { background-color: var(--money-expense); }
/* "No money has moved" — the fill for markers on rows that are not real yet
   (scheduled / future-dated). Deliberately NOT brand and NOT money-pos: those
   two mean settled, and a future row wearing one of them reads as done. */
.bg-money-neutral { background-color: var(--money-neutral); }
.border-brand { border-color: var(--brand); }

/* Visually hidden, like Tailwind's `sr-only`, but POSITION: FIXED rather than absolute.
 *
 * `sr-only` is `position: absolute`, so an element wearing it with no positioned
 * ancestor resolves its containing block to the INITIAL one and is laid out at its own
 * offset from the top of the document. Nothing looks wrong — it is 1x1 and clipped —
 * but `documentElement.scrollHeight` grows to reach it, which on a long page produces a
 * second, whole-document scrollbar beside the app shell's own, scrolling past the end of
 * the content into nothing. Measured on the showcase: body 900px, document 47,919px,
 * with the deepest escaped `input.sr-only` sitting at offsetTop 47,918.
 *
 * A fixed element is out of flow AND out of the document's scroll height, so this one
 * can be dropped anywhere — a live region inside a table, a hidden input inside a
 * dropzone — without its container having to remember to be `relative`. Prefer it for
 * anything the kit renders on a consumer's behalf, where there is no wrapper to fix.
 */
.sr-only-fixed {
  position: fixed;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border-width: 0;
}

html, body, #root {
  height: 100%;
}

/* NOT a `scrollbar-gutter` on the DOCUMENT — measured, and it is the wrong instrument.
 *
 * Below `AppShell`'s md breakpoint the viewport is the scroll container (main is only
 * `md:overflow-y-auto`), so nothing reserves a gutter down there and a classic
 * scrollbar takes ~15px out of the content box the moment a page outgrows the window:
 * Keksdose live #307's follow-up, reported as "resizes slightly horizontally".
 *
 * Reserving it on `html` fixes that and breaks something worse. `scrollbar-gutter` on
 * the root shrinks the INITIAL CONTAINING BLOCK, which is what `position: fixed` is
 * laid out against — so every full-bleed overlay in the app stops 15px short on each
 * edge. Measured at a 406px viewport: the row editor's dialog went from x=0/w=406 to
 * x=15/w=376, with live page showing through both strips and tappable. That is
 * Keksdose feedback #32's failure, worse: #32's strip at least belonged to the
 * backdrop, and a tap on this one reaches the register behind the form.
 *
 * The only fix without that side effect is to give the viewport's job to an ELEMENT —
 * dropping the `md:` gate on main's own `overflow-y-auto` — which changes how a phone
 * scrolls (the URL bar stops collapsing) for a bug phones do not have, since overlay
 * scrollbars take no layout space. Left as a decision rather than a silent trade. */

/* Checkboxes/radios default to a visible brand accent. The native dark fill
   blended into the dark UI, so a checked box was almost indistinguishable from
   an unchecked one (feedback #219). Must live in the base layer: unlayered
   styles beat Tailwind's layered utilities regardless of specificity, which
   silently overrode deliberate `accent-*` utilities like the destructive rose
   fresh-start checkbox (feedback #264). */
@layer base {
  :where(input[type="checkbox"], input[type="radio"]) {
    accent-color: var(--brand); /* unified indigo brand accent (theme-aware) */
  }
}

body {
  background-color: var(--bg-page);
  color: var(--text-primary);
}

/* Indeterminate progress bar (e.g. long imports, feedback #270): a third-width
   segment sweeping the track. */
@keyframes indeterminate-sweep {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(300%); }
}
.animate-indeterminate {
  animation: indeterminate-sweep 1.4s ease-in-out infinite;
}

/* Overlay motion (Keksdose live #320: *"Evaluate how much work it would be to have
   nicer transitions for the mobile users when clicking on a row … for all hoc derived
   tables"*).
 *
 * Two keyframes rather than a library. Every overlay in this package is bottom-anchored
 * on a phone and centred from md up, so one pair covers the row sheet, the modal, the
 * picker and the numpad — and every table in every consuming app gets it by getting
 * them, which is what the row asked for.
 *
 * Enter AND exit since the #320 rework — *"Recognized the opening transition. Choose
 * transition should be the same, only backwards"*. The first round shipped the enter
 * alone on the argument that an exit needs the element to stay mounted past the state
 * change; that is still true, and `useCloseTransition` is where that state machine now
 * lives, once, for every overlay in this package.
 *
 * The exit is the same two moves read backwards, at the same durations, on the mirrored
 * curve (`ease-in` against `ease-out`, and the sheet's cubic reversed): a close that
 * eases OUT of the screen at the speed it eased in reads as the same object leaving.
 *
 * 180ms and an ease-out curve: long enough to read as movement, short enough that it
 * never delays a screen whose data is already in memory — which most of these are. The
 * exit is what that budget is really spent on: every millisecond here is a millisecond
 * between a tap on "Close" and the list being usable again, which is why it does not
 * get the slower, more cinematic curve a first attempt reaches for.
 */
@keyframes overlay-fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes sheet-rise-in {
  from { transform: translateY(100%); }
  to { transform: none; }
}
@keyframes overlay-fade-out {
  from { opacity: 1; }
  to { opacity: 0; }
}
@keyframes sheet-lower-out {
  from { transform: none; }
  to { transform: translateY(100%); }
}
.animate-overlay { animation: overlay-fade-in 0.18s ease-out; }
.animate-sheet { animation: sheet-rise-in 0.22s cubic-bezier(0.32, 0.72, 0, 1); }
/* `forwards` on both: the element is unmounted by a timer, and without a retained end
   state the last painted frame before that timer fires is the panel back at full
   opacity in its opening position — a flash of the thing you just dismissed. */
.animate-overlay-out { animation: overlay-fade-out 0.18s ease-in forwards; }
.animate-sheet-out { animation: sheet-lower-out 0.22s cubic-bezier(1, 0, 0.68, 0.28) forwards; }

/* Motion is the first thing to drop here, not the last. The overlay still fades — a
   backdrop appearing instantly is a flash, not a transition — but nothing travels.
   The EXIT pair is not listed: `useCloseTransition` reads the same media query and
   closes immediately under it, so these classes are never applied at all. */
@media (prefers-reduced-motion: reduce) {
  .animate-sheet { animation: none; }
  .animate-overlay { animation-duration: 0.01ms; }
}

/* The sidebar's flyout panel: a short fade and a 4px slide out of the sidebar, so it
   reads as coming FROM the entry it belongs to rather than appearing beside it. The
   slide uses the inline axis, so it runs the right way under `dir="rtl"` too. Under
   reduced motion it only fades — nothing travels. */
@keyframes flyout-in {
  from { opacity: 0; translate: var(--flyout-from, -4px) 0; }
  to { opacity: 1; translate: 0 0; }
}
[dir="rtl"] .animate-flyout { --flyout-from: 4px; }
.animate-flyout { animation: flyout-in 0.14s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .animate-flyout { animation-name: overlay-fade-in; }
}

/* The phone's page row (AppShell `mobileSubNav`) rising out of the bottom bar. */
@keyframes subnav-in {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: none; }
}
.animate-subnav { animation: subnav-in 0.2s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .animate-subnav { animation-name: overlay-fade-in; }
}

/* Chart drilldown fan-out/-in (feedback #260 rework): the remounted chart eases
   in instead of snapping. */
@keyframes drill-in {
  from { opacity: 0; transform: scale(0.96); }
  to { opacity: 1; transform: none; }
}
.animate-drill {
  animation: drill-in 0.25s ease-out;
}
/* A drilldown still has to SAY the level changed, so under reduced motion the chart
   fades in rather than appearing in one frame — but it no longer scales. */
@media (prefers-reduced-motion: reduce) {
  .animate-drill { animation-name: overlay-fade-in; }
}

/* Scrollbars: keep light defaults; theme to slate when dark mode is active. */
.dark {
  color-scheme: dark;
  scrollbar-color: rgb(51 65 85) rgb(15 23 42); /* slate-700 thumb on slate-900 track */
}

.dark ::-webkit-scrollbar {
  width: 12px;
  height: 12px;
}

.dark ::-webkit-scrollbar-track {
  background-color: rgb(15 23 42); /* slate-900 */
}

.dark ::-webkit-scrollbar-thumb {
  background-color: rgb(51 65 85); /* slate-700 */
  border-radius: 6px;
  border: 2px solid rgb(15 23 42); /* slate-900 — creates inset padding */
}

.dark ::-webkit-scrollbar-thumb:hover {
  background-color: rgb(71 85 105); /* slate-600 */
}

.dark ::-webkit-scrollbar-corner {
  background-color: rgb(15 23 42); /* slate-900 */
}
