@codenhub/styles 0.5.0 → 0.6.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 (59) hide show
  1. package/README.md +7 -7
  2. package/dist/aesthetics/chunky-tile.css +1 -1
  3. package/dist/aesthetics/cyber.css +1 -1
  4. package/dist/aesthetics/glass.css +1 -1
  5. package/dist/aesthetics/index.css +1 -1
  6. package/dist/aesthetics/neobrutalism.css +1 -1
  7. package/dist/aesthetics/pixel.css +1 -1
  8. package/dist/aesthetics/sketch.css +1 -1
  9. package/dist/components.css +1 -1
  10. package/dist/index.css +1 -1
  11. package/dist/native.css +1 -1
  12. package/dist/palette.css +1 -1
  13. package/dist/theme.css +1 -1
  14. package/dist/tw/aesthetics/chunky-tile.css +39 -39
  15. package/dist/tw/aesthetics/cyber.css +16 -11
  16. package/dist/tw/aesthetics/glass.css +235 -10
  17. package/dist/tw/aesthetics/index.css +2 -1
  18. package/dist/tw/aesthetics/neobrutalism.css +23 -11
  19. package/dist/tw/aesthetics/pixel.css +18 -8
  20. package/dist/tw/aesthetics/sketch.css +14 -7
  21. package/dist/tw/box.css +49 -27
  22. package/dist/tw/components/button.css +35 -14
  23. package/dist/tw/components/feedback.css +59 -56
  24. package/dist/tw/components/form.css +200 -126
  25. package/dist/tw/components/index.css +3 -7
  26. package/dist/tw/components/loader.css +4 -5
  27. package/dist/tw/components/surface.css +71 -68
  28. package/dist/tw/components/tooltip.css +75 -41
  29. package/dist/tw/elevation.css +14 -22
  30. package/dist/tw/index.css +15 -19
  31. package/dist/tw/intent.css +31 -24
  32. package/dist/tw/native.css +15 -5
  33. package/dist/tw/palette.css +8 -5
  34. package/dist/tw/presentation.css +16 -30
  35. package/dist/tw/reset.css +21 -28
  36. package/dist/tw/surface.css +11 -12
  37. package/dist/tw/theme.css +58 -86
  38. package/dist/tw/utilities/content.css +268 -199
  39. package/dist/tw/utilities/layout.css +15 -25
  40. package/docs/accessibility.md +50 -33
  41. package/docs/changelog/0.6.0.md +52 -0
  42. package/docs/changelog/index.md +1 -0
  43. package/docs/concepts.md +6 -6
  44. package/docs/index.md +9 -0
  45. package/docs/integrating/nextjs.md +1 -1
  46. package/docs/integrating/tailwind.md +14 -6
  47. package/docs/setup.md +6 -6
  48. package/docs/usage/aesthetics.md +35 -8
  49. package/docs/usage/buttons.md +1 -1
  50. package/docs/usage/composing.md +1 -1
  51. package/docs/usage/content-and-layout.md +12 -11
  52. package/docs/usage/customizing.md +8 -9
  53. package/docs/usage/feedback.md +5 -3
  54. package/docs/usage/forms.md +51 -35
  55. package/docs/usage/theming.md +39 -40
  56. package/docs/usage/tooltips.md +37 -17
  57. package/llms-full.txt +331 -183
  58. package/llms.txt +6 -0
  59. package/package.json +5 -5
package/llms-full.txt CHANGED
@@ -40,16 +40,16 @@ For the Tailwind CSS v4 entrypoint, theme selection, the intent/presentation axe
40
40
 
41
41
  ## Requirements
42
42
 
43
- | Requirement | Details |
44
- | ------------- | -------------------------------------------------------------------------------------------------------------- |
45
- | CSS imports | Consumer tooling must resolve package CSS imports. |
46
- | Browsers | Chrome 123, Safari 17.5, or Firefox 121 and newer (`light-dark()` theming; `:has()` for `.input-group` state). |
47
- | Tailwind CSS | Version 4 or newer is required only for `/tw` source entrypoints. |
48
- | Accessibility | Consumers provide semantic HTML, ARIA, keyboard behavior, and focus management. |
43
+ | Requirement | Details |
44
+ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
+ | CSS imports | Consumer tooling must resolve package CSS imports. |
46
+ | Browsers | Chrome 123, Safari 17.5, or Firefox 128 and newer (`light-dark()` theming; `:has()` for `.input-group` state; `@property` for non-inheriting elevation). |
47
+ | Tailwind CSS | 4.3.2 or newer, below 5, is required only for `/tw` source entrypoints. |
48
+ | Accessibility | Consumers provide semantic HTML, ARIA, keyboard behavior, and focus management. |
49
49
 
50
50
  ## Notes
51
51
 
52
- This package has no JavaScript behavior; no version provides compatibility aliases for a removed class or token. `0.2.0` removes `.btn.loading` (compose a disabled `.btn` with a `.loader` child instead) and `.alert.icon`'s built-in glyph (drop a consumer-supplied icon into `.alert-icon` instead), and `.tooltip` now requires a `.tooltip-bubble` child element rather than a `data-tooltip` attribute. `0.5.0` removes the second names: `.dense`, `.compact`, and `.spacious` (use `.p-xs`, `.p-sm`, and `.p-lg`), `.danger` and `.error` (use `.destructive`, and `.hint.destructive` for `.hint.error`), and `.elevation-none`, `.elevation-sm`, and `.elevation-md` (use `.flat`, `.raised`, and `.floating`). It also renames two material tokens: an aesthetic sets its corner as `--ui-corner` and `--ui-corner-surface` rather than `--ui-radius` and `--ui-radius-surface`, which now take a complete radius as written, and `--ui-button-weight` and `--ui-button-tracking` are `--ui-label-weight` and `--ui-label-tracking`. See the [changelog](./docs/changelog/index.md) for the complete history.
52
+ This package has no JavaScript behavior. It ships no compatibility aliases: a removed class or token is documented in the [changelog](./docs/changelog/index.md), not kept under a second name.
53
53
 
54
54
  ## License
55
55
 
@@ -80,6 +80,15 @@ The package ships as ready-to-import compiled CSS, or as Tailwind CSS v4 source
80
80
  - No shipped font binaries. `.pixel`, `.chunky-tile`, `.cyber`, and `.sketch` accept a consumer-supplied font and fall back to a system stack when none is given.
81
81
  - No compatibility layer for removed tokens or classes. Breaking changes are documented, not silently aliased.
82
82
 
83
+ ## Trade-offs
84
+
85
+ Known costs of the design, stated so they can be weighed before adopting it:
86
+
87
+ - **Class names are short and unprefixed.** `.primary`, `.success`, `.dark`, `.light`, `.label`, `.field`, `.hint`, `.section`, `.stack`, `.flat`, and `.ghost` are global names, so another stylesheet on the same page that uses them collides with this one. The package sits in cascade layers, so your own unlayered CSS wins a collision, but a third-party library's styles for the same name still meet the package's.
88
+ - **The complete stylesheets are large before compression.** Minified, `@codenhub/styles` is about 183 KB, `/native` about 251 KB, and `/components` about 145 KB; served compressed they are about 15 KB, 18 KB, and 12 KB with gzip. The repetition that compression removes is the composition every component carries. Import the narrowest entrypoint that covers a page; see [Setup → Import paths](docs/setup.md#import-paths).
89
+ - **`--color-foreground` is a surface tone, not a text colour.** It is the plate a step off the page that a quiet card or panel rests on. The text colour is `--color-text`.
90
+ - **The success, warning, destructive, and info hover tones are fixed values.** `--color-success-hover` and its three siblings are precomputed rather than mixed from their base colour, because mixing them live crashed WebKit. Overriding `--color-success` alone leaves its hover on the old hue, so retheme the whole family; see [Theming → Color tokens](docs/usage/theming.md#color-tokens).
91
+
83
92
  ## Next steps
84
93
 
85
94
  - [Setup](docs/setup.md): Install the package, choose an entrypoint, and render a first component.
@@ -116,7 +125,7 @@ Import the compiled stylesheet for the complete token, reset, utility, and compo
116
125
 
117
126
  The full stylesheet applies global reset and focus-visible rules, but does not apply the optional classless native-element mappings, and does not include aesthetics. Import focused entrypoints when global rules are not appropriate; see [Import paths](#import-paths) below.
118
127
 
119
- Aesthetics are opt-in. Import the combined aesthetics stylesheet, or one aesthetic on its own, after the base stylesheet so its rules win:
128
+ Aesthetics are opt-in. Import the combined aesthetics stylesheet, or one aesthetic on its own. Order does not matter, because every entrypoint declares the [cascade layers](#cascade-layers) before it uses one:
120
129
 
121
130
  ```css
122
131
  @import "@codenhub/styles";
@@ -171,11 +180,11 @@ Compiled entrypoints are ready-to-import CSS and require only tooling that can r
171
180
  | Import path | Composition and effects |
172
181
  | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
173
182
  | `@codenhub/styles` | Compiled complete surface: Tailwind base/theme output, tokens, typography, utilities, components, and reset. Applies global reset, root theme, and focus rules. |
174
- | `@codenhub/styles/theme` | Compiled Tailwind theme variables, Codenhub tokens, and the presentation classes. Sets root theme properties and system/explicit theme selectors; no reset or component classes. |
183
+ | `@codenhub/styles/theme` | Compiled Tailwind theme variables, Codenhub tokens, and the intent, presentation, elevation, and corner-pattern classes. Sets root theme properties and system/explicit theme selectors; no reset or component classes. |
175
184
  | `@codenhub/styles/palette` | Generated, self-contained `--palette-*` custom properties: every intent/presentation cell's fill, text, and edge color, plus the flat `border`/`surface`/`text` neutral tokens, pre-composed to a flat value. For a consumer that cannot take this package as a build-time dependency (an optional peer, for example) but wants to look consistent with it. Not a replacement for the components themselves when Tailwind is available -- see [Customizing → Generated palette](docs/usage/customizing.md#generated-palette). |
176
- | `@codenhub/styles/components` | Compiled theme tokens, typography utilities, and every component class. Includes static reduced-motion loader masks; no reset, layout/content utilities, or native mappings. |
185
+ | `@codenhub/styles/components` | Compiled theme tokens, typography utilities, and every component class. Each component carries its own reduced-motion rule and the `<dialog>` fix travels with `.card`; no reset, layout/content utilities, or native mappings. |
177
186
  | `@codenhub/styles/native` | Compiled complete surface plus classless mappings for headings, text, content elements, form controls, and buttons. Applies the reset and broad element selectors. |
178
- | `@codenhub/styles/aesthetics` | Compiled `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`, `.cyber`, and `.sketch` classes, and each one's `-solo` class. Adds no tokens or components of its own; import after the base stylesheet. |
187
+ | `@codenhub/styles/aesthetics` | Compiled `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`, `.cyber`, and `.sketch` classes, and each one's `-solo` class. Adds no tokens or components of its own; import in either order. |
179
188
  | `@codenhub/styles/aesthetics/*` | One compiled aesthetic and its solo class: `/neobrutalism`, `/glass`, `/pixel`, `/chunky-tile`, `/cyber`, or `/sketch`. Same rules and ordering as the combined file. |
180
189
  | `@codenhub/styles/tw` | Source equivalent of the complete stylesheet. Imports Tailwind itself, theme, typography, utilities, components, and reset; applies the same global rules. |
181
190
  | `@codenhub/styles/tw/theme` | Source theme, tokens, and presentation classes. Imports Tailwind's theme layer; emits root/system/explicit theme selectors and defines the package's custom `dark:` variant. |
@@ -217,8 +226,8 @@ What that means in practice:
217
226
 
218
227
  - Consumer tooling must resolve package CSS imports.
219
228
  - Every shipped stylesheet can be pasted into an HTML `<style>` element, for critical-CSS inlining or a server-rendered head: none contains the `</style` sequence that would end the element early.
220
- - Chrome 123, Safari 17.5, or Firefox 121 and newer. Color tokens are declared with `light-dark()` and selected by `color-scheme` (Firefox 120), and `.input-group` propagates a nested control's invalid and disabled state with `:has()` (Firefox 121).
221
- - Tailwind CSS 4 or newer is required only for `/tw` source entrypoints.
229
+ - Chrome 123, Safari 17.5, or Firefox 128 and newer. Color tokens are declared with `light-dark()` and selected by `color-scheme` (Firefox 120), `.input-group` propagates a nested control's invalid and disabled state with `:has()` (Firefox 121), and the elevation multiplier is registered non-inheriting with `@property` (Firefox 128; without it an elevation class would reach the unclassed elements inside it).
230
+ - Tailwind CSS 4.3.2 or newer, below 5, is required only for `/tw` source entrypoints.
222
231
  - The package has no JavaScript runtime. Apps must provide semantic HTML, ARIA, keyboard behavior, focus management, validation, and announcements; see [Accessibility](docs/accessibility.md).
223
232
 
224
233
  <!-- Source: docs/concepts.md -->
@@ -231,11 +240,11 @@ Read this before the usage guides. It explains the mental model every component,
231
240
 
232
241
  Every component separates three independent choices, so each can change without a component-specific variant:
233
242
 
234
- | Axis | Question | Classes | Inheritance |
235
- | ------------ | --------------- | --------------------------------------------------------------------------------------------------- | ------------ |
236
- | Intent | Which color? | `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, `.info` | Element only |
237
- | Presentation | How much of it? | `.solid`, `.soft`, `.ghost`, `.edged`, `.edgeless` | Cascades |
238
- | Aesthetic | Made of what? | `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`, `.cyber` from an optional aesthetic stylesheet | Cascades |
243
+ | Axis | Question | Classes | Inheritance |
244
+ | ------------ | --------------- | -------------------------------------------------------------------------------------------------------------- | ------------ |
245
+ | Intent | Which color? | `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, `.info` | Element only |
246
+ | Presentation | How much of it? | `.solid`, `.soft`, `.ghost`, `.edged`, `.edgeless` | Cascades |
247
+ | Aesthetic | Made of what? | `.neobrutalism`, `.glass`, `.pixel`, `.chunky-tile`, `.cyber`, `.sketch` from an optional aesthetic stylesheet | Cascades |
239
248
 
240
249
  ```html
241
250
  <section class="soft pixel">
@@ -285,7 +294,7 @@ The root follows the operating-system `color-scheme` preference until an explici
285
294
 
286
295
  Setting `color-scheme` yourself on an element also re-themes the tokens below it, because that is the only signal the palette reads. This also themes native UI such as scrollbars and form-control internals to match.
287
296
 
288
- This mechanism works from Chrome 123, Safari 17.5, and Firefox 120; the package overall requires Firefox 121 for `:has()` (see [Setup](docs/setup.md#requirements)).
297
+ This mechanism works from Chrome 123, Safari 17.5, and Firefox 120; the package overall requires Firefox 128, for `@property` and `:has()` (see [Setup](docs/setup.md#requirements)).
289
298
 
290
299
  <!-- Source: docs/usage/index.md -->
291
300
 
@@ -397,7 +406,7 @@ The same split governs the edge: a cascaded `.edgeless` is floored and the eleme
397
406
 
398
407
  A tooltip bubble is filled, over a tinted ground. An intent fills it with that intent's own color: `.tooltip-bubble.primary` is the primary color, black on a light page and white on a dark one, and `.tooltip-bubble.destructive` is a red bubble.
399
408
 
400
- With no intent the bubble is `--color-tooltip`, the one plate in the package chosen per theme rather than derived from an intent. Composed like everything else it was `20%` of the page's ink over the surface tone, which steps _lighter_ than the surface on a dark page and _darker_ on a light one — so the dark bubble read as lifted and the light one as a mid-grey slab two steps darker than every card on the screen. The token states each direction instead: near-white over a light page, a mid grey over a dark one. `--color-tooltip-contrast` is its ink.
409
+ With no intent the bubble is `--color-tooltip`, the one plate in the package chosen per theme rather than derived from an intent: near-white over a light page, a mid grey over a dark one. `--color-tooltip-contrast` is its ink.
401
410
 
402
411
  Near-white is only 1.04:1 against a light page, so the bubble draws a hairline as well, and floors it: a container's `.solid` or `.edgeless` cannot take the boundary away from a bubble nobody classed, while `.tooltip-bubble.edgeless` still can. The ground keeps the bubble opaque at every fill, so a cascaded presentation leaves it readable over whatever it floats above. It reads the material tokens too, so an [aesthetic](docs/usage/aesthetics.md) in scope shapes the bubble like any other component. See [Tooltips](docs/usage/tooltips.md) for the full tooltip reference.
403
412
 
@@ -524,15 +533,15 @@ Every row is an override point: set it on `:root` to retheme globally, or on any
524
533
 
525
534
  Intent classes set these seven slots; components read them. Because every supporting component reads the same seven names, an intent works on every component that supports intent, and a custom intent needs no component changes.
526
535
 
527
- | Token | Purpose |
528
- | ------------------- | ------------------------------------------------------------------------------------- |
529
- | `--intent-color` | The intent's base color. What a fill is made of. |
530
- | `--intent-contrast` | Readable color on top of a filled `--intent-color`. |
531
- | `--intent-hover` | The intent's hovered base color. |
532
- | `--intent-strong` | The intent printed on a page. Text, icon, or border wherever the fill is not full. |
533
- | `--intent-subtle` | Low-emphasis tone. Tinted surfaces and tracks. Neutral resolves to `--color-surface`. |
534
- | `--intent-border` | Line color. Resolves to `--color-border` when no intent is set. |
535
- | `--intent-fill-max` | How far a fill of this intent is allowed to go. `100%` for every intent but neutral. |
536
+ | Token | Purpose |
537
+ | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
538
+ | `--intent-color` | The intent's base color. What a fill is made of. |
539
+ | `--intent-contrast` | Readable color on top of a filled `--intent-color`. |
540
+ | `--intent-hover` | The intent's hovered base color. |
541
+ | `--intent-strong` | The intent printed on a page. Text, icon, or border wherever the fill is not full. |
542
+ | `--intent-subtle` | Low-emphasis tone. Tinted surfaces and tracks. Neutral resolves to `--color-surface`. |
543
+ | `--intent-border` | Line color. With no intent it resolves to the aesthetic's `--ui-ink`, then `--color-border`; text controls and toggles read `--ui-control-ink`, then `--ui-ink`, then `--color-control-border`, and tables read `--ui-rule-ink` first. |
544
+ | `--intent-fill-max` | How far a fill of this intent is allowed to go. `100%` for every intent but neutral. |
536
545
 
537
546
  `--intent-color` and `--intent-strong` are two ends of the same family and not interchangeable. The base is a ground, chosen to be filled with; the strong tone is chosen to be read against a page. A `.soft` or `.ghost` component prints the strong one, which is why an unfilled warning reads in amber-800 rather than the amber-600 its solid sibling is filled with.
538
547
 
@@ -544,7 +553,7 @@ The strong tone has to stay recognisably its own hue, not just readable. On a da
544
553
 
545
554
  The foreground is gated by the plate rather than tied to the fill. `--intent-contrast` is the ink for a _full_ fill, so printing it at full strength on a fifth of one puts white on light grey. Contrast ink therefore appears only once the fill is past halfway and reaches full at a full fill; below that a component prints `--intent-strong`, the tone chosen to be read.
546
555
 
547
- That means a capped neutral takes the strong tone whole rather than a fraction of the way toward the page. Tying it to the fill instead cost a neutral `.solid` button four points of contrast and a neutral tooltip nearly ten. A state that lifts the cap on purpose — a checked checkbox is filled with its own intent by definition — reaches a full fill and gets the contrast whole.
556
+ So a capped neutral takes the strong tone whole rather than a fraction of the way toward the page. A state that lifts the cap on purpose — a checked checkbox is filled with its own intent by definition — reaches a full fill and gets the contrast whole.
548
557
 
549
558
  Set them directly to define an intent this package does not ship. **State every slot**, including `--intent-fill-max`: components reset all seven to the neutral values, so a slot your intent leaves out keeps neutral's — and an intent that omits the cap fills to 20% of its color instead of 100%.
550
559
 
@@ -572,36 +581,35 @@ Foundation tokens are not aliases for one color. They define layout, shape, moti
572
581
 
573
582
  The four `--elevation-*` values below are raw shadows for elements you style yourself. They are a separate thing from `.flat`/`.raised`/`.floating`, which scale a _component's own_ shadow geometry and default to no shadow at all until an aesthetic or one of those classes asks for depth — see [Composing → Elevation](docs/usage/composing.md#elevation) for that system. The two share `none`/`sm`/`md`/`lg` naming for the same weight of depth, but one is a fixed value and the other multiplies shadow parts, so they will not always paint identically.
574
583
 
575
- | Token | Purpose |
576
- | -------------------------- | ----------------------------------------------------------------------- |
577
- | `--font-default` | Default app font family. |
578
- | `--container-narrow` | Narrow content width. |
579
- | `--container-max` | Default content width. |
580
- | `--container-wide` | Wide content width. |
581
- | `--layout-gutter` | Responsive inline page padding. |
582
- | `--layout-section-block` | Responsive section vertical padding. |
583
- | `--layout-gap` | Shared gap for `.view`, `.stack`, `.cluster`, and `.auto-grid`. |
584
- | `--layout-grid-min` | Minimum column width for `.auto-grid`. |
585
- | `--radius-small` | Small radius for compact utility surfaces such as code elements. |
586
- | `--radius-control` | Radius for buttons, inputs, tooltips, and compact UI. |
587
- | `--radius-surface` | Radius for cards, panels, and alerts. |
588
- | `--control-height` | Default minimum height for controls. |
589
- | `--border-width` | Default border width. |
590
- | `--elevation-color` | Shadow color the four elevations compose from. Heavier in dark themes. |
591
- | `--elevation-none` | No shadow. Use on an element that should read as flat against its page. |
592
- | `--elevation-sm` | A light shadow for your own surface-level elements, such as cards. |
593
- | `--elevation-md` | A shadow for your own raised elements, such as dropdowns and popovers. |
594
- | `--elevation-lg` | A shadow for your own full overlays, such as modal dialogs. |
595
- | `--focus-ring` | Focus-visible ring color. |
596
- | `--focus-ring-offset` | Focus-visible outline offset. Negative, so the ring lands on the edge. |
597
- | `--focus-ring-width` | Focus-visible outline/ring width. |
598
- | `--motion-duration-fast` | Fast transition for buttons and input interactions (`120ms`). |
599
- | `--motion-duration-normal` | Normal transition for layout and modals (`200ms`). |
600
- | `--motion-duration-slow` | Slow animation for skeleton loaders and progress bars (`400ms`). |
601
- | `--motion-ease` | Default easing curve. |
602
- | `--z-popover` | Popover/tooltip z-index. |
603
- | `--breakpoint-xs` | Extra-small Tailwind responsive breakpoint. |
604
- | `--breakpoint-2xl` | Extended large Tailwind responsive breakpoint. |
584
+ | Token | Purpose |
585
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
586
+ | `--font-default` | Default app font family. |
587
+ | `--container-narrow` | Narrow content width. |
588
+ | `--container-max` | Default content width. |
589
+ | `--container-wide` | Wide content width. |
590
+ | `--layout-gutter` | Responsive inline page padding. |
591
+ | `--layout-section-block` | Responsive section vertical padding. |
592
+ | `--layout-gap` | Shared gap for `.view`, `.stack`, `.cluster`, and `.auto-grid`. |
593
+ | `--layout-grid-min` | Minimum column width for `.auto-grid`. |
594
+ | `--radius-small` | Small radius for compact utility surfaces such as code elements. |
595
+ | `--radius-control` | Radius for buttons, inputs, tooltips, and compact UI. |
596
+ | `--radius-surface` | Radius for cards, panels, and alerts. |
597
+ | `--control-height` | Default minimum height for controls. |
598
+ | `--border-width` | Default border width. |
599
+ | `--elevation-color` | Shadow color the four elevations compose from. Heavier in dark themes. |
600
+ | `--elevation-none` | No shadow. Use on an element that should read as flat against its page. |
601
+ | `--elevation-sm` | A light shadow for your own surface-level elements, such as cards. |
602
+ | `--elevation-md` | A shadow for your own raised elements, such as dropdowns and popovers. |
603
+ | `--elevation-lg` | A shadow for your own full overlays, such as modal dialogs. |
604
+ | `--focus-ring` | Focus-visible ring color. |
605
+ | `--focus-ring-offset` | Focus-visible outline offset. Unset: the negative of the focused element's line width, so the ring lands on the edge under every aesthetic. |
606
+ | `--focus-ring-width` | Focus-visible outline/ring width. |
607
+ | `--motion-duration-fast` | Fast transition for a table row's hover (`120ms`). |
608
+ | `--motion-duration-normal` | Every component's colour, border, shadow, and press transitions, toggle marks, and the tooltip bubble (`200ms`). |
609
+ | `--motion-duration-slow` | Progress value changes, and the base of the skeleton and progress shimmer cycle (`400ms`). |
610
+ | `--motion-ease` | Default easing curve. |
611
+ | `--z-popover` | Popover/tooltip z-index. |
612
+ | `--breakpoint-xs` | Extra-small Tailwind responsive breakpoint. |
605
613
 
606
614
  Next: [Customizing](docs/usage/customizing.md) covers presentation and material tokens — how much of an intent shows, and what a component is made of.
607
615
 
@@ -657,6 +665,7 @@ Components read each with a fallback, so leaving them unset produces the default
657
665
  | `--ui-border-max` | Ceiling on computed edge width. | `100px` |
658
666
  | `--ui-ink` | Neutral line color when no intent is set. | `--color-border` |
659
667
  | `--ui-control-ink` | Neutral line color of fields, selects, checkboxes, radios, and switches when no intent is set. | `--ui-ink`, then `--color-control-border` |
668
+ | `--ui-rule-ink` | Neutral line color of a table's frame and rules when no intent is set. | `--ui-ink`, then `--color-border` |
660
669
  | `--ui-rule` | Lines inside a table. Unset draws the head and foot boundaries only; `0%` draws none; `100%` rules every row. | unset |
661
670
  | `--ui-shadow-x` | Shadow horizontal offset. | `0px` |
662
671
  | `--ui-shadow-y` | Shadow vertical offset. | `0px` |
@@ -675,9 +684,9 @@ Components read each with a fallback, so leaving them unset produces the default
675
684
  | `--ui-shadow-edge` | Declared, even empty, when the shadow is the element's edge rather than its depth. Read for presence. | Unset |
676
685
  | `--ui-elevation` | Unitless multiplier over shadow geometry. | `1` |
677
686
  | `--ui-surface-shadow` | Complete multi-layer shadow accepted by surfaces only. | Unset |
678
- | `--ui-surface-ground` | Ground a surface sits on. | `--color-background` |
679
- | `--ui-bg-alpha` | Multiplier over fill for translucent materials. | `1` |
680
- | `--ui-backdrop` | Backdrop filter accepted by surfaces only. | `none` |
687
+ | `--ui-surface-ground` | Ground a surface, or a default or `.soft` table, sits on. | `--color-background` |
688
+ | `--ui-bg-alpha` | Multiplier over every fill, `.solid` included. No shipped aesthetic sets it. | `1` |
689
+ | `--ui-backdrop` | Backdrop filter accepted by surfaces, and by a default or `.soft` table, only. | `none` |
681
690
  | `--ui-surface-image` | A painted layer over a surface's plate: any `background-image` value. Surfaces only. | `none` |
682
691
  | `--ui-hover-transform` | Transform while an interactive element is hovered. | `none` |
683
692
  | `--ui-clip` | Structural component silhouette. | `none` |
@@ -751,8 +760,8 @@ The shipped [aesthetic classes](docs/usage/aesthetics.md) set material tokens fo
751
760
  | Token | Aesthetic | Purpose | Default |
752
761
  | --------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
753
762
  | `--neo-offset` | `.neobrutalism` | Hard shadow offset and distance the press travels. | `4px` |
754
- | `--glass-radius` | `.glass` | Control corner radius. | `0.75rem` |
755
- | `--glass-radius-surface` | `.glass` | Surface corner radius. | `1rem` |
763
+ | `--glass-radius` | `.glass`, `.glass-liquid` | Control corner radius. `1rem` under `.glass-liquid`. | `0.75rem` |
764
+ | `--glass-radius-surface` | `.glass`, `.glass-liquid` | Surface corner radius. `1.5rem` under `.glass-liquid`. | `1rem` |
756
765
  | `--pixel-unit` | `.pixel` | One pixel of the grid. The corner cut and the ring are one. | `4px` |
757
766
  | `--font-pixel` | `.pixel` | Consumer-supplied pixel font stack. | Falls back to the monospace stack |
758
767
  | `--tile-radius` | `.chunky-tile` | Corner radius, on controls and surfaces alike. | `0.75rem` |
@@ -789,7 +798,7 @@ Every aesthetic's knobs reach the same way: each knob is _read_ with its default
789
798
 
790
799
  Knobs and material tokens reach in opposite directions, and telling them apart matters. A knob resolves once at the aesthetic's own class and inherits down already-resolved, so it reaches the aesthetic **from above**: setting it on the element or any ancestor works, but setting it on a _descendant_ of the aesthetic class changes nothing — the value has already been read. `--ui-radius` and the other material tokens above are read fresh by each component as it draws, so they reach **at or below** the element carrying them, which is how you round one card differently from its neighbours without touching the knob at all.
791
800
 
792
- Shipped aesthetics set the shared material tokens above. Every shipped aesthetic sets `--ui-ink` for its neutral line. `.glass` and `.chunky-tile` also set `--ui-control-ink`, so their controls keep a heavier line than their surfaces; the others clear it and their controls take `--ui-ink`. Intent classes still override the intent slots on each component.
801
+ Shipped aesthetics set the shared material tokens above. Every shipped aesthetic sets `--ui-ink` for its neutral line. Every shipped aesthetic also sets `--ui-control-ink`: `.glass` and `.chunky-tile` to a line of their own, heavier than their surfaces', and the others to `--color-control-border`, since their ink is the primary colour in both themes. `.glass` sets `--ui-rule-ink` too, so a table's lines read on its plate; the others clear it. Intent classes still override the intent slots on each component.
793
802
 
794
803
  The same knobs tune each aesthetic's [solo class](docs/usage/aesthetics.md#solo-classes), which reads them on its own element.
795
804
 
@@ -816,7 +825,7 @@ Naming: `--palette-<intent>-<presentation>-<slot>`, where `<intent>` is one of t
816
825
  - A ground qualifier (`page` or `subtle`) inserted before the slot, present only on `.soft`/`.ghost`'s `bg` and `edge` -- `.solid` is ground-independent at full fill, and `fg` never varies by ground. No qualifier means the `transparent` ground (`.badge`/`.btn`'s ground): `--palette-success-soft-bg`. `page` is `.alert`'s ground (`--color-background`): `--palette-success-soft-page-bg`. `subtle` is `.pre`/`.code`/`.kbd`/`.tooltip-bubble`'s ground (that intent's own `--intent-subtle`): `--palette-success-soft-subtle-bg`.
817
826
  - A `-hover` suffix on `bg` and `edge` only -- `fg` never changes on hover: `--palette-success-soft-page-bg-hover`.
818
827
 
819
- Dark values are scoped the same way `@codenhub/styles`' own explicit theme override is: `.dark`, `.theme-dark`, and `[data-theme="dark"]`, on the element or an ancestor. There is no `prefers-color-scheme` fallback in this file -- a consumer reaching for a standalone palette is already handling its own theme switching, so the OS-preference case is left to `@codenhub/styles`' own `light-dark()`-based tokens when that package is actually in use.
828
+ Light values sit on `:root` and dark values are scoped the same way `@codenhub/styles`' own explicit theme override is: `.dark`, `.theme-dark`, and `[data-theme="dark"]`, on the element or an ancestor. The light values also answer `.light`, `.theme-light`, and `[data-theme="light"]`, so the nearest explicit theme wins at any depth -- a light subtree nested inside a dark one reads the light values. There is no `prefers-color-scheme` fallback in this file -- a consumer reaching for a standalone palette is already handling its own theme switching, so the OS-preference case is left to `@codenhub/styles`' own `light-dark()`-based tokens when that package is actually in use.
820
829
 
821
830
  Three more tokens sit alongside the intent cells for the same reason, without an intent, presentation, ground, or hover axis of their own: `--palette-border`, `--palette-surface`, and `--palette-text`, flat values for `--color-border`, `--color-surface`, and `--color-text` -- the resting colors anything that is not intent-colored needs (a container's border, a dialog's background, its text), which an optional-peer consumer needs just as much as the intent cells.
822
831
 
@@ -826,8 +835,6 @@ Component classes may define scoped implementation variables such as `--surface-
826
835
 
827
836
  Consumers should customize broad behavior through color, foundation, motion, elevation, radius, layout, and focus tokens first. The only supported component-scoped input is `--progress-value` on the `.progress` element, because consumers must provide a value such as `64%`.
828
837
 
829
- `--layout-gap` replaces the removed `--layout-stack-gap` and `--layout-cluster-gap` tokens. No compatibility aliases are provided.
830
-
831
838
  Next: [Aesthetics](docs/usage/aesthetics.md) covers each shipped aesthetic's complete look and its documented exceptions.
832
839
 
833
840
  <!-- Source: docs/usage/aesthetics.md -->
@@ -841,7 +848,7 @@ An aesthetic decides what a component is _made of_: its radius, border thickness
841
848
  @import "@codenhub/styles/aesthetics";
842
849
  ```
843
850
 
844
- Import them after the base stylesheet. `.neobrutalism` and `.pixel` replace the neutral border color, and they do so at zero specificity, so source order is what lets them win.
851
+ Load order does not matter: the aesthetic classes sit in the `components` cascade layer, and every entrypoint declares the layer order before it uses one, so an aesthetic imported before the base stylesheet wins exactly as one imported after it. See [Setup → Cascade layers](docs/setup.md#cascade-layers).
845
852
 
846
853
  Like presentation, an aesthetic class cascades to any subtree:
847
854
 
@@ -873,7 +880,7 @@ Each aesthetic that scales from one number publishes it as a knob — see [Custo
873
880
 
874
881
  ## Neobrutalism
875
882
 
876
- The shadow is cast in the component's own intent, so a success button throws a green shadow and a destructive card a red one. With no intent, both the outline and the shadow use the ink, which follows the theme rather than the palette.
883
+ The shadow is cast in the component's own intent, so a success button throws a green shadow and a destructive card a red one. With no intent, both the outline and the shadow use the ink, which follows the theme rather than the palette. Fields and toggles are the exception: they draw the theme's control border, as they do with no aesthetic, because the ink is the primary colour in both themes and a resting checkbox drawn in it reads as loudly as a primary one. The same holds under pixel, cyber, and sketch.
877
884
 
878
885
  `--neo-offset` is the knob: the shadow offset and the distance the press travels are the same number, so scaling the look scales both at once.
879
886
 
@@ -888,6 +895,7 @@ The shadow is cast in the component's own intent, so a success button throws a g
888
895
  - Content chips are left alone. A `.kbd`, `.code`, or `.pre` is content rather than structure, and an ink edge on one reads as a defect: a key cap keeps the quiet border color and a code chip keeps no border at all.
889
896
  - Only `.btn` and an opted-in `.card` respond to hover and press. A card opts in with `.interactive`, or with one of its halves — `.hoverable` for the hover response alone, `.pressable` for the press alone; a plain card, an alert, or a panel is a container and stays put.
890
897
  - An alert rests on the slab under this aesthetic and nowhere else. Here the slab is a second ink line rather than depth, and an alert is the only container the package rests flat, so without it a neobrutalist alert is the one box on the page that reads as unfinished. It is a resting default, so `.flat` still takes it off and `.floating` still doubles it.
898
+ - The tooltip trigger takes the zero corner like everything else and draws square; `.radio` keeps its circle.
891
899
 
892
900
  ## Glass
893
901
 
@@ -904,9 +912,35 @@ The shadow is cast in the component's own intent, so a success button throws a g
904
912
  **Exceptions:**
905
913
 
906
914
  - Every surface in a glass region is glass, a neutral `.card.soft`, `.panel`, and `.alert` included.
907
- - The blur applies to `.card`, `.panel`, `.alert`, and the tooltip bubble only. Controls stay solid and sit on the glass: a blur under every control of a dense cluster costs a composited layer apiece and reads as noise. Controls still take the aesthetic's radius, border, and shadow.
915
+ - The glass is the ground, not the fill. A presentation keeps its whole fill, so a `.solid` button, a checked toggle, and a `.solid` card or table are opaque; the default, `.soft`, and `.ghost` panes are the frosted ones.
916
+ - The blur applies to `.card`, `.panel`, `.alert`, the tooltip bubble, and a default or `.soft` `.data-table` only. Controls stay solid and sit on the glass: a blur under every control of a dense cluster costs a composited layer apiece and reads as noise. Controls still take the aesthetic's radius, border, and shadow.
908
917
  - Under `prefers-reduced-transparency: reduce`, glass surfaces drop the blur and become opaque.
909
- - Controls draw their line in the page's ink at 55% rather than the white hairline, which would erase an unchecked checkbox on a light page. It is `--ui-control-ink`; set it on the region to choose another.
918
+ - Buttons and an opted-in `.card` press with the plain `scale(0.97)`; reduced motion turns it off.
919
+ - Text controls and toggles draw their line in the page's ink at 55% rather than the white hairline, which would erase an unchecked checkbox on a light page. It is `--ui-control-ink`; set it on the region to choose another.
920
+ - A table's frame and rules draw the page's ink at 20%, because the hairline vanishes on the table's plate. It is `--ui-rule-ink`.
921
+ - A table with a `<caption>` blurs behind the caption as well as its rows in Chromium and Safari, whose table box includes the caption, and under `.glass-liquid` the rim and sheen frame the caption too. Firefox keeps the caption off the pane. Label the table from a heading outside it with `aria-labelledby` where that matters.
922
+
923
+ ### Liquid glass
924
+
925
+ `.glass-liquid` is a modifier of glass, after Apple's Liquid Glass: continuous corners, a bright rim where light catches the pane, a lighter blur, a clearer ground, and a lens that bends what is behind a surface at its edges. Put it beside `.glass` on the region, or on one surface inside a glass region.
926
+
927
+ ```html
928
+ <section class="glass glass-liquid">
929
+ <div class="card">A pane that bends the page behind it</div>
930
+ </section>
931
+ ```
932
+
933
+ The lens is drawn by an SVG filter in `backdrop-filter`, which Chromium renders and Firefox and Safari do not yet. Elsewhere the pane keeps everything but the lens: the rim, the blur, the ground, and the corners, round rather than continuous where `corner-shape` is not drawn. Nothing needs to be in your markup: the filter ships inside the stylesheet.
934
+
935
+ **Exceptions:**
936
+
937
+ - It reaches `.card`, `.panel`, `.alert`, the tooltip bubble, and a default or `.soft` `.data-table`. Controls stay solid, as they do under glass.
938
+ - It draws its blur and lens on each surface's `::before` and `::after`, so a surface inside a liquid region cannot carry a `::before` or `::after` of your own, and each surface becomes a positioned stacking context -- a surface you position yourself keeps your placement.
939
+ - The lens is 8px of erosion blurred over 10px at every size, and its strength is fixed. A filter cannot read a custom property, so neither is a knob. The corners are, through `--glass-radius` and `--glass-radius-surface`, with larger defaults: a continuous corner reads smaller than a round one of the same radius.
940
+ - A region of another aesthetic nested inside a liquid region, or of plain `.glass`, keeps its own material and takes no layers.
941
+ - Without `.glass` in scope it changes the corners and nothing else.
942
+ - Under `prefers-reduced-transparency: reduce` the blur and the lens go and the surface is opaque, as under glass; in forced colours the layers draw nothing.
943
+ - Text sits over a clearer pane than under glass. Over busy content, check that your text still reads -- the ground is 30% of the page colour, where glass's is 45%.
910
944
 
911
945
  ## Pixel
912
946
 
@@ -920,9 +954,9 @@ The shadow is cast in the component's own intent, so a success button throws a g
920
954
 
921
955
  **Exceptions:**
922
956
 
923
- - Corners are one unit or nothing. Chips square instead of stepping — badges, key caps, code, checkboxes, and switches all read `--ui-clip-tight`, which this aesthetic sets to none, because one unit off each corner of a 24px badge is a bite rather than a corner. Tables, progress bars, and skeletons square too, each for its own reason: a table's `overflow: hidden` fights the clip and the corners square off where the two meet, and progress and skeleton never read a clip at all — a squaring aesthetic reaches them through `border-radius` alone. `.pre` is the one exception that steps: it carries no clip override, so it inherits the same polygon a button or a card gets, and with no border by default the cut shows with no ring around it.
957
+ - Corners are one unit or nothing. Chips square instead of stepping — badges, key caps, code, checkboxes, and switches all read `--ui-clip-tight`, which this aesthetic sets to none, because one unit off each corner of a 24px badge is a bite rather than a corner. Tables, progress bars, and skeletons square too, each for its own reason: a table takes no clip, because in Chromium and WebKit its box includes its caption and the clip would cut the caption rather than the rows, and progress and skeleton never read a clip at all — a squaring aesthetic reaches them through `border-radius` alone. `.pre` is the one exception that steps: it carries no clip override, so it inherits the same polygon a button or a card gets, and with no border by default the cut shows with no ring around it.
924
958
  - The outline is an inset ring, because a clip removes a real border — and the focus ring, for the same reason, is a second inset layer rather than an outline. Both are the element's own edge rather than a shadow, so the border answers `.edged` and `.edgeless` the way a border does: a `.edgeless` badge and a `.solid` button draw none, a `.edged` card draws one, and a field keeps one whatever a container asks for.
925
- - The tooltip trigger and `.radio` are hardcoded past the clip rather than reached by it. `.tooltip-icon` forces `clip-path: none` because the docs call it a circular fixed identity, the same reason `.radio` forces it: the circle is the only thing telling it from a checkbox at a glance, and a stepped polygon would square either one.
959
+ - The tooltip trigger and `.radio` are hardcoded past the clip rather than reached by it. `.tooltip-icon` and `.radio` force `clip-path: none`: the circle is the only thing telling a radio from a checkbox at a glance, and a stepped polygon would bite a 20px trigger rather than shape it. The tooltip trigger still takes pixel's zero corner and draws square; the radio keeps its circle.
926
960
  - `--font-pixel` is yours to supply. The package ships no font binary, so the aesthetic has no network side effect and falls back to the monospace stack.
927
961
  - Numeral legibility depends on the face you pick. At UI sizes -- pagination, counters, small badges -- some pixel/bitmap faces render lookalike digits such as `2`/`8` or `3`/`9` closely enough to misread at a glance; this is a property of the font, not something the aesthetic's CSS can correct. Prefer a face with clearly distinct digit shapes wherever small numeric text matters.
928
962
 
@@ -998,7 +1032,7 @@ A parallelogram slants its sides into the content, so give a surface that takes
998
1032
 
999
1033
  ## Sketch
1000
1034
 
1001
- Hand-drawn outlines: controls take long, uneven elliptical corners, and cards, panels, and alerts take sharper ones that stay clear of their padding. Siblings rotate through four outlines by position, and a field's control takes its field's position, so a row of buttons or a stacked form does not repeat one shape. The rotation is stable for a given DOM order, not random on each render. Every outline is a complete `border-radius`, used as written rather than scaled by the size step or placed by `.cut-diagonal`.
1035
+ Hand-drawn outlines: controls take long, uneven elliptical corners, and cards, panels, alerts, and tables take sharper ones that stay clear of their padding. Siblings rotate through four outlines by position, and a field's control takes its field's position, so a row of buttons or a stacked form does not repeat one shape. The rotation is stable for a given DOM order, not random on each render. Every outline is a complete `border-radius`, used as written rather than scaled by the size step or placed by `.cut-diagonal`.
1002
1036
 
1003
1037
  The 1px ink line is solid; checkboxes, dividers, and table rules are dashed. `--ui-line-style: dashed` dashes any other component, and `--ui-rule-style: solid` puts a divider or table back to solid. The 2px hard shadow is a small neutral depth cue.
1004
1038
 
@@ -1038,7 +1072,7 @@ An aesthetic class sets material tokens and nothing paints until a component rea
1038
1072
 
1039
1073
  | Aesthetic | Solo class | Paints |
1040
1074
  | --------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1041
- | `.glass` | `.glass-solo` | Translucent ground, hairline edge, blur, the tucked two-layer shadow, the surface corner. |
1075
+ | `.glass` | `.glass-solo` | Translucent ground, hairline edge, blur, the tucked two-layer shadow, the surface corner. With `.glass-liquid`, the liquid pane on its `::before` and `::after`. |
1042
1076
  | `.neobrutalism` | `.neobrutalism-solo` | 2px ink edge, square corners, the hard offset slab. |
1043
1077
  | `.pixel` | `.pixel-solo` | The stepped silhouette, the inset ring in place of a border, the pixel font. |
1044
1078
  | `.chunky-tile` | `.chunky-tile-solo` | The tile corner, 2px edge, the bar under it, the rounded font. |
@@ -1099,7 +1133,7 @@ Size and shape classes:
1099
1133
  | `.p-xs` | Tightest padding modifier (`px-2 py-0.5`; icon `p-0.5`). Also drops the `--control-height` minimum so a chip-sized button fits a table cell or a dense toolbar; every other size and padding class keeps that floor for a labeled button. |
1100
1134
  | `.p-sm` | Compact padding modifier (`px-2.5 py-1`; icon `p-1`). |
1101
1135
  | `.p-lg` | Spacious padding modifier (`px-6 py-3`; icon `p-3`). |
1102
- | `.icon` | Square icon button, sized to its own padding rather than to `--control-height`. Use an accessible name in HTML. |
1136
+ | `.icon` | Square icon button, at least `--control-height` on each side, or 2rem with `.sm` and 3rem with `.lg`. With `.p-xs`, `.p-sm`, or `.p-lg` it drops that floor and sizes to its own padding. Use an accessible name in HTML. |
1103
1137
  | `.disabled`, `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` | Disabled styling. |
1104
1138
 
1105
1139
  Examples:
@@ -1128,55 +1162,69 @@ Use `<button>` for actions or an accessible link for navigation; see [Accessibil
1128
1162
 
1129
1163
  # Forms
1130
1164
 
1131
- | Class or Selector | Purpose |
1132
- | --------------------------------------------------------------------- | ------------------------------------------------------------------------ |
1133
- | `.field` | Vertical field wrapper. |
1134
- | `.label` | Form label text. |
1135
- | `.hint` | Secondary helper text. |
1136
- | `.hint.destructive` | Helper text with destructive intent. |
1137
- | `.surface` | Shared public container composition utility. |
1138
- | `.text-control` | Shared public text-control composition utility. |
1139
- | `.ipt` | Input control styling. |
1140
- | `.input-group` | Wrapper that owns the field box so a control can carry an icon or affix. |
1141
- | `.textarea` | Textarea control styling. |
1142
- | `.select` | Select control styling. |
1143
- | `input[type="checkbox"].checkbox` | Custom checkbox control styling. |
1144
- | `input[type="radio"].radio` | Custom radio control styling. |
1145
- | `input[type="checkbox"].switch` | Custom switch control styling. |
1146
- | `[aria-invalid="true"]` on controls | Destructive border and focus color. |
1147
- | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` on controls | Disabled styling. |
1165
+ | Class or Selector | Purpose |
1166
+ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
1167
+ | `.field` | Vertical field wrapper. |
1168
+ | `.label` | Form label text. |
1169
+ | `.hint` | Secondary helper text. |
1170
+ | `.hint.destructive` | Helper text with destructive intent. |
1171
+ | `.surface` | Shared public container composition utility. |
1172
+ | `.text-control` | Shared public text-control composition utility. |
1173
+ | `.ipt` | Input control styling. |
1174
+ | `.input-group` | Wrapper that owns the field box so a control can carry an icon or affix. |
1175
+ | `.unadorned` | Removes the browser's own field decorations, on the control or on its `.input-group`. |
1176
+ | `.textarea` | Textarea control styling. |
1177
+ | `.select` | Select control styling. With `multiple` or a `size` above 1 it is a list box: no chevron, as tall as its rows. |
1178
+ | `input[type="checkbox"].checkbox` | Custom checkbox control styling. |
1179
+ | `input[type="radio"].radio` | Custom radio control styling. |
1180
+ | `input[type="checkbox"].switch` | Custom switch control styling. |
1181
+ | `[aria-invalid="true"]` on controls | Destructive border and focus color. |
1182
+ | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]` on controls | Disabled styling. |
1148
1183
 
1149
1184
  ## Icons
1150
1185
 
1151
- No icon is shipped for text inputs — that's what this section covers. It used to paint one per input type as a `background-image` — a `data:` URI, which cannot read `currentColor` or a custom property, so every glyph shipped a light and a dark copy and a theme selector chose between them. That put icon artwork, and the package's only theme-by-selector branch, inside a CSS-only design system whose glyphs did not fit every aesthetic. Icons for `.input-group` are the consumer's to choose now.
1152
-
1153
- This is separate from the toggle marks below, which stay small, fixed, built-in glyphs. `.alert`'s icon followed the same path since — see [Feedback → Icon](docs/usage/feedback.md#example) — the package no longer paints one there either.
1186
+ The package ships no icon for text inputs. Icons for `.input-group` are the consumer's to choose: artwork painted as a `data:` URI could not read `currentColor` or a custom property, so it could not follow the theme or fit every aesthetic. The toggle marks below are the exception: small, fixed, built-in glyphs. `.alert` likewise takes a consumer-supplied icon; see [Feedback](docs/usage/feedback.md#example).
1154
1187
 
1155
1188
  `.input-group` is the wrapper for a control that carries one. It owns the field box — border, radius, fill, focus ring, invalid and disabled state — and the control inside it goes flush, so the boundary is drawn once. Any icon element works, placed before the control for a leading icon or after it for a trailing one: an inline `<svg>`, an `<img>`, or a class from an icon set such as `@codenhub/icons`.
1156
1189
 
1157
1190
  ```html
1158
- <label class="field">
1159
- <span class="label">Email</span>
1191
+ <div class="field">
1192
+ <label class="label" for="contact-email">Email</label>
1160
1193
  <div class="input-group">
1161
1194
  <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" aria-hidden="true">
1162
1195
  <rect width="20" height="16" x="2" y="4" rx="2" />
1163
1196
  <path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7" />
1164
1197
  </svg>
1165
- <input class="ipt" type="email" />
1198
+ <input class="ipt" type="email" id="contact-email" />
1166
1199
  </div>
1167
- </label>
1168
- <label class="field">
1169
- <span class="label">Search</span>
1200
+ </div>
1201
+ <div class="field">
1202
+ <label class="label" for="site-search">Search</label>
1170
1203
  <div class="input-group">
1171
- <input class="ipt" type="search" />
1204
+ <input class="ipt" type="search" id="site-search" />
1172
1205
  <i class="ic-search" aria-hidden="true"></i>
1173
1206
  </div>
1174
- </label>
1207
+ </div>
1175
1208
  ```
1176
1209
 
1177
1210
  The group reads intent and both [presentation](docs/usage/composing.md#presentation) axes the same way a lone `.ipt` does — `.input-group.soft.edgeless` is the sunk variant, `.input-group.primary` colors the boundary — and follows whichever [aesthetic](docs/usage/aesthetics.md) is in scope. `aria-invalid` or `disabled` on the control inside propagates to the group; so does either on the group itself. Focus is shown with `:focus-within`, so unlike a lone field the ring also appears on a mouse click.
1178
1211
 
1179
- Non-icon input types keep the browser's native `date` and `datetime-local` pickers. WebKit's native `search` decorations are still suppressed on `.text-control` to keep them from overlapping the value.
1212
+ Text controls keep the browser's own decorations: the date and time picker button, a number field's spin buttons, and a search field's clear button and magnifier. Each one is also behaviour -- the picker button is the only pointer route to the picker, and the clear button empties the field.
1213
+
1214
+ `.unadorned` removes all of them, for a field whose adornments you draw yourself. Put it on the control, or on its `.input-group` to reach the control inside. Removing a decoration removes what it did, so wire the behaviour back to your own adornment: `input.showPicker()` for a picker, `stepUp()`/`stepDown()` for spin buttons, and setting `value` to `""` and dispatching an `input` event for a clear button. Keyboard entry keeps working either way. Firefox exposes no way to remove its date picker button, so it stays there.
1215
+
1216
+ ```html
1217
+ <div class="field">
1218
+ <label class="label" for="query">Search</label>
1219
+ <div class="input-group unadorned">
1220
+ <i class="ic-search" aria-hidden="true"></i>
1221
+ <input class="ipt" type="search" id="query" />
1222
+ <button type="button" class="btn icon ghost p-xs" aria-label="Clear search">
1223
+ <i class="ic-x" aria-hidden="true"></i>
1224
+ </button>
1225
+ </div>
1226
+ </div>
1227
+ ```
1180
1228
 
1181
1229
  The slot works the same way for a text affix — a URL host, a currency symbol, a unit — not just an icon:
1182
1230
 
@@ -1213,6 +1261,8 @@ All three toggles rest at `.soft`. Presentation decides the _unchecked_ plate; c
1213
1261
 
1214
1262
  A checked checkbox and a checked switch cut their mark out of that pinned plate. A checked radio thickens its ring to twice the resting line and takes the intent whole on it, and the dot follows the same pinned plate, so `.radio.soft` and `.radio.solid` render the same filled circle once checked — presentation only changes what the radio looks like before it's picked.
1215
1263
 
1264
+ A checkbox whose `indeterminate` property is set -- a group some of whose items are checked -- takes the same pinned plate with a dash for its mark, and keeps the dash while it is also `checked`, as the browser's own checkbox does. The property is set from script (`input.indeterminate = true`); there is no attribute for it.
1265
+
1216
1266
  ## Text controls
1217
1267
 
1218
1268
  Text controls also take intent, which colors the resting border and the focus-visible border, and both [presentation](docs/usage/composing.md#presentation) axes. `.ghost` fills nothing, `.soft` takes `12%`, and `.solid` takes `20%` — quiet enough that typed text still reads on it, and ordered so the louder name draws the stronger tint. None of them touches the line.
@@ -1245,11 +1295,11 @@ The boundary is the edge axis's, and it splits the same way. A container's `.edg
1245
1295
  ## Example
1246
1296
 
1247
1297
  ```html
1248
- <label class="field">
1249
- <span class="label">Email</span>
1250
- <input class="ipt" type="email" aria-invalid="true" aria-describedby="email-error" />
1298
+ <div class="field">
1299
+ <label class="label" for="email">Email</label>
1300
+ <input class="ipt" type="email" id="email" aria-invalid="true" aria-describedby="email-error" />
1251
1301
  <span class="hint destructive" id="email-error">Enter a valid email.</span>
1252
- </label>
1302
+ </div>
1253
1303
  <label style="display: flex; gap: 0.5rem; align-items: center">
1254
1304
  <input type="checkbox" class="checkbox success" />
1255
1305
  <span>Accept terms</span>
@@ -1259,12 +1309,12 @@ The boundary is the edge axis's, and it splits the same way. A container's `.edg
1259
1309
  <span>Standard plan</span>
1260
1310
  </label>
1261
1311
  <label style="display: flex; gap: 0.5rem; align-items: center">
1262
- <input type="checkbox" class="switch destructive" />
1312
+ <input type="checkbox" class="switch destructive" role="switch" />
1263
1313
  <span>Enable</span>
1264
1314
  </label>
1265
1315
  ```
1266
1316
 
1267
- Use labels, `type`, validation logic, `aria-describedby`, and error message relationships; see [Accessibility](docs/accessibility.md).
1317
+ Keep a hint or error message outside the `<label>` and point `aria-describedby` at it, so it is announced as a description rather than read as part of the field's name. Give a `.switch` `role="switch"`. Use labels, `type`, validation logic, `aria-describedby`, and error message relationships; see [Accessibility](docs/accessibility.md).
1268
1318
 
1269
1319
  <!-- Source: docs/usage/feedback.md -->
1270
1320
 
@@ -1297,7 +1347,7 @@ Activity indicator modifier classes compose with `.loader`:
1297
1347
  | `.bars-wave` | Three vertical bars scaling in a wave. |
1298
1348
  | `.pulse-ring` | Two concentric rings pulsing outward. |
1299
1349
 
1300
- These variants retain their embedded SVG animations normally. Under `prefers-reduced-motion: reduce`, loader CSS substitutes a corresponding static mask. This fallback is part of focused loader, button, and components imports; it does not depend on the global reset. See [Accessibility](docs/accessibility.md).
1350
+ These variants retain their embedded SVG animations normally. Under `prefers-reduced-motion: reduce`, loader CSS substitutes a corresponding static mask. This fallback ships wherever the loader does -- the complete, native, and `/components` entrypoints, `/tw/components`, and `/tw/loader` -- and does not depend on the global reset. `/tw/button` does not include the loader; pair it with `/tw/loader`. See [Accessibility](docs/accessibility.md).
1301
1351
 
1302
1352
  Loader size modifiers:
1303
1353
 
@@ -1307,7 +1357,7 @@ Loader size modifiers:
1307
1357
  | `.sm` | Small (`1.25rem`). |
1308
1358
  | `.lg` | Large (`2.25rem`). |
1309
1359
 
1310
- Alerts, badges, progress bars, skeletons, and loaders accept `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, and `.info`. Without an intent, they use the text palette, except `.loader`, which keeps `currentColor` so it matches whatever content surrounds it.
1360
+ Alerts, badges, progress bars, skeletons, and loaders accept `.primary`, `.secondary`, `.success`, `.warning`, `.destructive`, and `.info`. Without an intent, alerts and badges use the text palette, while the indicators -- `.loader`, `.skeleton`, and `.progress` -- take `currentColor`, so they match whatever content surrounds them.
1311
1361
 
1312
1362
  Skeletons, loaders, and dividers are indicators: they read intent but ignore fill and edge [presentation](docs/usage/composing.md#presentation). `.progress` is not one of them — its track reads both, `.soft.edgeless` by default; only the moving value fill stays intent-colored at full strength regardless of presentation. `.solid` is unsupported on the track, and `.ghost.edgeless` renders but is discouraged, since neither leaves the track a visible frame. See [Composing → Component axis reference](docs/usage/composing.md#component-axis-reference) for the full table.
1313
1363
 
@@ -1315,7 +1365,9 @@ Skeletons, loaders, and dividers are indicators: they read intent but ignore fil
1315
1365
  <span class="loader success" aria-hidden="true"></span> <span class="skeleton info"></span>
1316
1366
  ```
1317
1367
 
1318
- The package ships no icon of its own: `.alert` is already `flex`, so an icon dropped in as a child is spaced from the message by its `gap-3` with nothing to trigger. `.alert-icon` only sizes and aligns whatever icon element you provide — an inline `<svg>`, an `<img>`, or a class from an icon set such as `@codenhub/icons` — the same contract [`.input-group`](docs/usage/forms.md#icons) documents.
1368
+ The package ships no icon of its own: `.alert` is already `flex`, so an icon dropped in as a child is spaced from the message by its `gap-3`. `.alert-icon` only sizes and aligns whatever icon element you provide — an inline `<svg>`, an `<img>`, or a class from an icon set such as `@codenhub/icons` — the same contract [`.input-group`](docs/usage/forms.md#icons) documents.
1369
+
1370
+ `.alert-icon` sizes the icon to `1.25rem`. A `@codenhub/icons` glyph sizes itself in an unlayered rule that no class of this package can outrank, so `.alert-icon` sets that set's `--ic-size` as well; an icon set with a different prefix keeps its own size unless you set its knob. Resize an alert icon with an ordinary rule of your own, and set `--ic-size` along with `width` and `height` for a `@codenhub/icons` glyph.
1319
1371
 
1320
1372
  Alerts and badges read the shared [presentation](docs/usage/composing.md#presentation) classes. Without one they use a tinted surface, intent-colored text, and a mixed intent border -- except a neutral `.alert` (no named intent), which rests untinted instead, the same no-named-intent carve-out `.card.soft` and `.panel` use: a plain `--color-foreground` plate rather than 12% of near-black ink over the page. A named intent (`.alert.success`, `.alert.destructive`, and so on) keeps its ordinary tint. `.badge` is unaffected either way -- it has no border to fall back on at rest, so untinting it would make a neutral badge disappear rather than read quieter.
1321
1373
 
@@ -1354,24 +1406,25 @@ Add `role="status"` or `role="alert"` to alerts based on announcement urgency, m
1354
1406
 
1355
1407
  # Tooltips
1356
1408
 
1357
- | Class or Attribute | Purpose |
1358
- | -------------------------------------- | ------------------------------------------------------------------------ |
1359
- | `.tooltip` | Wrapper around a trigger and a bubble. Paints nothing itself. |
1360
- | `.tooltip-bubble` | The message. A real element, styled like any other surface. |
1361
- | `.tooltip-icon` | Circular icon-style trigger, for a message with nothing else to hang on. |
1362
- | `[data-tooltip-position="top"]` | Positions the bubble above the trigger, on `.tooltip`. |
1363
- | `[data-tooltip-position="bottom"]` | Positions the bubble below the trigger, on `.tooltip`. |
1364
- | `[data-tooltip-position="left"]` | Positions the bubble left of the trigger, on `.tooltip`. |
1365
- | `[data-tooltip-position="right"]` | Positions the bubble right of the trigger, on `.tooltip`. |
1366
- | No `data-tooltip-position` | Defaults to top placement. |
1367
- | `[data-state="open"]` | Shows the bubble without hover, on `.tooltip`. |
1368
- | `.tooltip-bubble.raised` / `.floating` | Adds one or two units of depth. The bubble is flat by default. |
1369
-
1370
- A tooltip is composition, not a single class: `.tooltip` wraps a trigger element and a `.tooltip-bubble`, and shows the bubble on hover or focus within the wrapper. This replaced a `::after` pseudo-element that carried the message as `content: attr(data-tooltip)` — CSS generated content is not reliably exposed to assistive tech, and cannot be the target of an `aria-describedby`, since that needs a real element with a real `id`. A real bubble can be. Wire the association yourself: give the bubble an `id`, give the trigger `aria-describedby` pointing at it, and put `role="tooltip"` on the bubble.
1409
+ | Class or Attribute | Purpose |
1410
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1411
+ | `.tooltip` | Wrapper around a trigger and a bubble. Paints nothing itself. |
1412
+ | `.tooltip-bubble` | The message. A real element, styled like any other surface. |
1413
+ | `.tooltip-icon` | Round icon-style trigger, for a message with nothing else to hang on. It takes the aesthetic's corner: square under `.neobrutalism` and `.pixel`, a diamond under `.cyber`. |
1414
+ | `[data-tooltip-position="top"]` | Positions the bubble above the trigger, on `.tooltip`. |
1415
+ | `[data-tooltip-position="bottom"]` | Positions the bubble below the trigger, on `.tooltip`. |
1416
+ | `[data-tooltip-position="left"]` | Positions the bubble left of the trigger, on `.tooltip`. |
1417
+ | `[data-tooltip-position="right"]` | Positions the bubble right of the trigger, on `.tooltip`. |
1418
+ | No `data-tooltip-position` | Defaults to top placement. |
1419
+ | `[data-state="open"]` | Shows the bubble without hover, on `.tooltip`. |
1420
+ | `[data-state="closed"]` | Hides the bubble even while hovered or focused, on `.tooltip`. For an Escape handler. |
1421
+ | `.tooltip-bubble.raised` / `.floating` | Adds one or two units of depth. The bubble is flat by default. |
1422
+
1423
+ A tooltip is composition, not a single class: `.tooltip` wraps a trigger element and a `.tooltip-bubble`, and shows the bubble on hover or focus within the wrapper. The bubble is a real element because CSS generated content is not reliably exposed to assistive tech and cannot be the target of an `aria-describedby`, which needs a real element with a real `id`. Wire the association yourself: give the bubble an `id`, give the trigger `aria-describedby` pointing at it, and put `role="tooltip"` on the bubble.
1371
1424
 
1372
1425
  A tooltip bubble's fill, ground color, and floor behavior are covered in [Composing → Presentation](docs/usage/composing.md#presentation), and its material follows whichever [aesthetic](docs/usage/aesthetics.md) is in scope. Intent, presentation, aesthetic, and elevation classes go on `.tooltip-bubble` — the element that actually paints — not on `.tooltip`, the same rule every component in this package follows: put the class on the element that shows it.
1373
1426
 
1374
- The bubble rests flat. It draws no shadow of its own on a plain page, and zeroes an aesthetic's part-based depth (such as `.neobrutalism`'s slab) the same way `.flat` does elsewhere. Add [`.raised` or `.floating`](docs/usage/composing.md#elevation) to lift it, on the bubble itself — `.tooltip-bubble.floating` is the pre-0.1.0 look. An aesthetic whose shadow is a complete value, like `.glass`, still reaches the bubble as it reaches any surface.
1427
+ The bubble rests flat. It draws no shadow of its own on a plain page, and zeroes an aesthetic's part-based depth (such as `.neobrutalism`'s slab) the same way `.flat` does elsewhere. Add [`.raised` or `.floating`](docs/usage/composing.md#elevation) to lift it, on the bubble itself. An aesthetic whose shadow is a complete value, like `.glass`, still reaches the bubble as it reaches any surface.
1375
1428
 
1376
1429
  Intent works on a tooltip bubble the same way it works on a filled component: `.tooltip-bubble.primary` fills the bubble with the primary color, black text on a light page and white on a dark one; `.tooltip-bubble.destructive` is a red bubble with the same logic. With no intent, the bubble uses `--color-tooltip`, a plate chosen per theme rather than derived from an intent; see [Composing → Presentation](docs/usage/composing.md#presentation) for why.
1377
1430
 
@@ -1399,17 +1452,36 @@ Intent works on a tooltip bubble the same way it works on a filled component: `.
1399
1452
  </span>
1400
1453
 
1401
1454
  <span class="tooltip" data-tooltip-position="right">
1402
- <span class="tooltip-icon" tabindex="0" aria-label="More details" aria-describedby="icon-bubble">?</span>
1455
+ <button type="button" class="tooltip-icon" aria-label="More details" aria-describedby="icon-bubble">?</button>
1403
1456
  <span class="tooltip-bubble" role="tooltip" id="icon-bubble">More details</span>
1404
1457
  </span>
1405
1458
  ```
1406
1459
 
1407
1460
  The first two show plain triggers with default (top) and explicit bottom placement. The third combines an intent with `.raised` on the bubble to show they compose freely — the intent decides the bubble's fill, `.raised` decides whether it casts depth. The fourth uses `data-state="open"` on the wrapper to keep a tooltip visible without a hover or focus interaction, useful for a tooltip that should be demonstrated or tested without simulating pointer input. The fifth uses `.tooltip-icon` as the trigger, for a message with no other element to hang it on.
1408
1461
 
1409
- `.tooltip` composes with any trigger: wrap an existing `.btn` or `.badge` in it and both keep their own look, gaining a bubble beside them. `.tooltip-icon` is only the circular chip for the case where there is nothing else to be the trigger.
1462
+ `.tooltip` composes with any trigger: wrap an existing `.btn` or `.badge` in it and both keep their own look, gaining a bubble beside them. `.tooltip-icon` is only the round chip for the case where there is nothing else to be the trigger.
1410
1463
 
1411
1464
  Showing and hiding the bubble is CSS-only — `:hover` and `:focus-within` on `.tooltip` — but that is presentational. It is not a complete accessible tooltip implementation by itself: provide the `id`/`aria-describedby`/`role="tooltip"` association outside the CSS; see [Accessibility](docs/accessibility.md).
1412
1465
 
1466
+ The bubble is hoverable, as WCAG 1.4.13 asks of content that appears on hover: once the pointer leaves the trigger, the bubble stays for `--tooltip-linger` (`300ms`, set on `.tooltip`) and then fades, so the pointer can cross the gap onto it, and it stays open for as long as the pointer is over it. A closed bubble is `visibility: hidden`, so it is nothing under the pointer. It stays in the accessibility tree through `aria-describedby`, which reads hidden content, but a bubble with no `aria-describedby` pointing at it is no longer read in place while hidden.
1467
+
1468
+ 1.4.13 also asks that the message can be dismissed without moving the pointer or focus, which takes a key handler. Write `data-state="closed"` on `.tooltip` on Escape, and clear it when the pointer and focus leave:
1469
+
1470
+ ```js
1471
+ for (const tooltip of document.querySelectorAll(".tooltip")) {
1472
+ const reopen = () => tooltip.removeAttribute("data-state");
1473
+ tooltip.addEventListener("pointerleave", reopen);
1474
+ tooltip.addEventListener("focusout", reopen);
1475
+ }
1476
+
1477
+ document.addEventListener("keydown", (event) => {
1478
+ if (event.key !== "Escape") return;
1479
+ for (const tooltip of document.querySelectorAll(".tooltip:hover, .tooltip:focus-within")) {
1480
+ tooltip.dataset.state = "closed";
1481
+ }
1482
+ });
1483
+ ```
1484
+
1413
1485
  Next: [Content and layout](docs/usage/content-and-layout.md) covers layout helpers, content chips and tables, surfaces, and typography utilities.
1414
1486
 
1415
1487
  <!-- Source: docs/usage/content-and-layout.md -->
@@ -1424,14 +1496,11 @@ Layout helpers use the shared `--layout-gap` token. `.tight` sets it to `0.5rem`
1424
1496
  - `.stack` is a vertical flex stack.
1425
1497
  - `.cluster` is a wrapping horizontal flex row; `.between` adds `space-between` alignment.
1426
1498
  - `.auto-grid` is a responsive auto-fit grid using `--layout-grid-min`.
1427
- - `.tight` and `.loose` set the shared gap to `0.5rem` or `1.5rem` on views, stacks, clusters, and auto-grids.
1428
1499
  - `.section` adds responsive block padding and an inline gutter.
1429
1500
  - `.section-content` centers content at `--container-max`; `.narrow` and `.wide` select the corresponding container tokens.
1430
1501
  - `.divider` is horizontal; `.vertical` makes it self-stretch vertically. It takes intent, but presentation classes do not affect it.
1431
1502
  - `.visually-hidden` hides content visually while keeping it in the accessibility tree, using the standard clip-based recipe. Not named `.sr-only`: on a `/tw` entry the consumer's own Tailwind build already ships a real `.sr-only`, and a same-named utility of this package's own would silently collide with it in the cascade — the same reason `.data-table` is not named `.table`.
1432
1503
 
1433
- The removed `--layout-stack-gap` and `--layout-cluster-gap` tokens have no compatibility aliases.
1434
-
1435
1504
  ## Content
1436
1505
 
1437
1506
  | Class | Purpose | Intent affects |
@@ -1458,24 +1527,24 @@ A table draws three kinds of line, and `.ruled` and `.ruleless` move all of them
1458
1527
 
1459
1528
  ```html
1460
1529
  <table class="data-table">
1461
- Head and foot boundaries only
1530
+ <!-- Head and foot boundaries only -->
1462
1531
  </table>
1463
1532
  <table class="data-table ruled">
1464
- Every row separated
1533
+ <!-- Every row separated -->
1465
1534
  </table>
1466
1535
  <table class="data-table ruleless">
1467
- No lines inside the table at all
1536
+ <!-- No lines inside the table at all -->
1468
1537
  </table>
1469
1538
  ```
1470
1539
 
1471
- The boundaries under a head and above a foot separate the parts of a table from each other, so they are drawn by default where the row rules are not. Rules between body rows used to arrive on their own, because the component's published edge default is `edged` and the edge axis wrote the rules along with the boundary — so whether you got them depended on a registry default rather than on anything in your markup.
1540
+ The boundaries under a head and above a foot separate the parts of a table from each other, so they are drawn by default where the row rules are not. Rules between body rows are asked for by `.ruled` or `--ui-rule`, never by the edge axis, so whether you get them depends on your markup and not on a default you cannot see.
1472
1541
 
1473
1542
  `--ui-rule` is the token behind both classes, so a container can set the answer for a whole region without classing each table:
1474
1543
 
1475
1544
  ```html
1476
1545
  <section style="--ui-rule: 100%">
1477
1546
  <table class="data-table">
1478
- Ruled by the region
1547
+ <!-- Ruled by the region -->
1479
1548
  </table>
1480
1549
  </section>
1481
1550
  ```
@@ -1503,10 +1572,10 @@ This is the one place intent cascades, because a table's rows are parts of the t
1503
1572
 
1504
1573
  ```html
1505
1574
  <table class="data-table success soft edged">
1506
- ...
1575
+ <!-- ... -->
1507
1576
  </table>
1508
- <table class="data-table">
1509
- A ghost table: rules and type, no header plate
1577
+ <table class="data-table ghost">
1578
+ <!-- A ghost table: rules and type, no header plate -->
1510
1579
  </table>
1511
1580
  <kbd class="kbd primary solid">Ctrl</kbd>
1512
1581
  <kbd class="kbd primary soft edged">Shift</kbd>
@@ -1541,6 +1610,10 @@ Use `.table-wrap` around `.data-table` when table width may exceed its container
1541
1610
  </div>
1542
1611
  ```
1543
1612
 
1613
+ A table takes the surface corner, like a card beside it. Nothing clips it: the four corner cells take the corners, so a `<caption>` sits above the table untouched in every engine. A cell that spans rows down into a bottom corner leaves that corner square.
1614
+
1615
+ `.table-wrap` scrolls, and a scroll container clips whatever paints outside it. Under an aesthetic that draws a halo -- cyber's glow -- the wrapper pads itself by the halo's reach and pulls itself out by the same amount, so the glow fits inside it and the table stays where it would be. The wrapper then reaches that far past its parent, which only shows when the parent is flush with the viewport. Depth shadows, such as a `.raised` table's, are not given room and are still cut. The wrapper can only make room for a halo it is inside, so put the aesthetic class on `.table-wrap` or an ancestor rather than on a wrapped table alone: `<table class="data-table cyber">` inside a plain wrapper glows, and the wrapper cuts the glow. Leave the wrapper off a table that can never outgrow its container; keep it wherever one can, or a wide table widens the whole page.
1616
+
1544
1617
  ## Surfaces
1545
1618
 
1546
1619
  | Class | Purpose |
@@ -1641,20 +1714,20 @@ A project that already runs Tailwind CSS v4 can process this package's source in
1641
1714
  ## Import the source entrypoint
1642
1715
 
1643
1716
  ```css
1644
- @import "tailwindcss";
1645
1717
  @import "@codenhub/styles/tw";
1646
1718
 
1647
1719
  @source "./src";
1648
1720
  ```
1649
1721
 
1650
- Tailwind CSS v4 scans your project automatically — most projects need nothing beyond the two imports above. `@source` paths resolve relative to the CSS file that declares them, not your project root, so `./src` here means "the `src/` next to this stylesheet"; adjust it to match where your own global stylesheet actually lives. Add `@source` only for markup Tailwind's automatic scan won't reach on its own: a path excluded by default (`.gitignore`d, or inside `node_modules` — an external component library, for instance) or, in a monorepo, one outside this file's own directory tree. Point it at your app's markup, not at this package — `/tw` already ships every class definition it publishes, so scanning it too only slows the build without adding classes.
1722
+ `/tw` imports Tailwind itself, so it replaces your own `@import "tailwindcss"` rather than joining it: with both, Tailwind's Preflight is emitted twice. Every other `/tw/*` entrypoint expects your import to come first; see [Using a focused entrypoint](#using-a-focused-entrypoint).
1723
+
1724
+ Tailwind CSS v4 scans your project automatically — most projects need nothing beyond the import above. `@source` paths resolve relative to the CSS file that declares them, not your project root, so `./src` here means "the `src/` next to this stylesheet"; adjust it to match where your own global stylesheet actually lives. Add `@source` only for markup Tailwind's automatic scan won't reach on its own: a path excluded by default (`.gitignore`d, or inside `node_modules` — an external component library, for instance) or, in a monorepo, one outside this file's own directory tree. Point it at your app's markup, not at this package — `/tw` already ships every class definition it publishes, so scanning it too only slows the build without adding classes.
1651
1725
 
1652
1726
  ## Add aesthetics
1653
1727
 
1654
1728
  Add an aesthetic from the source entrypoint the same way as the compiled one, after the base import:
1655
1729
 
1656
1730
  ```css
1657
- @import "tailwindcss";
1658
1731
  @import "@codenhub/styles/tw";
1659
1732
  @import "@codenhub/styles/tw/aesthetics";
1660
1733
 
@@ -1665,6 +1738,14 @@ Add an aesthetic from the source entrypoint the same way as the compiled one, af
1665
1738
 
1666
1739
  Import a focused `/tw/*` entrypoint instead of the combined one — such as `/tw/button` or `/tw/form` — when a build only needs one component family; see the full [import path table](docs/setup.md#import-paths) for what each one composes.
1667
1740
 
1741
+ Unlike `/tw`, the focused entrypoints and `/tw/native` do not import Tailwind, so keep your own import first:
1742
+
1743
+ ```css
1744
+ @import "tailwindcss";
1745
+ @import "@codenhub/styles/tw/button";
1746
+ @import "@codenhub/styles/tw/loader";
1747
+ ```
1748
+
1668
1749
  ## Troubleshooting
1669
1750
 
1670
1751
  **Classes resolve to no styling**
@@ -1676,10 +1757,10 @@ Import a focused `/tw/*` entrypoint instead of the combined one — such as `/tw
1676
1757
  **Duplicate token and reset rules**
1677
1758
 
1678
1759
  - **What happened:** the built stylesheet is larger than expected, and browser devtools show the same custom properties or reset rules defined twice.
1679
- - **Why:** importing both a compiled entrypoint (`@codenhub/styles`) and a `/tw` entrypoint (`@codenhub/styles/tw`) in the same build compiles the same tokens and reset from two independent sources; Tailwind's build has no way to know they describe the same package.
1680
- - **Fix:** pick one path per project — `/tw` entrypoints for a project running Tailwind CSS v4, compiled entrypoints everywhere else.
1760
+ - **Why:** one of two things. Importing both a compiled entrypoint (`@codenhub/styles`) and a `/tw` entrypoint (`@codenhub/styles/tw`) in the same build compiles the same tokens and reset from two independent sources; Tailwind's build has no way to know they describe the same package. Or your stylesheet imports `tailwindcss` beside `/tw`, which already imports it, so Preflight is emitted twice.
1761
+ - **Fix:** pick one path per project — `/tw` entrypoints for a project running Tailwind CSS v4, compiled entrypoints everywhere else — and drop your own `@import "tailwindcss"` when you import `/tw`.
1681
1762
 
1682
- Tailwind CSS 4 or newer is required for every `/tw` entrypoint.
1763
+ Every `/tw` entrypoint requires Tailwind CSS 4.3.2 or newer, below 5: the source uses `@source not` and `@source inline()`, which arrived in 4.1, and it is tested against 4.3.2.
1683
1764
 
1684
1765
  <!-- Source: docs/integrating/nextjs.md -->
1685
1766
 
@@ -1722,7 +1803,7 @@ export default function App({ Component, pageProps }: AppProps) {
1722
1803
  }
1723
1804
  ```
1724
1805
 
1725
- Running Tailwind CSS v4? `@import "@codenhub/styles/tw"` is a CSS directive, so it belongs in a CSS file, not a bare import in `layout.tsx`/`_app.tsx` — add it (with `@import "tailwindcss"` and, if needed, `@source`) to your global CSS file, and import that file from the same root file shown above instead. See [Tailwind CSS v4](docs/integrating/tailwind.md).
1806
+ Running Tailwind CSS v4? `@import "@codenhub/styles/tw"` is a CSS directive, so it belongs in a CSS file, not a bare import in `layout.tsx`/`_app.tsx` — add it (with `@source` if needed; `/tw` imports Tailwind itself, so leave out your own `@import "tailwindcss"`) to your global CSS file, and import that file from the same root file shown above instead. See [Tailwind CSS v4](docs/integrating/tailwind.md).
1726
1807
 
1727
1808
  ## Force a theme
1728
1809
 
@@ -1855,14 +1936,14 @@ This package provides CSS hooks for accessible states. It does not provide seman
1855
1936
 
1856
1937
  ### Focus and state
1857
1938
 
1858
- | Feature | Behavior |
1859
- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1860
- | `:focus-visible` | Global focus-visible outline using `--focus-ring`, `--focus-ring-width`, and `--focus-ring-offset`. |
1861
- | Form control focus | `.ipt`, `.textarea`, and `.select` draw the global focus ring and move their line to the intent color. |
1862
- | Invalid controls | `[aria-invalid="true"]` applies destructive border/focus color on form controls. |
1863
- | Disabled controls | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]`, and `.disabled` apply disabled cursor/opacity where supported. |
1864
- | Open state | `[data-state="open"]` styles supported surfaces and tooltips. |
1865
- | Native `<dialog>` | `dialog:not([open])` and a centered `dialog[open]` are restored in author origin, so `.card` composed onto a `<dialog>` (the closest primitive the package ships) does not render open on load or lose its centering to another rule. Focus trapping, escape handling, and inert-background behavior stay outside this package. |
1939
+ | Feature | Behavior |
1940
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1941
+ | `:focus-visible` | Global focus-visible outline using `--focus-ring`, `--focus-ring-width`, and `--focus-ring-offset`. |
1942
+ | Form control focus | `.ipt`, `.textarea`, and `.select` draw the global focus ring and move their line to the intent color. |
1943
+ | Invalid controls | `[aria-invalid="true"]` applies destructive border/focus color on form controls. |
1944
+ | Disabled controls | `[disabled]`, `[aria-disabled="true"]`, `[data-disabled]`, and `.disabled` apply disabled cursor/opacity where supported. |
1945
+ | Open state | `[data-state="open"]` on `.tooltip` shows its bubble without hover or focus, and `[data-state="closed"]` hides it while hovered or focused. No other component reads either. |
1946
+ | Native `<dialog>` | On a `<dialog>` carrying `.card`, `.panel`, or `.surface`, the closed state and a centered open box are restored in author origin, so the dialog does not render open on load or lose its centering to another rule. It ships with those classes, on every entrypoint that has them, and leaves other dialogs alone. Focus trapping, escape handling, and inert-background behavior stay outside this package. |
1866
1947
 
1867
1948
  ### Visually hidden content
1868
1949
 
@@ -1872,22 +1953,39 @@ This package provides CSS hooks for accessible states. It does not provide seman
1872
1953
 
1873
1954
  | Feature | Behavior |
1874
1955
  | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1875
- | Control boundaries | Text controls rest at `--_line-rest` of their intent, under 3:1 by choice, so hover and focus have a whole tone to move the line to. |
1876
- | Toggle boundaries | `.checkbox`, `.radio`, and `.switch` draw their line whole, because it is the only thing marking an unchecked box. |
1877
- | Cascaded `.edgeless` | Floored on every text control: a container cannot erase the line of a field or toggle nobody classed. |
1956
+ | Control boundaries | Text controls rest at a fraction of their intent's line, under 3:1 by choice, so hover and focus have a whole tone to move the line to. |
1957
+ | Toggle boundaries | `.checkbox`, `.radio`, and `.switch` draw their line whole unless a `.switch` carries its own `.edgeless` (below), because the line is the only thing marking an unchecked box. |
1958
+ | Cascaded `.edgeless` | Floored on every text control and toggle: a container cannot erase the line of a field or toggle nobody classed. |
1878
1959
  | Own `.edgeless` | On the element, `.ipt`, `.textarea`, `.select`, and `.switch` do drop their line and do not meet 1.4.11 at rest, except `.switch.ghost`: `.ghost` already asks for zero fill, so dropping the line too would leave no boundary at all, and `.switch.ghost.edgeless` keeps it up instead. |
1879
1960
  | `.checkbox`/`.radio` | Never drop their line, on the element or from a container. `.edgeless` on them is unsupported, not merely discouraged. |
1880
1961
  | Progress track | By choice, the value carries the reading and the track is a quiet secondary cue. `.progress`'s registered default, `.soft.edgeless`, rests its track at ~1.1-1.3:1 against the page; the value clears 3:1 against that track for every intent but warning in light (~2.7:1) and info in dark (~2.9:1). `.ghost.edged` clears 3:1 for both the track's outline and the value, for every intent, in both themes -- choose it where the track itself must meet 1.4.11. |
1881
1962
 
1882
1963
  ### Motion and transparency
1883
1964
 
1884
- | Feature | Behavior |
1885
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
1886
- | Reduced-motion loaders | Loader variants use animated embedded SVGs normally and static masks when `prefers-reduced-motion: reduce` matches. |
1887
- | Reduced-motion document | The reset additionally shortens document animations and transitions when `prefers-reduced-motion: reduce` matches. |
1888
- | Reduced transparency | `.glass` drops its blur and becomes an opaque surface when `prefers-reduced-transparency: reduce` matches. |
1965
+ | Feature | Behavior |
1966
+ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1967
+ | Reduced-motion loaders | Loader variants use animated embedded SVGs normally and static masks when `prefers-reduced-motion: reduce` matches. |
1968
+ | Reduced-motion components | Every component stops its own transitions and animations -- the skeleton and progress shimmer, the indeterminate sweep, the tooltip, toggle marks, and the press -- when `prefers-reduced-motion: reduce` matches. |
1969
+ | Reduced-motion document | The reset additionally shortens every other animation and transition on the page when `prefers-reduced-motion: reduce` matches. |
1970
+ | Reduced transparency | `.glass` and `.glass-liquid` drop their blur and lens and become an opaque surface when `prefers-reduced-transparency: reduce` matches. |
1971
+
1972
+ The loader fallback and each component's own reduced-motion rule ship with the component, so every entrypoint that includes it honors reduced motion without the reset -- `@codenhub/styles/components`, `@codenhub/styles/tw/components`, and each focused `/tw/*` component entrypoint included. The complete and native entrypoints add the reset's page-wide shortening on top.
1889
1973
 
1890
- The loader fallback is embedded in loader CSS, so focused imports that include loaders honor reduced motion without the reset. This includes `@codenhub/styles/components`, `@codenhub/styles/tw/components`, and `@codenhub/styles/tw/loader`. The complete and native entrypoints include both that fallback and the reset's broader animation and transition shortening.
1974
+ The reset's rule is `!important` inside the `base` layer, and a layered `!important` beats an unlayered one, so your own `!important` cannot switch an animation back on. To keep one running for users who ask for reduced motion -- a progress indicator you consider essential, say -- declare it `!important` in a layer ordered before `base`. The order is set by the first `@layer` statement the browser meets, so yours goes before the package loads:
1975
+
1976
+ ```css
1977
+ @layer essential-motion, theme, base, components, utilities;
1978
+ @import "@codenhub/styles";
1979
+
1980
+ @layer essential-motion {
1981
+ @media (prefers-reduced-motion: reduce) {
1982
+ .upload-spinner {
1983
+ animation-duration: 1s !important;
1984
+ animation-iteration-count: infinite !important;
1985
+ }
1986
+ }
1987
+ }
1988
+ ```
1891
1989
 
1892
1990
  ### Color and contrast
1893
1991
 
@@ -1902,10 +2000,10 @@ The loader fallback is embedded in loader CSS, so focused imports that include l
1902
2000
 
1903
2001
  ### Forced colors and direction
1904
2002
 
1905
- | Feature | Behavior |
1906
- | -------------- | ------------------------------------------------------------------------------------------------------------------ |
1907
- | Forced colors | `forced-colors: active` preserves visible borders and checked states using system colors. |
1908
- | Text direction | Quote bar, alert icon, progress fill, vertical divider, and switch knob mirror under `dir="rtl"`. No class needed. |
2003
+ | Feature | Behavior |
2004
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
2005
+ | Forced colors | `forced-colors: active` preserves visible borders and checked states using system colors. |
2006
+ | Text direction | Quote bar, alert icon, progress fill, vertical divider, switch knob, table alignment, and a select's chevron mirror under `dir="rtl"`. No class needed. |
1909
2007
 
1910
2008
  In forced colors, class-based form controls including `.text-control` receive a 2px `Highlight` system-color focus outline. The native entrypoint provides the same visible system outline for unclassed text inputs, selects, textareas, checkboxes, and radios.
1911
2009
 
@@ -1915,16 +2013,16 @@ Custom checkboxes and radios use the system `Canvas`, `CanvasText`, `Highlight`,
1915
2013
 
1916
2014
  Use semantic HTML and behavior appropriate for the component.
1917
2015
 
1918
- | UI | Required outside CSS |
1919
- | ----------------- | --------------------------------------------------------------------------------------------------------------- |
1920
- | Buttons | Use `<button>` for actions or accessible links for navigation. Add accessible names for `.btn.icon`. |
1921
- | Forms | Use labels, `type`, validation logic, `aria-describedby`, and error message relationships. |
1922
- | Alerts | Add `role="status"` or `role="alert"` based on announcement urgency. |
1923
- | Toasts | Add live-region behavior, dismissal behavior, focus rules, and pause/timeout logic when needed. |
1924
- | Tooltips | Give the bubble a real `id` and `role="tooltip"`, and reference it from the trigger's `aria-describedby`. |
1925
- | Progress | Use semantic progress elements or ARIA values when numeric progress must be announced. |
1926
- | Skeletons/loaders | Mark decorative loading visuals with `aria-hidden="true"` and expose loading state elsewhere when needed. |
1927
- | Popovers/modals | Provide focus trapping, escape handling, inert background behavior, labels, and roles outside this CSS package. |
2016
+ | UI | Required outside CSS |
2017
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2018
+ | Buttons | Use `<button>` for actions or accessible links for navigation. Add accessible names for `.btn.icon`. |
2019
+ | Forms | Use labels, `type`, validation logic, `aria-describedby`, and error message relationships. Keep hints and errors outside the `<label>`, and give a `.switch` `role="switch"`. |
2020
+ | Alerts | Add `role="status"` or `role="alert"` based on announcement urgency. |
2021
+ | Toasts | Add live-region behavior, dismissal behavior, focus rules, and pause/timeout logic when needed. |
2022
+ | Tooltips | Give the bubble a real `id` and `role="tooltip"`, and reference it from the trigger's `aria-describedby`. Dismiss it on Escape by writing `data-state="closed"` (see [Tooltips](docs/usage/tooltips.md)); the bubble is already hoverable. |
2023
+ | Progress | Use semantic progress elements or ARIA values when numeric progress must be announced. |
2024
+ | Skeletons/loaders | Mark decorative loading visuals with `aria-hidden="true"` and expose loading state elsewhere when needed. |
2025
+ | Popovers/modals | Provide focus trapping, escape handling, inert background behavior, labels, and roles outside this CSS package. |
1928
2026
 
1929
2027
  ## State attribute guidance
1930
2028
 
@@ -1939,13 +2037,63 @@ Use ARIA or data attributes when native attributes are not available for the ele
1939
2037
  ```html
1940
2038
  <a class="btn secondary" aria-disabled="true">Unavailable</a>
1941
2039
  <span class="tooltip" data-state="open">
1942
- <span class="tooltip-icon" tabindex="0" aria-label="More details" aria-describedby="more-details-bubble">?</span>
2040
+ <button type="button" class="tooltip-icon" aria-label="More details" aria-describedby="more-details-bubble">?</button>
1943
2041
  <span class="tooltip-bubble" role="tooltip" id="more-details-bubble">More details</span>
1944
2042
  </span>
1945
2043
  ```
1946
2044
 
1947
2045
  `aria-disabled="true"` communicates disabled state but does not prevent activation. JavaScript or element choice must prevent activation when required.
1948
2046
 
2047
+ <!-- Source: docs/changelog/0.6.0.md -->
2048
+
2049
+ # 0.6.0
2050
+
2051
+ A liquid-glass modifier, glass that keeps every presentation's fill whole, hoverable tooltips, native field decorations with an `.unadorned` opt-out, indeterminate checkboxes and list-box selects, and a round of fixes to tables, controls, the focus ring, right-to-left layout, and reduced motion on every entrypoint.
2052
+
2053
+ ## Added
2054
+
2055
+ - **`.glass-liquid`**, a modifier of `.glass`: continuous corners, a bright rim, a lighter blur, a clearer ground, and a lens that bends what is behind a surface at its edges (Chromium today; Firefox and Safari keep the rim, blur, and ground). Put it beside `.glass` on the region, or on one surface inside a glass region; `.glass-solo.glass-liquid` paints the same pane on an element the package does not style. Without `.glass` in scope it changes the corners and nothing else. See [Aesthetics → Liquid glass](docs/usage/aesthetics.md#liquid-glass).
2056
+ - **`.unadorned`** removes the browser's own field decorations -- the date picker button, number spin buttons, and search clear button and magnifier -- on a control or on its `.input-group`, for a field whose adornments you draw yourself. See [Forms → Icons](docs/usage/forms.md#icons).
2057
+ - **`[data-state="closed"]` on `.tooltip`** hides the bubble while it is hovered or focused, for an Escape handler, and **`--tooltip-linger`** (300ms) sets how long a bubble stays after the pointer leaves its trigger. See [Tooltips](docs/usage/tooltips.md).
2058
+ - **An indeterminate `.checkbox`** (and a native one under `/native`) draws the checked plate with a dash for its mark, and a Highlight plate in forced colors. See [Forms → Toggles](docs/usage/forms.md#toggles).
2059
+ - **A multi-row `.select`** -- `multiple`, or a `size` above 1 -- draws as a list box: no chevron, as tall as its rows, with the chevron's room given back. It used to keep the chevron and a one-row height.
2060
+ - **`--ui-rule-ink`**, the neutral line color of a table's frame and rules, read ahead of `--ui-ink`. Glass sets it so a table's lines read on its plate; every other aesthetic clears it.
2061
+
2062
+ ## Changed
2063
+
2064
+ - **Glass no longer thins fills.** It stopped setting `--ui-bg-alpha: 0.8`, so a `.solid` button, a checked toggle, and a `.solid` card are opaque under glass like everywhere else. Translucency comes from the surface ground alone. Glass buttons also take the base `scale(0.97)` press (it was `none`).
2065
+ - **A neutral `.solid` rests on the page background.** Its capped 20% fill was painted over nothing and showed whatever was behind it; it is now an opaque grey. The generated `--palette-neutral-solid-bg` and `-bg-hover` values changed to match, in both themes.
2066
+ - **Tables frost under glass and keep their lines.** A default or `.soft` `.data-table` blurs like a surface, its frame and rules read `--ui-rule-ink`, and a bare `<table>` under `/native` reads it too.
2067
+ - **A table takes the surface corner and is never clipped.** The four corner cells round instead of an `overflow` clip, so a `<caption>` is untouched in every engine; a cell spanning rows into a bottom corner leaves that corner square. `.table-wrap` makes room for an aesthetic's halo (cyber's glow) inside its scroll box, so put the aesthetic class on the wrapper or an ancestor.
2068
+ - **Table rules draw at the aesthetic's line width**, uncapped, so `.chunky-tile`'s 2px frame no longer sits over a 1px row rule and `.pixel` draws them at all.
2069
+ - **The focus ring is pulled in by the focused element's line width.** `--focus-ring-offset` is no longer declared at `:root`; its default is computed on the element, so it lands on the edge under every aesthetic. Setting `--focus-ring-offset` yourself still wins, but reading `var(--focus-ring-offset)` in your own CSS now needs a fallback.
2070
+ - **Ink aesthetics draw controls in the control border.** Under `.neobrutalism`, `.pixel`, `.cyber`, and `.sketch`, a resting neutral field or toggle uses `--color-control-border` instead of the near-primary ink. A bare native checkbox or radio under `/native` takes the control ink too, and a `.neutral` field under glass takes it like an unclassed one.
2071
+ - **Text controls keep the browser's own decorations.** The date picker button, number spin buttons, and search clear button were hidden on every text control, which also removed what they do. They are back by default; add `.unadorned` to remove them.
2072
+ - **Buttons and fields take the surrounding font on every entrypoint.** `<button>` under `/native`, and every `.btn` and text control under `/components`, used the user agent's smaller control font (13.33px), so the same label printed at three sizes across elements and entrypoints. They inherit the page's font now, as they already did on `.`.
2073
+ - **A default `.btn` is `--control-height` tall**, 40px, the same as `.ipt` beside it; it was 42px, and 44px under a 2px aesthetic line. Its vertical padding gives back the line width.
2074
+ - **`/tw` keeps Tailwind's `2xl` breakpoint.** It redefined `--breakpoint-2xl` as 90rem, which moved every `2xl:` utility in a consumer's own Tailwind build from 96rem. `--breakpoint-xs` (30rem) is still added. Nothing in the package used the override, so the compiled entrypoints are unchanged.
2075
+ - **Components honour reduced motion and fix `<dialog>` on every entrypoint.** `/components` and the focused `/tw/*` entries carry no reset, so the skeleton and progress shimmer, the indeterminate sweep, and the tooltip, toggle, and press transitions kept moving under `prefers-reduced-motion: reduce`, and a `<dialog class="card">` rendered open on load. Each component now stops its own motion, and the `<dialog>` fix ships beside `.card`. The fix now reaches only a dialog carrying `.card`, `.panel`, or `.surface`, so a dialog the package does not style keeps its own margin.
2076
+ - **A tooltip bubble is hoverable.** It lingers for `--tooltip-linger` after the pointer leaves the trigger and stays open while the pointer is over it, as WCAG 1.4.13 asks; it used to vanish on the way. A closed bubble is `visibility: hidden` rather than only transparent, so a bubble no `aria-describedby` points at is no longer read in place while closed.
2077
+ - **`.alert-icon` sizes without `!important`.** It set its 1.25rem size `!important`, which no consumer rule could override. It sets `--ic-size` for `@codenhub/icons` glyphs and a plain size for any other icon now, so your own rule resizes it. An icon set that sizes itself in an unlayered rule under another knob keeps its own size.
2078
+ - **The Tailwind CSS peer range is `>=4.3.2 <5`.** It claimed `>=4.0.0`, but the `/tw` source needs `@source not` and `@source inline()` (Tailwind 4.1) and is tested against 4.3.2. The peer is still optional, and only `/tw` needs it.
2079
+ - **Browser floor is Firefox 128.** Non-inheriting elevation relies on `@property`; the documented floor was Firefox 121.
2080
+
2081
+ ## Fixed
2082
+
2083
+ - **A switch's knob sits inside `.pixel`'s inset ring** instead of covering it, so an unchecked and a checked switch no longer read as two halves.
2084
+ - **The sketch solo shadow is cast in the depth colour** rather than full ink.
2085
+ - **No stray `.contents` rule.** The compiled `.`, `/native`, and `/components` stylesheets shipped Tailwind's `.contents{display:contents}`, picked up from the license text; the build no longer scans the package's root files.
2086
+ - **Links under `/native` keep the size of their text.** `a` was mapped to body copy, so a link inside a heading printed at 14px and regular weight.
2087
+ - **The Tailwind guide no longer imports Tailwind twice.** `/tw` imports Tailwind itself, so it replaces your own `@import "tailwindcss"`; with both, an unminified build carried two copies of Preflight. The focused `/tw/*` entrypoints and `/tw/native` still expect your import first.
2088
+ - **`.tooltip-icon` keeps its size on a `<button>`.** The docs now use a real `<button type="button">` as the trigger, since `aria-label` on a role-less `<span>` is not reliably announced; the chip resets the padding and minimum size a button brings under `/native` and `/components`. The docs also no longer call it circular under every aesthetic: it is square under `.neobrutalism` and `.pixel`.
2089
+ - **A wrapped `.hint` has room between its lines.** Its line height equalled its 14px size, so wrapped lines touched; it takes `text-sm`'s 20px leading now.
2090
+ - **`/palette` follows a light subtree nested in a dark one.** Its light values sat on `:root` alone, so a `.light` region inside `.dark` kept the dark values. They answer the explicit light selectors too now.
2091
+ - **An unchecked switch's knob is `CanvasText` in forced colors.** The switch opts out of forced colors to keep its states apart, and its unchecked knob kept the package grey on the system `Canvas`.
2092
+ - **A select's chevron mirrors under `dir="rtl"`.** It and the padding kept for it stayed on the right, so a right-to-left value started beside the chevron.
2093
+ - **`.btn.icon` follows the size step.** `.sm` left an icon button at the default 40px, and under WebKit a `.lg` one was 40px wide and 48px tall. They are square at 32px and 48px now.
2094
+ - **Tables align to the inline start.** `.data-table` cells and its caption used `text-left`, so a right-to-left table stayed left-aligned.
2095
+ - **A region of one aesthetic nested inside another keeps its own material.** Every aesthetic now restates the depth colour and clears the focus inset, hover transform, and shadow tokens it does not use, so glass inside `.chunky-tile` no longer casts a black slab under every card, glass inside `.cyber` casts its shadow again, and a region inside `.pixel` no longer inherits pixel's inset focus layer.
2096
+
1949
2097
  <!-- Source: docs/changelog/0.5.0.md -->
1950
2098
 
1951
2099
  # 0.5.0