/*
 * Lumeo — Base Theme Variables
 *
 * Default theme: Zinc — matches the Lumeo logo's monochrome identity.
 * Override CSS custom properties or use data-theme="blue|green|rose|orange" for alternate palettes.
 *
 * Usage with Tailwind CSS v4:
 *   @import "./_content/Lumeo/css/lumeo.css" layer(base);
 *
 * Usage without Tailwind:
 *   <link href="_content/Lumeo/css/lumeo.css" rel="stylesheet" />
 */

/* shadcn's base layer: every border defaults to the border token, so a bare `border`
   class draws the hairline instead of currentColor. Layered so utilities still win. */
@layer base {
  *, ::before, ::after, ::backdrop, ::file-selector-button {
    border-color: var(--color-border);
  }
}

:root {
  /* Content height of a Default-variant BottomNav, reserved by
     BottomNav.ReserveSpace so a fixed bottom nav doesn't overlap page content.
     Derived from BottomNavItem (icon h-5 1.25rem + gap-0.5 0.125rem + text-xs
     label ~1rem + item py-1.5 0.75rem) plus the inner container pt-1.5/pb-1.5
     (0.75rem) = 3.875rem; rounded up to 4rem for a small safety margin.
     EXCLUDES the safe-area inset (the spacer adds env(safe-area-inset-bottom)
     exactly once, mirroring the nav's own interior padding-bottom — they do not
     stack). Override per app if your nav is taller (e.g. two-line labels). */
  --lumeo-bottom-nav-height: 4rem;

  --color-background: var(--background, oklch(1 0 0));
  --color-foreground: var(--foreground, oklch(0.1405 0.0044 285.82));

  --color-card: var(--card, oklch(1 0 0));
  --color-card-foreground: var(--card-foreground, oklch(0.1405 0.0044 285.82));

  --color-popover: var(--popover, oklch(1 0 0));
  --color-popover-foreground: var(--popover-foreground, oklch(0.1405 0.0044 285.82));

  --color-primary: var(--primary, oklch(0.2103 0.0059 285.88));
  --color-primary-foreground: var(--primary-foreground, oklch(0.9848 0 0));

  --color-secondary: var(--secondary, oklch(0.9676 0.0013 286.38));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.2103 0.0059 285.88));

  --color-muted: var(--muted, oklch(0.9676 0.0013 286.38));
  --color-muted-foreground: var(--muted-foreground, oklch(0.5519 0.0137 285.94));

  --color-accent: var(--accent, oklch(0.9676 0.0013 286.38));
  --color-accent-foreground: var(--accent-foreground, oklch(0.2103 0.0059 285.88));

  --color-destructive: var(--destructive, oklch(0.6356 0.2082 25.38));
  --color-destructive-foreground: var(--destructive-foreground, oklch(1 0 0));
  --color-destructive-light: var(--destructive-light, oklch(0.9492 0.0195 17.5));
  --color-destructive-text: var(--destructive-text, oklch(0.441 0.1603 26.89));

  --color-positive: var(--positive, oklch(0.6596 0.1414 157.72));
  --color-positive-foreground: var(--positive-foreground, oklch(1 0 0));
  --color-positive-light: var(--positive-light, oklch(0.9699 0.0155 166.46));
  --color-positive-text: var(--positive-text, oklch(0.517 0.1084 158.1));

  --color-success: var(--success, oklch(0.6596 0.1414 157.72));
  --color-success-foreground: var(--success-foreground, oklch(1 0 0));
  --color-success-light: var(--success-light, oklch(0.9699 0.0155 166.46));

  --color-warning: var(--warning, oklch(0.7697 0.1645 70.61));
  --color-warning-foreground: var(--warning-foreground, oklch(0.1405 0.0044 285.82));
  --color-warning-light: var(--warning-light, oklch(0.9724 0.0189 81.5));

  --color-info: var(--info, oklch(0.633 0.162 252.07));
  --color-info-foreground: var(--info-foreground, oklch(1 0 0));
  --color-info-light: var(--info-light, oklch(0.9584 0.0131 247.96));
  --color-info-text: var(--info-text, oklch(0.4637 0.1332 253.3));

  --color-success-text: var(--success-text, oklch(0.517 0.1084 158.1));
  --color-warning-text: var(--warning-text, oklch(0.5583 0.1073 75.77));

  --color-rating: var(--rating, oklch(0.7858 0.1598 85.31));

  --color-progress-stripe: var(--progress-stripe, rgba(255, 255, 255, 0.15));
  --color-overlay-backdrop: var(--overlay-backdrop, rgba(0, 0, 0, 0.5));

  --color-chart-1: var(--chart-1, oklch(0.56 0.1909 258));
  --color-chart-2: var(--chart-2, oklch(0.6 0.0942 195));
  --color-chart-3: var(--chart-3, oklch(0.58 0.1679 145));
  --color-chart-4: var(--chart-4, oklch(0.65 0.1481 55));
  --color-chart-5: var(--chart-5, oklch(0.56 0.205 320));

  --color-sidebar: var(--sidebar, oklch(1 0 0));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.1405 0.0044 285.82));
  --color-sidebar-primary: var(--sidebar-primary, oklch(0.2103 0.0059 285.88));
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground, oklch(0.9848 0 0));
  --color-sidebar-accent: var(--sidebar-accent, oklch(0.9676 0.0013 286.38));
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground, oklch(0.2103 0.0059 285.88));
  --color-sidebar-border: var(--sidebar-border, oklch(0.9197 0.004 286.32));
  --color-sidebar-ring: var(--sidebar-ring, oklch(0.2103 0.0059 285.88));

  --color-border: var(--border, oklch(0.9197 0.004 286.32));
  --color-input: var(--input, oklch(0.9197 0.004 286.32));
  --color-ring: var(--ring, oklch(0.2103 0.0059 285.88));

  /* Card inset and gap, shadcn's --card-spacing (1rem measured live 2026-09-02, reui the
     same). Override it to re-space every card; Card's Density moves it to 0.75 / 1.5rem. */
  --card-spacing: 1rem;

  /* Geometry tokens (#434): the fixed set a consumer tunes instead of overriding class
     literals. Each component reads var(--lumeo-…, <this default>) on its Comfortable rung;
     Compact and Spacious stay one step below and above as fixed presets. Values follow
     Tailwind's --spacing so a rescaled spacing scale moves them too. */
  --lumeo-control-h-xs: calc(var(--spacing, 0.25rem) * 6);     /* Button Xs: 24px */
  --lumeo-control-h-sm: calc(var(--spacing, 0.25rem) * 7);     /* Button Sm: 28px */
  --lumeo-control-h: calc(var(--spacing, 0.25rem) * 8);        /* Button, Input, Select and every field-shaped control: 32px */
  --lumeo-control-h-lg: calc(var(--spacing, 0.25rem) * 9);     /* Button Lg: 36px */
  --lumeo-icon-size: calc(var(--spacing, 0.25rem) * 4);        /* an unsized icon inside a button, menu item, tab or sidebar item: 16px */
  --lumeo-sidebar-item-h-sm: calc(var(--spacing, 0.25rem) * 7); /* SidebarMenuButton Sm: 28px */
  --lumeo-sidebar-item-h: calc(var(--spacing, 0.25rem) * 8);    /* SidebarMenuButton: 32px */
  --lumeo-sidebar-item-h-lg: calc(var(--spacing, 0.25rem) * 12); /* SidebarMenuButton Lg: 48px */
  --lumeo-table-head-h: calc(var(--spacing, 0.25rem) * 10);    /* TableHead: 40px */
  --lumeo-table-cell-p: calc(var(--spacing, 0.25rem) * 2);     /* TableCell padding, TableHead horizontal padding: 8px */
  --lumeo-grid-cell-px: calc(var(--spacing, 0.25rem) * 3);     /* DataGrid header and body cell horizontal padding: 12px */
  --lumeo-grid-cell-py: calc(var(--spacing, 0.25rem) * 2);     /* DataGrid header and body cell vertical padding: 8px */
  --lumeo-calendar-cell-size: calc(var(--spacing, 0.25rem) * 8); /* Calendar day cell: 32px */
  --lumeo-calendar-p: calc(var(--spacing, 0.25rem) * 3);       /* Calendar frame padding: 12px */

  --radius: 0.625rem;
  /* shadcn v4 scale (measured live 2026-09-01, --radius 0.625rem): sm 6px, md 8px,
     lg 10px, xl 14px. It used to be 0.5r / r / 1.25r / 1.5r = 5 / 10 / 12.5 / 15px, which made
     every rounded-md corner 2px rounder than shadcn's and every rounded-lg 2.5px. */
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);

  /* Shadow scale exposed as variables so a pasted shadcn/tweakcn --shadow-* drives the
     shadow-* utilities 1:1. @theme maps shadow-* -> var(--shadow-*), overriding only
     --tw-shadow, so the box-shadow ring/inset composition (focus rings) is preserved.
     Defaults mirror Tailwind's built-in scale and keep the --tw-shadow-color hook so
     shadow-<color> utilities still recolor. */
  --shadow-2xs: 0 1px var(--tw-shadow-color, #0000000d);
  --shadow-xs: 0 1px 2px 0 var(--tw-shadow-color, #0000000d);
  --shadow-sm: 0 1px 3px 0 var(--tw-shadow-color, #0000001a), 0 1px 2px -1px var(--tw-shadow-color, #0000001a);
  --shadow: 0 1px 3px 0 var(--tw-shadow-color, #0000001a), 0 1px 2px -1px var(--tw-shadow-color, #0000001a);
  --shadow-md: 0 4px 6px -1px var(--tw-shadow-color, #0000001a), 0 2px 4px -2px var(--tw-shadow-color, #0000001a);
  --shadow-lg: 0 10px 15px -3px var(--tw-shadow-color, #0000001a), 0 4px 6px -4px var(--tw-shadow-color, #0000001a);
  --shadow-xl: 0 20px 25px -5px var(--tw-shadow-color, #0000001a), 0 8px 10px -6px var(--tw-shadow-color, #0000001a);
  --shadow-2xl: 0 25px 50px -12px var(--tw-shadow-color, #00000040);
  /* Event chips (Scheduler) paint the event's colour as TEXT on an 18% tint of that
     same colour. That pairing is illegible on its own — measured 1.13:1 to 2.19:1 in
     dark mode and 1.41:1 to 2.88:1 in light, against WCAG's 3:1 floor for UI text.
     Mixing the text toward an anchor that FLIPS with the theme fixes it for ANY colour
     a caller supplies, not just the palette Lumeo ships. Lives here rather than in
     lumeo-scheduler.css because that file is a package asset no host page links. */
  --lumeo-event-fg-anchor: #000;

}

.dark {
  --color-background: var(--background, oklch(0.1405 0.0044 285.82));
  --color-foreground: var(--foreground, oklch(0.9848 0 0));

  --color-card: var(--card, oklch(0.1405 0.0044 285.82));
  --color-card-foreground: var(--card-foreground, oklch(0.9848 0 0));

  --color-popover: var(--popover, oklch(0.1405 0.0044 285.82));
  --color-popover-foreground: var(--popover-foreground, oklch(0.9848 0 0));

  --color-primary: var(--primary, oklch(0.9848 0 0));
  --color-primary-foreground: var(--primary-foreground, oklch(0.2103 0.0059 285.88));

  --color-secondary: var(--secondary, oklch(0.2741 0.0055 286.03));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.9848 0 0));

  --color-muted: var(--muted, oklch(0.2741 0.0055 286.03));
  --color-muted-foreground: var(--muted-foreground, oklch(0.7119 0.0129 286.07));

  --color-accent: var(--accent, oklch(0.2741 0.0055 286.03));
  --color-accent-foreground: var(--accent-foreground, oklch(0.9848 0 0));

  --color-destructive: var(--destructive, oklch(0.6356 0.2082 25.38));
  --color-destructive-foreground: var(--destructive-foreground, oklch(1 0 0));
  --color-destructive-light: var(--destructive-light, oklch(0.2482 0.0611 22.58));
  --color-destructive-text: var(--destructive-text, oklch(0.706 0.1513 21.67));

  --color-positive: var(--positive, oklch(0.5775 0.1224 157.91));
  --color-positive-foreground: var(--positive-foreground, oklch(1 0 0));
  --color-positive-light: var(--positive-light, oklch(0.2932 0.0353 163.01));
  --color-positive-text: var(--positive-text, oklch(0.7921 0.1381 160.44));

  --color-success: var(--success, oklch(0.5775 0.1224 157.91));
  --color-success-foreground: var(--success-foreground, oklch(1 0 0));
  --color-success-light: var(--success-light, oklch(0.2932 0.0353 163.01));

  --color-warning: var(--warning, oklch(0.6888 0.141 73.42));
  --color-warning-foreground: var(--warning-foreground, oklch(0.9848 0 0));
  --color-warning-light: var(--warning-light, oklch(0.3004 0.0431 79.59));

  --color-info: var(--info, oklch(0.5987 0.1773 253.7));
  --color-info-foreground: var(--info-foreground, oklch(1 0 0));
  --color-info-light: var(--info-light, oklch(0.2636 0.0356 249.31));
  --color-info-text: var(--info-text, oklch(0.7478 0.1093 249.48));

  --color-success-text: var(--success-text, oklch(0.7921 0.1381 160.44));
  --color-warning-text: var(--warning-text, oklch(0.815 0.1237 79.21));

  --color-rating: var(--rating, oklch(0.8467 0.1613 88.63));

  --color-progress-stripe: var(--progress-stripe, rgba(255, 255, 255, 0.15));
  --color-overlay-backdrop: var(--overlay-backdrop, rgba(0, 0, 0, 0.6));

  --color-chart-1: var(--chart-1, oklch(0.6 0.2016 258));
  --color-chart-2: var(--chart-2, oklch(0.65 0.1021 195));
  --color-chart-3: var(--chart-3, oklch(0.65 0.1882 145));
  --color-chart-4: var(--chart-4, oklch(0.71 0.1618 55));
  --color-chart-5: var(--chart-5, oklch(0.6 0.205 320));

  --color-sidebar: var(--sidebar, oklch(0.1405 0.0044 285.82));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.9848 0 0));
  --color-sidebar-primary: var(--sidebar-primary, oklch(0.9848 0 0));
  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground, oklch(0.2103 0.0059 285.88));
  --color-sidebar-accent: var(--sidebar-accent, oklch(0.2741 0.0055 286.03));
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground, oklch(0.9848 0 0));
  --color-sidebar-border: var(--sidebar-border, oklch(0.2741 0.0055 286.03));
  --color-sidebar-ring: var(--sidebar-ring, oklch(0.9848 0 0));

  --color-border: var(--border, oklch(0.2741 0.0055 286.03));
  --color-input: var(--input, oklch(0.2741 0.0055 286.03));
  --color-ring: var(--ring, oklch(0.8709 0.0055 286.29));
  --lumeo-event-fg-anchor: #fff;

}

/* =====================================================
 * Base Color Variations
 * These adjust neutral/gray tones for muted, border, input, secondary, and card backgrounds.
 * The "slate" base is the default (no attribute needed).
 * ===================================================== */

[data-base-color="gray"] {
  --color-background: var(--background, oklch(0.9924 0 0));
  --color-foreground: var(--foreground, oklch(0.1284 0.0267 261.59));
  --color-card: var(--card, oklch(1 0 0));
  --color-card-foreground: var(--card-foreground, oklch(0.1284 0.0267 261.59));
  --color-popover: var(--popover, oklch(1 0 0));
  --color-popover-foreground: var(--popover-foreground, oklch(0.1284 0.0267 261.59));
  --color-muted: var(--muted, oklch(0.9679 0.0027 264.54));
  --color-muted-foreground: var(--muted-foreground, oklch(0.5503 0.0235 264.36));
  --color-border: var(--border, oklch(0.9278 0.0058 264.53));
  --color-input: var(--input, oklch(0.9278 0.0058 264.53));
  --color-secondary: var(--secondary, oklch(0.9679 0.0027 264.54));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.2333 0.0076 264.42));
  --color-accent: var(--accent, oklch(0.9679 0.0027 264.54));
  --color-accent-foreground: var(--accent-foreground, oklch(0.2333 0.0076 264.42));
  --color-sidebar: var(--sidebar, oklch(1 0 0));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.1284 0.0267 261.59));
  --color-sidebar-border: var(--sidebar-border, oklch(0.9278 0.0058 264.53));
}

[data-base-color="gray"].dark,
.dark[data-base-color="gray"] {
  --color-background: var(--background, oklch(0.1284 0.0267 261.59));
  --color-foreground: var(--foreground, oklch(0.9843 0.0017 247.84));
  --color-card: var(--card, oklch(0.1871 0.0107 264.27));
  --color-card-foreground: var(--card-foreground, oklch(0.9843 0.0017 247.84));
  --color-popover: var(--popover, oklch(0.1871 0.0107 264.27));
  --color-popover-foreground: var(--popover-foreground, oklch(0.9843 0.0017 247.84));
  --color-muted: var(--muted, oklch(0.2721 0.0203 256.82));
  --color-muted-foreground: var(--muted-foreground, oklch(0.6538 0.0299 256.78));
  --color-border: var(--border, oklch(0.3123 0.022 256.82));
  --color-input: var(--input, oklch(0.3123 0.022 256.82));
  --color-secondary: var(--secondary, oklch(0.2721 0.0203 256.82));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.9676 0.007 247.9));
  --color-accent: var(--accent, oklch(0.2721 0.0203 256.82));
  --color-accent-foreground: var(--accent-foreground, oklch(0.9676 0.007 247.9));
  --color-sidebar: var(--sidebar, oklch(0.1871 0.0107 264.27));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.9843 0.0017 247.84));
  --color-sidebar-border: var(--sidebar-border, oklch(0.3123 0.022 256.82));
}

[data-base-color="zinc"] {
  --color-background: var(--background, oklch(1 0 0));
  --color-foreground: var(--foreground, oklch(0.1417 0.0044 285.82));
  --color-card: var(--card, oklch(1 0 0));
  --color-card-foreground: var(--card-foreground, oklch(0.1417 0.0044 285.82));
  --color-popover: var(--popover, oklch(1 0 0));
  --color-popover-foreground: var(--popover-foreground, oklch(0.1417 0.0044 285.82));
  --color-muted: var(--muted, oklch(0.9683 0.0014 286.37));
  --color-muted-foreground: var(--muted-foreground, oklch(0.5504 0.0144 285.92));
  --color-border: var(--border, oklch(0.9197 0.0041 286.32));
  --color-input: var(--input, oklch(0.9197 0.0041 286.32));
  --color-secondary: var(--secondary, oklch(0.9683 0.0014 286.37));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.2102 0.006 285.87));
  --color-accent: var(--accent, oklch(0.9683 0.0014 286.37));
  --color-accent-foreground: var(--accent-foreground, oklch(0.2102 0.006 285.87));
  --color-sidebar: var(--sidebar, oklch(1 0 0));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.1417 0.0044 285.82));
  --color-sidebar-border: var(--sidebar-border, oklch(0.9197 0.0041 286.32));
}

[data-base-color="zinc"].dark,
.dark[data-base-color="zinc"] {
  --color-background: var(--background, oklch(0.1417 0.0044 285.82));
  --color-foreground: var(--foreground, oklch(0.9848 0 0));
  --color-card: var(--card, oklch(0.1886 0.0049 285.92));
  --color-card-foreground: var(--card-foreground, oklch(0.9848 0 0));
  --color-popover: var(--popover, oklch(0.1886 0.0049 285.92));
  --color-popover-foreground: var(--popover-foreground, oklch(0.9848 0 0));
  --color-muted: var(--muted, oklch(0.2747 0.006 286));
  --color-muted-foreground: var(--muted-foreground, oklch(0.652 0.0158 285.95));
  --color-border: var(--border, oklch(0.3146 0.0072 285.98));
  --color-input: var(--input, oklch(0.3146 0.0072 285.98));
  --color-secondary: var(--secondary, oklch(0.2747 0.006 286));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.9848 0 0));
  --color-accent: var(--accent, oklch(0.2747 0.006 286));
  --color-accent-foreground: var(--accent-foreground, oklch(0.9848 0 0));
  --color-sidebar: var(--sidebar, oklch(0.1886 0.0049 285.92));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.9848 0 0));
  --color-sidebar-border: var(--sidebar-border, oklch(0.3146 0.0072 285.98));
}

[data-base-color="neutral"] {
  --color-background: var(--background, oklch(1 0 0));
  --color-foreground: var(--foreground, oklch(0.1457 0 0));
  --color-card: var(--card, oklch(1 0 0));
  --color-card-foreground: var(--card-foreground, oklch(0.1457 0 0));
  --color-popover: var(--popover, oklch(1 0 0));
  --color-popover-foreground: var(--popover-foreground, oklch(0.1457 0 0));
  --color-muted: var(--muted, oklch(0.9696 0 0));
  --color-muted-foreground: var(--muted-foreground, oklch(0.5547 0 0));
  --color-border: var(--border, oklch(0.9234 0 0));
  --color-input: var(--input, oklch(0.9234 0 0));
  --color-secondary: var(--secondary, oklch(0.9696 0 0));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.2044 0 0));
  --color-accent: var(--accent, oklch(0.9696 0 0));
  --color-accent-foreground: var(--accent-foreground, oklch(0.2044 0 0));
  --color-sidebar: var(--sidebar, oklch(1 0 0));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.1457 0 0));
  --color-sidebar-border: var(--sidebar-border, oklch(0.9234 0 0));
}

[data-base-color="neutral"].dark,
.dark[data-base-color="neutral"] {
  --color-background: var(--background, oklch(0.1457 0 0));
  --color-foreground: var(--foreground, oklch(0.9848 0 0));
  --color-card: var(--card, oklch(0.1815 0 0));
  --color-card-foreground: var(--card-foreground, oklch(0.9848 0 0));
  --color-popover: var(--popover, oklch(0.1815 0 0));
  --color-popover-foreground: var(--popover-foreground, oklch(0.9848 0 0));
  --color-muted: var(--muted, oklch(0.2697 0 0));
  --color-muted-foreground: var(--muted-foreground, oklch(0.6662 0 0));
  --color-border: var(--border, oklch(0.3008 0 0));
  --color-input: var(--input, oklch(0.3008 0 0));
  --color-secondary: var(--secondary, oklch(0.2697 0 0));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.9848 0 0));
  --color-accent: var(--accent, oklch(0.2697 0 0));
  --color-accent-foreground: var(--accent-foreground, oklch(0.9848 0 0));
  --color-sidebar: var(--sidebar, oklch(0.1815 0 0));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.9848 0 0));
  --color-sidebar-border: var(--sidebar-border, oklch(0.3008 0 0));
}

[data-base-color="stone"] {
  --color-background: var(--background, oklch(1 0 0));
  --color-foreground: var(--foreground, oklch(0.1454 0.004 49.63));
  --color-card: var(--card, oklch(1 0 0));
  --color-card-foreground: var(--card-foreground, oklch(0.1454 0.004 49.63));
  --color-popover: var(--popover, oklch(1 0 0));
  --color-popover-foreground: var(--popover-foreground, oklch(0.1454 0.004 49.63));
  --color-muted: var(--muted, oklch(0.9697 0.0014 58.34));
  --color-muted-foreground: var(--muted-foreground, oklch(0.5559 0.0111 58.09));
  --color-border: var(--border, oklch(0.9238 0.0035 58.31));
  --color-input: var(--input, oklch(0.9238 0.0035 58.31));
  --color-secondary: var(--secondary, oklch(0.9697 0.0014 58.34));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.204 0.0058 48.42));
  --color-accent: var(--accent, oklch(0.9697 0.0014 58.34));
  --color-accent-foreground: var(--accent-foreground, oklch(0.204 0.0058 48.42));
  --color-sidebar: var(--sidebar, oklch(1 0 0));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.1454 0.004 49.63));
  --color-sidebar-border: var(--sidebar-border, oklch(0.9238 0.0035 58.31));
}

[data-base-color="stone"].dark,
.dark[data-base-color="stone"] {
  --color-background: var(--background, oklch(0.1454 0.004 49.63));
  --color-foreground: var(--foreground, oklch(0.97 0.0017 67.8));
  --color-card: var(--card, oklch(0.1819 0.0046 56.09));
  --color-card-foreground: var(--card-foreground, oklch(0.97 0.0017 67.8));
  --color-popover: var(--popover, oklch(0.1819 0.0046 56.09));
  --color-popover-foreground: var(--popover-foreground, oklch(0.97 0.0017 67.8));
  --color-muted: var(--muted, oklch(0.2691 0.0072 48.44));
  --color-muted-foreground: var(--muted-foreground, oklch(0.6673 0.0099 58.16));
  --color-border: var(--border, oklch(0.3002 0.0084 48.43));
  --color-input: var(--input, oklch(0.3002 0.0084 48.43));
  --color-secondary: var(--secondary, oklch(0.2691 0.0072 48.44));
  --color-secondary-foreground: var(--secondary-foreground, oklch(0.97 0.0017 67.8));
  --color-accent: var(--accent, oklch(0.2691 0.0072 48.44));
  --color-accent-foreground: var(--accent-foreground, oklch(0.97 0.0017 67.8));
  --color-sidebar: var(--sidebar, oklch(0.1819 0.0046 56.09));
  --color-sidebar-foreground: var(--sidebar-foreground, oklch(0.97 0.0017 67.8));
  --color-sidebar-border: var(--sidebar-border, oklch(0.3002 0.0084 48.43));
}

/* =====================================================
 * Style: New York
 * Compact style with tighter radius
 * ===================================================== */

.style-new-york {
  /* shadcn's new-york style has been 0.625rem since v4; 0.5rem was the pre-v4 value. */
  --radius: 0.625rem;
}

/* =====================================================
 * Menu Accent Variations
 * ===================================================== */

[data-menu-accent="bold"] {
  --color-sidebar-accent: var(--sidebar-accent, var(--color-primary));
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground, var(--color-primary-foreground));
}

[data-menu-accent="outline"] {
  --color-sidebar-accent: var(--sidebar-accent, transparent);
  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground, var(--color-sidebar-foreground));
}

/* Custom Scrollbar */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}

*::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

*::-webkit-scrollbar-track {
  background: transparent;
}

*::-webkit-scrollbar-thumb {
  background-color: var(--color-border);
  border-radius: 9999px;
}

*::-webkit-scrollbar-thumb:hover {
  background-color: var(--color-muted-foreground);
}

/* Hide scrollbars entirely (used by wheel-style pickers) */
.scrollbar-none {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.scrollbar-none::-webkit-scrollbar {
  display: none;
  width: 0;
  height: 0;
}

/* Slider thumb: shadcn's — a 16px white disc with a primary hairline and shadow-sm,
   growing a 4px ring on hover and keyboard focus. */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  background: #fff;
  border: 1px solid var(--color-primary);
  border-radius: 9999px;
  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
  cursor: pointer;
  transition: box-shadow 150ms;
}

input[type="range"]::-moz-range-thumb {
  width: 16px;
  height: 16px;
  background: #fff;
  border: 1px solid var(--color-primary);
  border-radius: 9999px;
  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
  cursor: pointer;
  transition: box-shadow 150ms;
}

input[type="range"]:not(:disabled)::-webkit-slider-thumb:hover,
input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-ring) 50%, transparent);
}

input[type="range"]:not(:disabled)::-moz-range-thumb:hover,
input[type="range"]:focus-visible::-moz-range-thumb {
  box-shadow: 0 0 0 4px color-mix(in oklab, var(--color-ring) 50%, transparent);
}

/* ColorPicker SV (saturation/value) 2D picker */
.lumeo-cp-sv {
  aspect-ratio: 4 / 3;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}

/* ColorPicker sliders — smaller, lighter thumb with white ring */
.lumeo-cp-slider {
  -webkit-appearance: none;
  appearance: none;
  height: 10px;
  border-radius: 9999px;
  outline: none;
}

/* Hue slider: full spectrum gradient */
.lumeo-cp-hue {
  background: linear-gradient(to right,
    hsl(0, 100%, 50%),
    hsl(60, 100%, 50%),
    hsl(120, 100%, 50%),
    hsl(180, 100%, 50%),
    hsl(240, 100%, 50%),
    hsl(300, 100%, 50%),
    hsl(360, 100%, 50%)
  );
}
/* Bug C — SV indicator circle: border-white is always correct (contrasts against the colored canvas),
   but the outer ring shadow adapts to the theme via color-mix so it's visible in both light and dark. */
.lumeo-cp-sv-indicator {
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-foreground) 35%, transparent);
}

/* Bug D — Slider thumbs: was hardcoded #fff / rgba(0,0,0,0.15) which is invisible in dark mode.
   Now uses theme variables: background matches the page background, border uses theme border. */
.lumeo-cp-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  background: var(--color-background);
  border: 2px solid var(--color-border);
  border-radius: 9999px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.25);
  cursor: pointer;
  transition: transform 120ms ease;
}
.lumeo-cp-slider::-webkit-slider-thumb:hover {
  transform: scale(1.1);
}
.lumeo-cp-slider::-moz-range-thumb {
  width: 16px;
  height: 16px;
  background: var(--color-background);
  border: 2px solid var(--color-border);
  border-radius: 9999px;
  box-shadow: 0 1px 4px rgba(0,0,0,0.25);
  cursor: pointer;
}

/* Alpha slider: checkerboard beneath a color→transparent gradient.
   Bug E — checkerboard was rgba(0,0,0,0.08) which is invisible in dark mode.
   Now uses color-mix against --color-muted so the contrast tiles are always visible. */
.lumeo-cp-slider-alpha {
  background-image:
    linear-gradient(to right, transparent, var(--lumeo-cp-color, #000)),
    linear-gradient(45deg, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)) 25%, transparent 25%, transparent 75%, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)) 75%),
    linear-gradient(45deg, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)) 25%, transparent 25%, transparent 75%, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)) 75%);
  background-size: 100% 100%, 8px 8px, 8px 8px;
  background-position: 0 0, 0 0, 4px 4px;
}

/* Color preview chip — checkerboard shows through partial opacity.
   Bug E — same fix as alpha slider: uses theme-aware color-mix instead of hardcoded rgba(0,0,0,0.08). */
.lumeo-cp-chip {
  background-image:
    linear-gradient(45deg, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)) 25%, transparent 25%, transparent 75%, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)) 75%),
    linear-gradient(45deg, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)) 25%, transparent 25%, transparent 75%, color-mix(in srgb, var(--color-muted) 60%, var(--color-background)) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
}
.lumeo-cp-chip[style*="background-color"] {
  /* The inline background-color overlays the checkerboard via the `style` attr */
  background-blend-mode: normal;
}

/* =====================================================
 * Animation Keyframes
 *
 * Used by Dialog, Sheet, Drawer, Accordion, Collapsible,
 * Toast, AlertDialog, Popover, DropdownMenu, Select,
 * and other overlay/transition components.
 * ===================================================== */

@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes zoom-in {
  /* NOTE: `to: transform none` does NOT prevent the containing-block trap —
     under fill-mode:both the browser resolves the filled end state to the
     identity matrix (measured: computed transform matrix(1,0,0,1,0,0) on the
     settled panel), and even with a computed transform of `none` a live
     `animation` declaration keeps Chrome treating the element as a containing
     block for position:fixed descendants (compositor pre-creates the layer).
     The actual guarantee comes from Dialog/AlertDialogContent calling
     AttachOverlaySlideEnd, which sets inline `animation:none !important` +
     `transform:none !important` once the animation finishes (rc.25 pattern,
     shared with Sheet/Drawer). Keeping `to: none` here is still preferable to
     `scale(1)` — it minimizes the window during the animation itself. */
  from { opacity: 0; transform: scale(0.95); }
  to   { opacity: 1; transform: none; }
}

/* Slide-in keyframes for Sheet / Drawer / Toast. Important: per CSS spec,
   any non-`none` transform — including identity `translateX(0)` AND `to:
   none` after `animation-fill-mode: both` (which Chrome resolves to
   identity matrix) — establishes a containing block for `position: fixed`
   descendants. That breaks popovers (Select / DatePicker / Combobox)
   rendered inside the slid-in panel.

   The fix lives in SheetContent.razor / DrawerContent.razor: an
   `@onanimationend` handler removes the `animate-slide-in-*` class once
   the animation completes, so `animation-fill-mode: both` no longer
   forces the element to retain a transform. The static post-animation
   state has no transform → no containing block → popovers position
   relative to the viewport as expected. */
@keyframes slide-in-from-bottom {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}

@keyframes slide-in-from-top {
  from { transform: translateY(-100%); }
  to   { transform: translateY(0); }
}

@keyframes slide-in-from-left {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}

@keyframes slide-in-from-right {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}

/* EditForm-in-Sheet/Dialog flex compatibility. When a consumer wraps
   their sheet body in <EditForm>, the rendered <form> is content-sized
   by default — the SheetFooter then sticks below the last field instead
   of pinning to the bottom. Make direct <form> children of a Lumeo
   sheet/dialog inherit the flex-column layout so the footer pins
   correctly without consumer-side CSS workarounds.

   Deliberately NOT extended to [id^="drawer-"] (field report #464, finding 1):
   Sheet's Left/Right panel and Dialog's panel both have a DEFINITE height
   (h-full / a centered max-height box), so flex:1 1 0 on the form has real
   free space to grow into. A Drawer's default Top/Bottom panel is h-auto,
   content-hugging up to its max-height cap — no definite height. Per the flex
   algorithm, a flex-column container with an indefinite (auto) main size
   computes its OWN size from its children's hypothetical main sizes, which
   for a flex-basis:0 item is exactly 0 — so the panel (and everything in it)
   collapsed to zero height the moment its ONLY child was a <form> instead of
   a plain <div> (a div gets no such override and was never affected). A
   Left/Right or Size=Full Drawer DOES have a definite height, but for
   consistency — a form must behave the SAME as a div in every Drawer
   configuration, exactly as docs/long-forms-in-sheets already tells
   consumers to opt a <div>/<EditForm> body into flex-col scrolling
   THEMSELVES via explicit classes — Drawer gets no automatic override at
   all; see DrawerPage's "Drawer with a form" example. */
[role="dialog"][id^="sheet-"] > form,
[role="dialog"][id^="dialog-"] > form {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* vaul-style background scaling (#346, Drawer.ScaleBackground). The CONSUMER
   marks their own page-root element with data-lumeo-drawer-wrapper — Lumeo
   never renders this element itself, it lives outside any Drawer's DOM.
   DrawerContent toggles data-lumeo-drawer-scaled on it via
   Interop.SetDrawerBackgroundScaled (components.js) whenever a bottom
   Drawer with ScaleBackground is open; this is the CSS half. Mirrors vaul's
   own [data-vaul-drawer-wrapper][data-vaul-drawer-visible] transform/radius,
   using theme tokens instead of a raw rgba background darken. */
[data-lumeo-drawer-wrapper] {
    transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
}

[data-lumeo-drawer-wrapper][data-lumeo-drawer-scaled="true"] {
    overflow: hidden;
    border-radius: 8px;
    background-color: var(--color-muted);
    transform: scale(0.97) translateY(14px);
}

/* Accordion / Collapsible open/close transitions are now driven by a CSS
 * grid-template-rows: 0fr -> 1fr trick on the content wrapper, so no
 * accordion-down keyframe is needed — and crucially, no JS measurement
 * of the content height (the prior `height: 0 -> var(--radix-...)` approach
 * couldn't interpolate to `auto` and snapped instantly). */

/* `animate-toast-in` (below) uses `animation-fill-mode: both`, so once this
   settles it permanently pins opacity:1/transform:none on the toast — a
   filled keyframe always wins over a plain declared value for the same
   property regardless of selector specificity. Left alone that silently
   overrides the "Toast stacking" transforms below the moment a toast is no
   longer at data-index="0" (translateY/scale never visibly apply) and, on
   dismiss, made animate-toast-out never win either (both classes at equal
   specificity, animate-toast-in declared later). Toast.razor strips the
   `animate-toast-in` class once the entrance finishes — via a local fallback
   timer sized comfortably above this animation's duration (no JS callback:
   Toast.razor is vendored verbatim into consumer projects, so it can't
   depend on any Lumeo interop surface newer than whatever package is
   referenced) — so a settled toast carries NO animation and both the
   stacking transform and a later exit apply cleanly. */
@keyframes toast-in {
  from { opacity: 0; transform: translateX(100%); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes toast-out {
  from { opacity: 1; transform: translateX(0); max-height: 200px; margin-top: 0; margin-bottom: 0; padding-top: var(--lumeo-toast-py, 1rem); padding-bottom: var(--lumeo-toast-py, 1rem); }
  to   { opacity: 0; transform: translateX(110%); max-height: 0; margin-top: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; }
}

.animate-toast-out {
  animation: toast-out 0.22s cubic-bezier(0.4, 0, 1, 1) both;
  pointer-events: none;
}

/* =====================================================
 * Toast stacking (sonner-style) — ToastProvider.StackToasts (default true)
 *
 * ToastViewport stamps `data-stacked`/`data-expanded`/`data-stack-edge` on
 * the group container and each <Toast> gets `data-index` (0 = newest/front)
 * from ToastProvider. All rules below are scoped to
 * `.lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"])`
 * — hover/focus-within on the group flips `data-expanded="true"`
 * (ToastViewport's onmouseenter/leave + onfocusin/out), which drops out of
 * every selector here and the group instantly reverts to the plain flex-col
 * list with gaps (pre-stacking behavior, untouched).
 *
 * PR #357 (Codex finding): the `.lumeo-toast-viewport` class (stamped on
 * ToastViewport's own root div, see its CssClass doc comment) is REQUIRED on
 * every selector below — data-stacked/data-index are generic enough that,
 * loaded app-wide, a host app or third-party widget rendering its own
 * data-stacked="true"/data-index="N" markup would otherwise also match these
 * rules and have its non-zero children silently pulled out of flow and made
 * non-interactive.
 *
 * Only `data-index="0"` stays in normal flow (`position: relative`, so
 * z-index applies) — every other index is pulled out of flow via
 * `position: absolute` anchored to the container's own top edge. That means
 * the container's intrinsic height is driven by the front toast alone (no
 * JS measurement needed): "the group's hit-area follows the front toast".
 * `data-stack-edge` ("up" for Bottom* positions, "down" for Top*) sets the
 * peek direction so the older toasts always peek out AWAY from the screen
 * edge the group anchors to (bottom groups: peek above the front toast;
 * top groups: peek below it), per sonner. The absolute anchor follows the
 * peeking edge — top-aligned for an upward peek, bottom-aligned for a
 * downward peek — so the peek stays visible whatever the toast heights.
 *
 * Leaving toasts keep animating correctly while stacked: `.animate-toast-out`
 * is a CSS `animation`, and a running keyframe animation always wins over a
 * plain declared value for the same property (transform/opacity) regardless
 * of selector specificity — so the exit slide plays normally even on a
 * stacked/transformed element.
 * ===================================================== */

.lumeo-toast-viewport[data-stack-edge="up"] { --lumeo-stack-y1: -10px; --lumeo-stack-y2: -20px; }
.lumeo-toast-viewport[data-stack-edge="down"] { --lumeo-stack-y1: 10px; --lumeo-stack-y2: 20px; }

.lumeo-toast-viewport[data-stacked="true"] {
  transition: max-height 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

.lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) {
  max-height: max-content;
}

.lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) > [data-index] {
  transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease;
}

.lumeo-toast-viewport[data-stack-edge="up"]:not([data-expanded="true"]) > [data-index] { transform-origin: top center; }
.lumeo-toast-viewport[data-stack-edge="down"]:not([data-expanded="true"]) > [data-index] { transform-origin: bottom center; }

.lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) > [data-index="0"] {
  position: relative;
  z-index: 3;
  transform: none;
}

.lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) > [data-index]:not([data-index="0"]) {
  position: absolute;
  inset-inline: 0;
  pointer-events: none;
}

/* Anchor the hidden toasts to the PEEKING edge so the offset stays visible
   regardless of per-toast height differences. */
.lumeo-toast-viewport[data-stack-edge="up"]:not([data-expanded="true"]) > [data-index]:not([data-index="0"]) { top: 0; }
.lumeo-toast-viewport[data-stack-edge="down"]:not([data-expanded="true"]) > [data-index]:not([data-index="0"]) { bottom: 0; }

.lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) > [data-index="1"] {
  transform: translateY(var(--lumeo-stack-y1)) scale(0.94);
  z-index: 2;
}

.lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) > [data-index="2"] {
  transform: translateY(var(--lumeo-stack-y2)) scale(0.88);
  z-index: 1;
}

/* Index 3+ collapse into the same (invisible) 3rd slot so a promotion to
   index 2 (front toasts dismissed) animates in from a defined position
   instead of popping in from nowhere. `visibility: hidden` (not just
   opacity: 0) is load-bearing here: a depth>=3 toast that auto-dismisses or
   gets evicted WHILE still collapsed also picks up `.animate-toast-out`, and
   a running CSS `animation` always wins over a plain declared value for the
   SAME property regardless of specificity — `toast-out`'s `from { opacity:
   1; }` would otherwise flash this hidden toast visible as it slides out
   from behind the stack. `visibility` isn't one of the properties
   `@keyframes toast-out` animates, so the plain declaration here keeps
   winning throughout the exit.
   PR #357 round-2: a depth>=3 toast's `data-index` used to be recomputed
   every render from its live position in the (oldest-first) toast list —
   so if a NEWER toast ahead of it finished dismissing/timing out WHILE this
   one was still mid-exit, ToastProvider would promote it (e.g. depth 3 -> 2)
   and it fell out of this selector with `.animate-toast-out` still running
   from `opacity: 1`, flashing it visible for the rest of its exit. Fixed at
   the source, not here: ToastProvider now freezes a toast's `data-index` the
   INSTANT it starts leaving (`ToastItem.FrozenIndex`, stamped in
   RemoveWithExitAsync) — reshuffling among its still-live siblings can no
   longer move it out of whatever selector (this one included) it was in the
   moment dismissal began. */
.lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) > [data-index]:not([data-index="0"]):not([data-index="1"]):not([data-index="2"]) {
  transform: translateY(var(--lumeo-stack-y2)) scale(0.88);
  opacity: 0;
  visibility: hidden;
  z-index: 0;
}

@media (prefers-reduced-motion: reduce) {
  .lumeo-toast-viewport[data-stacked="true"],
  .lumeo-toast-viewport[data-stacked="true"] > [data-index] {
    transition: none !important;
  }
}

/* Below `sm` (640px) ToastViewport's PositionClasses anchor EVERY position —
   including Top* — to a bottom strip (see the mobile comment there), but
   StackEdge still reflects the desktop intent: Top* groups keep emitting
   data-stack-edge="down" (peek trails below the front toast). On a group
   that's actually bottom-anchored, a downward peek points off the bottom of
   the viewport instead of trailing toward the (mobile) anchored edge. Force
   every stacked group to the "up" peek below `sm`, regardless of the emitted
   edge attribute — matching the real mobile layout. Selector specificity
   matches the base `[data-stack-edge="*"]` rules above 1:1 (attribute-level
   selector for selector); being declared later in the same cascade layer is
   what makes this win for `data-stack-edge="down"` groups on mobile only. */
@media (max-width: 639.98px) {
  .lumeo-toast-viewport[data-stacked="true"] {
    --lumeo-stack-y1: -10px;
    --lumeo-stack-y2: -20px;
  }

  .lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) > [data-index] {
    transform-origin: top center;
  }

  .lumeo-toast-viewport[data-stacked="true"]:not([data-expanded="true"]) > [data-index]:not([data-index="0"]) {
    top: 0;
    bottom: auto;
  }
}

/* =====================================================
 * Animation Utility Classes
 * ===================================================== */

.animate-fade-in {
  animation: fade-in 0.15s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.animate-zoom-in {
  animation: zoom-in 0.15s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.animate-slide-in-from-bottom {
  animation: slide-in-from-bottom 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.animate-slide-in-from-top {
  animation: slide-in-from-top 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.animate-slide-in-from-left {
  animation: slide-in-from-left 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.animate-slide-in-from-right {
  animation: slide-in-from-right 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
}

.animate-toast-in {
  animation: toast-in 0.3s cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* Exit animations for Sheet (and any future slide-in overlay) — the close
   counterpart to fade-in / slide-in-from-*. The component keeps the panel
   mounted for the duration of these animations, then unmounts it, so the user
   sees it slide/fade out instead of vanishing instantly. Exit easing is the
   accelerate curve (fast-out) which reads better for dismissals. The slide-out
   keyframes intentionally finish at a translated (off-screen) transform: the
   element is removed from the DOM right after, so the lingering transform never
   establishes a containing block for fixed descendants (unlike the slide-in
   case, which clears its transform via AttachOverlaySlideEnd). */
@keyframes fade-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}

/* --lumeo-sheet-exit-from (field report #464, finding 3) — a slow swipe-to-close
   release used to snap the panel back to fully open before this keyframe took
   over, because `from` always started at 0 regardless of the drag offset the
   panel was actually sitting at. SheetContent now stamps this custom property
   (a signed px length along the SAME axis the keyframe animates) on the panel
   right before the exit class is applied, so a swipe release continues smoothly
   from wherever the finger let go. The default (0) preserves the original
   behaviour for every non-swipe close (Escape / backdrop / X / programmatic) —
   those never set the property, so `var(..., 0)` is what runs. Also picked up
   by Drawer (which shares these keyframes) for the same reason, defaulting to 0
   there since DrawerContent doesn't set it. */
@keyframes slide-out-to-bottom {
  from { transform: translateY(var(--lumeo-sheet-exit-from, 0)); }
  to   { transform: translateY(100%); }
}

@keyframes slide-out-to-top {
  from { transform: translateY(var(--lumeo-sheet-exit-from, 0)); }
  to   { transform: translateY(-100%); }
}

@keyframes slide-out-to-left {
  from { transform: translateX(var(--lumeo-sheet-exit-from, 0)); }
  to   { transform: translateX(-100%); }
}

@keyframes slide-out-to-right {
  from { transform: translateX(var(--lumeo-sheet-exit-from, 0)); }
  to   { transform: translateX(100%); }
}

/* Zoom-out — the close counterpart to zoom-in (Dialog / AlertDialog). Mirrors
   zoom-in in reverse: the panel scales down slightly and fades while it is kept
   mounted for the exit window, then unmounts. Like the slide-out keyframes it
   ends at the dismissed state (scale 0.95 + opacity 0); the element is removed
   from the DOM immediately after, so the lingering transform never establishes
   a containing block for fixed descendants. */
@keyframes zoom-out {
  from { opacity: 1; transform: none; }
  to   { opacity: 0; transform: scale(0.95); }
}

.animate-fade-out {
  /* 0.15s mirrors animate-fade-in 0.15s — enter/exit pairs are now symmetric. */
  animation: fade-out 0.15s cubic-bezier(0.4, 0, 1, 1) both;
}

.animate-slide-out-to-bottom {
  /* 0.3s mirrors animate-slide-in-from-bottom 0.3s — enter/exit symmetric. */
  animation: slide-out-to-bottom 0.3s cubic-bezier(0.4, 0, 1, 1) both;
}

.animate-slide-out-to-top {
  animation: slide-out-to-top 0.3s cubic-bezier(0.4, 0, 1, 1) both;
}

.animate-slide-out-to-left {
  animation: slide-out-to-left 0.3s cubic-bezier(0.4, 0, 1, 1) both;
}

.animate-slide-out-to-right {
  animation: slide-out-to-right 0.3s cubic-bezier(0.4, 0, 1, 1) both;
}

.animate-zoom-out {
  animation: zoom-out 0.15s cubic-bezier(0.4, 0, 1, 1) both;
}

/* Overlay exit animations honor reduced-motion too: collapse the duration so
   dismissal is instant (animationend still fires for listeners) instead of a
   visible slide. `.animate-toast-out` joins this family for the same reason:
   ToastProvider's removal timer (ExitAnimationMs, 220ms) is independent of
   this animation-duration, so collapsing it to 1ms only changes what's
   visually shown during that fixed unmount window — a snap/fade instead of
   the 220ms translate-X slide — without touching the unmount timing itself. */
@media (prefers-reduced-motion: reduce) {
  .animate-fade-out,
  .animate-zoom-out,
  .animate-slide-out-to-bottom,
  .animate-slide-out-to-top,
  .animate-slide-out-to-left,
  .animate-slide-out-to-right,
  .animate-toast-out {
    animation-duration: 1ms !important;
  }
}

/* Overlay ENTRY animations honor reduced-motion too: collapse the duration so
   content appears instantly instead of sliding/zooming/fading in for users who
   request reduced motion (WCAG 2.3.3). 1ms (not 0) preserves the `both` fill and
   keeps any animationend listeners firing — matching the exit guard above. */
@media (prefers-reduced-motion: reduce) {
  .animate-fade-in,
  .animate-zoom-in,
  .animate-slide-in-from-bottom,
  .animate-slide-in-from-top,
  .animate-slide-in-from-left,
  .animate-slide-in-from-right,
  .animate-toast-in {
    animation-duration: 1ms !important;
  }
}

/* =====================================================
 * === motion ===
 * Keyframes and utilities for the motion primitives:
 * Marquee, BorderBeam, ShimmerButton, Sparkles, TextReveal, BlurFade.
 * ===================================================== */

/* --- Marquee --- */
@keyframes lumeo-marquee-x {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

@keyframes lumeo-marquee-x-reverse {
  from { transform: translateX(-50%); }
  to   { transform: translateX(0); }
}

@keyframes lumeo-marquee-y {
  from { transform: translateY(0); }
  to   { transform: translateY(-50%); }
}

@keyframes lumeo-marquee-y-reverse {
  from { transform: translateY(-50%); }
  to   { transform: translateY(0); }
}

.lumeo-marquee {
  overflow: hidden;
  display: flex;
  position: relative;
  --lumeo-marquee-duration: 30s;
}
.lumeo-marquee[data-vertical="true"] { flex-direction: column; }

.lumeo-marquee-track {
  display: flex;
  flex-shrink: 0;
  gap: 1rem;
  animation: lumeo-marquee-x var(--lumeo-marquee-duration) linear infinite;
  min-width: 100%;
}
.lumeo-marquee[data-direction="right"] .lumeo-marquee-track {
  animation-name: lumeo-marquee-x-reverse;
}
.lumeo-marquee[data-vertical="true"] .lumeo-marquee-track {
  flex-direction: column;
  animation-name: lumeo-marquee-y;
  min-width: 0;
  min-height: 100%;
}
.lumeo-marquee[data-vertical="true"][data-direction="right"] .lumeo-marquee-track {
  animation-name: lumeo-marquee-y-reverse;
}
.lumeo-marquee[data-pause-hover="true"]:hover .lumeo-marquee-track {
  animation-play-state: paused;
}

/* Marquee — reduced motion: stop auto-scrolling; content stays static and visible. */
@media (prefers-reduced-motion: reduce) {
  .lumeo-marquee-track {
    animation: none;
    transform: none;
  }
}

/* --- BorderBeam --- */
@property --lumeo-beam-angle {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

@keyframes lumeo-border-beam {
  to { --lumeo-beam-angle: 360deg; }
}

.lumeo-border-beam {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.lumeo-border-beam::before {
  content: '';
  position: absolute;
  inset: 0;
  padding: var(--lumeo-beam-size, 1.5px);
  border-radius: inherit;
  background: conic-gradient(
    from var(--lumeo-beam-angle),
    transparent 0deg,
    var(--lumeo-beam-from, var(--color-primary)) 90deg,
    var(--lumeo-beam-to, var(--color-primary)) 180deg,
    transparent 270deg,
    transparent 360deg
  );
  -webkit-mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask:
    linear-gradient(#000 0 0) content-box,
    linear-gradient(#000 0 0);
          mask-composite: exclude;
  animation: lumeo-border-beam var(--lumeo-beam-duration, 8s) linear infinite;
  pointer-events: none;
}

/* BorderBeam — reduced motion: stop the rotating conic sweep. */
@media (prefers-reduced-motion: reduce) {
  .lumeo-border-beam::before {
    animation: none;
  }
}

/* --- ShimmerButton --- */
@keyframes lumeo-shimmer-sweep {
  0%   { transform: translateX(-150%); }
  100% { transform: translateX(250%); }
}

.lumeo-shimmer {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}
.lumeo-shimmer::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 40%;
  background: linear-gradient(
    100deg,
    transparent 20%,
    var(--lumeo-shimmer-color, rgb(255 255 255 / 0.45)) 50%,
    transparent 80%
  );
  transform: translateX(-150%);
  animation: lumeo-shimmer-sweep 2.5s linear infinite;
  pointer-events: none;
  mix-blend-mode: overlay;
}

/* ShimmerButton — reduced motion: drop the sweeping highlight entirely. */
@media (prefers-reduced-motion: reduce) {
  .lumeo-shimmer::before {
    animation: none;
    display: none;
  }
}

/* --- Sparkles --- */
@keyframes lumeo-sparkle-twinkle {
  0%, 100% { opacity: 0; transform: scale(0); }
  50%      { opacity: 1; transform: scale(1); }
}

/* :where() keeps specificity at 0 so consumer utilities like `absolute inset-0` can still win. */
:where(.lumeo-sparkles) {
  position: relative;
  display: inline-block;
}

/* --- ImageCompare: transparent range input with a full-height invisible thumb so
   clicks anywhere on the image move the divider exactly under the cursor. --- */
.lumeo-image-compare-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 100%;
  background: transparent;
  cursor: inherit;
}
.lumeo-image-compare-slider::-moz-range-thumb {
  width: 100%;
  height: 100%;
  background: transparent;
  border: 0;
  cursor: inherit;
}
.lumeo-image-compare-slider::-webkit-slider-runnable-track { background: transparent; }
.lumeo-image-compare-slider::-moz-range-track { background: transparent; }
.lumeo-image-compare-slider:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; opacity: 0.001; }
.lumeo-sparkle {
  position: absolute;
  pointer-events: none;
  opacity: 0;
  animation: lumeo-sparkle-twinkle 2.2s ease-in-out infinite;
  transform-origin: center;
  will-change: transform, opacity;
}

/* Sparkles — reduced motion: silence the decorative twinkle. The wrapped
   children stay visible; only the ornamental sparkle layer is suppressed. */
@media (prefers-reduced-motion: reduce) {
  .lumeo-sparkle {
    animation: none;
    opacity: 0;
  }
}

/* --- TextReveal --- */
.lumeo-reveal [data-motion-word] {
  display: inline-block;
  opacity: 0;
  transform: translateY(0.35em);
  transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
              transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
  will-change: opacity, transform;
}
.lumeo-reveal[data-motion-revealed="true"] [data-motion-word] {
  opacity: 1;
  transform: translateY(0);
}

/* TextReveal — reduced motion: reveal every word at once, no per-word stagger. */
@media (prefers-reduced-motion: reduce) {
  .lumeo-reveal [data-motion-word] {
    transition: none;
    opacity: 1;
    transform: none;
  }
}

/* --- BlurFade ---
 * Default: visible. The old default of `opacity: 0` caused a 1-2s blank flash on
 * prerendered pages because above-the-fold elements stayed hidden until Blazor
 * WASM finished loading and the IntersectionObserver fired. Now the element is
 * visible as soon as CSS paints; the JS observer only HIDES elements that it has
 * explicitly marked as "not yet seen" via data-motion-visible="false", and
 * transitions them back to visible on scroll-in. Above-the-fold content stays
 * visible throughout; below-fold still gets the fade-in animation. */
.lumeo-blur-fade {
  transition:
    opacity var(--lumeo-blur-duration, 600ms) cubic-bezier(0.16, 1, 0.3, 1),
    filter var(--lumeo-blur-duration, 600ms) cubic-bezier(0.16, 1, 0.3, 1),
    transform var(--lumeo-blur-duration, 600ms) cubic-bezier(0.16, 1, 0.3, 1);
  will-change: opacity, filter, transform;
}
.lumeo-blur-fade[data-motion-visible="false"] {
  opacity: 0;
  filter: blur(var(--lumeo-blur-px, 8px));
  transform: translateY(var(--lumeo-blur-y, 8px));
}
.lumeo-blur-fade[data-motion-visible="true"] {
  opacity: 1;
  filter: blur(0);
  transform: translateY(0);
}

/* BlurFade — reduced motion: show content immediately, no blur/translate. */
@media (prefers-reduced-motion: reduce) {
  .lumeo-blur-fade {
    transition: none;
  }
  .lumeo-blur-fade[data-motion-visible="false"] {
    opacity: 1;
    filter: blur(0);
    transform: none;
  }
}

/* =====================================================
 * === Motion: AnimatedBeam ===
 * SVG beam that traces between two DOM nodes.
 * The beam path + gradient are drawn by motion.js.
 * ===================================================== */
.lumeo-animated-beam {
  pointer-events: none;
  overflow: visible;
}

/* =====================================================
 * === Motion: Dock ===
 * macOS-style icon dock with magnification effect.
 * Per-item scale is applied via motion.js mousemove handler.
 * ===================================================== */
.lumeo-dock {
  display: flex;
  align-items: flex-end;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border-radius: 1rem;
  background: color-mix(in srgb, var(--color-card) 80%, transparent);
  border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.lumeo-dock > * {
  transform-origin: bottom center;
  transition: transform 120ms ease-out;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .lumeo-dock > * { transition: none; }
}

/* =====================================================
 * === Motion: Confetti ===
 * Canvas overlay for confetti bursts.
 * ===================================================== */
.lumeo-confetti {
  display: inline-block;
}

.lumeo-confetti-canvas {
  /* Idle: out of flow AND zero-size, so the canvas neither shifts the trigger
     (host stays content-sized → trigger stays centred) nor contributes any overflow
     of its own. A bare <canvas> is 300x150 by default; leaving that in flow widened
     the host and pushed the trigger off-centre. confettiFire() positions the canvas
     position:fixed over the trigger (viewport-relative, so the oversized burst never
     adds a page scrollbar) when it bursts. */
  position: absolute;
  width: 0;
  height: 0;
  display: block;
  pointer-events: none;
}

/* =====================================================
 * === motion: component integrations ===
 * Opt-in animations for Steps, Timeline, Progress, Tabs,
 * Switch, Checkbox, Badge, and BottomNav. Enabled via
 * per-component parameters (Animated, AnimatedIndicator,
 * Animation = Stripe). All keyframe names are prefixed
 * with lumeo- to avoid collisions.
 * ===================================================== */

/* --- Steps & Timeline: active-dot pulse --- */
@keyframes lumeo-steps-pulse {
  0%, 100% {
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-primary) 55%, transparent);
  }
  50% {
    box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-primary) 0%, transparent);
  }
}

.lumeo-steps-pulse {
  animation: lumeo-steps-pulse 1.6s ease-in-out infinite;
}

/* --- Steps & Timeline: completed connector line draw-in / unwind --- */
/* Shared line-draw keyframes — used by Steps (forward direction) and Timeline
   progression. Forward: scales from 0 to 1 with origin on the leading edge. */
@keyframes lumeo-line-draw-x {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

@keyframes lumeo-line-draw-y {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

/* Reverse: the drawn line retracts back to 0 with origin on the trailing edge,
   so the visual reads as "rewinding" rather than "wiping out". */
@keyframes lumeo-line-unwind-x {
  from { transform: scaleX(1); }
  to   { transform: scaleX(0); }
}

@keyframes lumeo-line-unwind-y {
  from { transform: scaleY(1); }
  to   { transform: scaleY(0); }
}

/* Legacy aliases (kept for back-compat) */
@keyframes lumeo-steps-line-draw-x {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

@keyframes lumeo-steps-line-draw-y {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

.lumeo-steps-line-draw-x {
  transform-origin: left center;
  animation: lumeo-line-draw-x 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

.lumeo-steps-line-draw-y {
  transform-origin: top center;
  animation: lumeo-line-draw-y 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* Reverse (Steps Previous) — a drawn connector retracts toward its origin,
   collapsing back into the still-completed step. Matches the forward origin
   so the animation reads as "rewind". */
.lumeo-steps-line-unwind-x {
  transform-origin: left center;
  animation: lumeo-line-unwind-x 300ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

.lumeo-steps-line-unwind-y {
  transform-origin: top center;
  animation: lumeo-line-unwind-y 300ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* --- Timeline: staggered item entry --- */
@keyframes lumeo-timeline-enter-x {
  from { opacity: 0; transform: translateY(-8px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes lumeo-timeline-enter-y {
  from { opacity: 0; transform: translateX(-8px); }
  to   { opacity: 1; transform: translateX(0); }
}

.lumeo-timeline-enter-vertical {
  animation: lumeo-timeline-enter-y 400ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
  opacity: 0;
}

.lumeo-timeline-enter-horizontal {
  animation: lumeo-timeline-enter-x 400ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
  opacity: 0;
}

/* --- Progress: animated diagonal stripes --- */
@keyframes lumeo-progress-stripes {
  from { background-position: 0 0; }
  to   { background-position: 20px 0; }
}

.lumeo-progress-stripes {
  background-image: linear-gradient(
    45deg,
    var(--color-progress-stripe) 25%,
    transparent 25%,
    transparent 50%,
    var(--color-progress-stripe) 50%,
    var(--color-progress-stripe) 75%,
    transparent 75%,
    transparent
  );
  background-size: 20px 20px;
  animation: lumeo-progress-stripes 1s linear infinite;
}

/* --- Progress: indeterminate slider (narrow segment sweeping across) ---
   Tuned so the bar's exit off the right edge lines up with the opacity fade-out:
   at t=85% the right edge is at the viewport edge (translateX=70% with 30% bar width),
   and opacity hits 0 right as the bar has fully exited — no visible snap. */
@keyframes lumeo-progress-indeterminate {
  0%   { transform: translateX(-100%); opacity: 0; }
  15%  { opacity: 1; }
  85%  { transform: translateX(70%); opacity: 1; }
  100% { transform: translateX(100%); opacity: 0; }
}

.lumeo-progress-indeterminate {
  position: relative;
  overflow: hidden;
}

.lumeo-progress-indeterminate::before {
  content: "";
  position: absolute;
  inset: 0;
  width: 30%;
  border-radius: inherit;
  background: currentColor;
  animation: lumeo-progress-indeterminate 1.6s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}

/* --- Progress: glow pulse (breathing box-shadow) --- */
@keyframes lumeo-progress-glow {
  0%, 100% {
    box-shadow: 0 0 4px 0 color-mix(in oklab, var(--color-primary) 40%, transparent);
  }
  50% {
    box-shadow: 0 0 12px 2px color-mix(in oklab, var(--color-primary) 70%, transparent);
  }
}

.lumeo-progress-glow {
  animation: lumeo-progress-glow 1.6s ease-in-out infinite;
}

/* --- Tabs: sliding indicator --- */
.lumeo-tabs-list {
  position: relative;
}

/* Pill indicator — rounded full-height background for Default/Pill variants.
   Uses color-mix against the foreground to guarantee contrast against the
   surrounding `bg-muted` track in *both* light and dark mode; prior rule used
   `--color-background` which in dark themes is near-pure black and read as a
   "dark blob" against the muted surface. */
.lumeo-tabs-indicator {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  border-radius: var(--radius-md);
  background: color-mix(in oklab, var(--color-background) 70%, var(--color-foreground) 8%);
  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
  pointer-events: none;
  z-index: 0;
  transform: translateX(var(--lumeo-tabs-indicator-x, 0px));
  width: var(--lumeo-tabs-indicator-w, 0px);
  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
              width 260ms cubic-bezier(0.22, 1, 0.36, 1);
  opacity: 0;
}
.lumeo-tabs-indicator[data-ready="true"] {
  opacity: 1;
}

/* Underline indicator — 2px bar at the bottom for Card variant */
.lumeo-tabs-underline {
  position: absolute;
  bottom: -1px;
  left: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--color-primary);
  pointer-events: none;
  transform: translateX(var(--lumeo-tabs-indicator-x, 0px));
  width: var(--lumeo-tabs-indicator-w, 0px);
  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
              width 260ms cubic-bezier(0.22, 1, 0.36, 1);
  opacity: 0;
}
.lumeo-tabs-underline[data-ready="true"] {
  opacity: 1;
}

/* Hide the active trigger's default background when the sliding pill is in charge */
.lumeo-tabs-list[data-sliding-pill="true"] [data-state="active"] {
  background: transparent !important;
  box-shadow: none !important;
  position: relative;
  z-index: 1;
}

/* --- Tabs: icon-reveal label (Tabs IconReveal mode) ---
   Inactive triggers show icon-only; the active trigger animates its label open.
   The grid 0fr→1fr trick animates the label to its natural (auto) width — which a
   plain width transition can't do — while the inner span clips the text mid-slide. */
.lumeo-tab-label {
  display: grid;
  grid-template-columns: 0fr;
  margin-left: 0;
  opacity: 0;
  transition: grid-template-columns 0.3s ease, opacity 0.25s ease, margin-left 0.3s ease;
}
.lumeo-tab-label > span {
  overflow: hidden;
  min-width: 0;
  white-space: nowrap;
}
.lumeo-tab-label.lumeo-tab-label-open {
  grid-template-columns: 1fr;
  margin-left: 0.375rem;
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .lumeo-tab-label { transition: none; }
}

/* --- Switch: spring thumb --- */
.lumeo-switch-spring {
  transition: transform 360ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
}

/* --- Checkbox: draw-in check + bounce --- */
@keyframes lumeo-checkbox-check {
  to { stroke-dashoffset: 0; }
}

@keyframes lumeo-checkbox-bounce {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.08); }
  100% { transform: scale(1); }
}

.lumeo-checkbox-animated {
  animation: lumeo-checkbox-bounce 200ms ease-out;
  /* Do NOT clip the check path while the box scale-bounces — a prior
     overflow:hidden would have clipped the partially-rendered stroke. */
  overflow: visible;
}

/* Draw the full check path regardless of the exact SVG path length. The
   Lucide Check path is ~22 units long so a dasharray of 16 clipped the
   tail of the stroke. We target the inline SVG the check icon renders —
   every element with a pathLength attribute normalises to 0..1 — falling
   back to a large fixed dasharray for any icon that doesn't set pathLength. */
.lumeo-checkbox-animated svg path,
.lumeo-checkbox-animated svg polyline,
.lumeo-checkbox-animated svg line {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: lumeo-checkbox-check 260ms ease-out forwards;
}

/* --- Badge: entrance pop --- */
@keyframes lumeo-badge-enter {
  from { opacity: 0; transform: scale(0.6); }
  to   { opacity: 1; transform: scale(1); }
}

.lumeo-badge-animated {
  animation: lumeo-badge-enter 280ms cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
}

/* --- BottomNav: smooth role transitions --- */
.lumeo-bottom-nav-animated {
  transition: background-color 200ms ease, color 200ms ease;
}
.lumeo-bottom-nav-animated > span[aria-hidden="true"] {
  transition: transform 240ms cubic-bezier(0.34, 1.56, 0.64, 1),
              opacity 200ms ease;
}

/* Reduced motion — disable decorative loops */
@media (prefers-reduced-motion: reduce) {
  .lumeo-steps-pulse,
  .lumeo-progress-stripes,
  .lumeo-progress-glow {
    animation: none;
  }
  .lumeo-progress-indeterminate::before {
    animation: none;
    transform: translateX(0);
    width: 100%;
  }
  .lumeo-steps-line-draw-x,
  .lumeo-steps-line-draw-y,
  .lumeo-steps-line-unwind-x,
  .lumeo-steps-line-unwind-y,
  .lumeo-timeline-enter-vertical,
  .lumeo-timeline-enter-horizontal,
  .lumeo-checkbox-animated,
  .lumeo-badge-animated {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .lumeo-checkbox-animated svg path,
  .lumeo-checkbox-animated svg polyline,
  .lumeo-checkbox-animated svg line {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
    animation: none;
  }
  .lumeo-switch-spring {
    transition: transform 120ms ease !important;
  }
}

/* =====================================================
 * RTL Support
 *
 * Tailwind v4 logical utilities (ms-*, me-*, ps-*, pe-*, start-*, end-*,
 * text-start / text-end, border-s, border-e) already flip automatically in
 * RTL because they resolve to CSS logical properties. The rules below fix
 * the few hand-written slide / translate animations that use the *physical*
 * X axis and therefore look wrong when the document direction is flipped.
 *
 * Icons with inherent directional meaning (ChevronLeft/Right, ArrowLeft/Right,
 * breadcrumb chevrons) are NOT mirrored automatically — consumers should
 * swap those at the component level if desired.
 * ===================================================== */

[dir="rtl"] .animate-slide-in-from-left {
  animation-name: slide-in-from-right-rtl;
}
[dir="rtl"] .animate-slide-in-from-right {
  animation-name: slide-in-from-left-rtl;
}

@keyframes slide-in-from-right-rtl {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}

@keyframes slide-in-from-left-rtl {
  from { transform: translateX(-100%); }
  to   { transform: translateX(0); }
}

/* Exit counterparts: a left-anchored sheet in RTL exits toward the visual
   left (positive X), and vice-versa — swap the left/right slide-out names so
   the dismissal direction tracks the (mirrored) anchor edge. */
[dir="rtl"] .animate-slide-out-to-left {
  animation-name: slide-out-to-right;
}
[dir="rtl"] .animate-slide-out-to-right {
  animation-name: slide-out-to-left;
}

/* Toast default slides from the right-hand edge. In RTL the visual edge
 * that the toast emerges from is the left, so flip the X direction. Same for
 * the exit: `.animate-toast-out` translates physical +X (toward the right),
 * which in RTL is the WRONG edge (mirrors the entrance mistake this file
 * already fixed for toast-in) — dismissing a toast must exit back toward the
 * left, the same edge it entered from. */
[dir="rtl"] .toast-enter,
[dir="rtl"] .animate-toast-in {
  animation-name: toast-in-rtl;
}

[dir="rtl"] .animate-toast-out {
  animation-name: toast-out-rtl;
}

@keyframes toast-in-rtl {
  from { opacity: 0; transform: translateX(-100%); }
  to   { opacity: 1; transform: translateX(0); }
}

@keyframes toast-out-rtl {
  from { opacity: 1; transform: translateX(0); max-height: 200px; margin-top: 0; margin-bottom: 0; padding-top: var(--lumeo-toast-py, 1rem); padding-bottom: var(--lumeo-toast-py, 1rem); }
  to   { opacity: 0; transform: translateX(-110%); max-height: 0; margin-top: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; }
}

/* ===== RichTextEditor (TipTap / ProseMirror) =====================================
 * TipTap replaces the mount element with a .ProseMirror contenteditable div.
 * We scope all styles under .lumeo-rte-content so nothing leaks into the host app.
 */
.lumeo-rte-content .ProseMirror {
  min-height: inherit;
  padding: 1rem;
  outline: none;
  color: var(--color-foreground);
  font-size: 0.875rem;
  line-height: 1.6;
}
.lumeo-rte-content .ProseMirror > * + * {
  margin-top: 0.75em;
}
.lumeo-rte-content .ProseMirror h1 { font-size: 1.5rem; font-weight: 700; line-height: 1.25; }
.lumeo-rte-content .ProseMirror h2 { font-size: 1.25rem; font-weight: 700; line-height: 1.3; }
.lumeo-rte-content .ProseMirror h3 { font-size: 1.125rem; font-weight: 600; line-height: 1.35; }
.lumeo-rte-content .ProseMirror p { margin: 0; }
.lumeo-rte-content .ProseMirror ul,
.lumeo-rte-content .ProseMirror ol { padding-left: 1.25rem; }
.lumeo-rte-content .ProseMirror ul { list-style: disc; }
.lumeo-rte-content .ProseMirror ol { list-style: decimal; }
.lumeo-rte-content .ProseMirror li > p { margin: 0; }
.lumeo-rte-content .ProseMirror blockquote {
  border-left: 3px solid var(--color-border);
  padding-left: 0.75rem;
  color: var(--color-muted-foreground);
  font-style: italic;
}
.lumeo-rte-content .ProseMirror code {
  background: var(--color-muted);
  color: var(--color-foreground);
  padding: 0.1rem 0.3rem;
  border-radius: 0.25rem;
  font-size: 0.85em;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
.lumeo-rte-content .ProseMirror pre {
  background: var(--color-muted);
  border-radius: 0.375rem;
  padding: 0.75rem 1rem;
  overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  font-size: 0.8125rem;
}
.lumeo-rte-content .ProseMirror pre code { background: transparent; padding: 0; }
.lumeo-rte-content .ProseMirror hr {
  border: 0;
  border-top: 1px solid var(--color-border);
  margin: 1rem 0;
}
.lumeo-rte-content .ProseMirror a {
  color: var(--color-primary);
  text-decoration: underline;
  text-underline-offset: 2px;
}
/* TipTap placeholder: rendered on the first empty node via the Placeholder extension. */
.lumeo-rte-content .ProseMirror p.is-editor-empty:first-child::before {
  content: attr(data-placeholder);
  float: left;
  color: var(--color-muted-foreground);
  pointer-events: none;
  height: 0;
}

/* Frappe Gantt — blend the upstream blue defaults into the Lumeo theme.
   Only the high-contrast visuals are overridden; grid spacing and typography
   rely on Frappe's own CSS, which is injected once in gantt.js. */
.lumeo-gantt-host .gantt .grid-background { fill: var(--color-card); }
.lumeo-gantt-host .gantt .grid-header { fill: var(--color-muted); stroke: var(--color-border); }
.lumeo-gantt-host .gantt .grid-row { fill: var(--color-card); }
.lumeo-gantt-host .gantt .grid-row:nth-child(even) { fill: var(--color-muted); }
.lumeo-gantt-host .gantt .row-line { stroke: var(--color-border); }
.lumeo-gantt-host .gantt .tick { stroke: var(--color-border); }
.lumeo-gantt-host .gantt .today-highlight { fill: var(--color-accent); opacity: 0.35; }
.lumeo-gantt-host .gantt .lower-text,
.lumeo-gantt-host .gantt .upper-text { fill: var(--color-muted-foreground); }
.lumeo-gantt-host .gantt .bar-wrapper .bar { fill: var(--color-primary); }
.lumeo-gantt-host .gantt .bar-wrapper .bar-progress { fill: var(--color-primary); filter: brightness(0.75); }
.lumeo-gantt-host .gantt .bar-wrapper .bar-label { fill: var(--color-primary-foreground); }
.lumeo-gantt-host .gantt .arrow { stroke: var(--color-muted-foreground); }

/* === datagrid column resize handle === */
/* ReUI-grade resize affordance: a ~12px hit strip pinned to the column's logical
   inline-end edge (so it lands on the LEFT in RTL automatically), with a ::before
   divider that idles as a 1px border line and becomes a 2px primary line on hover,
   keyboard focus, active drag, or while [data-resizing] is set by JS during a
   pointer drag (which survives the pointer straying off the strip once capture
   engages, unlike :active). Kept inside the column so the strip never overhangs
   the last column and inflates the grid's horizontal scrollWidth. */
.lumeo-dg-resize-handle {
  position: absolute;
  top: 0;
  height: 100%;
  width: 12px;
  inset-inline-end: 0;
  z-index: 10;
  cursor: col-resize;
  user-select: none;
  touch-action: none;
  outline: none;
}
.lumeo-dg-resize-handle::before {
  content: "";
  position: absolute;
  top: 0;
  height: 100%;
  width: 1px;
  inset-inline-end: 0;
  background: var(--color-border);
  transition: background-color 0.12s ease, width 0.12s ease;
}
.lumeo-dg-resize-handle:hover::before,
.lumeo-dg-resize-handle:focus-visible::before,
.lumeo-dg-resize-handle:active::before,
.lumeo-dg-resize-handle[data-resizing="true"]::before {
  background: var(--color-primary);
  width: 2px;
}

/* === datagrid header drag redesign (zone model) === */
/* Reorder grip (redesign point 5): hover-only affordance on pointer-fine devices —
   idle invisible, revealed when the header cell is hovered OR contains focus (roving
   tabindex keeps the actual focused element at the <th>/sort-button level, so
   :focus-within on the header cell is the right scope). Always visible on touch —
   there's no hover state to reveal it there, and it's the "no-wait" initiator for
   the long-press header-wide gesture. */
.lumeo-dg-reorder-grip {
  opacity: 0;
  transition: opacity 0.12s ease;
}
[data-slot="datagrid-header-cell"]:hover .lumeo-dg-reorder-grip,
[data-slot="datagrid-header-cell"]:focus-within .lumeo-dg-reorder-grip {
  opacity: 1;
}
@media (hover: none) {
  .lumeo-dg-reorder-grip {
    opacity: 1;
  }
}

/* Non-reorderable "can't drag this" feedback (redesign point 7): a tiny 2px
   nudge-and-spring, triggered by components.js's registerColumnReorder toggling
   this class on an attempted-but-rejected drag. No lift (opacity/shadow) — this is
   deliberately a lighter, more "refused" gesture than the real lift affordance. */
@keyframes lumeo-dg-reorder-nudge {
  0% { transform: translateX(0); }
  30% { transform: translateX(-2px); }
  65% { transform: translateX(2px); }
  100% { transform: translateX(0); }
}
.lumeo-dg-reorder-nudge {
  animation: lumeo-dg-reorder-nudge 150ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .lumeo-dg-reorder-grip {
    transition: none;
  }
  .lumeo-dg-reorder-nudge {
    animation: none;
  }
}

/* Unified drag-to-group (rc.42): the group panel's drop-accept highlight is now
   toggled directly by components.js's registerColumnReorder via a data attribute
   (data-drop-target) — no Blazor round-trip per pointermove, mirroring the resize
   handle's own [data-resizing="true"] state toggle above. Lights up border +
   background to the primary accent while an armed, Groupable column drag hovers
   the panel; releasing there adds a grouping level. */
[data-slot="datagrid-group-panel"] {
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
[data-slot="datagrid-group-panel"][data-drop-target="true"] {
  border-color: var(--color-primary);
  background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
}
@media (prefers-reduced-motion: reduce) {
  [data-slot="datagrid-group-panel"] {
    transition: none;
  }
}

/* === datagrid fullscreen === */
/* Ensures the grid inside the fullscreen modal fills the available width and
   height regardless of any Height / max-width the host set on the grid. */
.lumeo-datagrid-fullscreen [data-slot="datagrid"] {
  width: 100% !important;
  max-width: 100% !important;
  height: 100% !important;
  max-height: none !important;
}
/* Inner scroll region should expand too, so the table occupies the overlay. */
.lumeo-datagrid-fullscreen [data-slot="datagrid"] > div,
.lumeo-datagrid-fullscreen [data-slot="datagrid"] table {
  width: 100%;
}

/* =============================================================
 * Press feedback — opt-in via PressEffect param on Button,
 * ShimmerButton, Card (when OnClick bound), BottomNavItem,
 * ToggleGroupItem, Chip (when Clickable). The classes are
 * neutral so they can be applied to any tactile surface.
 * Respects `prefers-reduced-motion`.
 * ============================================================= */

.lumeo-press-scale {
  transition: transform 120ms ease-out;
}
.lumeo-press-scale:active:not(:disabled) {
  transform: scale(0.97);
}

.lumeo-press-brightness {
  transition: filter 120ms ease-out;
}
.lumeo-press-brightness:active:not(:disabled) {
  filter: brightness(0.9);
}

.lumeo-press-ripple {
  position: relative;
  overflow: hidden;
}
.lumeo-ripple-dot {
  position: absolute;
  border-radius: 9999px;
  background: color-mix(in oklab, var(--color-foreground) 20%, transparent);
  pointer-events: none;
  transform: scale(0);
  animation: lumeo-ripple 500ms ease-out forwards;
}
@keyframes lumeo-ripple {
  from { transform: scale(0); opacity: 0.6; }
  to   { transform: scale(2.5); opacity: 0; }
}

/* --- TouchRipple: Blazor component-driven ripple (configurable via CSS vars) --- */
.lumeo-touch-ripple {
  position: relative;
  overflow: hidden;
}
.lumeo-touch-ripple-span {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
  width: 100px;
  height: 100px;
  margin-left: -50px;
  margin-top: -50px;
  background: currentColor;
  opacity: var(--ripple-opacity, 0.25);
  animation: lumeo-touch-ripple var(--ripple-duration, 500ms) ease-out forwards;
}
@keyframes lumeo-touch-ripple {
  from { transform: scale(0); opacity: var(--ripple-opacity, 0.25); }
  to   { transform: scale(2); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .lumeo-press-scale,
  .lumeo-press-brightness {
    transition: none;
  }
  .lumeo-press-scale:active:not(:disabled) {
    transform: none;
  }
  .lumeo-press-brightness:active:not(:disabled) {
    filter: none;
  }
  .lumeo-ripple-dot {
    animation: none;
    display: none;
  }
  /* TouchRipple — the component skips spawning spans under reduced motion
     (see TouchRipple.razor); this neutralises any span already in flight when
     the OS setting changes, matching the press-feedback ripple above. */
  .lumeo-touch-ripple-span {
    animation: none;
    display: none;
  }
}


/* === datagrid fullscreen: open animation === */
.lumeo-datagrid-fullscreen-backdrop {
  background-color: color-mix(in oklab, var(--color-background) 70%, var(--color-foreground) 8%);
  backdrop-filter: blur(2px);
  animation: lumeo-datagrid-fs-backdrop-in 200ms ease-out both;
}
.lumeo-datagrid-fullscreen-panel {
  animation: lumeo-datagrid-fs-panel-in 260ms cubic-bezier(0.22, 1, 0.36, 1) both;
  transform-origin: center;
}
/* In fullscreen, the grid itself must fill the modal so the rows area grows and pagination
   sticks to the bottom instead of floating with a big empty gap below. The grid root already
   uses `flex flex-col`, so making it 100% height + dropping the redundant outer border gives
   a clean single-surface look. */
.lumeo-datagrid-fullscreen-panel [data-slot="datagrid"] {
  height: 100%;
  border: 0;
  border-radius: 0;
  background: transparent;
}
@keyframes lumeo-datagrid-fs-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes lumeo-datagrid-fs-panel-in {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .lumeo-datagrid-fullscreen-backdrop,
  .lumeo-datagrid-fullscreen-panel {
    animation: none;
  }
}
.lumeo-datagrid-fullscreen-backdrop-out {
  animation: lumeo-datagrid-fs-backdrop-out 180ms ease-in both !important;
}
.lumeo-datagrid-fullscreen-panel-out {
  animation: lumeo-datagrid-fs-panel-out 180ms cubic-bezier(0.55, 0, 0.68, 0) both !important;
}
@keyframes lumeo-datagrid-fs-backdrop-out {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes lumeo-datagrid-fs-panel-out {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(0.96); }
}

/* =================================================================
 * ChartSkeleton — shape-aware loading placeholder for Chart.
 * Each <ChartSkeleton Kind="..."/> renders SVG primitives with the
 * class `.lumeo-chart-skel-shape`; we animate opacity with a gentle
 * ripple. Staggered delays come from inline `animation-delay` set
 * by the Blazor render fragments.
 * Respects `prefers-reduced-motion`.
 * ============================================================= */
.lumeo-chart-skeleton {
  display: flex;
  align-items: center;
  justify-content: center;
}
.lumeo-chart-skel-svg {
  display: block;
}
.lumeo-chart-skel-shape {
  animation: lumeo-chart-skel-pulse 1.4s ease-in-out infinite;
  transform-origin: center;
  transform-box: fill-box;
}
@keyframes lumeo-chart-skel-pulse {
  0%, 100% { opacity: 0.3; }
  50%      { opacity: 0.7; }
}

/* Bars — height/y animated via SVG <animate> elements in the markup, so
   we only pulse opacity here (the height animation is browser-native SVG). */
.lumeo-chart-skel-svg [data-skel-shape="bar"] {
  animation: lumeo-chart-skel-pulse 1.6s ease-in-out infinite;
}

/* Line / Area — stroke 'draws itself' across the path with stroke-dasharray. */
.lumeo-chart-skel-svg [data-skel-shape="line"] {
  stroke-dasharray: 240;
  stroke-dashoffset: 240;
  animation: lumeo-chart-skel-line-draw 2.2s ease-in-out infinite;
  opacity: 0.75;
}
@keyframes lumeo-chart-skel-line-draw {
  0%   { stroke-dashoffset:  240; }
  50%  { stroke-dashoffset:    0; }
  100% { stroke-dashoffset: -240; }
}

/* Pie slices — scale + translate handled via SVG native <animateTransform>
   in the markup (reliable across browsers, anchored to the pie centre). We
   still pulse opacity here so slices shimmer in/out as they breathe. */
.lumeo-chart-skel-svg [data-skel-shape="pie-slice"] {
  animation: lumeo-chart-skel-pulse 1.6s ease-in-out infinite;
}

/* Scatter dots — different pace, slight travel to feel alive. */
.lumeo-chart-skel-svg [data-skel-shape="dot"] {
  animation:
    lumeo-chart-skel-pulse 1.4s ease-in-out infinite,
    lumeo-chart-skel-dot-breath 2s ease-in-out infinite;
}
@keyframes lumeo-chart-skel-dot-breath {
  0%, 100% { transform: scale(0.8); }
  50%      { transform: scale(1.15); }
}

@media (prefers-reduced-motion: reduce) {
  .lumeo-chart-skel-shape,
  .lumeo-chart-skel-svg [data-skel-shape] {
    animation: none !important;
    opacity: 0.4 !important;
    transform: none !important;
    stroke-dashoffset: 0 !important;
  }
}

/* ScrollArea (#256) — cross-browser scrollbar styling + Type visibility modes.
   The global "* { scrollbar-width: thin }" rule above gives Firefox a styled
   bar; here we add the webkit thumb/track for the ScrollArea box itself and let
   data-scroll-type switch when the bar is shown. Firefox only exposes
   thin/none, so on Firefox "always"/"auto"/"scroll"/"hover" all render the
   thin bar (no overlay-on-hover granularity), which is the best the platform
   allows without a custom JS overlay. */
.lumeo-scrollarea {
  scrollbar-width: thin;
  scrollbar-color: var(--color-border) transparent;
}
.lumeo-scrollarea::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.lumeo-scrollarea::-webkit-scrollbar-track {
  background: transparent;
}
.lumeo-scrollarea::-webkit-scrollbar-thumb {
  background-color: var(--color-border);
  border-radius: 9999px;
}
.lumeo-scrollarea::-webkit-scrollbar-thumb:hover {
  background-color: color-mix(in oklab, var(--color-muted-foreground) 30%, transparent);
}

/* auto (default): scrollbars present whenever content overflows — overlay
   browsers (macOS) still auto-hide them when idle, matching Radix "auto". */
.lumeo-scrollarea[data-scroll-type="auto"] {
  overflow: auto;
}

/* always: reserve and keep the gutter/bar visible at all times. */
.lumeo-scrollarea[data-scroll-type="always"] {
  overflow: scroll;
  scrollbar-gutter: stable;
}

/* scroll: bar appears while scrolling. overflow:overlay where supported keeps
   the bar off the content box; falls back to auto elsewhere. */
.lumeo-scrollarea[data-scroll-type="scroll"] {
  overflow: auto;
}

/* hover: the webkit bar is transparent until the box (or scrollbar) is
   hovered, mirroring Radix "hover". Firefox keeps its thin bar (no hover
   granularity available). */
.lumeo-scrollarea[data-scroll-type="hover"] {
  overflow: auto;
}
.lumeo-scrollarea[data-scroll-type="hover"]::-webkit-scrollbar-thumb {
  background-color: transparent;
}
.lumeo-scrollarea[data-scroll-type="hover"]:hover::-webkit-scrollbar-thumb,
.lumeo-scrollarea[data-scroll-type="hover"]:focus-within::-webkit-scrollbar-thumb {
  background-color: var(--color-border);
}

/* Filters, advanced builder: the panel gutters follow --filter-panel-pad (0.75rem in a popover,
   0 inline) and the three content cells share ReUI's column widths, overridable per bar with
   --filter-field-width / --filter-operator-width / --filter-value-width. Kept as plain CSS: the
   utility extractor does not carry var() fallbacks inside a flex-basis. */
[data-slot="filters-advanced"] { padding-block: var(--filter-panel-pad, 0.75rem); }
[data-slot="filters-advanced"] .lumeo-filter-gutter { padding-inline: var(--filter-panel-pad, 0.75rem); }
[data-slot="filters-advanced"] .lumeo-filter-footer { padding-inline: var(--filter-panel-pad, 0.75rem) calc(var(--filter-panel-pad, 0.75rem) + 4px); }
[data-slot="filters-advanced"] .lumeo-filter-footer[data-drop-into]::after {
  content: "";
  position: absolute;
  inset-inline: var(--filter-panel-pad, 0.75rem) calc(var(--filter-panel-pad, 0.75rem) + 4px);
  top: 0;
  height: 2px;
  border-radius: 9999px;
  background: var(--color-primary);
}
.lumeo-filter-cell-field { flex-basis: var(--filter-field-width, 11rem); }
.lumeo-filter-cell-operator { flex-basis: var(--filter-operator-width, 9rem); }
.lumeo-filter-cell-value { flex-basis: var(--filter-value-width, 12rem); }

/* An editor's Input fills its row. Input's root is a flex column sized to the field's intrinsic
   width, which in a fused row left the buttons clipped and the field short; here it stretches. */
[data-slot="filter-editor"] [data-slot="input"] { flex: 1 1 0%; min-width: 0; width: 100%; align-items: stretch; }
