/*
 * 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" />
 */

: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));

  --radius: 0.625rem;
  --radius-sm: calc(var(--radius) * 0.5);
  --radius-md: var(--radius);
  --radius-lg: calc(var(--radius) * 1.25);
  --radius-xl: calc(var(--radius) * 1.5);

  /* 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 {
  --radius: 0.5rem;
}

/* =====================================================
 * 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 */
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  background: var(--color-primary);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
}

input[type="range"]::-moz-range-thumb {
  width: 20px;
  height: 20px;
  background: var(--color-primary);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
}

/* 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. */
[role="dialog"][id^="sheet-"] > form,
[role="dialog"][id^="dialog-"] > form,
[role="dialog"][id^="drawer-"] > form {
    flex: 1 1 0;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* 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; }
}

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

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

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

@keyframes slide-out-to-right {
  from { transform: translateX(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: calc(var(--radius-md) - 2px);
  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);
}

