@aortl/admin-css 0.0.1 → 0.1.0

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 (38) hide show
  1. package/README.md +0 -22
  2. package/dist/admin.css +2787 -248
  3. package/dist/admin.min.css +1 -1
  4. package/dist/admin.scoped.css +3252 -0
  5. package/dist/admin.scoped.min.css +46 -0
  6. package/package.json +15 -3
  7. package/src/base.css +13 -7
  8. package/src/components/accordion.css +79 -0
  9. package/src/components/alert.css +83 -0
  10. package/src/components/app-shell.css +59 -0
  11. package/src/components/badge.css +44 -0
  12. package/src/components/brand-tile.css +9 -0
  13. package/src/components/breadcrumbs.css +38 -0
  14. package/src/components/button-group.css +73 -0
  15. package/src/components/button.css +50 -1
  16. package/src/components/card.css +1 -1
  17. package/src/components/checkbox.css +38 -0
  18. package/src/components/field.css +29 -2
  19. package/src/components/file-input.css +36 -0
  20. package/src/components/footer.css +26 -0
  21. package/src/components/index.css +23 -0
  22. package/src/components/input-group.css +38 -0
  23. package/src/components/input.css +7 -0
  24. package/src/components/menu.css +88 -0
  25. package/src/components/navbar.css +66 -0
  26. package/src/components/pagination.css +43 -0
  27. package/src/components/progress.css +97 -0
  28. package/src/components/radio.css +45 -0
  29. package/src/components/select.css +114 -0
  30. package/src/components/sidebar.css +225 -0
  31. package/src/components/spinner.css +40 -0
  32. package/src/components/switch.css +62 -0
  33. package/src/components/table.css +124 -0
  34. package/src/components/tabs.css +172 -0
  35. package/src/components/textarea.css +33 -0
  36. package/src/fonts.css +88 -0
  37. package/src/index.css +1 -0
  38. package/src/theme.css +122 -29
@@ -0,0 +1,172 @@
1
+ @layer components {
2
+ .tabs {
3
+ @apply flex flex-col;
4
+ }
5
+
6
+ /* List + tab buttons. Same classes are produced by Base UI (React) and the
7
+ vanilla radio-input markup, so styling is shared. All selectors are
8
+ scoped under `.tabs` so generic `class="tab"` markup elsewhere on the
9
+ page (e.g. Starlight's own <Tabs> component) doesn't pick them up. */
10
+ .tabs .tab-list {
11
+ @apply inline-flex items-center gap-1 border-b border-border;
12
+ /* Anchor for the optional .tab-indicator */
13
+ position: relative;
14
+ }
15
+
16
+ .tabs .tab {
17
+ @apply inline-flex items-center gap-1.5
18
+ px-3 h-9
19
+ text-sm leading-none font-medium
20
+ text-text-muted bg-transparent
21
+ border-0 cursor-pointer select-none
22
+ transition-colors duration-150
23
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary
24
+ hover:text-text
25
+ disabled:opacity-50 disabled:cursor-not-allowed;
26
+ }
27
+
28
+ .tabs .tab[data-selected],
29
+ .tabs .tab[aria-selected="true"] {
30
+ @apply text-text;
31
+ }
32
+
33
+ /* Vanilla radio-input pattern: hide the inputs visually, treat the following
34
+ <label class="tab"> as the selected tab when the radio is :checked.
35
+ Pairs with `:has(input:checked)` below to show the right panel. */
36
+ .tabs .tab-input {
37
+ position: absolute;
38
+ width: 1px;
39
+ height: 1px;
40
+ margin: 0;
41
+ padding: 0;
42
+ border: 0;
43
+ opacity: 0;
44
+ pointer-events: none;
45
+ }
46
+
47
+ .tabs .tab-input:checked + .tab {
48
+ @apply text-text;
49
+ }
50
+
51
+ .tabs .tab-input:focus-visible + .tab {
52
+ @apply outline-2 outline-offset-2 outline-primary;
53
+ }
54
+
55
+ /* Bordered (default) — underline marker on the active tab. The pseudo sits
56
+ below the .tab, overlapping the .tab-list bottom border. */
57
+ .tabs-bordered .tab,
58
+ .tabs:not(.tabs-boxed) .tab {
59
+ position: relative;
60
+ }
61
+
62
+ .tabs-bordered .tab::after,
63
+ .tabs:not(.tabs-boxed) .tab::after {
64
+ content: "";
65
+ position: absolute;
66
+ inset-inline: 0;
67
+ inset-block-end: -1px;
68
+ block-size: 2px;
69
+ background-color: var(--color-primary);
70
+ transform: scaleX(0);
71
+ transition: transform 150ms ease;
72
+ }
73
+
74
+ .tabs-bordered .tab[data-selected]::after,
75
+ .tabs-bordered .tab[aria-selected="true"]::after,
76
+ .tabs-bordered .tab-input:checked + .tab::after,
77
+ .tabs:not(.tabs-boxed) .tab[data-selected]::after,
78
+ .tabs:not(.tabs-boxed) .tab[aria-selected="true"]::after,
79
+ .tabs:not(.tabs-boxed) .tab-input:checked + .tab::after {
80
+ transform: scaleX(1);
81
+ }
82
+
83
+ /* Boxed (segmented control) — adjacent tabs share borders. */
84
+ .tabs-boxed .tab-list {
85
+ @apply gap-0 p-0.5 border border-border rounded-md bg-surface-muted;
86
+ }
87
+
88
+ .tabs-boxed .tab {
89
+ @apply rounded;
90
+ }
91
+
92
+ .tabs-boxed .tab[data-selected],
93
+ .tabs-boxed .tab[aria-selected="true"],
94
+ .tabs-boxed .tab-input:checked + .tab {
95
+ @apply bg-surface text-text shadow-sm;
96
+ }
97
+
98
+ /* Full width — list stretches across the container, tabs share space evenly. */
99
+ .tabs-full-width:not([data-orientation="vertical"]) .tab-list {
100
+ @apply flex w-full;
101
+ }
102
+
103
+ .tabs-full-width:not([data-orientation="vertical"]) .tab {
104
+ @apply flex-1 justify-center;
105
+ }
106
+
107
+ /* Sizes — cascade to nested tabs. */
108
+ .tabs-sm .tab {
109
+ @apply px-2 h-7 text-xs;
110
+ }
111
+
112
+ .tabs-lg .tab {
113
+ @apply px-4 h-11 text-base;
114
+ }
115
+
116
+ /* Panels — Base UI handles hidden state via data-hidden / hidden attr. */
117
+ .tabs .tab-panel {
118
+ @apply pt-3 outline-none;
119
+ }
120
+
121
+ /* Vanilla radio-input pattern: only the panel whose data-for matches the
122
+ checked input is shown. Each .tabs root scopes its own selectors. */
123
+ .tabs .tab-panel {
124
+ display: none;
125
+ }
126
+
127
+ .tabs:has(.tab-input[value="1"]:checked) .tab-panel[data-value="1"],
128
+ .tabs:has(.tab-input[value="2"]:checked) .tab-panel[data-value="2"],
129
+ .tabs:has(.tab-input[value="3"]:checked) .tab-panel[data-value="3"],
130
+ .tabs:has(.tab-input[value="4"]:checked) .tab-panel[data-value="4"],
131
+ .tabs:has(.tab-input[value="5"]:checked) .tab-panel[data-value="5"],
132
+ .tabs:has(.tab-input[value="6"]:checked) .tab-panel[data-value="6"] {
133
+ display: block;
134
+ }
135
+
136
+ /* Base UI path: it renders the panel and applies `hidden`, so the rule
137
+ above is a no-op there. Reinstate display when the panel is shown by
138
+ Base UI. */
139
+ .tabs .tab-panel:not([hidden]):not([data-value]) {
140
+ display: block;
141
+ }
142
+
143
+ /* Vertical orientation. */
144
+ .tabs[data-orientation="vertical"] {
145
+ @apply flex-row gap-3;
146
+ }
147
+
148
+ .tabs[data-orientation="vertical"] .tab-list {
149
+ @apply flex-col items-stretch border-b-0 border-r border-border;
150
+ }
151
+
152
+ .tabs[data-orientation="vertical"] .tab {
153
+ @apply w-full justify-start;
154
+ }
155
+
156
+ .tabs[data-orientation="vertical"].tabs-bordered .tab::after,
157
+ .tabs[data-orientation="vertical"]:not(.tabs-boxed) .tab::after {
158
+ inset-inline: auto;
159
+ inset-inline-end: -1px;
160
+ inset-block: 0;
161
+ inline-size: 2px;
162
+ block-size: auto;
163
+ transform: scaleY(0);
164
+ }
165
+
166
+ .tabs[data-orientation="vertical"].tabs-bordered .tab[data-selected]::after,
167
+ .tabs[data-orientation="vertical"].tabs-bordered .tab[aria-selected="true"]::after,
168
+ .tabs[data-orientation="vertical"]:not(.tabs-boxed) .tab[data-selected]::after,
169
+ .tabs[data-orientation="vertical"]:not(.tabs-boxed) .tab[aria-selected="true"]::after {
170
+ transform: scaleY(1);
171
+ }
172
+ }
@@ -0,0 +1,33 @@
1
+ @layer components {
2
+ .textarea {
3
+ @apply block w-full px-3 py-2 min-h-20 resize-y
4
+ rounded-lg text-sm
5
+ bg-surface text-text
6
+ border border-transparent
7
+ transition-colors duration-150
8
+ placeholder:text-text-muted
9
+ focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary
10
+ disabled:opacity-50 disabled:cursor-not-allowed;
11
+ }
12
+
13
+ .textarea-bordered {
14
+ @apply border-border hover:border-border-strong;
15
+ }
16
+
17
+ .textarea-ghost {
18
+ @apply bg-transparent hover:bg-surface-muted;
19
+ }
20
+
21
+ .textarea-danger {
22
+ @apply border-danger focus-visible:outline-danger;
23
+ }
24
+
25
+ /* Sizes */
26
+ .textarea-sm {
27
+ @apply text-xs px-2.5 py-1.5 min-h-16;
28
+ }
29
+
30
+ .textarea-lg {
31
+ @apply text-base px-4 py-2.5 min-h-24;
32
+ }
33
+ }
package/src/fonts.css ADDED
@@ -0,0 +1,88 @@
1
+ /*
2
+ * IBM Plex Sans + Mono — the default UI typeface for this design system.
3
+ *
4
+ * Hosted on Google's CDN (fonts.gstatic.com), latin + latin-ext subsets
5
+ * only. Plex Sans is served as a variable font (one file covers weights
6
+ * 400–600 via the wght axis); Plex Mono ships as discrete weight files.
7
+ *
8
+ * `font-display: optional` — the browser uses the fallback stack from
9
+ * `--font-sans` / `--font-mono` if Plex isn't ready in ~100ms, then
10
+ * caches it for the next page load. No FOUT, no layout shift, and on
11
+ * fast networks the typeface still wins on first paint.
12
+ *
13
+ * Opt out by overriding `--font-sans` / `--font-mono`, or by importing
14
+ * admin-css's source files individually and skipping this one.
15
+ */
16
+
17
+ /* IBM Plex Sans — variable, weights 400–600 */
18
+ @font-face {
19
+ font-family: "IBM Plex Sans";
20
+ font-style: normal;
21
+ font-weight: 400 600;
22
+ font-display: optional;
23
+ src: url(https://fonts.gstatic.com/s/ibmplexsans/v23/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxQKYbABA.woff2)
24
+ format("woff2");
25
+ unicode-range:
26
+ U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
27
+ U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
28
+ U+A720-A7FF;
29
+ }
30
+ @font-face {
31
+ font-family: "IBM Plex Sans";
32
+ font-style: normal;
33
+ font-weight: 400 600;
34
+ font-display: optional;
35
+ src: url(https://fonts.gstatic.com/s/ibmplexsans/v23/zYXzKVElMYYaJe8bpLHnCwDKr932-G7dytD-Dmu1syxeKYY.woff2)
36
+ format("woff2");
37
+ unicode-range:
38
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
39
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
40
+ }
41
+
42
+ /* IBM Plex Mono — discrete weights 400, 500 */
43
+ @font-face {
44
+ font-family: "IBM Plex Mono";
45
+ font-style: normal;
46
+ font-weight: 400;
47
+ font-display: optional;
48
+ src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F63fjptAgt5VM-kVkqdyU8n1iEq129k.woff2)
49
+ format("woff2");
50
+ unicode-range:
51
+ U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
52
+ U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
53
+ U+A720-A7FF;
54
+ }
55
+ @font-face {
56
+ font-family: "IBM Plex Mono";
57
+ font-style: normal;
58
+ font-weight: 400;
59
+ font-display: optional;
60
+ src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F63fjptAgt5VM-kVkqdyU8n1i8q1w.woff2)
61
+ format("woff2");
62
+ unicode-range:
63
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
64
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
65
+ }
66
+ @font-face {
67
+ font-family: "IBM Plex Mono";
68
+ font-style: normal;
69
+ font-weight: 500;
70
+ font-display: optional;
71
+ src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F6qfjptAgt5VM-kVkqdyU8n3twJwl5FgtIU.woff2)
72
+ format("woff2");
73
+ unicode-range:
74
+ U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
75
+ U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
76
+ U+A720-A7FF;
77
+ }
78
+ @font-face {
79
+ font-family: "IBM Plex Mono";
80
+ font-style: normal;
81
+ font-weight: 500;
82
+ font-display: optional;
83
+ src: url(https://fonts.gstatic.com/s/ibmplexmono/v20/-F6qfjptAgt5VM-kVkqdyU8n3twJwlBFgg.woff2)
84
+ format("woff2");
85
+ unicode-range:
86
+ U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
87
+ U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
88
+ }
package/src/index.css CHANGED
@@ -1,5 +1,6 @@
1
1
  @import "tailwindcss";
2
2
 
3
+ @import "./fonts.css";
3
4
  @import "./theme.css";
4
5
  @import "./base.css";
5
6
  @import "./components/index.css";
package/src/theme.css CHANGED
@@ -7,11 +7,19 @@
7
7
  *
8
8
  * 1. Palette — the Flexoki ramps (paper, black, base, red, orange,
9
9
  * yellow, green, cyan, blue, purple, magenta). Replaces Tailwind's
10
- * default colors entirely.
10
+ * default colors entirely. Tones are absolute and identical in
11
+ * light/dark mode — Flexoki's inverted ramp pairs (paper↔black,
12
+ * base-50↔base-950, blue-600↔blue-400, …) live in the semantic
13
+ * layer below.
11
14
  *
12
15
  * 2. Semantic — purpose-named aliases (primary, surface, danger, ...)
13
- * pointing at palette tones. Components use these, never the palette
14
- * directly, so reskinning means remapping the alias:
16
+ * pointing at palette tones via `light-dark()`. The active value
17
+ * tracks the cascaded `color-scheme` declared on :root: auto by
18
+ * default (system preference), forced via `[data-theme="dark"]` or
19
+ * `[data-theme="light"]`. Components only reference these aliases,
20
+ * so they pick up dark mode for free.
21
+ *
22
+ * Reskin by remapping the alias:
15
23
  *
16
24
  * :root { --color-primary: var(--color-green-600); }
17
25
  *
@@ -163,46 +171,131 @@
163
171
  }
164
172
 
165
173
  @theme static {
174
+ /*
175
+ * Typography — only the font stack is tokenized. Sizes, weights, and
176
+ * line-heights compose from Tailwind's default scale at the call site
177
+ * (`text-sm`, `font-semibold`, `leading-tight`, …) — the system is
178
+ * dense and small enough that an extra semantic layer would just be
179
+ * indirection. Override `--font-sans` to swap the entire UI font.
180
+ */
181
+ --font-sans:
182
+ "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
183
+ --font-mono:
184
+ "IBM Plex Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono",
185
+ monospace;
186
+ }
187
+
188
+ @theme static {
189
+ /*
190
+ * Semantic tokens — each declared once via `light-dark()`. The two
191
+ * branches are Flexoki's symmetric ramp pairs:
192
+ * - paper ↔ black, base-50 ↔ base-950, base-150 ↔ base-850, …
193
+ * - accent-600 ↔ accent-400, accent-50 ↔ accent-950
194
+ *
195
+ * On accent buttons in dark mode the base shifts to the lighter
196
+ * accent-400 tone, so `content` (text on top) flips to black for
197
+ * legibility — the same trade-off `warning` already makes in both
198
+ * modes against bright yellow.
199
+ */
200
+
166
201
  /* Surfaces */
167
- --color-surface: var(--color-paper);
168
- --color-surface-muted: var(--color-base-50);
169
- --color-surface-strong: var(--color-base-100);
202
+ --color-surface: light-dark(var(--color-paper), var(--color-black));
203
+ --color-surface-muted: light-dark(var(--color-base-50), var(--color-base-950));
204
+ --color-surface-strong: light-dark(var(--color-base-100), var(--color-base-900));
170
205
 
171
206
  /* Text */
172
- --color-text: var(--color-black);
173
- --color-text-muted: var(--color-base-600);
207
+ --color-text: light-dark(var(--color-black), var(--color-base-200));
208
+ --color-text-muted: light-dark(var(--color-base-600), var(--color-base-500));
174
209
 
175
210
  /* Borders */
176
- --color-border: var(--color-base-150);
177
- --color-border-strong: var(--color-base-300);
211
+ --color-border: light-dark(var(--color-base-150), var(--color-base-850));
212
+ --color-border-strong: light-dark(var(--color-base-300), var(--color-base-700));
178
213
 
179
214
  /* Primary — Flexoki blue */
180
- --color-primary: var(--color-blue-600);
181
- --color-primary-hover: var(--color-blue-700);
182
- --color-primary-muted: var(--color-blue-50);
183
- --color-primary-content: var(--color-paper);
215
+ --color-primary: light-dark(var(--color-blue-600), var(--color-blue-400));
216
+ --color-primary-hover: light-dark(var(--color-blue-700), var(--color-blue-300));
217
+ --color-primary-muted: light-dark(var(--color-blue-50), var(--color-blue-950));
218
+ --color-primary-content: light-dark(var(--color-paper), var(--color-black));
219
+
220
+ /* System accent — per-app brand signal. Drives the navbar's 2px bottom
221
+ stripe and the footer's matching top stripe (both always on), plus
222
+ `.brand-tile` backgrounds. Defaults to a neutral gray so an un-branded
223
+ shell reads as chrome rather than a brand mark; override at :root (or
224
+ via inline style on .app-shell) to brand-shift a whole app. The -hover,
225
+ -muted, and -content siblings derive from the base via `color-mix`,
226
+ so a single override propagates. Mixing toward `--color-text` and
227
+ `--color-surface` (themselves `light-dark()` pairs) keeps the
228
+ derivations in step with light/dark mode without nesting light-dark()
229
+ inside color-mix(). Bright accents (e.g. yellow) may want a manual
230
+ -content override for contrast. */
231
+ --color-system-accent: light-dark(var(--color-base-600), var(--color-base-400));
232
+ --color-system-accent-hover: color-mix(
233
+ in oklch,
234
+ var(--color-system-accent),
235
+ var(--color-text) 12%
236
+ );
237
+ --color-system-accent-muted: color-mix(
238
+ in oklch,
239
+ var(--color-system-accent) 12%,
240
+ var(--color-surface)
241
+ );
242
+ --color-system-accent-content: light-dark(var(--color-paper), var(--color-black));
184
243
 
185
244
  /* Danger — red */
186
- --color-danger: var(--color-red-600);
187
- --color-danger-hover: var(--color-red-700);
188
- --color-danger-muted: var(--color-red-50);
189
- --color-danger-content: var(--color-paper);
245
+ --color-danger: light-dark(var(--color-red-600), var(--color-red-400));
246
+ --color-danger-hover: light-dark(var(--color-red-700), var(--color-red-300));
247
+ --color-danger-muted: light-dark(var(--color-red-50), var(--color-red-950));
248
+ --color-danger-content: light-dark(var(--color-paper), var(--color-black));
190
249
 
191
250
  /* Success — green */
192
- --color-success: var(--color-green-600);
193
- --color-success-hover: var(--color-green-700);
194
- --color-success-muted: var(--color-green-50);
195
- --color-success-content: var(--color-paper);
251
+ --color-success: light-dark(var(--color-green-600), var(--color-green-400));
252
+ --color-success-hover: light-dark(var(--color-green-700), var(--color-green-300));
253
+ --color-success-muted: light-dark(var(--color-green-50), var(--color-green-950));
254
+ --color-success-content: light-dark(var(--color-paper), var(--color-black));
196
255
 
197
- /* Warning — yellow (dark text for legibility on the bright base) */
256
+ /* Warning — yellow (stays bright in both modes; dark text either way) */
198
257
  --color-warning: var(--color-yellow-400);
199
- --color-warning-hover: var(--color-yellow-500);
200
- --color-warning-muted: var(--color-yellow-50);
258
+ --color-warning-hover: light-dark(var(--color-yellow-500), var(--color-yellow-300));
259
+ --color-warning-muted: light-dark(var(--color-yellow-50), var(--color-yellow-950));
201
260
  --color-warning-content: var(--color-black);
202
261
 
203
262
  /* Info — cyan */
204
- --color-info: var(--color-cyan-600);
205
- --color-info-hover: var(--color-cyan-700);
206
- --color-info-muted: var(--color-cyan-50);
207
- --color-info-content: var(--color-paper);
263
+ --color-info: light-dark(var(--color-cyan-600), var(--color-cyan-400));
264
+ --color-info-hover: light-dark(var(--color-cyan-700), var(--color-cyan-300));
265
+ --color-info-muted: light-dark(var(--color-cyan-50), var(--color-cyan-950));
266
+ --color-info-content: light-dark(var(--color-paper), var(--color-black));
267
+ }
268
+
269
+ /*
270
+ * Activate `light-dark()` resolution. `light dark` follows the OS
271
+ * preference; the attribute overrides force a specific mode and also
272
+ * flip native form controls, scrollbars, etc. The attribute selector
273
+ * isn't scoped to :root so subtrees can opt into a different mode (e.g.
274
+ * a dark hero section on an otherwise light page).
275
+ */
276
+ :root {
277
+ color-scheme: light dark;
278
+ }
279
+ [data-theme="dark"] {
280
+ color-scheme: dark;
281
+ }
282
+ [data-theme="light"] {
283
+ color-scheme: light;
284
+ }
285
+
286
+ /*
287
+ * Align Tailwind's `dark:` variant with the same resolution rules so
288
+ * authored utilities (`dark:shadow-lg`, …) match what the tokens do.
289
+ * Default v4 uses prefers-color-scheme alone, which would ignore the
290
+ * [data-theme] override.
291
+ */
292
+ @custom-variant dark {
293
+ &:where([data-theme="dark"], [data-theme="dark"] *) {
294
+ @slot;
295
+ }
296
+ @media (prefers-color-scheme: dark) {
297
+ &:where(:not([data-theme="light"], [data-theme="light"] *)) {
298
+ @slot;
299
+ }
300
+ }
208
301
  }