@fiestaboard/ui 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/dist/components/board/board-display.d.ts +35 -0
  4. package/dist/components/board/board-display.js +514 -0
  5. package/dist/components/board/board-display.js.map +1 -0
  6. package/dist/components/board/board-teaser.d.ts +10 -0
  7. package/dist/components/board/board-teaser.js +110 -0
  8. package/dist/components/board/board-teaser.js.map +1 -0
  9. package/dist/components/board/scaled-board-display.d.ts +37 -0
  10. package/dist/components/board/scaled-board-display.js +118 -0
  11. package/dist/components/board/scaled-board-display.js.map +1 -0
  12. package/dist/components/board/static-board-display.d.ts +17 -0
  13. package/dist/components/board/static-board-display.js +140 -0
  14. package/dist/components/board/static-board-display.js.map +1 -0
  15. package/dist/components/chrome/board-icon.d.ts +11 -0
  16. package/dist/components/chrome/board-icon.js +31 -0
  17. package/dist/components/chrome/board-icon.js.map +1 -0
  18. package/dist/components/chrome/board-selector.d.ts +30 -0
  19. package/dist/components/chrome/board-selector.js +37 -0
  20. package/dist/components/chrome/board-selector.js.map +1 -0
  21. package/dist/components/chrome/fiesta-icon.d.ts +28 -0
  22. package/dist/components/chrome/fiesta-icon.js +129 -0
  23. package/dist/components/chrome/fiesta-icon.js.map +1 -0
  24. package/dist/components/chrome/fiesta-logo.d.ts +6 -0
  25. package/dist/components/chrome/fiesta-logo.js +20 -0
  26. package/dist/components/chrome/fiesta-logo.js.map +1 -0
  27. package/dist/components/chrome/language-selector.d.ts +21 -0
  28. package/dist/components/chrome/language-selector.js +29 -0
  29. package/dist/components/chrome/language-selector.js.map +1 -0
  30. package/dist/components/chrome/main-content.d.ts +28 -0
  31. package/dist/components/chrome/main-content.js +33 -0
  32. package/dist/components/chrome/main-content.js.map +1 -0
  33. package/dist/components/chrome/page-header.d.ts +23 -0
  34. package/dist/components/chrome/page-header.js +69 -0
  35. package/dist/components/chrome/page-header.js.map +1 -0
  36. package/dist/components/chrome/page-layout.d.ts +10 -0
  37. package/dist/components/chrome/page-layout.js +17 -0
  38. package/dist/components/chrome/page-layout.js.map +1 -0
  39. package/dist/components/chrome/page-toolbar.d.ts +7 -0
  40. package/dist/components/chrome/page-toolbar.js +21 -0
  41. package/dist/components/chrome/page-toolbar.js.map +1 -0
  42. package/dist/components/chrome/sidebar.d.ts +83 -0
  43. package/dist/components/chrome/sidebar.js +318 -0
  44. package/dist/components/chrome/sidebar.js.map +1 -0
  45. package/dist/components/chrome/skip-to-content.d.ts +8 -0
  46. package/dist/components/chrome/skip-to-content.js +15 -0
  47. package/dist/components/chrome/skip-to-content.js.map +1 -0
  48. package/dist/components/chrome/theme-toggle.d.ts +22 -0
  49. package/dist/components/chrome/theme-toggle.js +28 -0
  50. package/dist/components/chrome/theme-toggle.js.map +1 -0
  51. package/dist/components/seasons/aurora-canvas.d.ts +22 -0
  52. package/dist/components/seasons/aurora-canvas.js +88 -0
  53. package/dist/components/seasons/aurora-canvas.js.map +1 -0
  54. package/dist/components/seasons/sidebar-aurora-horizontal.d.ts +3 -0
  55. package/dist/components/seasons/sidebar-aurora-horizontal.js +47 -0
  56. package/dist/components/seasons/sidebar-aurora-horizontal.js.map +1 -0
  57. package/dist/components/seasons/sidebar-aurora.d.ts +3 -0
  58. package/dist/components/seasons/sidebar-aurora.js +57 -0
  59. package/dist/components/seasons/sidebar-aurora.js.map +1 -0
  60. package/dist/components/ui/accordion.d.ts +16 -0
  61. package/dist/components/ui/accordion.js +55 -0
  62. package/dist/components/ui/accordion.js.map +1 -0
  63. package/dist/components/ui/alert-dialog.d.ts +16 -0
  64. package/dist/components/ui/alert-dialog.js +78 -0
  65. package/dist/components/ui/alert-dialog.js.map +1 -0
  66. package/dist/components/ui/alert.d.ts +9 -0
  67. package/dist/components/ui/alert.js +41 -0
  68. package/dist/components/ui/alert.js.map +1 -0
  69. package/dist/components/ui/aurora.d.ts +9 -0
  70. package/dist/components/ui/aurora.js +105 -0
  71. package/dist/components/ui/aurora.js.map +1 -0
  72. package/dist/components/ui/badge.d.ts +9 -0
  73. package/dist/components/ui/badge.js +35 -0
  74. package/dist/components/ui/badge.js.map +1 -0
  75. package/dist/components/ui/box.d.ts +20 -0
  76. package/dist/components/ui/box.js +14 -0
  77. package/dist/components/ui/box.js.map +1 -0
  78. package/dist/components/ui/button.d.ts +10 -0
  79. package/dist/components/ui/button.js +52 -0
  80. package/dist/components/ui/button.js.map +1 -0
  81. package/dist/components/ui/card.d.ts +11 -0
  82. package/dist/components/ui/card.js +57 -0
  83. package/dist/components/ui/card.js.map +1 -0
  84. package/dist/components/ui/checkbox.d.ts +4 -0
  85. package/dist/components/ui/checkbox.js +16 -0
  86. package/dist/components/ui/checkbox.js.map +1 -0
  87. package/dist/components/ui/code.d.ts +8 -0
  88. package/dist/components/ui/code.js +15 -0
  89. package/dist/components/ui/code.js.map +1 -0
  90. package/dist/components/ui/collapsible.d.ts +8 -0
  91. package/dist/components/ui/collapsible.js +28 -0
  92. package/dist/components/ui/collapsible.js.map +1 -0
  93. package/dist/components/ui/dialog.d.ts +17 -0
  94. package/dist/components/ui/dialog.js +77 -0
  95. package/dist/components/ui/dialog.js.map +1 -0
  96. package/dist/components/ui/dropdown-menu.d.ts +32 -0
  97. package/dist/components/ui/dropdown-menu.js +129 -0
  98. package/dist/components/ui/dropdown-menu.js.map +1 -0
  99. package/dist/components/ui/empty-state.d.ts +23 -0
  100. package/dist/components/ui/empty-state.js +49 -0
  101. package/dist/components/ui/empty-state.js.map +1 -0
  102. package/dist/components/ui/flex.d.ts +24 -0
  103. package/dist/components/ui/flex.js +61 -0
  104. package/dist/components/ui/flex.js.map +1 -0
  105. package/dist/components/ui/grid.d.ts +20 -0
  106. package/dist/components/ui/grid.js +77 -0
  107. package/dist/components/ui/grid.js.map +1 -0
  108. package/dist/components/ui/heading.d.ts +18 -0
  109. package/dist/components/ui/heading.js +26 -0
  110. package/dist/components/ui/heading.js.map +1 -0
  111. package/dist/components/ui/input.d.ts +4 -0
  112. package/dist/components/ui/input.js +16 -0
  113. package/dist/components/ui/input.js.map +1 -0
  114. package/dist/components/ui/label.d.ts +4 -0
  115. package/dist/components/ui/label.js +15 -0
  116. package/dist/components/ui/label.js.map +1 -0
  117. package/dist/components/ui/list.d.ts +25 -0
  118. package/dist/components/ui/list.js +46 -0
  119. package/dist/components/ui/list.js.map +1 -0
  120. package/dist/components/ui/react-bits/decrypted-text.d.ts +14 -0
  121. package/dist/components/ui/react-bits/decrypted-text.js +88 -0
  122. package/dist/components/ui/react-bits/decrypted-text.js.map +1 -0
  123. package/dist/components/ui/react-bits/fade-content.d.ts +11 -0
  124. package/dist/components/ui/react-bits/fade-content.js +48 -0
  125. package/dist/components/ui/react-bits/fade-content.js.map +1 -0
  126. package/dist/components/ui/scroll-area.d.ts +5 -0
  127. package/dist/components/ui/scroll-area.js +39 -0
  128. package/dist/components/ui/scroll-area.js.map +1 -0
  129. package/dist/components/ui/select.d.ts +26 -0
  130. package/dist/components/ui/select.js +132 -0
  131. package/dist/components/ui/select.js.map +1 -0
  132. package/dist/components/ui/sheet.d.ts +29 -0
  133. package/dist/components/ui/sheet.js +114 -0
  134. package/dist/components/ui/sheet.js.map +1 -0
  135. package/dist/components/ui/skeleton.d.ts +2 -0
  136. package/dist/components/ui/skeleton.js +14 -0
  137. package/dist/components/ui/skeleton.js.map +1 -0
  138. package/dist/components/ui/slider.d.ts +9 -0
  139. package/dist/components/ui/slider.js +39 -0
  140. package/dist/components/ui/slider.js.map +1 -0
  141. package/dist/components/ui/stack.d.ts +16 -0
  142. package/dist/components/ui/stack.js +44 -0
  143. package/dist/components/ui/stack.js.map +1 -0
  144. package/dist/components/ui/switch.d.ts +4 -0
  145. package/dist/components/ui/switch.js +24 -0
  146. package/dist/components/ui/switch.js.map +1 -0
  147. package/dist/components/ui/table.d.ts +13 -0
  148. package/dist/components/ui/table.js +54 -0
  149. package/dist/components/ui/table.js.map +1 -0
  150. package/dist/components/ui/tabs.d.ts +7 -0
  151. package/dist/components/ui/tabs.js +37 -0
  152. package/dist/components/ui/tabs.js.map +1 -0
  153. package/dist/components/ui/text-link.d.ts +9 -0
  154. package/dist/components/ui/text-link.js +15 -0
  155. package/dist/components/ui/text-link.js.map +1 -0
  156. package/dist/components/ui/text.d.ts +22 -0
  157. package/dist/components/ui/text.js +48 -0
  158. package/dist/components/ui/text.js.map +1 -0
  159. package/dist/components/ui/textarea.d.ts +3 -0
  160. package/dist/components/ui/textarea.js +15 -0
  161. package/dist/components/ui/textarea.js.map +1 -0
  162. package/dist/components/ui/tooltip.d.ts +16 -0
  163. package/dist/components/ui/tooltip.js +38 -0
  164. package/dist/components/ui/tooltip.js.map +1 -0
  165. package/dist/fonts.css +16 -0
  166. package/dist/index.d.ts +66 -0
  167. package/dist/index.js +59 -0
  168. package/dist/lib/board-characters.d.ts +55 -0
  169. package/dist/lib/board-characters.js +151 -0
  170. package/dist/lib/board-characters.js.map +1 -0
  171. package/dist/lib/board-colors.d.ts +48 -0
  172. package/dist/lib/board-colors.js +93 -0
  173. package/dist/lib/board-colors.js.map +1 -0
  174. package/dist/lib/board-dimensions.d.ts +41 -0
  175. package/dist/lib/board-dimensions.js +38 -0
  176. package/dist/lib/board-dimensions.js.map +1 -0
  177. package/dist/lib/board-metrics.d.ts +19 -0
  178. package/dist/lib/board-metrics.js +22 -0
  179. package/dist/lib/board-metrics.js.map +1 -0
  180. package/dist/lib/seasons-drafts.d.ts +26 -0
  181. package/dist/lib/seasons-drafts.js +106 -0
  182. package/dist/lib/seasons-drafts.js.map +1 -0
  183. package/dist/lib/seasons.d.ts +52 -0
  184. package/dist/lib/seasons.js +59 -0
  185. package/dist/lib/seasons.js.map +1 -0
  186. package/dist/lib/select-items.d.ts +29 -0
  187. package/dist/lib/select-items.js +35 -0
  188. package/dist/lib/select-items.js.map +1 -0
  189. package/dist/lib/utils.d.ts +2 -0
  190. package/dist/lib/utils.js +11 -0
  191. package/dist/lib/utils.js.map +1 -0
  192. package/dist/seasons/christmas.css +64 -0
  193. package/dist/seasons/easter.css +62 -0
  194. package/dist/seasons/fathers-day.css +62 -0
  195. package/dist/seasons/halloween.css +69 -0
  196. package/dist/seasons/mothers-day.css +63 -0
  197. package/dist/seasons/new-year.css +66 -0
  198. package/dist/seasons/pride.css +68 -0
  199. package/dist/seasons/thanksgiving.css +64 -0
  200. package/dist/theme.css +1174 -0
  201. package/package.json +108 -0
package/dist/theme.css ADDED
@@ -0,0 +1,1174 @@
1
+ /**
2
+ * FiestaUI design tokens + base layer.
3
+ *
4
+ * This file is the design-system half of what used to be FiestaBoard's
5
+ * app/globals.css. It must be imported into a Tailwind v4 pipeline:
6
+ *
7
+ * @import "tailwindcss";
8
+ * @import "@fiestaboard/ui/fonts.css"; <- opt-in Geist @font-face registrations;
9
+ * @import "@fiestaboard/ui/theme.css"; skip fonts.css if you self-host Geist
10
+ * @source "../node_modules/@fiestaboard/ui/dist";
11
+ *
12
+ * Dark mode is class-based: toggle the `dark` class on <html>.
13
+ * Values are copied verbatim from FiestaBoard app/globals.css — visual
14
+ * parity with the app is the contract; do not reformat or "improve"
15
+ * token values here without a parity check on the consumer side.
16
+ */
17
+ @import "tw-animate-css";
18
+ /* Seasonal themes — one file per season, inert unless the season class
19
+ is stamped on <html>. Only the LIVE season (pride) is inlined here so
20
+ consumers get it automatically; the design drafts are NOT imported —
21
+ they'd ship ~18 KB of rule families keyed on classes the app never
22
+ stamps. A consumer that promotes a draft opts in with:
23
+ @import "@fiestaboard/ui/seasons/<id>.css";
24
+ (see README "Seasonal theming"). Storybook imports every draft
25
+ directly in storybook.css so the Season toolbar can preview them. */
26
+ @import "./seasons/pride.css";
27
+
28
+ @custom-variant dark (&:is(.dark *));
29
+
30
+ @theme inline {
31
+ --color-background: var(--background);
32
+ --color-foreground: var(--foreground);
33
+ --font-sans: var(--font-geist-sans);
34
+ --font-mono: var(--font-geist-mono);
35
+ --color-sidebar-ring: var(--sidebar-ring);
36
+ --color-sidebar-border: var(--sidebar-border);
37
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
38
+ --color-sidebar-accent: var(--sidebar-accent);
39
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
40
+ --color-sidebar-primary: var(--sidebar-primary);
41
+ --color-sidebar-foreground: var(--sidebar-foreground);
42
+ --color-sidebar: var(--sidebar);
43
+ --color-chart-5: var(--chart-5);
44
+ --color-chart-4: var(--chart-4);
45
+ --color-chart-3: var(--chart-3);
46
+ --color-chart-2: var(--chart-2);
47
+ --color-chart-1: var(--chart-1);
48
+ --color-ring: var(--ring);
49
+ --color-input: var(--input);
50
+ --color-border: var(--border);
51
+ --color-destructive: var(--destructive);
52
+ --color-destructive-foreground: var(--destructive-foreground);
53
+ --color-accent-foreground: var(--accent-foreground);
54
+ --color-accent: var(--accent);
55
+ --color-muted-foreground: var(--muted-foreground);
56
+ --color-muted: var(--muted);
57
+ --color-secondary-foreground: var(--secondary-foreground);
58
+ --color-secondary: var(--secondary);
59
+ --color-primary-foreground: var(--primary-foreground);
60
+ --color-primary: var(--primary);
61
+ --color-popover-foreground: var(--popover-foreground);
62
+ --color-popover: var(--popover);
63
+ --color-card-foreground: var(--card-foreground);
64
+ --color-card: var(--card);
65
+ --radius-sm: calc(var(--radius) - 4px);
66
+ --radius-md: calc(var(--radius) - 2px);
67
+ --radius-lg: var(--radius);
68
+ --radius-xl: calc(var(--radius) + 4px);
69
+ --radius-2xl: calc(var(--radius) + 8px);
70
+ --radius-3xl: calc(var(--radius) + 12px);
71
+ --radius-4xl: calc(var(--radius) + 16px);
72
+
73
+ /* Status/intent semantic colors */
74
+ --color-info: var(--info);
75
+ --color-info-foreground: var(--info-foreground);
76
+ --color-success: var(--success);
77
+ --color-success-foreground: var(--success-foreground);
78
+ --color-warning: var(--warning);
79
+ --color-warning-foreground: var(--warning-foreground);
80
+
81
+ /* Tag/badge accent colors */
82
+ --color-tag-variable: var(--tag-variable);
83
+ --color-tag-variable-foreground: var(--tag-variable-foreground);
84
+ --color-tag-success: var(--tag-success);
85
+ --color-tag-success-foreground: var(--tag-success-foreground);
86
+ --color-tag-formula: var(--tag-formula);
87
+ --color-tag-formula-foreground: var(--tag-formula-foreground);
88
+
89
+ /* Overlay */
90
+ --color-overlay: var(--overlay);
91
+
92
+ /* Brand accent (sidebar active, links, key UI) - FiestaBoard orange from taco icon #f5a623 */
93
+ --color-brand: var(--brand);
94
+ --color-brand-foreground: var(--brand-foreground);
95
+ --color-brand-emphasis: var(--brand-emphasis);
96
+
97
+ /* Navigation active state (indigo) */
98
+ --color-nav-active: var(--nav-active);
99
+ --color-nav-active-foreground: var(--nav-active-foreground);
100
+ --color-nav-active-hover: var(--nav-active-hover);
101
+
102
+ /* Board Official Colors - use these for all status/tile colors */
103
+ --color-board-red: #eb4034;
104
+ --color-board-orange: #f5a623;
105
+ --color-board-yellow: #f8e71c;
106
+ --color-board-green: #7ed321;
107
+ --color-board-blue: #4a90d9;
108
+ --color-board-violet: #9b59b6;
109
+ --color-board-white: #ffffff;
110
+ --color-board-black: #1a1a1a;
111
+
112
+ /* Board hardware surface colors (independent of UI theme) */
113
+ --color-board-surface-light: #fafafa;
114
+ --color-board-surface-dark: #0d0d0d;
115
+ --color-board-text-on-light: #0d0d0d;
116
+ --color-board-text-on-dark: #f0f0e8;
117
+ --color-board-bezel-light: #f8f8f8;
118
+ --color-board-bezel-dark: #050505;
119
+ --color-board-bezel-border-light: #d8d8d8;
120
+ --color-board-bezel-border-dark: #1a1a1a;
121
+
122
+ /* Backward compatibility aliases */
123
+ --color-fiesta-red: var(--color-board-red);
124
+ --color-fiesta-orange: var(--color-board-orange);
125
+ --color-fiesta-yellow: var(--color-board-yellow);
126
+ --color-fiesta-green: var(--color-board-green);
127
+ --color-fiesta-blue: var(--color-board-blue);
128
+ --color-fiesta-violet: var(--color-board-violet);
129
+ --color-fiesta-white: var(--color-board-white);
130
+ --color-fiesta-black: var(--color-board-black);
131
+
132
+ /* Elevation shadows (theme-aware via CSS vars in :root / .dark) */
133
+ --shadow-card: var(--elevation-card);
134
+ --shadow-card-hover: var(--elevation-card-hover);
135
+ --shadow-modal: var(--elevation-modal);
136
+ }
137
+
138
+ :root {
139
+ /* Font family tokens. The "Geist Variable" / "Geist Mono Variable"
140
+ names match the @font-face declarations registered by the opt-in
141
+ @fiestaboard/ui/fonts.css subpath (which wraps
142
+ @fontsource-variable/geist and @fontsource-variable/geist-mono).
143
+ Consumers that self-host Geist (next/font, CDN, subset) skip
144
+ fonts.css; without it these tokens degrade gracefully to the
145
+ system stacks below. Previously these CSS vars were set by
146
+ Next.js's next/font wrapper in layout.tsx — moved here so they're
147
+ owned by the stylesheet, not a build-time JS wrapper. */
148
+ --font-geist-sans: "Geist Variable", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
149
+ --font-geist-mono: "Geist Mono Variable", ui-monospace, SFMono-Regular, "SF Mono", Monaco, Menlo, monospace;
150
+ --radius: 0.625rem;
151
+ --background: oklch(0.985 0 0);
152
+ --foreground: oklch(0.145 0 0);
153
+ --card: oklch(1 0 0);
154
+ --card-foreground: oklch(0.145 0 0);
155
+ --popover: oklch(1 0 0);
156
+ --popover-foreground: oklch(0.145 0 0);
157
+ --primary: oklch(0.52 0.16 60);
158
+ --primary-foreground: oklch(0.985 0 0);
159
+ --secondary: oklch(0.96 0 0);
160
+ --secondary-foreground: oklch(0.205 0 0);
161
+ --muted: oklch(0.955 0 0);
162
+ /* Muted text needs >=4.5:1 contrast vs --background to meet WCAG AA. */
163
+ --muted-foreground: oklch(0.4 0 0);
164
+ --accent: oklch(0.95 0 0);
165
+ --accent-foreground: oklch(0.205 0 0);
166
+ --destructive: oklch(0.43 0.245 27.325);
167
+ --destructive-foreground: oklch(0.985 0 0);
168
+ --border: oklch(0.905 0 0);
169
+ --input: oklch(0.885 0 0);
170
+ --ring: oklch(0.52 0.16 60);
171
+ --chart-1: oklch(0.646 0.222 41.116);
172
+ --chart-2: oklch(0.6 0.118 184.704);
173
+ --chart-3: oklch(0.398 0.07 227.392);
174
+ --chart-4: oklch(0.65 0.189 84.429);
175
+ --chart-5: oklch(0.65 0.188 70.08);
176
+ --sidebar: oklch(0.975 0 0);
177
+ --sidebar-foreground: oklch(0.145 0 0);
178
+ --sidebar-primary: oklch(0.52 0.16 60);
179
+ --sidebar-primary-foreground: oklch(0.985 0 0);
180
+ --sidebar-accent: oklch(0.92 0 0);
181
+ --sidebar-accent-foreground: oklch(0.205 0 0);
182
+ --sidebar-border: oklch(0.915 0 0);
183
+ --sidebar-ring: oklch(0.52 0.16 60);
184
+ --info: oklch(0.45 0.15 250);
185
+ --info-foreground: oklch(0.985 0 0);
186
+ --success: oklch(0.4 0.16 150);
187
+ --success-foreground: oklch(0.985 0 0);
188
+ --warning: oklch(0.45 0.16 70);
189
+ --warning-foreground: oklch(0.985 0 0);
190
+ --tag-variable: oklch(0.45 0.2 280);
191
+ --tag-variable-foreground: oklch(0.35 0.1 280);
192
+ --tag-success: oklch(0.45 0.2 160);
193
+ --tag-success-foreground: oklch(0.35 0.1 160);
194
+ --tag-formula: oklch(0.45 0.15 80);
195
+ --tag-formula-foreground: oklch(0.35 0.1 80);
196
+ --overlay: oklch(0 0 0 / 80%);
197
+ /* Brand accent - darkened for WCAG 2.2 AA (≥4.5:1 on light bg) */
198
+ --brand: oklch(0.56 0.15 75);
199
+ --brand-emphasis: oklch(0.52 0.16 60);
200
+ --brand-foreground: oklch(1 0 0);
201
+ /* Navigation active state — indigo palette */
202
+ --nav-active: oklch(0.44 0.19 265); /* indigo-600 */
203
+ --nav-active-foreground: oklch(1 0 0); /* white */
204
+ --nav-active-hover: oklch(0.5 0.17 265 / 15%); /* indigo tint hover */
205
+ /* Fiesta gradient palette — used by sidebar and page icon gradient */
206
+ --fiesta-red: #c97a72;
207
+ --fiesta-orange: #c99662;
208
+ --fiesta-purple: #9b7bb0;
209
+ /* Page icon gradient stops — 6 vars so pride month can expand to a full rainbow */
210
+ --icon-g1: var(--fiesta-red);
211
+ --icon-g2: var(--fiesta-orange);
212
+ --icon-g3: var(--fiesta-orange);
213
+ --icon-g4: var(--fiesta-orange);
214
+ --icon-g5: var(--fiesta-purple);
215
+ --icon-g6: var(--fiesta-purple);
216
+ /* Elevation - subtle card and modal shadows */
217
+ --elevation-card: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 2px 8px -2px rgb(0 0 0 / 0.06);
218
+ --elevation-card-hover: 0 2px 8px -2px rgb(0 0 0 / 0.08), 0 4px 16px -4px rgb(0 0 0 / 0.06);
219
+ --elevation-modal: 0 4px 12px -2px rgb(0 0 0 / 0.08), 0 4px 6px -4px rgb(0 0 0 / 0.04);
220
+
221
+ /* ============================================================
222
+ * Motion tokens (B4) — one duration scale + named easings.
223
+ * Values are lifted verbatim from the pre-token declarations;
224
+ * visual parity is the contract (see file header).
225
+ *
226
+ * Durations:
227
+ * fast 100ms micro fades (collapsed-sidebar labels; = Tailwind duration-100)
228
+ * control 150ms control feedback: button/switch/scrollbar (= Tailwind duration-150)
229
+ * base 200ms standard fades, accordion, dialog (= Tailwind duration-200)
230
+ * exit 250ms surface exit slides + companion fades
231
+ * slow 300ms surface & page entrances, sidebar collapse
232
+ * slower 350ms large-surface entrances (slide-up page, mobile nav/menu)
233
+ * slowest 400ms sheet/drawer slide-in, desktop sidebar entrance
234
+ *
235
+ * Easings:
236
+ * out ease-out standard decelerate (fades, entrances)
237
+ * spring cubic-bezier(0.16, 1, 0.3, 1) out-expo "spring" (modal/page/sheet entrances)
238
+ * in cubic-bezier(0.4, 0, 1, 1) accelerate (exits; = Tailwind ease-in)
239
+ * out-cubic cubic-bezier(0.33, 1, 0.68, 1) gentle decelerate (sidebar collapse/expand)
240
+ * standard cubic-bezier(0.25, 0.1, 0.25, 1) = CSS `ease` keyword (sheet slides)
241
+ *
242
+ * Deliberately left inline (true one-offs): 117s sidebar gradient
243
+ * flow, 220ms board-switch exits, 0.8s pride-burst, 1s card-fade-in,
244
+ * 50ms page-toolbar stagger delay.
245
+ * ============================================================ */
246
+ --motion-duration-fast: 100ms;
247
+ --motion-duration-control: 150ms;
248
+ --motion-duration-base: 200ms;
249
+ --motion-duration-exit: 250ms;
250
+ --motion-duration-slow: 300ms;
251
+ --motion-duration-slower: 350ms;
252
+ --motion-duration-slowest: 400ms;
253
+ --motion-ease-out: ease-out;
254
+ --motion-ease-spring: cubic-bezier(0.16, 1, 0.3, 1);
255
+ --motion-ease-in: cubic-bezier(0.4, 0, 1, 1);
256
+ --motion-ease-out-cubic: cubic-bezier(0.33, 1, 0.68, 1);
257
+ --motion-ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1);
258
+
259
+ /* ============================================================
260
+ * Radius role scale (B3) — which UI role gets which corner.
261
+ * Roles are derived from what shipped components already use;
262
+ * every value equals the exact pre-token computed value (visual
263
+ * parity is the contract — see file header).
264
+ *
265
+ * Role | token | = class | value | used by
266
+ * control-inset | --radius-control-inset | rounded-sm | 6px | items nested inside a rounded control/popover: dropdown & select items, tabs trigger, dialog close
267
+ * control | --radius-control | rounded-md | 8px | buttons, input, textarea, select trigger, tabs list, skeleton, skip-link, and popover surfaces (dropdown/select/tooltip popups)
268
+ * surface | --radius-surface | rounded-lg | 10px | modal surfaces (dialog, alert-dialog), alert, sidebar nav pills
269
+ * card | --radius-card | rounded-xl | 12px | card, page-header icon tile
270
+ * pill | --radius-pill | rounded-full | ∞ | badge, switch, slider, sheet close, scroll-area thumb, sidebar edge toggle, empty-state icon
271
+ * chrome | --radius-chrome | (css/inline) | 14px | desktop sidebar shell + its aurora inset
272
+ * chrome-mobile | --radius-chrome-mobile | (css/inline) | 16px | mobile header/menu shell, its aurora inset, menu clip-path reveal
273
+ *
274
+ * Components KEEP their Tailwind rounded-* classes — those already
275
+ * resolve through the --radius calc chain, so each role token below
276
+ * is defined with the same math and stays in lockstep with them.
277
+ * New components pick a ROLE from this table instead of a raw size.
278
+ * The chrome radii are deliberately fixed px (not on the rem-based
279
+ * --radius chain): they match the aurora canvas insets exactly.
280
+ *
281
+ * Deliberately off-scale (left as-is): BoardIcon bare `rounded`
282
+ * (4px micro tile), pride-burst particle 2px, scroll-area viewport
283
+ * rounded-[inherit].
284
+ * ============================================================ */
285
+ --radius-control-inset: calc(var(--radius) - 4px);
286
+ --radius-control: calc(var(--radius) - 2px);
287
+ --radius-surface: var(--radius);
288
+ --radius-card: calc(var(--radius) + 4px);
289
+ --radius-pill: calc(infinity * 1px);
290
+ --radius-chrome: 14px;
291
+ --radius-chrome-mobile: 16px;
292
+
293
+ /* ============================================================
294
+ * Z-index layer scale (B3) — the app's stacking story, bottom to
295
+ * top. Values are exactly the pre-token numbers (parity contract).
296
+ *
297
+ * --z-popover 50 dropdown-menu popovers (see gotcha below)
298
+ * --z-sidebar 50 desktop sidebar rail
299
+ * --z-sidebar-toggle 51 edge collapse button — LOCAL value: the
300
+ * fixed z-50 aside is its own stacking
301
+ * context, so 51 only beats the aurora
302
+ * canvas / z-[1] content column inside it
303
+ * --z-mobile-backdrop 90 dimming layer behind the mobile menu
304
+ * --z-mobile-menu 95 mobile slide-down menu panel
305
+ * --z-mobile-header 100 mobile header — beats the open menu (95)
306
+ * and backdrop (90) so the hamburger/close
307
+ * button stays clickable
308
+ * --z-sheet 110 sheet/drawer + its backdrop — covers all
309
+ * chrome including the mobile header
310
+ * --z-select 120 Select popup — MUST stay above
311
+ * --z-mobile-header: the header hosts the
312
+ * mobile board selector, and its portalled
313
+ * popup wins by VALUE (Base UI portals into
314
+ * <body>; all these layers are fixed in the
315
+ * root stacking context)
316
+ * --z-modal 130 dialog + alert-dialog (and backdrops) —
317
+ * above sheets, so dialogs can launch from
318
+ * them
319
+ * --z-tooltip 140 tooltip — topmost overlay, readable over
320
+ * modals
321
+ * --z-skip-link 200 skip-to-content link when focused
322
+ * --z-confetti 9999 season burst particles — above everything
323
+ *
324
+ * Popover gotcha: --z-popover (50) ties with the desktop sidebar.
325
+ * Dropdowns still stack above it because Base UI portals append to
326
+ * <body> AFTER the sidebar — DOM order breaks the tie. A dropdown
327
+ * opened FROM the mobile header (100) would lose; nothing does that
328
+ * today (the header's board selector is a Select at 120). Don't put
329
+ * DropdownMenus in mobile chrome without revisiting this.
330
+ *
331
+ * Not on the scale (local stacking / one-offs): select scroll
332
+ * buttons z-10 (sticky inside the popup), sidebar/header content
333
+ * z-[1] (above the aurora canvas), board-switch view-transition
334
+ * groups 0/1 (::view-transition pseudo-tree only).
335
+ * ============================================================ */
336
+ --z-popover: 50;
337
+ --z-sidebar: 50;
338
+ --z-sidebar-toggle: 51;
339
+ --z-mobile-backdrop: 90;
340
+ --z-mobile-menu: 95;
341
+ --z-mobile-header: 100;
342
+ --z-sheet: 110;
343
+ --z-select: 120;
344
+ --z-modal: 130;
345
+ --z-tooltip: 140;
346
+ --z-skip-link: 200;
347
+ --z-confetti: 9999;
348
+ }
349
+
350
+ .dark {
351
+ --background: oklch(0.13 0 0);
352
+ --foreground: oklch(0.985 0 0);
353
+ --card: oklch(0.16 0 0);
354
+ --card-foreground: oklch(0.985 0 0);
355
+ --popover: oklch(0.16 0 0);
356
+ --popover-foreground: oklch(0.985 0 0);
357
+ --primary: oklch(0.922 0 0);
358
+ --primary-foreground: oklch(0.205 0 0);
359
+ --secondary: oklch(0.215 0 0);
360
+ --secondary-foreground: oklch(0.985 0 0);
361
+ --muted: oklch(0.2 0 0);
362
+ --muted-foreground: oklch(0.83 0 0);
363
+ --accent: oklch(0.19 0 0);
364
+ --accent-foreground: oklch(0.985 0 0);
365
+ --destructive: oklch(0.8 0.191 22.216);
366
+ --destructive-foreground: oklch(0.15 0 0);
367
+ --border: oklch(1 0 0 / 10%);
368
+ --input: oklch(1 0 0 / 14%);
369
+ --ring: oklch(0.55 0 0);
370
+ --chart-1: oklch(0.55 0.243 264.376);
371
+ --chart-2: oklch(0.696 0.17 162.48);
372
+ --chart-3: oklch(0.769 0.188 70.08);
373
+ --chart-4: oklch(0.627 0.265 303.9);
374
+ --chart-5: oklch(0.645 0.246 16.439);
375
+ --sidebar: oklch(0.145 0 0);
376
+ --sidebar-foreground: oklch(0.985 0 0);
377
+ --sidebar-primary: oklch(0.488 0.243 264.376);
378
+ --sidebar-primary-foreground: oklch(0.985 0 0);
379
+ --sidebar-accent: oklch(0.28 0 0);
380
+ --sidebar-accent-foreground: oklch(0.985 0 0);
381
+ --sidebar-border: oklch(1 0 0 / 8%);
382
+ --sidebar-ring: oklch(0.55 0 0);
383
+ --info: oklch(0.75 0.18 250);
384
+ --info-foreground: oklch(0.15 0 0);
385
+ --success: oklch(0.68 0.18 145);
386
+ --success-foreground: oklch(0.15 0 0);
387
+ --warning: oklch(0.78 0.14 75);
388
+ --warning-foreground: oklch(0.15 0 0);
389
+ --tag-variable: oklch(0.65 0.15 280);
390
+ --tag-variable-foreground: oklch(0.85 0.08 280);
391
+ --tag-success: oklch(0.65 0.15 160);
392
+ --tag-success-foreground: oklch(0.85 0.08 160);
393
+ --tag-formula: oklch(0.65 0.15 80);
394
+ --tag-formula-foreground: oklch(0.85 0.08 80);
395
+ --overlay: oklch(0 0 0 / 80%);
396
+ --brand: oklch(0.78 0.14 75);
397
+ --brand-emphasis: oklch(0.55 0.16 60);
398
+ --brand-foreground: oklch(1 0 0);
399
+ /* Navigation active state — indigo palette (lighter for dark mode) */
400
+ --nav-active: oklch(0.59 0.19 265); /* indigo-500 */
401
+ --nav-active-foreground: oklch(1 0 0); /* white */
402
+ --nav-active-hover: oklch(0.59 0.19 265 / 20%); /* indigo tint hover */
403
+ /* Fiesta gradient palette — lighter for visibility on dark card backgrounds */
404
+ --fiesta-red: #e08a82;
405
+ --fiesta-orange: #d4a870;
406
+ --fiesta-purple: #b08ec8;
407
+ --elevation-card: 0 1px 3px 0 rgb(0 0 0 / 0.2), 0 0 0 1px oklch(1 0 0 / 6%);
408
+ --elevation-card-hover: 0 2px 8px -2px rgb(0 0 0 / 0.3), 0 0 0 1px oklch(1 0 0 / 10%);
409
+ --elevation-modal: 0 8px 24px -4px rgb(0 0 0 / 0.25), 0 4px 8px -4px rgb(0 0 0 / 0.15);
410
+ }
411
+
412
+ @layer base {
413
+ * {
414
+ @apply border-border outline-ring;
415
+ }
416
+ html {
417
+ /* Reserve space for scrollbar to prevent layout shift */
418
+ scrollbar-gutter: stable;
419
+ }
420
+ body {
421
+ @apply bg-background text-foreground;
422
+ font-feature-settings: "ss01";
423
+ /* Clip horizontal overflow at the document root so the body itself stays the
424
+ * vertical scroll container. Applying overflow-x to <main> would force its
425
+ * computed overflow-y to `auto`, recreating a nested scrollbar. */
426
+ overflow-x: hidden;
427
+ }
428
+ }
429
+
430
+ /* Accordion animation classes.
431
+ *
432
+ * These resolve to tw-animate-css's `accordion-down`/`accordion-up`
433
+ * keyframes (registered via its @theme import at the top of this file),
434
+ * which animate `height: 0 <-> var(--radix-accordion-content-height,
435
+ * ... auto)`. That is the ONLY version of these keyframes that has ever
436
+ * shipped: a same-named grid-template-rows pair that used to live here was
437
+ * silently discarded by the Tailwind build in favor of tw-animate's
438
+ * (issue #71). Restoring the grid technique means renaming the keyframes
439
+ * (fui- prefix) and re-wiring these classes — a behavior change that needs
440
+ * visual review, tracked separately from the dead-CSS cleanup.
441
+ *
442
+ * `display: grid` is kept deliberately: it shipped with these classes all
443
+ * along, and removing it is a rendered-layout change (grid vs block
444
+ * formatting context) outside the pixel-identical scope of the cleanup.
445
+ */
446
+ .animate-accordion-down {
447
+ display: grid;
448
+ animation: accordion-down var(--motion-duration-base) var(--motion-ease-out);
449
+ }
450
+
451
+ .animate-accordion-up {
452
+ display: grid;
453
+ animation: accordion-up var(--motion-duration-base) var(--motion-ease-out);
454
+ }
455
+
456
+ /* ===== 60FPS SHEET/DRAWER ANIMATIONS ===== */
457
+ /* Direct keyframe animations for smooth sheet transitions */
458
+
459
+ @keyframes sheet-overlay-in {
460
+ from {
461
+ opacity: 0;
462
+ }
463
+ to {
464
+ opacity: 1;
465
+ }
466
+ }
467
+
468
+ @keyframes sheet-slide-in-right {
469
+ from {
470
+ transform: translate3d(100%, 0, 0);
471
+ }
472
+ to {
473
+ transform: translate3d(0, 0, 0);
474
+ }
475
+ }
476
+
477
+ @keyframes sheet-slide-in-left {
478
+ from {
479
+ transform: translate3d(-100%, 0, 0);
480
+ }
481
+ to {
482
+ transform: translate3d(0, 0, 0);
483
+ }
484
+ }
485
+
486
+ @keyframes sheet-slide-in-top {
487
+ from {
488
+ transform: translate3d(0, -100%, 0);
489
+ }
490
+ to {
491
+ transform: translate3d(0, 0, 0);
492
+ }
493
+ }
494
+
495
+ @keyframes sheet-slide-in-bottom {
496
+ from {
497
+ transform: translate3d(0, 100%, 0);
498
+ }
499
+ to {
500
+ transform: translate3d(0, 0, 0);
501
+ }
502
+ }
503
+
504
+ /* Prevent iOS Safari from zooming on input focus */
505
+ /* iOS Safari zooms when input font-size is < 16px */
506
+ @supports (-webkit-touch-callout: none) {
507
+ input,
508
+ textarea,
509
+ select {
510
+ font-size: 16px !important;
511
+ }
512
+ }
513
+
514
+ /* ============================================================
515
+ * App chrome — sidebar, navigation, page transitions, festive.
516
+ * Extracted verbatim from FiestaBoard app/globals.css (phase 2).
517
+ * Consumed by the chrome components (Sidebar, PageHeader, …).
518
+ * ============================================================ */
519
+
520
+ /* Pride styling lives in seasons/pride.css alongside the draft seasons. */
521
+
522
+ /* Season easter egg — confetti burst particles spawned by JS on logo click
523
+ (class name kept as pride-burst-* for app compatibility; every season's
524
+ fireSeasonBurst() uses it) */
525
+ @keyframes pride-burst {
526
+ 0% {
527
+ transform: translate(0, 0) rotate(0deg) scale(1);
528
+ opacity: 1;
529
+ }
530
+ 100% {
531
+ transform: translate(var(--tx), var(--ty)) rotate(var(--rot)) scale(0.2);
532
+ opacity: 0;
533
+ }
534
+ }
535
+ /* Wrapper so the 48 particles append to the live tree in a single mutation
536
+ and share one animationend listener. Fixed (out of flow) so the empty
537
+ container never affects body layout; no transform/filter/contain here, so
538
+ the fixed particles still resolve against the viewport. */
539
+ .pride-burst {
540
+ position: fixed;
541
+ top: 0;
542
+ left: 0;
543
+ /* The fixed container forms a stacking context, trapping the particles'
544
+ z-index inside it — so the container itself must sit at confetti level
545
+ or the burst paints under sheets/modals/tooltips. */
546
+ z-index: var(--z-confetti);
547
+ pointer-events: none;
548
+ }
549
+ .pride-burst-particle {
550
+ position: fixed;
551
+ width: 7px;
552
+ height: 7px;
553
+ border-radius: 2px;
554
+ pointer-events: none;
555
+ z-index: var(--z-confetti);
556
+ /* 0.8s default is a one-off (JS overrides via --dur), so it stays inline */
557
+ animation: pride-burst var(--dur, 0.8s) var(--motion-ease-out) forwards;
558
+ }
559
+
560
+ /* fireSeasonBurst() already skips the burst under reduced motion, but mirror
561
+ every other motion surface in this file with a CSS gate as a backstop. */
562
+ @media (prefers-reduced-motion: reduce) {
563
+ .pride-burst-particle {
564
+ animation: none !important;
565
+ }
566
+ }
567
+
568
+ /* Board switch transition — the app-wide board selector wraps its state
569
+ update in document.startViewTransition() (see current-board-context.tsx),
570
+ stamping html[data-board-switch="forward"|"backward"]. Only the page
571
+ content participates (view-transition-name on <main>): the old page slides
572
+ out one way while the new page slides in from the other, so changing boards
573
+ reads as a SWITCH instead of the page re-entering from the bottom. */
574
+ #main-content {
575
+ view-transition-name: board-page;
576
+ }
577
+
578
+ /* The sidebar and mobile header get their own (static) snapshot groups so
579
+ they can be stacked ABOVE the sliding page — the page slides under the
580
+ app chrome, never over it. */
581
+ aside.sidebar-gradient {
582
+ view-transition-name: app-sidebar;
583
+ }
584
+
585
+ header.sidebar-gradient-horizontal {
586
+ view-transition-name: app-mobile-header;
587
+ }
588
+
589
+ html[data-board-switch]::view-transition-group(board-page) {
590
+ z-index: 0;
591
+ }
592
+
593
+ html[data-board-switch]::view-transition-group(app-sidebar),
594
+ html[data-board-switch]::view-transition-group(app-mobile-header) {
595
+ z-index: 1;
596
+ }
597
+
598
+ @keyframes board-switch-out-left {
599
+ to {
600
+ opacity: 0;
601
+ transform: translateX(-20%);
602
+ }
603
+ }
604
+
605
+ @keyframes board-switch-in-right {
606
+ from {
607
+ opacity: 0;
608
+ transform: translateX(20%);
609
+ }
610
+ }
611
+
612
+ @keyframes board-switch-out-right {
613
+ to {
614
+ opacity: 0;
615
+ transform: translateX(20%);
616
+ }
617
+ }
618
+
619
+ @keyframes board-switch-in-left {
620
+ from {
621
+ opacity: 0;
622
+ transform: translateX(-20%);
623
+ }
624
+ }
625
+
626
+ /* 220ms outgoing-page exit is a deliberate one-off (slightly quicker than the
627
+ 250ms exit tier so the incoming page reads as leading the switch) */
628
+ html[data-board-switch="forward"]::view-transition-old(board-page) {
629
+ animation: board-switch-out-left 220ms var(--motion-ease-in) both;
630
+ }
631
+
632
+ html[data-board-switch="forward"]::view-transition-new(board-page) {
633
+ animation: board-switch-in-right var(--motion-duration-slow) var(--motion-ease-spring) both;
634
+ }
635
+
636
+ html[data-board-switch="backward"]::view-transition-old(board-page) {
637
+ animation: board-switch-out-right 220ms var(--motion-ease-in) both;
638
+ }
639
+
640
+ html[data-board-switch="backward"]::view-transition-new(board-page) {
641
+ animation: board-switch-in-left var(--motion-duration-slow) var(--motion-ease-spring) both;
642
+ }
643
+
644
+ @media (prefers-reduced-motion: reduce) {
645
+ html[data-board-switch]::view-transition-old(board-page),
646
+ html[data-board-switch]::view-transition-new(board-page) {
647
+ animation: none !important;
648
+ }
649
+ }
650
+
651
+ /* Page Transitions - CSS-based (works in all browsers) */
652
+ @layer base {
653
+ /* Animate page content on route changes */
654
+ main {
655
+ animation: page-enter var(--motion-duration-slow) var(--motion-ease-out);
656
+ }
657
+
658
+ /* Slide-up animation for modal-like pages (edit/new) */
659
+ html[data-transition="slide-up"] main {
660
+ animation: slide-up-enter var(--motion-duration-slower) var(--motion-ease-spring);
661
+ }
662
+
663
+ /* Slide-down animation for returning from modals */
664
+ html[data-transition="slide-down"] main {
665
+ animation: slide-down-enter var(--motion-duration-slow) var(--motion-ease-spring);
666
+ }
667
+
668
+ /* Scale-fade for content areas */
669
+ html[data-transition="scale-fade"] main {
670
+ animation: scale-fade-enter var(--motion-duration-slow) var(--motion-ease-spring);
671
+ }
672
+
673
+ /* Respect user's motion preferences */
674
+ @media (prefers-reduced-motion: reduce) {
675
+ main {
676
+ animation: none !important;
677
+ }
678
+ }
679
+ .reduce-motion main {
680
+ animation: none !important;
681
+ }
682
+ }
683
+
684
+ /* Page Transition Keyframes */
685
+ @keyframes page-enter {
686
+ from {
687
+ opacity: 0;
688
+ transform: translateY(8px);
689
+ }
690
+ to {
691
+ opacity: 1;
692
+ transform: translateY(0);
693
+ }
694
+ }
695
+
696
+ @keyframes slide-up-enter {
697
+ from {
698
+ opacity: 0;
699
+ transform: translateY(20px) scale(0.98);
700
+ }
701
+ to {
702
+ opacity: 1;
703
+ transform: translateY(0) scale(1);
704
+ }
705
+ }
706
+
707
+ @keyframes slide-down-enter {
708
+ from {
709
+ opacity: 0;
710
+ transform: translateY(-12px);
711
+ }
712
+ to {
713
+ opacity: 1;
714
+ transform: translateY(0);
715
+ }
716
+ }
717
+
718
+ @keyframes scale-fade-enter {
719
+ from {
720
+ opacity: 0;
721
+ transform: scale(0.96);
722
+ }
723
+ to {
724
+ opacity: 1;
725
+ transform: scale(1);
726
+ }
727
+ }
728
+
729
+ /* Sidebar & mobile header: Fiesta-brand gradient (red, orange, purple).
730
+ Repeating gradient for seamless loop (no flash).
731
+ Static when prefers-reduced-motion. */
732
+ @keyframes sidebar-gradient-flow {
733
+ /* Runs on .sidebar-gradient-layer::before (see below): translate exactly one
734
+ repeat period (300% of the container = 75% of the 400% layer) for a
735
+ seamless loop — no flash at end. transform-only so the loop stays on the
736
+ compositor with zero per-frame repaint (issue #57). */
737
+ 0% {
738
+ transform: translateY(0);
739
+ }
740
+ 100% {
741
+ transform: translateY(-75%);
742
+ }
743
+ }
744
+
745
+ /* Logo in light mode: deep warm brown Fiesta + black Board for contrast across gradient */
746
+ .sidebar-gradient:not(.dark) .logo-on-gradient,
747
+ .sidebar-gradient-horizontal:not(.dark) .logo-on-gradient {
748
+ --brand: oklch(0.28 0.08 50);
749
+ --sidebar-foreground: oklch(0 0 0);
750
+ }
751
+
752
+ /* Logo in dark mode: white Board text, brighter brand for contrast on dark gradient */
753
+ .dark .sidebar-gradient .logo-on-gradient,
754
+ .dark .sidebar-gradient-horizontal .logo-on-gradient {
755
+ --brand: oklch(0.78 0.14 75);
756
+ --sidebar-foreground: oklch(1 0 0);
757
+ }
758
+
759
+ /* Light mode: soft gradient (coral → peach → lavender).
760
+ Stops derive from the canonical --fiesta-* brand gradient tokens —
761
+ never fork the hexes here; seasons retint via --sidebar-fiesta-*. */
762
+ .sidebar-gradient {
763
+ --sidebar-fiesta-red: var(--fiesta-red);
764
+ --sidebar-fiesta-orange: var(--fiesta-orange);
765
+ --sidebar-fiesta-purple: var(--fiesta-purple);
766
+ background: repeating-linear-gradient(
767
+ 170deg,
768
+ var(--sidebar-fiesta-red) 0%,
769
+ var(--sidebar-fiesta-orange) 33.33%,
770
+ var(--sidebar-fiesta-purple) 66.66%,
771
+ var(--sidebar-fiesta-red) 100%
772
+ );
773
+ background-size: 100% 300%;
774
+ background-position: 50% 0%;
775
+ background-repeat: repeat-y;
776
+ color: oklch(0 0 0);
777
+ border-radius: var(--radius-chrome);
778
+ --sidebar-foreground: oklch(0 0 0);
779
+ --sidebar-accent: oklch(1 0 0 / 35%);
780
+ --sidebar-accent-foreground: oklch(0 0 0);
781
+ --sidebar-border: oklch(0 0 0 / 12%);
782
+ --foreground: oklch(0 0 0);
783
+ --muted-foreground: oklch(0.15 0 0);
784
+ --accent: oklch(1 0 0 / 28%);
785
+ --accent-foreground: oklch(0 0 0);
786
+ --border: oklch(0 0 0 / 12%);
787
+ }
788
+ /* Dark mode: rich Fiesta gradient — deep-toned stops defined ONCE for both
789
+ the vertical sidebar and the mobile horizontal header. */
790
+ .dark .sidebar-gradient,
791
+ .dark .sidebar-gradient-horizontal {
792
+ --sidebar-fiesta-red: #7a2a24;
793
+ --sidebar-fiesta-orange: #6d5014;
794
+ --sidebar-fiesta-purple: #4e3070; /* board violet, darker for dark theme */
795
+ }
796
+ .dark .sidebar-gradient {
797
+ color: oklch(1 0 0);
798
+ --sidebar-foreground: oklch(1 0 0);
799
+ --sidebar-accent: oklch(1 0 0 / 24%);
800
+ --sidebar-accent-foreground: oklch(1 0 0);
801
+ --sidebar-border: oklch(1 0 0 / 18%);
802
+ --foreground: oklch(1 0 0);
803
+ --muted-foreground: oklch(1 0 0 / 85%);
804
+ --accent: oklch(1 0 0 / 20%);
805
+ --accent-foreground: oklch(1 0 0);
806
+ --border: oklch(1 0 0 / 18%);
807
+ }
808
+
809
+ .sidebar-gradient-horizontal {
810
+ /* Light mode: identical stops to the vertical sidebar — one brand gradient
811
+ on every viewport (previously the mobile header ran subtly warmer). */
812
+ --sidebar-fiesta-red: var(--fiesta-red);
813
+ --sidebar-fiesta-orange: var(--fiesta-orange);
814
+ --sidebar-fiesta-purple: var(--fiesta-purple);
815
+ background: repeating-linear-gradient(
816
+ 135deg,
817
+ var(--sidebar-fiesta-red) 0,
818
+ var(--sidebar-fiesta-orange) 33.33%,
819
+ var(--sidebar-fiesta-purple) 66.66%,
820
+ var(--sidebar-fiesta-red) 100%
821
+ );
822
+ background-size: 400% 100%;
823
+ background-position: 0% 50%;
824
+ color: oklch(0 0 0);
825
+ border-radius: var(--radius-chrome-mobile);
826
+ --foreground: oklch(0 0 0);
827
+ --sidebar-accent: oklch(1 0 0 / 35%);
828
+ --sidebar-accent-foreground: oklch(0 0 0);
829
+ --muted-foreground: oklch(0.15 0 0);
830
+ --accent: oklch(1 0 0 / 28%);
831
+ --accent-foreground: oklch(0 0 0);
832
+ --border: oklch(0 0 0 / 12%);
833
+ }
834
+ @keyframes sidebar-gradient-flow-h {
835
+ /* Runs on .sidebar-gradient-layer::before: one period (33.33% of the 400%
836
+ background box = 100% of the container = 50% of the 200% layer) per loop —
837
+ same wrap as the old background-position scroll, compositor-only. */
838
+ 0% {
839
+ transform: translateX(0);
840
+ }
841
+ 100% {
842
+ transform: translateX(-50%);
843
+ }
844
+ }
845
+ .dark .sidebar-gradient-horizontal {
846
+ color: oklch(1 0 0);
847
+ --foreground: oklch(1 0 0);
848
+ --sidebar-accent: oklch(1 0 0 / 24%);
849
+ --sidebar-accent-foreground: oklch(1 0 0);
850
+ --muted-foreground: oklch(1 0 0 / 85%);
851
+ --accent: oklch(1 0 0 / 24%);
852
+ --accent-foreground: oklch(1 0 0);
853
+ --border: oklch(1 0 0 / 18%);
854
+ }
855
+
856
+ /* Compositor-friendly gradient flow (issue #57).
857
+ The .sidebar-gradient(-horizontal) containers above keep a STATIC copy of
858
+ the gradient as their background — the single surface seasons override
859
+ (light retint via --sidebar-fiesta-*, dark seasons flat-hex via
860
+ `background` + `animation: none`) and the graceful static fallback for
861
+ markup without the layer element. The moving copy lives on
862
+ .sidebar-gradient-layer::before: it re-inherits that same background
863
+ (container → layer → ::before), overscans it by exactly one repeat
864
+ period, and translates with a transform loop — so the flow runs on the
865
+ compositor with no per-frame rasterization, and the .nav-active
866
+ backdrop-filter pills get a cacheable backdrop (#93). Dark seasons stay
867
+ correct for free: the inherited background is their flat hex, so the
868
+ layer paints the same flat color. The layer clips itself to the rounded
869
+ corners because the desktop <aside> cannot take overflow:hidden (the
870
+ edge toggle button overhangs it). */
871
+ .sidebar-gradient,
872
+ .sidebar-gradient-horizontal {
873
+ /* Keep the z-index:-1 layer above this element's own background even when
874
+ the container isn't otherwise a stacking context (e.g. story wrappers). */
875
+ isolation: isolate;
876
+ }
877
+ .sidebar-gradient-layer {
878
+ position: absolute;
879
+ inset: 0;
880
+ z-index: -1; /* under the container's content, above its background */
881
+ overflow: hidden;
882
+ border-radius: inherit;
883
+ background: inherit; /* inheritance link: ::before re-inherits from here */
884
+ pointer-events: none;
885
+ }
886
+ .sidebar-gradient-layer::before {
887
+ content: "";
888
+ position: absolute;
889
+ top: 0;
890
+ left: 0;
891
+ background: inherit;
892
+ will-change: transform;
893
+ }
894
+ /* Vertical: the container's gradient box is 100% × 300%. The ::before
895
+ overscans to 400% height (100% visible + one 300% travel period), so
896
+ translateY(-75%) [= -300% of the container] wraps onto the repeat. */
897
+ .sidebar-gradient .sidebar-gradient-layer::before {
898
+ width: 100%;
899
+ height: 400%;
900
+ background-size: 100% 75%; /* same box as the container's 100% 300% */
901
+ background-position: 50% 0%; /* pinned to the container's t=0 anchor */
902
+ background-repeat: repeat-y;
903
+ animation: sidebar-gradient-flow 117s linear infinite;
904
+ }
905
+ /* Horizontal: the container's gradient box is 400% × 100%; one loop travels
906
+ 33.33% background-position = 100% of the container, so the ::before
907
+ overscans to 200% width and translateX(-50%) reproduces one period. */
908
+ .sidebar-gradient-horizontal .sidebar-gradient-layer::before {
909
+ width: 200%;
910
+ height: 100%;
911
+ background-size: 200% 100%; /* same box as the container's 400% 100% */
912
+ background-position: 0% 50%;
913
+ background-repeat: repeat;
914
+ animation: sidebar-gradient-flow-h 117s linear 0.35s infinite;
915
+ }
916
+
917
+ /* Navigation entrance animations */
918
+ @keyframes nav-slide-in-left {
919
+ from {
920
+ opacity: 0;
921
+ transform: translateX(-8px);
922
+ }
923
+ to {
924
+ opacity: 1;
925
+ transform: translateX(0);
926
+ }
927
+ }
928
+ @keyframes nav-slide-in-top {
929
+ from {
930
+ opacity: 0;
931
+ transform: translateY(-6px);
932
+ }
933
+ to {
934
+ opacity: 1;
935
+ transform: translateY(0);
936
+ }
937
+ }
938
+ /* Desktop: entrance on the container; the gradient flow runs on
939
+ .sidebar-gradient-layer::before and starts after 400ms. */
940
+ /* 117s gradient loop is an ambient one-off; its 0.4s/0.35s start delays
941
+ mirror the entrance durations (slowest/slower) and stay literal with them */
942
+ @media (min-width: 1024px) {
943
+ .sidebar-gradient {
944
+ animation: nav-slide-in-left var(--motion-duration-slowest) var(--motion-ease-out) both;
945
+ }
946
+ .sidebar-gradient .sidebar-gradient-layer::before {
947
+ animation-delay: 0.4s;
948
+ }
949
+ }
950
+ .sidebar-gradient-horizontal {
951
+ animation: nav-slide-in-top var(--motion-duration-slower) var(--motion-ease-out) both;
952
+ }
953
+
954
+ /* Sidebar collapse/expand transition (#92).
955
+ Only the fixed sidebar transitions width — its layout scope is just the
956
+ sidebar itself (the contain below keeps that honest). The content root
957
+ (<main id="main-content">) must never transition layout properties: every
958
+ frame of a width/padding transition there relayouts the whole page
959
+ subtree. Instead, MainContent snaps its paddings instantly and slides an
960
+ inner .content-shift wrapper with a compositor-only transform (FLIP),
961
+ using the same 300ms easing so sidebar and content move in sync. */
962
+ .sidebar-transition {
963
+ transition: width var(--motion-duration-slow) var(--motion-ease-out-cubic);
964
+ contain: layout style;
965
+ }
966
+ /* Content root: keeps the containment it always had; paddings snap. */
967
+ .content-root {
968
+ contain: layout style;
969
+ }
970
+ /* Inner content wrapper: the only thing that moves during the slide.
971
+ will-change is scoped to the transition — transform is compositor-promoted,
972
+ so (unlike the old width/padding hint) the hint actually does something. */
973
+ .content-shift {
974
+ transition: transform var(--motion-duration-slow) var(--motion-ease-out-cubic);
975
+ }
976
+ .content-shift.is-transitioning {
977
+ will-change: transform;
978
+ }
979
+ @media (prefers-reduced-motion: reduce) {
980
+ .sidebar-transition,
981
+ .content-shift {
982
+ transition: none;
983
+ }
984
+ .sidebar-gradient {
985
+ animation: none;
986
+ background-position: 50% 25%;
987
+ }
988
+ .sidebar-gradient-horizontal {
989
+ animation: none;
990
+ background-position: 25% 50%;
991
+ }
992
+ /* Freeze the compositor layer at the same intended mid-gradient pose
993
+ (equivalent to background-position 50% 25% / 25% 50% on the container). */
994
+ .sidebar-gradient .sidebar-gradient-layer::before {
995
+ animation: none;
996
+ transform: translateY(-12.5%); /* -50% of the container = bg-position 50% 25% */
997
+ }
998
+ .sidebar-gradient-horizontal .sidebar-gradient-layer::before {
999
+ animation: none;
1000
+ transform: translateX(-37.5%); /* -75% of the container = bg-position 25% 50% */
1001
+ }
1002
+ }
1003
+ .reduce-motion .sidebar-transition,
1004
+ .reduce-motion .content-shift {
1005
+ transition: none;
1006
+ }
1007
+ .reduce-motion .sidebar-gradient {
1008
+ animation: none;
1009
+ background-position: 50% 25%;
1010
+ }
1011
+ .reduce-motion .sidebar-gradient-horizontal {
1012
+ animation: none;
1013
+ background-position: 25% 50%;
1014
+ }
1015
+ /* Same freeze pose as the prefers-reduced-motion block above. */
1016
+ .reduce-motion .sidebar-gradient .sidebar-gradient-layer::before {
1017
+ animation: none;
1018
+ transform: translateY(-12.5%);
1019
+ }
1020
+ .reduce-motion .sidebar-gradient-horizontal .sidebar-gradient-layer::before {
1021
+ animation: none;
1022
+ transform: translateX(-37.5%);
1023
+ }
1024
+
1025
+ /* Navigation active state — solid fallback (non-gradient surfaces, no backdrop support) */
1026
+ .nav-active {
1027
+ background: var(--nav-active);
1028
+ color: var(--nav-active-foreground);
1029
+ }
1030
+ .nav-active-hover:hover {
1031
+ background: var(--nav-active-hover);
1032
+ }
1033
+
1034
+ /*
1035
+ * Fiesta sidebar active pill: frosted glass over the animated gradient.
1036
+ * backdrop-filter samples the colors behind the row so the highlight moves with the gradient.
1037
+ * A minimum-opacity cool veil keeps white text ≥ WCAG 2.2 AA on the lightest gradient stops.
1038
+ */
1039
+ .sidebar-gradient .nav-active,
1040
+ .sidebar-gradient-horizontal .nav-active {
1041
+ background: oklch(0.26 0.09 285 / 0.62);
1042
+ color: oklch(1 0 0);
1043
+ box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.18);
1044
+ backdrop-filter: blur(14px) saturate(1.35);
1045
+ -webkit-backdrop-filter: blur(14px) saturate(1.35);
1046
+ }
1047
+
1048
+ .dark .sidebar-gradient .nav-active,
1049
+ .dark .sidebar-gradient-horizontal .nav-active {
1050
+ background: oklch(0.22 0.08 285 / 0.48);
1051
+ box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.14);
1052
+ backdrop-filter: blur(16px) saturate(1.5) brightness(1.06);
1053
+ -webkit-backdrop-filter: blur(16px) saturate(1.5) brightness(1.06);
1054
+ }
1055
+
1056
+ .sidebar-gradient .nav-active-hover:hover,
1057
+ .sidebar-gradient-horizontal .nav-active-hover:hover {
1058
+ background: oklch(1 0 0 / 0.18);
1059
+ backdrop-filter: blur(10px) saturate(1.15);
1060
+ -webkit-backdrop-filter: blur(10px) saturate(1.15);
1061
+ }
1062
+
1063
+ .dark .sidebar-gradient .nav-active-hover:hover,
1064
+ .dark .sidebar-gradient-horizontal .nav-active-hover:hover {
1065
+ background: oklch(1 0 0 / 0.12);
1066
+ backdrop-filter: blur(12px) saturate(1.25);
1067
+ -webkit-backdrop-filter: blur(12px) saturate(1.25);
1068
+ }
1069
+
1070
+ @supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
1071
+ .sidebar-gradient .nav-active,
1072
+ .sidebar-gradient-horizontal .nav-active {
1073
+ background: var(--nav-active);
1074
+ box-shadow: none;
1075
+ backdrop-filter: none;
1076
+ -webkit-backdrop-filter: none;
1077
+ }
1078
+ .sidebar-gradient .nav-active-hover:hover,
1079
+ .sidebar-gradient-horizontal .nav-active-hover:hover {
1080
+ background: var(--nav-active-hover);
1081
+ backdrop-filter: none;
1082
+ -webkit-backdrop-filter: none;
1083
+ }
1084
+ }
1085
+
1086
+ @media (prefers-reduced-transparency: reduce) {
1087
+ .sidebar-gradient .nav-active,
1088
+ .sidebar-gradient-horizontal .nav-active {
1089
+ background: var(--nav-active);
1090
+ box-shadow: none;
1091
+ backdrop-filter: none;
1092
+ -webkit-backdrop-filter: none;
1093
+ }
1094
+ .sidebar-gradient .nav-active-hover:hover,
1095
+ .sidebar-gradient-horizontal .nav-active-hover:hover {
1096
+ background: var(--nav-active-hover);
1097
+ backdrop-filter: none;
1098
+ -webkit-backdrop-filter: none;
1099
+ }
1100
+ }
1101
+
1102
+ @media (prefers-contrast: more) {
1103
+ .sidebar-gradient .nav-active,
1104
+ .sidebar-gradient-horizontal .nav-active {
1105
+ background: var(--nav-active);
1106
+ color: var(--nav-active-foreground);
1107
+ box-shadow: inset 0 0 0 2px oklch(1 0 0 / 0.35);
1108
+ backdrop-filter: none;
1109
+ -webkit-backdrop-filter: none;
1110
+ }
1111
+ .sidebar-gradient .nav-active-hover:hover,
1112
+ .sidebar-gradient-horizontal .nav-active-hover:hover {
1113
+ background: var(--nav-active-hover);
1114
+ backdrop-filter: none;
1115
+ -webkit-backdrop-filter: none;
1116
+ }
1117
+ }
1118
+
1119
+ /* Card fade-in animation (inspired by reactbits CardNav) */
1120
+ @keyframes card-fade-in {
1121
+ from {
1122
+ opacity: 0;
1123
+ transform: translateY(30px);
1124
+ }
1125
+ to {
1126
+ opacity: 1;
1127
+ transform: translateY(0);
1128
+ }
1129
+ }
1130
+
1131
+ .animate-card-fade-in {
1132
+ /* 1s hero-entrance duration is a one-off, so it stays inline */
1133
+ animation: card-fade-in 1s var(--motion-ease-spring) both;
1134
+ }
1135
+
1136
+ /* Type scale: ratio 1.25; body 1rem (16px). Tailwind text-xs/sm/base/lg/xl/2xl/3xl/4xl align. */
1137
+ /* Page typography - consistent titles and descriptions */
1138
+ .page-title {
1139
+ @apply text-lg sm:text-xl font-medium tracking-tight;
1140
+ text-wrap: balance;
1141
+ }
1142
+ .page-description {
1143
+ @apply text-muted-foreground mt-1 text-sm sm:text-base leading-relaxed;
1144
+ }
1145
+
1146
+ /* ─── Board display ─── */
1147
+ /* Split-flap board preview (components/board). The flap keyframes
1148
+ (flapDown/flapUp/flapShadow) are injected into <head> by the component
1149
+ itself so the animation works without CSS import-order coupling; this
1150
+ section carries only the global kill switches, copied verbatim from
1151
+ FiestaBoard app/globals.css. The app stamps these classes on <html>
1152
+ from the user's board-animation setting. */
1153
+
1154
+ /* Board animation kill switch. The board flap structure (4 layers) is
1155
+ already gated in JS so the extra DOM never renders when this is set,
1156
+ but the CSS belt-and-braces guarantees no leftover transforms animate
1157
+ if a flap node is mid-render when the setting flips. */
1158
+ .board-animations-off [data-board-preview] *,
1159
+ .board-animations-off [data-board-preview] *::before,
1160
+ .board-animations-off [data-board-preview] *::after {
1161
+ animation: none !important;
1162
+ transition: none !important;
1163
+ }
1164
+
1165
+ /* Desktop-only mode: suppress board animation below the `md` breakpoint
1166
+ (768px — matches Tailwind's default). */
1167
+ @media (max-width: 767px) {
1168
+ .board-animations-desktop-only [data-board-preview] *,
1169
+ .board-animations-desktop-only [data-board-preview] *::before,
1170
+ .board-animations-desktop-only [data-board-preview] *::after {
1171
+ animation: none !important;
1172
+ transition: none !important;
1173
+ }
1174
+ }