@burojs/ui-shadcn 0.2.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/chunk-ABUQPOOE.js +11 -0
  4. package/dist/chunk-ABUQPOOE.js.map +1 -0
  5. package/dist/chunk-DBGGWBFG.cjs +199 -0
  6. package/dist/chunk-DBGGWBFG.cjs.map +1 -0
  7. package/dist/chunk-FMX4GLCQ.js +6 -0
  8. package/dist/chunk-FMX4GLCQ.js.map +1 -0
  9. package/dist/chunk-UC57Z243.js +143 -0
  10. package/dist/chunk-UC57Z243.js.map +1 -0
  11. package/dist/chunk-UPGPO75B.js +164 -0
  12. package/dist/chunk-UPGPO75B.js.map +1 -0
  13. package/dist/chunk-VXJAXJ3U.cjs +8 -0
  14. package/dist/chunk-VXJAXJ3U.cjs.map +1 -0
  15. package/dist/chunk-W7AEYE7A.cjs +13 -0
  16. package/dist/chunk-W7AEYE7A.cjs.map +1 -0
  17. package/dist/chunk-YF5IW525.cjs +155 -0
  18. package/dist/chunk-YF5IW525.cjs.map +1 -0
  19. package/dist/index.cjs +17752 -0
  20. package/dist/index.cjs.map +1 -0
  21. package/dist/index.d.cts +6091 -0
  22. package/dist/index.d.ts +6091 -0
  23. package/dist/index.js +17369 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/layout/theme-storage-key.cjs +12 -0
  26. package/dist/layout/theme-storage-key.cjs.map +1 -0
  27. package/dist/layout/theme-storage-key.d.cts +23 -0
  28. package/dist/layout/theme-storage-key.d.ts +23 -0
  29. package/dist/layout/theme-storage-key.js +3 -0
  30. package/dist/layout/theme-storage-key.js.map +1 -0
  31. package/dist/markdown/index.cjs +550 -0
  32. package/dist/markdown/index.cjs.map +1 -0
  33. package/dist/markdown/index.d.cts +221 -0
  34. package/dist/markdown/index.d.ts +221 -0
  35. package/dist/markdown/index.js +492 -0
  36. package/dist/markdown/index.js.map +1 -0
  37. package/dist/rich-text-6V7WZ3WV.cjs +518 -0
  38. package/dist/rich-text-6V7WZ3WV.cjs.map +1 -0
  39. package/dist/rich-text-N2FZEJJV.js +491 -0
  40. package/dist/rich-text-N2FZEJJV.js.map +1 -0
  41. package/dist/styles.css +794 -0
  42. package/dist/theme/theme-css.cjs +105 -0
  43. package/dist/theme/theme-css.cjs.map +1 -0
  44. package/dist/theme/theme-css.d.cts +49 -0
  45. package/dist/theme/theme-css.d.ts +49 -0
  46. package/dist/theme/theme-css.js +102 -0
  47. package/dist/theme/theme-css.js.map +1 -0
  48. package/dist/theme/theme-storage.cjs +51 -0
  49. package/dist/theme/theme-storage.cjs.map +1 -0
  50. package/dist/theme/theme-storage.d.cts +75 -0
  51. package/dist/theme/theme-storage.d.ts +75 -0
  52. package/dist/theme/theme-storage.js +45 -0
  53. package/dist/theme/theme-storage.js.map +1 -0
  54. package/package.json +165 -0
@@ -0,0 +1,794 @@
1
+ /* Buro design tokens + Tailwind v4 entry point.
2
+ *
3
+ * The consumer imports ONLY this file — it pulls in Tailwind itself,
4
+ * declares the scan sources, the dark-mode variant, and the token mapping.
5
+ * `@source` resolves relative to THIS file, so classes from the package's
6
+ * dist reach the consumer's output without a single line in their config;
7
+ * `node_modules` is excluded from Tailwind's auto-detection, so without
8
+ * this line none of the kit's classes make it into the build — silently.
9
+ */
10
+ @import "tailwindcss";
11
+
12
+ /* Radix overlay animations. This is a CSS library, NOT a plugin: `@plugin
13
+ * "tw-animate-css"` fails with `"." is not exported under the conditions
14
+ * ["node","require"]`. Verified by build. Both @import lines must come
15
+ * first — CSS requires @import to precede all other rules. */
16
+ @import "tw-animate-css";
17
+
18
+ /* The handbook's markdown rendering (`prose`, `prose-sm`, `prose-invert`,
19
+ * `prose-headings`, ...) needs this registered somewhere Tailwind actually
20
+ * scans, or every `prose*` class compiles to nothing and the docs render
21
+ * unstyled. Opposite of the two imports above: this is a real Tailwind
22
+ * plugin, not a CSS library, so it takes `@plugin`, not `@import`. */
23
+ @plugin '@tailwindcss/typography';
24
+
25
+ /* Resolves relative to THIS file. Published consumers scan compiled JS;
26
+ * source-aliased development hosts scan TS/TSX. Both are required because
27
+ * node_modules is excluded from Tailwind's automatic detection. */
28
+ @source './**/*.{js,ts,tsx}';
29
+
30
+ /* The documentation panel's opening.
31
+ *
32
+ * An ANIMATION, not a transition, and that distinction is the whole point: a
33
+ * transition needs a previous value to interpolate from, so it cannot animate
34
+ * an element that has just been inserted. React coalesces the mount and the
35
+ * expand into one commit, `requestAnimationFrame` (even nested) runs before
36
+ * that commit, a `setTimeout` gets batched with it, and `flushSync` fixes it
37
+ * only by doing something React explicitly objects to from inside an effect.
38
+ * A keyframe animation plays from the moment the element exists and needs none
39
+ * of that. Closing keeps the transition — there the element is already on
40
+ * screen, so it has a value to leave from.
41
+ *
42
+ * The target width is read from a custom property so it is declared once, on
43
+ * the element, and the keyframe and the resting width cannot drift apart. */
44
+ @keyframes doc-panel-in {
45
+ from {
46
+ width: 0;
47
+ }
48
+ to {
49
+ width: var(--doc-panel-w);
50
+ }
51
+ }
52
+
53
+ @custom-variant dark (&:is(.dark *));
54
+
55
+ :root {
56
+ /* Neutral base, values copied verbatim from shadcn v4's default theme.
57
+ * Stored as oklch, not hsl, for the same reason the severity scale is:
58
+ * these exist to reproduce a specific external palette exactly, and only
59
+ * that palette's own notation guarantees it. The previous default was
60
+ * shadcn's older slate base (`hsl(222 47% 11%)` and friends) — a visibly
61
+ * blue grey, which is what this change removes. */
62
+ --background: oklch(1 0 0);
63
+ --foreground: oklch(0.145 0 0);
64
+ --primary: oklch(0.205 0 0);
65
+ --primary-foreground: oklch(0.985 0 0);
66
+ --secondary: oklch(0.97 0 0);
67
+ --secondary-foreground: oklch(0.205 0 0);
68
+ --muted: oklch(0.97 0 0);
69
+ /* Re-measured against the new neutral surfaces (final-review-findings.md
70
+ * M3), value UNCHANGED — this is shadcn v4's own default and every other
71
+ * consumer of this palette matches it: 6.62:1 (old blue-grey background)
72
+ * -> 4.73:1 on the new --background, 4.34:1 on a solid --muted surface
73
+ * (primitives/tabs.tsx inactive tab labels, enum/enum-picker.tsx,
74
+ * resource/resource-list.tsx, notification/toaster.tsx). The first still
75
+ * clears WCAG AA's 4.5:1; 4.34 on solid --muted does not — a known tight
76
+ * spot, recorded here rather than left silent, not a fix for this pass. */
77
+ --muted-foreground: oklch(0.556 0 0);
78
+ --accent: oklch(0.97 0 0);
79
+ --accent-foreground: oklch(0.205 0 0);
80
+ /* Measured against the NEW neutral --background in all THREE roles this
81
+ * token is actually used in (see task-2's contrast script, re-run for the
82
+ * final review — final-review-findings.md I2):
83
+ * - red --destructive text on --background: 5.63:1
84
+ * - white --destructive-foreground on a solid --destructive fill: 5.63:1
85
+ * (the SAME computation as the row above, not a second measurement —
86
+ * --background and --destructive-foreground are both pure white here,
87
+ * so "red on background" and "white on red fill" reduce to one
88
+ * number. A prior pass of this comment paired 4.76 with 4.76 for
89
+ * exactly this reason and read as two data points; it wasn't.)
90
+ * - red text on a 10%-red tint over --background
91
+ * (`bg-destructive/10 text-destructive` — badges, banners, inline
92
+ * errors; see forms/declarative-form.tsx, resource/resource-form.tsx,
93
+ * auth/login-form.tsx, resource/list-error-state.tsx,
94
+ * resource/tab-body.tsx, document-show-banner.tsx and others):
95
+ * 4.74:1
96
+ * (`bg-destructive/5` variant: 5.17:1.) THIS is the case that
97
+ * actually constrains the value — it is the tightest of the three.
98
+ * shadcn v4's own candidate, `oklch(0.577 0.245 27.325)`, passes the
99
+ * first two roles at 4.91:1 but only reaches ≈4.0:1 in this one,
100
+ * below WCAG AA's 4.5:1 — so it does NOT replace the hand-tuned value
101
+ * below, despite passing the two roles that look, at a glance, like
102
+ * the only ones that matter.
103
+ * All four numbers above clear 4.5:1. Restores the pre-migration
104
+ * hand-tuned `hsl(0 84% 44%)`, tuned against the OLD blue-grey background
105
+ * this change removes and re-verified here against the new one. shadcn
106
+ * itself drops --destructive-foreground and hardcodes white; this file
107
+ * keeps the token, valued the same as every other solid-fill `-foreground`
108
+ * below (pure white, kept in hsl() for readability — see the
109
+ * severity-scale comment for why pure black/white stay hsl()). Kept in
110
+ * hsl(), matching the file's default convention: oklch is reserved for
111
+ * values reproducing an external palette exactly, and a hand-tuned value
112
+ * is not that. */
113
+ --destructive: hsl(0 84% 44%);
114
+ --destructive-foreground: hsl(0 0% 100%);
115
+
116
+ /* Severity scale. Task 3 of the token-contract plan: the tree hardcoded 11
117
+ * palette colours across status badges, the history timeline, the log
118
+ * viewer, and a couple of one-off notices (25 amber / 19 emerald / 11 zinc
119
+ * / 11 violet / 9 red / 9 blue / 7 orange / 6 sky / 2 yellow / 2 purple /
120
+ * 2 indigo uses — see the task report for the full meaning → colour →
121
+ * site table). Deriving MEANING from those sites (not guessing from the
122
+ * plan's hypothesis) produced 7 distinct meanings, not the plan's 6:
123
+ * success, warning, info, neutral, highlight, active, and caution. `error`
124
+ * needed no new token — every error/failure site already reduces to the
125
+ * existing `--destructive` above. `active` (blue: a workflow actually
126
+ * RUNNING right now) and `caution` (orange: TimedOut, one notch more
127
+ * severe than warning's Paused/Canceled) are real, separately-rendered
128
+ * meanings the plan didn't anticipate — folding either into `info`/
129
+ * `warning` would visibly change what's on screen once tasks 4-5 wire
130
+ * these up, which is exactly the kind of silent drift this contract
131
+ * exists to prevent. The one deliberate MERGE going the other way:
132
+ * `resource-form.tsx`'s yellow-500 draft-banner collapses into `warning`
133
+ * (amber) — Tailwind's own OKLCH hue distance from amber-500 is 16°
134
+ * (amber↔yellow) vs 22.5° for both amber↔orange and sky↔blue, and L/C are
135
+ * nearly identical (76.9%/0.188 vs 79.5%/0.184) — this reads as accidental
136
+ * drift (whoever wrote that banner grabbed "yellow" for the same "heads
137
+ * up" register everyone else spells "amber"), not a second meaning.
138
+ *
139
+ * FIX ROUND 1 (task 4): `-foreground` briefly carried two incompatible
140
+ * meanings in this file. Every OTHER `-foreground` pair here
141
+ * (`--primary-foreground`, `--destructive-foreground`, ...) is shadcn's
142
+ * own convention: near-white text ON A SOLID FILL of that colour. Task 3's
143
+ * severity `-foreground`s were something else — a severity's text colour
144
+ * on a TINTED background (`badge.tsx`'s 15% tint) — because a tinted
145
+ * badge was the only context that existed when task 3 measured them. Task
146
+ * 4 initially reused the name `-foreground` for that tinted-text value
147
+ * too, and it broke at the one real filled-surface site that exists
148
+ * today: `resource-wizard.tsx`'s completed-step circle
149
+ * (`bg-success text-success-foreground`) rendered dark-green-on-green at
150
+ * 2.19:1 — *worse* than the `text-white` it replaced. Split into two
151
+ * names so the shape (`bg-<x> text-<x>-foreground`) means the same thing
152
+ * for every token pair in this file, severity or not:
153
+ * `--<severity>-foreground` — shadcn's meaning, restored: text on a
154
+ * SOLID `bg-<severity>` fill. Near-white, reproducing what the
155
+ * wizard's literal `text-white` actually rendered (`rgb(255, 255,
156
+ * 255)`) — the only filled-surface severity site that exists today.
157
+ * Not repeated in `.dark`: the fill itself (`--success` etc.) doesn't
158
+ * vary by mode either (see below), so neither does its contrasting
159
+ * text. Only `--success-foreground` has a live consumer right now;
160
+ * the other six carry the same value for symmetry within the family,
161
+ * unverified against a real filled site of their own — see task 4's
162
+ * fix-round report.
163
+ * `--<severity>-emphasis` — task 3's original measurement, renamed,
164
+ * VALUES the same COLOURS (see FIX ROUND 2 below for why the stored
165
+ * FORM changed): a severity's text colour on a TINTED background
166
+ * (`badge.tsx`'s `bg-<severity>/15`), genuinely different between
167
+ * :root and .dark because the source (`text-emerald-700
168
+ * dark:text-emerald-400` etc.) really does swap by `dark:` — see
169
+ * .dark below.
170
+ *
171
+ * FIX ROUND 2 (task 4): every value below — the 7 base tints AND all 14
172
+ * `-emphasis` values (7 light + 7 dark) — is stored as Tailwind's OWN
173
+ * `oklch(...)` string, copied verbatim from the installed
174
+ * `tailwindcss@4.3.3`'s `theme.css`, NOT converted to hsl(). This
175
+ * contradicts task 1's project-wide convention of writing colours as
176
+ * `hsl(...)` (chosen there to keep the computed-style gate's diffs
177
+ * readable — see that task's report) — the severity tokens are the one
178
+ * deliberate exception, for a reason specific to them and nowhere else in
179
+ * this file: they exist to reproduce specific Tailwind palette entries
180
+ * exactly, and only Tailwind's own notation can guarantee that.
181
+ *
182
+ * Fix round 1 stored these as `hsl(...)`, converted from Tailwind's oklch
183
+ * source (oklch → linear sRGB → sRGB → HSL). That conversion is WRONG,
184
+ * not just imprecise, for any of these values that fall outside the sRGB
185
+ * gamut — and checking all 21 shades actually in play (7 severities × 3
186
+ * shades: 500 base, 700 and 400 emphasis) against sRGB, ALL BUT FIVE do:
187
+ * every emerald/amber/sky/orange shade, plus violet-500/400 and
188
+ * blue-500/400. Only zinc (all 3 shades) and violet-700/blue-700 land
189
+ * inside sRGB. `emerald-400` is the clearest example: Tailwind's
190
+ * `oklch(76.5% 0.177 163.223)` is a soft mint (`#34d399`,
191
+ * `rgb(52, 211, 153)`) — converting it via the naive per-channel-clip an
192
+ * hsl() round-trip forces (linear red channel computes to -0.031, which
193
+ * has no representation in hsl() at all) collapses it to
194
+ * `rgb(0, 214, 146)`, a visibly more saturated, wrongly-vivid green — the
195
+ * red channel is off by 52, not a rounding-scale error. This is not a
196
+ * bug in the conversion arithmetic (verified independently twice, by two
197
+ * different people, and both derivations agreed) — the mistake was
198
+ * converting through hsl() AT ALL for a colour hsl() cannot represent.
199
+ * The fix is to stop converting: the browser computes a literal
200
+ * `oklch(...)` custom property as `oklch(...)`, so storing Tailwind's own
201
+ * oklch string makes the computed value byte-identical to Tailwind's
202
+ * native utility classes, and the gate's diff disappears rather than
203
+ * approximating it.
204
+ *
205
+ * `--overlay` and the two `-foreground` whites below are NOT Tailwind
206
+ * palette entries (pure black / pure white, invented for this contract,
207
+ * not looked up), and pure black/white have zero chroma — there is no
208
+ * gamut to fall outside of, in any notation, so they stay in `hsl()` for
209
+ * readability; the notation-mismatch concern this fix round exists for
210
+ * does not apply to them. Confirmed by the pairing analysis in task 4's
211
+ * fix-round-2 report: the overlay's `hsl(0 0% 0%)` pairing was
212
+ * byte-identical against the live gate, no conversion issue.
213
+ *
214
+ * The base tint itself does NOT vary by mode in the source today (no
215
+ * `dark:bg-*` on any of these sites), so it is declared once here and NOT
216
+ * repeated in .dark. */
217
+ --success: oklch(69.6% 0.17 162.48); /* emerald-500 */
218
+ --success-foreground: hsl(0 0% 100%); /* text on a SOLID bg-success fill (not a palette colour) */
219
+ --success-emphasis: oklch(50.8% 0.118 165.612); /* emerald-700, text on a TINTED bg-success/15 */
220
+ --warning: oklch(76.9% 0.188 70.08); /* amber-500 */
221
+ --warning-foreground: hsl(0 0% 100%); /* text on a SOLID bg-warning fill (not a palette colour) */
222
+ --warning-emphasis: oklch(55.5% 0.163 48.998); /* amber-700, text on a TINTED bg-warning/15 */
223
+ --info: oklch(68.5% 0.169 237.323); /* sky-500 */
224
+ --info-foreground: hsl(0 0% 100%); /* text on a SOLID bg-info fill (not a palette colour) */
225
+ --info-emphasis: oklch(50% 0.134 242.749); /* sky-700, text on a TINTED bg-info/15 */
226
+ --neutral: oklch(55.2% 0.016 285.938); /* zinc-500 */
227
+ --neutral-foreground: hsl(0 0% 100%); /* text on a SOLID bg-neutral fill (not a palette colour) */
228
+ --neutral-emphasis: oklch(37% 0.013 285.805); /* zinc-700, text on a TINTED bg-neutral/15 */
229
+ --highlight: oklch(60.6% 0.25 292.717); /* violet-500 */
230
+ --highlight-foreground: hsl(0 0% 100%); /* text on a SOLID bg-highlight fill (not a palette colour) */
231
+ --highlight-emphasis: oklch(49.1% 0.27 292.581); /* violet-700, text on a TINTED bg-highlight/15 */
232
+ --active: oklch(62.3% 0.214 259.815); /* blue-500 */
233
+ --active-foreground: hsl(0 0% 100%); /* text on a SOLID bg-active fill (not a palette colour) */
234
+ --active-emphasis: oklch(48.8% 0.243 264.376); /* blue-700, text on a TINTED bg-active/15 */
235
+ --caution: oklch(70.5% 0.213 47.604); /* orange-500 */
236
+ --caution-foreground: hsl(0 0% 100%); /* text on a SOLID bg-caution fill (not a palette colour) */
237
+ --caution-emphasis: oklch(55.3% 0.195 38.402); /* orange-700, text on a TINTED bg-caution/15 */
238
+
239
+ /* Modal/sheet/alert-dialog backdrop veil. Was a literal `bg-black/80` in
240
+ * three primitives (alert-dialog.tsx, dialog.tsx, sheet.tsx) — black at a
241
+ * fixed lightness needs no light/dark variant, so this is declared once. */
242
+ --overlay: hsl(0 0% 0%);
243
+
244
+ --border: oklch(0.922 0 0);
245
+ --input: oklch(0.922 0 0);
246
+ /* Disabled-field surface. `disabled:opacity-50` (input/textarea/select
247
+ * trigger/command input) is shadcn's canonical disabled signal, and it
248
+ * works when the control's own foreground/background pair has enough
249
+ * luminance separation for a 50%-alpha fade to read as "faded". It stops
250
+ * working the moment a theme narrows that gap — a disabled field can then
251
+ * look pixel-identical to an enabled one (reported against a live theme).
252
+ * A dedicated background makes the signal independent of luminance
253
+ * contrast: the field visibly changes SURFACE, not just opacity, so the
254
+ * two cues reinforce each other instead of one silently doing nothing.
255
+ * Value equals --muted's shipped value on purpose (a muted surface, not a
256
+ * tint of --background) but is its own token, not `var(--muted)`, so a
257
+ * theme can tune "this field is inert" independently of `--muted`'s other
258
+ * jobs (zebra stripes, badges, etc. don't reason about disabled state). */
259
+ --input-disabled: oklch(0.97 0 0);
260
+ /* DEPARTURE from shadcn: v4's own default here is oklch(0.708 0 0), and it
261
+ * fails WCAG 1.4.11's 3:1 focus-indicator minimum in this kit — measured
262
+ * at 2.59:1 against --background and 2.38:1 against --muted/--accent/
263
+ * --secondary (the worse case, and the one call sites actually hit with
264
+ * no ring offset: primitives/button.tsx, input.tsx, select.tsx,
265
+ * textarea.tsx, badge.tsx and ~15 more). shadcn can afford 0.708 because
266
+ * its own components draw focus with `border-ring` PLUS `ring-ring/50
267
+ * ring-[3px]` — two signals; this kit kept the older single-signal
268
+ * treatment, an opaque `ring-2 ring-ring` with `outline-hidden` killing
269
+ * the native fallback, so the ring alone has to clear 3:1. Raised to
270
+ * 0.556 — the same lightness this file already uses for light
271
+ * --muted-foreground and dark --ring, not a new arbitrary stop — which
272
+ * clears 3:1 with margin in both places this ring lands:
273
+ * - vs --background (oklch(1 0 0)): 4.73:1
274
+ * - vs --muted/--accent/--secondary (oklch(0.97 0 0)): 4.34:1
275
+ * Dark --ring is untouched: it already passes (4.18:1 on --background,
276
+ * 3.79:1 on --card/--popover/--sidebar). */
277
+ --ring: oklch(0.556 0 0);
278
+ --popover: oklch(1 0 0);
279
+ --popover-foreground: oklch(0.145 0 0);
280
+ --card: oklch(1 0 0);
281
+ --card-foreground: oklch(0.145 0 0);
282
+ --radius: 0.5rem;
283
+ --sidebar: oklch(0.985 0 0);
284
+ --sidebar-foreground: oklch(0.145 0 0);
285
+ --sidebar-primary: oklch(0.205 0 0);
286
+ --sidebar-primary-foreground: oklch(0.985 0 0);
287
+ --sidebar-accent: oklch(0.97 0 0);
288
+ --sidebar-accent-foreground: oklch(0.205 0 0);
289
+ --sidebar-border: oklch(0.922 0 0);
290
+ --sidebar-ring: oklch(0.708 0 0);
291
+
292
+ /* Tailwind v4's built-in default `--font-sans` is not the same stack v3
293
+ * shipped (verified: `tailwindcss@3.4.19`'s
294
+ * `stubs/config.full.js` → `theme.fontFamily.sans` vs v4's
295
+ * `tailwindcss/theme.css` → `--font-sans`). Neither the old preset nor
296
+ * this file ever pinned `font-sans` before, so the v4 default was free to
297
+ * leak through onto nearly every element on every page — a site-wide,
298
+ * unaccepted visual change caught by the computed-style gate (task 7).
299
+ * Pinning the exact v3 value here keeps the migration invisible, per plan
300
+ * B's contract: nothing visible changes except what's on the accepted
301
+ * list. This is the BASE value only — declared in this unlayered `:root`
302
+ * (not inside `@theme inline`) and re-exposed to `@theme inline` below via
303
+ * `var(--font-sans)`, the same indirection the colour tokens above use.
304
+ * That is load-bearing, not decorative: a value written as a literal
305
+ * inside `@theme inline` gets compiled into the generated utility at
306
+ * build time and emits no custom property at all, so a later theme's
307
+ * runtime `:root { --font-sans: … }` would compile fine and do nothing.
308
+ * Declaring it here first means Tailwind's own `@layer theme` declaration
309
+ * of `--font-sans` loses to this unlayered one regardless of source
310
+ * order, and a theme injected later at runtime (also unlayered) wins over
311
+ * this one in turn — so a later plan CAN override `--font-sans` per-theme.
312
+ * Confirmed by compiling a fixture against the built stylesheet, then
313
+ * again with an extra unlayered `:root { --font-sans: "Comic Sans MS" }`
314
+ * appended: `.font-sans` picked up the override (see the plan's fix
315
+ * report for both compiled outputs).
316
+ * `--font-mono` needed no such pin: v3's and v4's default mono stacks are
317
+ * byte-identical (same two files), confirmed by the gate showing zero
318
+ * font-family drift on any `font-mono` element. */
319
+ --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
320
+ "Segoe UI Symbol", "Noto Color Emoji";
321
+
322
+ /* v4 redefines every named font-size's paired line-height as a UNITLESS
323
+ * ratio (e.g. `--text-xs--line-height: calc(1 / 0.75)`); v3 used a FIXED
324
+ * length (`1rem`) instead. CSS inherits a number by recomputing it at each
325
+ * descendant from THAT descendant's own font-size; it inherits a length
326
+ * unchanged. So the two are only equivalent when an element's own
327
+ * font-size matches the ancestor supplying its line-height — anywhere a
328
+ * smaller/larger override (`text-[10px]`, a nested heading, ...) sits
329
+ * inside a `text-*` ancestor, v4 silently recomputes a different
330
+ * line-height than v3 did. Task 7's computed-style gate caught exactly
331
+ * one instance of this (the command palette's `text-[10px]` kbd chips,
332
+ * nested in `text-xs`, went 16px -> 13.3333px) but the mechanism is
333
+ * general, not particular to kbd — `resource-form.tsx`,
334
+ * `resource-show.tsx`, `file.tsx` and others carry `text-[10px]`/
335
+ * `text-[11px]` on routes this gate doesn't cover, and would have drifted
336
+ * silently. Pinning every named size actually used in this kit
337
+ * (`grep -rE 'text-(xs|sm|base|lg|xl|2xl|3xl)\b' packages/ui-shadcn/src
338
+ * apps/playground/src`) to v3's fixed value closes the whole class of bug
339
+ * at the token level, not just the four kbd elements that happened to be
340
+ * gated — those per-element `leading-[1rem]` pins have been removed as
341
+ * redundant. Values read verbatim from the installed
342
+ * `tailwindcss@3.4.19`'s `stubs/config.full.js` `fontSize` block.
343
+ * `text-4xl` and up are unused except `text-8xl`, which v3 and v4 both
344
+ * define as the unitless ratio `1` — identical in both, no pin needed.
345
+ * Declared here (unlayered `:root`), not as a literal inside `@theme
346
+ * inline`, for the same runtime-override reason as `--font-sans` above. */
347
+ --text-xs--line-height: 1rem;
348
+ --text-sm--line-height: 1.25rem;
349
+ --text-base--line-height: 1.5rem;
350
+ --text-lg--line-height: 1.75rem;
351
+ --text-xl--line-height: 1.75rem;
352
+ --text-2xl--line-height: 2rem;
353
+ --text-3xl--line-height: 2.25rem;
354
+
355
+ /* Chart palette. A theme with no opinion on data-viz colour still needs
356
+ * five slots to land in — these are shadcn v4's own default chart palette
357
+ * (its current oklch chart preset, not the older HSL chart-docs example
358
+ * this file previously carried: https://v3.shadcn.com/docs/components/chart),
359
+ * stored as oklch, not hsl, for the same reason the severity scale is: a
360
+ * token whose job is to reproduce a specific external palette entry is
361
+ * stored in that palette's own notation. Genuinely different between
362
+ * light and dark, not a lazy copy: the light set is tuned to read against
363
+ * a white canvas, the dark set trades some of that saturation for
364
+ * mid-lightness so the same five hues stay legible on a dark surface.
365
+ * Nothing in this kit renders a chart yet, so there's no existing look
366
+ * these numbers could contradict — first-run defaults for whenever one
367
+ * shows up, and immediately overridable by any theme that brings its own
368
+ * five. */
369
+ --chart-1: oklch(0.646 0.222 41.116);
370
+ --chart-2: oklch(0.6 0.118 184.704);
371
+ --chart-3: oklch(0.398 0.07 227.392);
372
+ --chart-4: oklch(0.828 0.189 84.429);
373
+ --chart-5: oklch(0.769 0.188 70.08);
374
+
375
+ /* Tailwind v4's own default --font-serif, verbatim from the installed
376
+ * tailwindcss@4.3.3's theme.css. Unlike --font-sans above, nothing in this
377
+ * kit sets `font-serif` anywhere today, so there is no prior look to
378
+ * preserve here — this is just the first-run default a pasted theme can
379
+ * override. Declared here (unlayered `:root`), re-exposed via `@theme
380
+ * inline` below through `var(--font-serif)`, for the identical
381
+ * runtime-override reason spelled out above --font-sans. */
382
+ --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
383
+
384
+ /* Tailwind v4's own defaults for these two (theme.css: `--spacing:
385
+ * 0.25rem`, `--tracking-normal: 0em`), pinned here explicitly rather than
386
+ * left to Tailwind's built-in `@layer theme` default. `--spacing` was
387
+ * already runtime-overridable even before this line — Tailwind emits its
388
+ * own default inside a cascade layer, which any unlayered `:root`
389
+ * override (ours or a theme's) already beats regardless of source order —
390
+ * but declaring it here makes that contract visible in this file instead
391
+ * of implicit in Tailwind's internals, matches how every other themeable
392
+ * token in this file is declared, and pins the value against any future
393
+ * upstream default change. `--tracking-normal` had no existing Tailwind
394
+ * default to inherit an override from in this way (see the tracking-*
395
+ * family below), so for it this declaration is load-bearing, not just
396
+ * documentation. */
397
+ --spacing: 0.25rem;
398
+ --tracking-normal: 0em;
399
+
400
+ /* Six raw shadow components, in the shape tweakcn's own default preset
401
+ * ships them (color/opacity/blur/spread/offset-x/offset-y) — so that a
402
+ * theme built around varying these six has somewhere for them to land.
403
+ * Nothing in THIS file reads them: Tailwind's stock shadow scale is not
404
+ * one shadow linearly scaled by a color/opacity/blur/spread/offset tuple,
405
+ * it is hand-tuned per step, and several steps (md, lg, xl) stack TWO
406
+ * box-shadow layers with independently-chosen blur/spread/offset pairs
407
+ * that no single set of six numbers reproduces via calc(). The eight
408
+ * computed shadows below are pinned to Tailwind's actual literal values
409
+ * instead of being assembled from these six — see the comment there and
410
+ * the task report's before/after compile diff for the proof. */
411
+ --shadow-color: hsl(0 0% 0%);
412
+ --shadow-opacity: 0.1;
413
+ --shadow-blur: 3px;
414
+ --shadow-spread: 0px;
415
+ --shadow-offset-x: 0px;
416
+ --shadow-offset-y: 1px;
417
+
418
+ /* Eight computed shadows. Values are copied verbatim from the installed
419
+ * tailwindcss@4.3.3's theme.css/index.css default shadow scale (`0 1px
420
+ * rgb(0 0 0 / 0.05)` through `0 25px 50px -12px rgb(0 0 0 / 0.25)`) —
421
+ * checked by reading that file, not from memory — so every `shadow-*`
422
+ * utility keeps rendering exactly what it renders today; see the task
423
+ * report for the compiled-fixture diff proving it. Deliberately NOT
424
+ * calc()'d from the six raw components above (see that comment). Declared
425
+ * here in the unlayered `:root`, not as literals inside `@theme inline`,
426
+ * for the same runtime-override reason as --font-sans: the indirection
427
+ * through `var(--shadow-x)` is what lets a theme's own unlayered `:root`
428
+ * override win later. */
429
+ --shadow-2xs: 0 1px rgb(0 0 0 / 0.05);
430
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
431
+ --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
432
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
433
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
434
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
435
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
436
+ --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
437
+
438
+ /* Overlay timing. Declared here, in the unlayered `:root`, for the same
439
+ * reason the shadows and fonts are: a theme pasted at runtime (tweakcn
440
+ * emits `:root` and `.dark` blocks, and `stripThemeCss` keeps exactly
441
+ * those) can then retime the whole UI along with recolouring it.
442
+ *
443
+ * These reach the animations without any new machinery:
444
+ * `duration-[var(--duration-overlay)]` sets Tailwind's `--tw-duration`
445
+ * and `ease-[var(--ease-overlay)]` sets `--tw-ease`, and
446
+ * `tw-animate-css` declares
447
+ * --animate-in: enter var(--tw-animation-duration, var(--tw-duration,.15s)) var(--tw-ease, ease) …
448
+ * — i.e. it already reads both. Verified against the package source.
449
+ *
450
+ * `--duration-panel` is separate because a sheet travels the width of
451
+ * its edge, not 5% of its own size like a zooming dialog; the same
452
+ * number would read as a snap on one and a crawl on the other. */
453
+ --duration-overlay: 200ms;
454
+ --duration-panel: 300ms;
455
+ --ease-overlay: cubic-bezier(0.16, 1, 0.3, 1);
456
+ }
457
+
458
+ .dark {
459
+ --background: oklch(0.145 0 0);
460
+ --foreground: oklch(0.985 0 0);
461
+ --primary: oklch(0.922 0 0);
462
+ --primary-foreground: oklch(0.205 0 0);
463
+ --secondary: oklch(0.269 0 0);
464
+ --secondary-foreground: oklch(0.985 0 0);
465
+ --muted: oklch(0.269 0 0);
466
+ /* Re-measured against the new neutral dark surfaces (final-review-findings.md
467
+ * M3), value unchanged: 7.63:1 on --background, 6.91:1 on
468
+ * --card/--popover/--sidebar — both comfortably clear WCAG AA's 4.5:1. */
469
+ --muted-foreground: oklch(0.708 0 0);
470
+ --accent: oklch(0.269 0 0);
471
+ --accent-foreground: oklch(0.985 0 0);
472
+ /* ⚠️ Dark needs BOTH values changed together: one token serves red TEXT on
473
+ a dark surface AND a solid red FILL under a label, which pull in opposite
474
+ directions. At 0 62% 50% the fill passed (4.87:1) while red menu text
475
+ measured 3.51:1. A lighter red with a DARK label satisfies both —
476
+ 5.29:1 either way — and is the conventional dark-mode treatment.
477
+ shadcn v4's own candidate (`oklch(0.704 0.191 22.216)`) was measured
478
+ against the new dark --background too: red text passes at 6.84:1, but
479
+ its white --destructive-foreground on that same fill only reaches
480
+ 2.89:1 — below WCAG AA's 4.5:1 — so this half keeps the hand-tuned
481
+ values above instead of adopting shadcn's.
482
+ M1 (final-review-findings.md): --destructive-foreground was still the
483
+ slate hsl(222 47% 11%) named at the :root comment's :56 as exactly what
484
+ this whole change exists to remove — the last surviving occurrence.
485
+ Neutralised to oklch(0.145 0 0) (this file's own near-black stop —
486
+ light --foreground / dark --background). Re-measured against the dark
487
+ --destructive fill above: 5.31:1 -> 5.87:1, an improvement, not just a
488
+ wash. */
489
+ --destructive: hsl(0 72% 64%);
490
+ --destructive-foreground: oklch(0.145 0 0);
491
+
492
+ /* Severity-scale `-emphasis` overrides, dark-mode half — see the long
493
+ * comment on the :root block above (fix rounds 1 and 2) for where these
494
+ * come from and why they're `oklch(...)`, not `hsl(...)` (fix round 2:
495
+ * Tailwind's own oklch values verbatim, not converted — several of these
496
+ * fall outside sRGB gamut and an hsl() conversion silently mangles them).
497
+ * Only `-emphasis` (tinted-badge text) varies by `.dark` — the source
498
+ * genuinely swaps `text-emerald-700 dark:text-emerald-400` etc.
499
+ * `-foreground` (solid-fill text) is NOT repeated here, for the same
500
+ * reason the base tints aren't: neither side of that pair varies by mode
501
+ * in the source today. */
502
+ --success-emphasis: oklch(76.5% 0.177 163.223); /* emerald-400 */
503
+ --warning-emphasis: oklch(82.8% 0.189 84.429); /* amber-400 */
504
+ --info-emphasis: oklch(74.6% 0.16 232.661); /* sky-400 */
505
+ --neutral-emphasis: oklch(70.5% 0.015 286.067); /* zinc-400 */
506
+ --highlight-emphasis: oklch(70.2% 0.183 293.541); /* violet-400 */
507
+ --active-emphasis: oklch(70.7% 0.165 254.624); /* blue-400 */
508
+ --caution-emphasis: oklch(75% 0.183 55.934); /* orange-400 */
509
+
510
+ /* Re-measured against the new neutral dark surfaces (final-review-findings.md
511
+ * M3): --border/10 reads 1.25:1 against the page --background and 1.32:1
512
+ * against a --card; the old opaque dark border (hsl(217 33% 22%) on the
513
+ * old slate hsl(222 47% 11%) background) was 1.42:1 — fainter, but in the
514
+ * same family. A border is a structural divider, not text, so WCAG's
515
+ * text-contrast minimums don't gate it; recorded for the next person who
516
+ * wants to know how much margin is left, not because it's failing. */
517
+ --border: oklch(1 0 0 / 10%);
518
+ /* --input/15 reads 1.74:1 against a --card and 1.47:1 over the page
519
+ * --background — see the --border comment above for why these are
520
+ * recorded despite not being text. */
521
+ --input: oklch(1 0 0 / 15%);
522
+ /* Dark half of --input-disabled above — same relationship to --muted's
523
+ * dark value that the light half has. */
524
+ --input-disabled: oklch(0.269 0 0);
525
+ --ring: oklch(0.556 0 0);
526
+ --popover: oklch(0.205 0 0);
527
+ --popover-foreground: oklch(0.985 0 0);
528
+ --card: oklch(0.205 0 0);
529
+ --card-foreground: oklch(0.985 0 0);
530
+ --sidebar: oklch(0.205 0 0);
531
+ --sidebar-foreground: oklch(0.985 0 0);
532
+ /* DEPARTURE from shadcn: it ships oklch(0.488 0.243 264.376) here — the
533
+ * same blue as --chart-1 — and that colour lands under the active nav
534
+ * item. Keeping the neutral --primary value instead; a blue active row is
535
+ * exactly the tint this palette change set out to remove. */
536
+ --sidebar-primary: oklch(0.922 0 0);
537
+ --sidebar-primary-foreground: oklch(0.205 0 0);
538
+ --sidebar-accent: oklch(0.269 0 0);
539
+ --sidebar-accent-foreground: oklch(0.985 0 0);
540
+ --sidebar-border: oklch(1 0 0 / 10%);
541
+ --sidebar-ring: oklch(0.556 0 0);
542
+
543
+ /* Dark half of the chart palette declared in :root above — see that
544
+ * comment for the source and why the two sets of five are genuinely
545
+ * different values, not a copy. */
546
+ --chart-1: oklch(0.488 0.243 264.376);
547
+ --chart-2: oklch(0.696 0.17 162.48);
548
+ --chart-3: oklch(0.769 0.188 70.08);
549
+ --chart-4: oklch(0.627 0.265 303.9);
550
+ --chart-5: oklch(0.645 0.246 16.439);
551
+ }
552
+
553
+ /* Maps tokens to utilities. `inline` is required: without it, values get
554
+ * copied in at build time and swapping the variable at runtime stops
555
+ * working — i.e. plan D's runtime themes won't work. */
556
+ @theme inline {
557
+ --color-background: var(--background);
558
+ --color-foreground: var(--foreground);
559
+ --color-primary: var(--primary);
560
+ --color-primary-foreground: var(--primary-foreground);
561
+ --color-secondary: var(--secondary);
562
+ --color-secondary-foreground: var(--secondary-foreground);
563
+ --color-muted: var(--muted);
564
+ --color-muted-foreground: var(--muted-foreground);
565
+ --color-accent: var(--accent);
566
+ --color-accent-foreground: var(--accent-foreground);
567
+ --color-destructive: var(--destructive);
568
+ --color-destructive-foreground: var(--destructive-foreground);
569
+
570
+ /* Severity scale — see the :root block above for the meaning → colour
571
+ * derivation and fix round 1's `-foreground`/`-emphasis` split. `inline`
572
+ * here is exactly as load-bearing as it is for every other token above:
573
+ * without it, `bg-success/15` etc. cannot resolve to a runtime
574
+ * `color-mix()` against a swappable custom property. */
575
+ --color-success: var(--success);
576
+ --color-success-foreground: var(--success-foreground);
577
+ --color-success-emphasis: var(--success-emphasis);
578
+ --color-warning: var(--warning);
579
+ --color-warning-foreground: var(--warning-foreground);
580
+ --color-warning-emphasis: var(--warning-emphasis);
581
+ --color-info: var(--info);
582
+ --color-info-foreground: var(--info-foreground);
583
+ --color-info-emphasis: var(--info-emphasis);
584
+ --color-neutral: var(--neutral);
585
+ --color-neutral-foreground: var(--neutral-foreground);
586
+ --color-neutral-emphasis: var(--neutral-emphasis);
587
+ --color-highlight: var(--highlight);
588
+ --color-highlight-foreground: var(--highlight-foreground);
589
+ --color-highlight-emphasis: var(--highlight-emphasis);
590
+ --color-active: var(--active);
591
+ --color-active-foreground: var(--active-foreground);
592
+ --color-active-emphasis: var(--active-emphasis);
593
+ --color-caution: var(--caution);
594
+ --color-caution-foreground: var(--caution-foreground);
595
+ --color-caution-emphasis: var(--caution-emphasis);
596
+ --color-overlay: var(--overlay);
597
+
598
+ --color-popover: var(--popover);
599
+ --color-popover-foreground: var(--popover-foreground);
600
+ --color-card: var(--card);
601
+ --color-card-foreground: var(--card-foreground);
602
+ --color-border: var(--border);
603
+ --color-input: var(--input);
604
+ --color-input-disabled: var(--input-disabled);
605
+ --color-ring: var(--ring);
606
+ --color-sidebar: var(--sidebar);
607
+ --color-sidebar-foreground: var(--sidebar-foreground);
608
+ --color-sidebar-primary: var(--sidebar-primary);
609
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
610
+ --color-sidebar-accent: var(--sidebar-accent);
611
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
612
+ --color-sidebar-border: var(--sidebar-border);
613
+ --color-sidebar-ring: var(--sidebar-ring);
614
+
615
+ --color-chart-1: var(--chart-1);
616
+ --color-chart-2: var(--chart-2);
617
+ --color-chart-3: var(--chart-3);
618
+ --color-chart-4: var(--chart-4);
619
+ --color-chart-5: var(--chart-5);
620
+
621
+ --radius-lg: var(--radius);
622
+ --radius-md: calc(var(--radius) - 2px);
623
+ --radius-sm: calc(var(--radius) - 4px);
624
+ /* shadcn's canonical v4 block also derives -xl from the base; without this
625
+ * line -xl stayed pinned at Tailwind's literal 0.75rem, so a themed
626
+ * `--radius` rescaled every corner except `rounded-xl` sites (e.g. the
627
+ * sidebar's inset-variant shell in primitives/sidebar.tsx). */
628
+ --radius-xl: calc(var(--radius) + 4px);
629
+
630
+ /* All declared in the unlayered `:root` above, not as literals here —
631
+ * see the comment there for why the indirection is load-bearing
632
+ * (runtime overridability), not decorative. */
633
+ --font-sans: var(--font-sans);
634
+ --font-serif: var(--font-serif);
635
+ --text-xs--line-height: var(--text-xs--line-height);
636
+ --text-sm--line-height: var(--text-sm--line-height);
637
+ --text-base--line-height: var(--text-base--line-height);
638
+ --text-lg--line-height: var(--text-lg--line-height);
639
+ --text-xl--line-height: var(--text-xl--line-height);
640
+ --text-2xl--line-height: var(--text-2xl--line-height);
641
+ --text-3xl--line-height: var(--text-3xl--line-height);
642
+
643
+ --spacing: var(--spacing);
644
+
645
+ /* Only --tracking-normal is a real declared token in :root (the base a
646
+ * theme sets); the other five are computed from it here so overriding the
647
+ * base ripples through the whole scale — the same shape tweakcn's own
648
+ * generated output uses. At the default --tracking-normal: 0em these
649
+ * resolve to exactly Tailwind's own built-in absolute values
650
+ * (-0.05em/-0.025em/0.025em/0.05em/0.1em), so nothing currently rendered
651
+ * moves. */
652
+ --tracking-tighter: calc(var(--tracking-normal) - 0.05em);
653
+ --tracking-tight: calc(var(--tracking-normal) - 0.025em);
654
+ --tracking-normal: var(--tracking-normal);
655
+ --tracking-wide: calc(var(--tracking-normal) + 0.025em);
656
+ --tracking-wider: calc(var(--tracking-normal) + 0.05em);
657
+ --tracking-widest: calc(var(--tracking-normal) + 0.1em);
658
+
659
+ --shadow-2xs: var(--shadow-2xs);
660
+ --shadow-xs: var(--shadow-xs);
661
+ --shadow-sm: var(--shadow-sm);
662
+ --shadow: var(--shadow);
663
+ --shadow-md: var(--shadow-md);
664
+ --shadow-lg: var(--shadow-lg);
665
+ --shadow-xl: var(--shadow-xl);
666
+ --shadow-2xl: var(--shadow-2xl);
667
+ }
668
+
669
+ /* This can now ship from the package — in v3 it couldn't, because plain CSS
670
+ * doesn't pass through @apply, and every adopter had to copy this rule
671
+ * themselves. Its absence is invisible in light mode and gives 27 glowing
672
+ * borders out of 99 in dark. */
673
+ @layer base {
674
+ * {
675
+ @apply border-border;
676
+ }
677
+ body {
678
+ @apply bg-background text-foreground;
679
+ }
680
+
681
+ /* v4 preflight makes buttons `cursor: default`; v3 gave them `pointer`.
682
+ * Restoring it here keeps the migration invisible to users. */
683
+ button:not(:disabled),
684
+ [role="button"]:not(:disabled) {
685
+ cursor: pointer;
686
+ }
687
+
688
+ /* The other half of v3's preflight pair. v3 shipped BOTH
689
+ * `button, [role="button"] { cursor: pointer }` AND
690
+ * `:disabled { cursor: default }`; v4 dropped both, and the rule above
691
+ * only restores the first half via a `:not(:disabled)` guard, which stops
692
+ * a disabled control from claiming `pointer` but does not force it back
693
+ * to `default`. `cursor` inherits, so without this a disabled control
694
+ * inherits whatever cursor its ancestor set instead of being pinned —
695
+ * real nestings exist (clickable rows in `resource-list.tsx`,
696
+ * `relation-picker-dialog.tsx`, `<summary className="cursor-pointer">`
697
+ * in `resource-form.tsx` and `list-error-state.tsx`), and any disabled
698
+ * input without an explicit `disabled:cursor-not-allowed` loses v3's
699
+ * behaviour too. */
700
+ :disabled {
701
+ cursor: default;
702
+ }
703
+
704
+ /* v4 preflight's `*, ::before, ::after, ::backdrop, ::file-selector-button
705
+ * { padding: 0 }` is a genuinely universal reset. v3's never was: it only
706
+ * zeroed padding on `button/input/optgroup/select/textarea`
707
+ * (`tailwindcss@3.4.19` .../lib/css/preflight.css) and never touched
708
+ * `th`/`td`, so the browser's own UA default (`th, td { padding: 1px }`)
709
+ * was left standing under v3. `TableHead`/`TableCell`
710
+ * (`packages/ui-shadcn/src/primitives/table.tsx`) never set their own
711
+ * vertical padding — only `px-2` horizontal, with `h-10` + `align-middle`
712
+ * for row height — so v4's broader reset silently shaved 1px off every
713
+ * table row's top/bottom padding (caught by task 7's computed-style gate
714
+ * on the `list` route). Restoring the pre-v4 default here, same as the
715
+ * cursor rule above, keeps row heights identical to v3. `px-2` still wins
716
+ * on left/right: Tailwind's `@layer utilities` always beats `@layer base`
717
+ * regardless of specificity, and this shorthand only reaches top/bottom in
718
+ * practice. */
719
+ th,
720
+ td {
721
+ padding: 1px;
722
+ }
723
+ }
724
+
725
+ /* Standard root. `text-base` therefore computes to 16px on mobile; editable
726
+ * controls that need compact desktop typography opt into `md:text-sm` locally
727
+ * instead of globally rescaling rem-based dimensions.
728
+ */
729
+ html {
730
+ font-size: 16px;
731
+ }
732
+
733
+ /* Scrollbars follow the palette.
734
+ *
735
+ * Unstyled, they keep the operating system's own colours, so under any theme
736
+ * they are the one chrome that never repaints — which is exactly how they
737
+ * looked wrong once themes arrived. There are 42 scroll containers across the
738
+ * kit and the app, so this belongs here as one rule rather than at each site.
739
+ *
740
+ * `--border` for the thumb and a transparent track: the thumb reads as a
741
+ * boundary rather than as content, it inherits every theme automatically, and
742
+ * no new token enters the contract. On hover it steps up to
743
+ * `--muted-foreground` so the target is findable without being loud at rest.
744
+ *
745
+ * ⚠️ What CSS does NOT decide here is whether the bar overlays the content or
746
+ * takes layout width. There is no standard property for it — CSS Scrollbars
747
+ * Level 1 defines only `scrollbar-width` and `scrollbar-color`, `overflow:
748
+ * overlay` was removed, and `scrollbar-gutter` reserves space rather than
749
+ * lifting the bar out of flow. Overlay versus classic is the operating
750
+ * system's and browser's call: macOS set to show bars automatically overlays
751
+ * them, Windows and Linux with classic settings do not. If content visibly
752
+ * jumps as a container crosses the fits/does-not-fit boundary, the fix is
753
+ * `scrollbar-gutter: stable` on that container — it always reserves, so the
754
+ * width stops changing. It is deliberately NOT applied globally, because on a
755
+ * platform that already overlays it would carve out a gutter nobody asked for.
756
+ *
757
+ * The two strips that hide their scrollbar on purpose — the tab rail in
758
+ * `tabs-layout.tsx` and the mobile rail in `secondary-sidebar-layout.tsx` —
759
+ * set `scrollbar-width: none` themselves and are unaffected: `none` wins over
760
+ * the `thin` set here by ordinary cascade.
761
+ */
762
+ @layer base {
763
+ * {
764
+ scrollbar-width: thin;
765
+ scrollbar-color: var(--border) transparent;
766
+ }
767
+
768
+ /* Safari does not implement the standard properties above; these reproduce
769
+ * the same look through the WebKit pseudo-elements. Chromium honours both
770
+ * and the standard ones win, so this costs nothing there. */
771
+ *::-webkit-scrollbar {
772
+ width: 10px;
773
+ height: 10px;
774
+ }
775
+
776
+ *::-webkit-scrollbar-track {
777
+ background: transparent;
778
+ }
779
+
780
+ *::-webkit-scrollbar-thumb {
781
+ background-color: var(--border);
782
+ border: 3px solid transparent;
783
+ border-radius: 9999px;
784
+ background-clip: content-box;
785
+ }
786
+
787
+ *::-webkit-scrollbar-thumb:hover {
788
+ background-color: var(--muted-foreground);
789
+ }
790
+
791
+ *::-webkit-scrollbar-corner {
792
+ background: transparent;
793
+ }
794
+ }