@zeyos/zx 4.4.0 → 5.0.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 (182) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +42 -10
  3. package/dist/types/components/activity-list/activity-list.d.ts +22 -0
  4. package/dist/types/components/app-sidebar/app-sidebar.d.ts +5 -3
  5. package/dist/types/components/avatar/avatar.d.ts +46 -7
  6. package/dist/types/components/badge/badge.d.ts +21 -1
  7. package/dist/types/components/button/button.d.ts +15 -0
  8. package/dist/types/components/chart/chart.d.ts +10 -0
  9. package/dist/types/components/checklist/checklist.d.ts +9 -0
  10. package/dist/types/components/code-editor/code-editor.d.ts +6 -17
  11. package/dist/types/components/datebox/date-range-box.d.ts +5 -1
  12. package/dist/types/components/datebox/datebox.d.ts +7 -2
  13. package/dist/types/components/dialog/dialog.d.ts +46 -6
  14. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  15. package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
  16. package/dist/types/components/field/field.d.ts +24 -2
  17. package/dist/types/components/fieldset/fieldset.d.ts +19 -1
  18. package/dist/types/components/file-item/file-item.d.ts +34 -6
  19. package/dist/types/components/form/form.d.ts +12 -1
  20. package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
  21. package/dist/types/components/launcher/launcher.d.ts +260 -15
  22. package/dist/types/components/list-toolbar/list-toolbar.d.ts +1 -1
  23. package/dist/types/components/menu-button/menu-button.d.ts +33 -5
  24. package/dist/types/components/message/message.d.ts +119 -17
  25. package/dist/types/components/modal/modal.d.ts +13 -2
  26. package/dist/types/components/select/color.d.ts +12 -0
  27. package/dist/types/components/select/glyphs.d.ts +124 -0
  28. package/dist/types/components/select/select.d.ts +11 -3
  29. package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
  30. package/dist/types/components/tabbox/tabbox.d.ts +9 -0
  31. package/dist/types/components/table/table.d.ts +46 -1
  32. package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
  33. package/dist/types/components/tree/tree.d.ts +13 -0
  34. package/dist/types/core/breakpoint.d.ts +1 -1
  35. package/dist/types/core/icons.d.ts +23 -25
  36. package/dist/types/core/motion.d.ts +59 -0
  37. package/dist/types/core/overlay-host.d.ts +15 -1
  38. package/dist/types/index.d.ts +5 -3
  39. package/dist/types/zeyos/connect.d.ts +6 -0
  40. package/dist/types/zeyos/form.d.ts +50 -9
  41. package/dist/types/zeyos/index.d.ts +1 -1
  42. package/dist/types/zeyos/query.d.ts +79 -0
  43. package/dist/types/zeyos/schema.d.ts +24 -1
  44. package/dist/types/zeyos/table.d.ts +100 -14
  45. package/dist/types/zeyos/view.d.ts +10 -0
  46. package/dist/zx-zeyos.esm.js +1621 -443
  47. package/dist/zx-zeyos.esm.min.js +53 -5
  48. package/dist/zx.css +1411 -660
  49. package/dist/zx.esm.js +1976 -703
  50. package/dist/zx.esm.min.js +56 -8
  51. package/dist/zx.global.js +1978 -705
  52. package/dist/zx.global.min.js +56 -8
  53. package/dist/zx.min.css +1 -1
  54. package/docs/api.json +219 -34
  55. package/docs/llms.md +472 -110
  56. package/docs/llms.txt +3 -3
  57. package/package.json +1 -1
  58. package/src/components/account-menu/account-menu.css +35 -10
  59. package/src/components/activity-item/activity-item.css +1 -1
  60. package/src/components/activity-item/activity-item.js +3 -0
  61. package/src/components/activity-list/activity-list.css +38 -3
  62. package/src/components/activity-list/activity-list.js +34 -0
  63. package/src/components/app-icon/app-icon.css +1 -1
  64. package/src/components/app-rail/app-rail.css +40 -79
  65. package/src/components/app-sidebar/app-sidebar.css +109 -174
  66. package/src/components/app-sidebar/app-sidebar.js +21 -6
  67. package/src/components/aurora/aurora.css +5 -3
  68. package/src/components/avatar/avatar.css +32 -7
  69. package/src/components/avatar/avatar.js +72 -8
  70. package/src/components/badge/badge.css +54 -2
  71. package/src/components/badge/badge.js +29 -2
  72. package/src/components/button/button.css +67 -9
  73. package/src/components/button/button.js +38 -0
  74. package/src/components/calendar/calendar.css +46 -46
  75. package/src/components/card/card.css +2 -2
  76. package/src/components/card-view/card-view.css +9 -7
  77. package/src/components/chart/chart.css +4 -1
  78. package/src/components/chart/chart.js +12 -0
  79. package/src/components/check-button/check-button.css +10 -2
  80. package/src/components/checklist/checklist.css +9 -2
  81. package/src/components/checklist/checklist.js +11 -5
  82. package/src/components/checklist/field-adapter.js +3 -1
  83. package/src/components/code-editor/code-editor.css +6 -5
  84. package/src/components/code-editor/code-editor.js +19 -1
  85. package/src/components/context-menu/context-menu.css +9 -4
  86. package/src/components/copy/copy-button.css +6 -0
  87. package/src/components/copy/copy-input.css +8 -3
  88. package/src/components/data-filter/data-filter.css +5 -4
  89. package/src/components/date-picker/date-picker.css +6 -6
  90. package/src/components/date-picker/date-range-picker.css +3 -3
  91. package/src/components/date-picker/time-picker.css +1 -1
  92. package/src/components/datebox/date-range-box.css +79 -2
  93. package/src/components/datebox/date-range-box.js +13 -2
  94. package/src/components/datebox/datebox.css +79 -2
  95. package/src/components/datebox/datebox.js +13 -2
  96. package/src/components/dialog/dialog.css +26 -0
  97. package/src/components/dialog/dialog.js +99 -17
  98. package/src/components/dropdown/dropdown.css +1 -1
  99. package/src/components/dropdown/dropdown.js +5 -3
  100. package/src/components/empty-state/empty-state.css +2 -2
  101. package/src/components/entity-ref/entity-ref.css +1 -1
  102. package/src/components/entity-ref/entity-ref.js +1 -1
  103. package/src/components/field/field.css +28 -9
  104. package/src/components/field/field.js +68 -26
  105. package/src/components/field-upload/field-upload.css +1 -1
  106. package/src/components/fieldset/fieldset.css +5 -3
  107. package/src/components/fieldset/fieldset.js +28 -5
  108. package/src/components/file-item/file-item.js +144 -10
  109. package/src/components/filter/filter.js +7 -3
  110. package/src/components/filter-panel/filter-panel.css +1 -1
  111. package/src/components/finder/finder.css +15 -13
  112. package/src/components/form/form.css +5 -4
  113. package/src/components/form/form.js +11 -2
  114. package/src/components/grid/grid.css +1 -1
  115. package/src/components/groupbox/groupbox.css +1 -1
  116. package/src/components/kanban-view/kanban-card.js +11 -3
  117. package/src/components/kanban-view/kanban-view.css +39 -15
  118. package/src/components/kanban-view/kanban-view.js +4 -4
  119. package/src/components/launcher/launcher.css +107 -25
  120. package/src/components/launcher/launcher.js +503 -41
  121. package/src/components/list-toolbar/list-toolbar.css +13 -8
  122. package/src/components/list-toolbar/list-toolbar.js +1 -1
  123. package/src/components/master-panel/master-panel.css +2 -0
  124. package/src/components/menu-button/menu-button.css +9 -4
  125. package/src/components/menu-button/menu-button.js +65 -9
  126. package/src/components/message/message.css +33 -3
  127. package/src/components/message/message.js +217 -15
  128. package/src/components/modal/modal.js +18 -3
  129. package/src/components/navigation-bar/navigation-bar.css +18 -9
  130. package/src/components/number-field/field-adapter.js +1 -0
  131. package/src/components/number-field/number-field.css +70 -0
  132. package/src/components/number-field/number-field.js +2 -0
  133. package/src/components/pagination/pagination.css +8 -6
  134. package/src/components/panel/panel.css +1 -1
  135. package/src/components/rating/rating.css +7 -0
  136. package/src/components/select/color.js +22 -0
  137. package/src/components/select/field-adapter.js +2 -0
  138. package/src/components/select/glyphs.js +165 -0
  139. package/src/components/select/select.css +138 -45
  140. package/src/components/select/select.js +83 -47
  141. package/src/components/sheet/sheet.css +11 -8
  142. package/src/components/slider/slider.css +16 -2
  143. package/src/components/slider/slider.js +1 -1
  144. package/src/components/sort-control/sort-control.css +4 -1
  145. package/src/components/stat-tile/stat-tile.css +41 -46
  146. package/src/components/stat-tile/stat-tile.js +17 -6
  147. package/src/components/tabbox/tabbox.css +32 -27
  148. package/src/components/tabbox/tabbox.js +5 -1
  149. package/src/components/table/table.css +104 -29
  150. package/src/components/table/table.js +90 -16
  151. package/src/components/tag-picker/tag-picker.css +83 -5
  152. package/src/components/tag-picker/tag-picker.js +25 -8
  153. package/src/components/timebox/timebox.css +22 -2
  154. package/src/components/toggle/toggle.css +12 -1
  155. package/src/components/toolbar/toolbar.js +2 -2
  156. package/src/components/tooltip/tooltip.css +4 -0
  157. package/src/components/tooltip/tooltip.js +10 -3
  158. package/src/components/tree/tree.css +25 -16
  159. package/src/components/tree/tree.js +31 -0
  160. package/src/components/value-list/value-list.css +3 -3
  161. package/src/components/view/record-view.js +1 -1
  162. package/src/core/breakpoint.js +1 -1
  163. package/src/core/icons.js +193 -60
  164. package/src/core/motion.js +147 -0
  165. package/src/core/overlay-host.js +21 -2
  166. package/src/index.js +5 -3
  167. package/src/internal/app-rail.js +1 -2
  168. package/src/zeyos/connect.js +6 -2
  169. package/src/zeyos/form.js +67 -27
  170. package/src/zeyos/index.js +1 -1
  171. package/src/zeyos/query.js +153 -0
  172. package/src/zeyos/schema.js +144 -27
  173. package/src/zeyos/table.js +68 -5
  174. package/src/zeyos/view.js +6 -0
  175. package/styles/base.css +56 -0
  176. package/styles/icons.css +11 -1
  177. package/styles/tokens/dark.css +113 -37
  178. package/styles/tokens/global.css +121 -27
  179. package/styles/tokens/semantic.css +100 -31
  180. package/styles/tokens/themes.css +7 -6
  181. package/styles/zx.css +0 -1
  182. package/styles/tokens/density.css +0 -9
package/docs/llms.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Xenon is a dependency-free design system for browser applications; Zx is its product-agnostic
4
4
  > vanilla-JavaScript implementation. ES2022 modules, WAI-ARIA-accessible native controls, semantic `--zx-*` design
5
- > tokens (light/dark + cozy/compact), and lifecycle-safe components. Zero runtime dependencies.
5
+ > tokens (light/dark, one token-driven geometry), and lifecycle-safe components. Zero runtime dependencies.
6
6
  > This file is the single, complete reference a coding agent needs to build UI with Zx; each
7
7
  > component below is wrapped in `<!-- doc:<name> -->` markers so tools can extract a single
8
8
  > component's section (`website/docs.html` renders them as each component's Reference tab).
@@ -49,13 +49,23 @@ const t = new Table(target, options);
49
49
 
50
50
  ## Theming
51
51
 
52
- Load `zx.css`, then set theme/density on any ancestor (usually `<html>`):
52
+ Load `zx.css`, then set the theme on any ancestor (usually `<html>`):
53
53
 
54
54
  ```html
55
- <html data-zx-theme="dark" data-zx-density="compact">
55
+ <html data-zx-theme="dark">
56
56
  ```
57
57
 
58
- Themes: `light` | `dark` | `auto` (follows OS). Density: `cozy` | `compact`.
58
+ Themes: `light` | `dark` | `auto` (follows OS).
59
+
60
+ **One geometry.** Spacing steps are multiples of one unit
61
+ (`--zx-space-unit`, 4px: `--zx-space-1…8` = 4/8/12/16/20/24/32/40px), radius steps are fractions of
62
+ one base (`--zx-radius`, 4px: `-sm/-md/-lg/-xl` = 2/3/4/6px; `-full` only for shapes whose roundness
63
+ is the meaning — badges, avatars, toggles), and controls are `--zx-control-height` (30px) tall.
64
+ To tighten or loosen an application, override those tokens on `:root` or on any region:
65
+
66
+ ```css
67
+ :root { --zx-space-unit: 3.5px; --zx-control-height: 28px; }
68
+ ```
59
69
 
60
70
  **Accent presets.** A second, orthogonal attribute picks the accent ramp:
61
71
  `data-zx-preset="zx" | "zeyos" | "ocean" | "violet" | "rose" | "slate"` (`zx` is the default and
@@ -76,7 +86,7 @@ Keep the 600 stop at 4.5:1 or better against white and the 400 stop at 4.5:1 aga
76
86
  — that is what makes `--zx-color-on-accent` legible without being restated per theme.
77
87
 
78
88
  The theme studio treats those stable IDs as complete recipes: selecting a preset also changes its
79
- density, neutral tint, radius, control height, base text size, font stack, and material strength.
89
+ spacing unit, neutral tint, radius, control height, base text size, font stack, and material strength.
80
90
  Its **Material** control is CSS-first (`Flat`, `Glass`, or `Deep glass`). Aurora is the independent
81
91
  ambient canvas layer; material controls how surfaces respond to that light. The shared recipe
82
92
  applies to AppIcons, ordinary controls, persistent chrome, raised cards, and every transient overlay: Dropdown, ContextMenu,
@@ -92,14 +102,24 @@ Top-layer components remain DOM children of their nearest Zx theme scope, so a t
92
102
  set on an application region continues to inherit through its menus, dialogs, tooltips, and toasts.
93
103
 
94
104
  Define a product theme by overriding **semantic** tokens under a `[data-zx-theme="name"]` selector:
95
- `--zx-color-bg-page/surface/raised/control/hover/selected`, `--zx-color-glass-chrome/surface`,
105
+ `--zx-color-bg-page/surface/raised/control/muted/hover/selected`, `--zx-color-bg-current` (the
106
+ neutral "you are here" fill of the current nav item, row, tab, or pressed toggle — never the
107
+ accent), `--zx-color-bg-chrome` (navigation and inspector columns, one tone off the canvas),
108
+ `--zx-color-glass-chrome/surface`,
96
109
  `--zx-color-overlay-surface/panel/backdrop/border/hover/selected/divider/scrim`,
97
- `--zx-overlay-surface-image/toast-image/panel-image`, `--zx-color-border(-strong/-control)`,
98
- `--zx-color-text(-muted/-placeholder)`, `--zx-color-accent(-hover)/on-accent`,
99
- `--zx-color-danger/warning/success/info(+ -bg)`, `--zx-focus-ring`, `--zx-control-height/-radius`,
100
- `--zx-space-*`, `--zx-radius-*`, `--zx-text-*`. Never use tier-1 palette tokens (`--zx-gray-*`,
101
- `--zx-green-*`) or raw color literals in application/component CSS. Prefer the least-specific
102
- override that works (global token → then, if needed, a component style).
110
+ `--zx-overlay-surface-image/toast-image/panel-image`,
111
+ `--zx-color-border(-subtle/-strong/-control)` (subtle for row and grid dividers),
112
+ `--zx-color-text(-muted/-subtle/-placeholder)` (subtle for icons and non-essential meta),
113
+ `--zx-color-accent(-hover/-subtle)/on-accent`, `--zx-color-accent-solid(-hover/-active)` with
114
+ `--zx-color-on-accent-solid` (the one filled primary button per screen),
115
+ `--zx-color-danger/warning/success/info(+ -bg)`, the categorical tone palette
116
+ `--zx-color-tone-{blue,teal,green,amber,orange,red,pink,violet,slate}-bg/-fg` (tags, chips, icon
117
+ tiles, avatars — neither the accent nor a status), the chart series `--zx-color-chart-1…8`,
118
+ `--zx-focus-ring`, `--zx-control-height/-radius`, `--zx-space-*`, `--zx-radius-*`, `--zx-text-*`
119
+ (`-3xl` for document titles), `--zx-weight-medium/-semibold`, `--zx-tracking-tight/-title`. Never
120
+ use tier-1 palette tokens (`--zx-gray-*`, `--zx-green-*`, `--zx-emerald-*`, `--zx-tone-*`) or raw
121
+ color literals in application/component CSS. Prefer the least-specific override that works (global
122
+ token → then, if needed, a component style).
103
123
 
104
124
  **Print.** `zx.css` imports `styles/print.css` last. It releases sticky headers and scroll
105
125
  containers, drops fills, shadows, zebra striping and focus rings, hides overlays and navigation
@@ -108,6 +128,20 @@ URLs in prose but never inside a table cell or on a button. Status badges keep t
108
128
  because there it carries meaning beside the word. An application that pins `data-zx-theme="dark"`
109
129
  still prints the dark token set; `auto` resolves to light under `@media print` and is unaffected.
110
130
 
131
+ **Motion.** Motion is a setting, beside the theme: put `data-zx-motion="reduced"` on `<html>` (or
132
+ on any region) to stop movement inside it. There, every `--zx-dur-*` token is `0ms` and a base rule
133
+ collapses CSS animations and transitions to their end state — menus still open, toasts still leave,
134
+ a spinner stops turning. The operating system's reduced-motion preference does the same for the
135
+ whole page. There is no value that turns motion back on: someone who asked their system for less
136
+ motion gets less, whatever an application or region says. Apply the attribute before first paint
137
+ (the documentation site stores it with the theme). Code that animates in script asks
138
+ `prefersReducedMotion(element?)` — true under the system preference or inside a reduced region,
139
+ across shadow roots — and subscribes with `watchMotion(element, (reduced) => …, {signal, immediate})`,
140
+ which returns a disposer; Chart already does. Name motion with the curves rather than inventing
141
+ them: `--zx-ease` for state changes, `--zx-ease-spring` (a small overshoot) for things that pop in,
142
+ `--zx-ease-drawer` (a decisive ease-out) for panels that slide, as Sheet does; and lift an item
143
+ being dragged with `--zx-drag-shadow`, as KanbanView's drag preview and keyboard-grabbed card do.
144
+
111
145
  ## Talking to ZeyOS — use `@zeyos/client`
112
146
 
113
147
  For ZeyOS business data use the **dedicated client library** `@zeyos/client`
@@ -182,13 +216,20 @@ await records.saveView({ name: 'My open invoices', setDefault: true });
182
216
  operation receives a query built by `buildListQuery`.
183
217
  - `zeyosForm(client, resource, opts)` returns
184
218
  `{form, ready, load(id), save(), getForm(), destroy()}`. `fields` is an ordered allow-list;
185
- `exclude`, `labels`, `title`, `columns`, `value|id`, and `onSaved` curate behavior. `load` maps
186
- stored timestamps to `Date`; `save` validates and creates/updates, maps back to Unix seconds,
187
- calls `onSaved`, and shows a success toast.
219
+ `exclude`, `labels`, `formats`, `timeZone`, `title`, `columns`, `value|id`, and `onSaved` curate
220
+ behavior. `load` maps stored timestamps to `Date`; `save` validates and creates/updates, maps back
221
+ to Unix seconds, calls `onSaved`, and shows a success toast. Its toasts (that confirmation and a
222
+ failed request's report) appear in the theme scope of `scope` — default: the form's own element,
223
+ so a form in a scoped dialog toasts inside that scope; `notify: false` raises none, leaving the
224
+ promise's result or rejection to the caller.
188
225
  - `zeyosTable(client, resource, opts)` returns
189
226
  `{table, load(), setSearch(), setFilters(), loadMore(), count, page, hasMore, destroy()}`. Columns
190
227
  come from schema metadata, sorting uses `sortMode:'server'`, and page zero replaces rows while
191
- later pages append them using server `limit`/`offset` and `count`.
228
+ later pages append them using server `limit`/`offset` and `count`. Table presentation options and
229
+ listeners pass through — `variant`, `zebra`, `stickyHeader`, `responsive`, `growing`, `emptyText`,
230
+ `rowClass`, `rowKind(s)`, `columnVisibility`, `columnReorder`, `hiddenColumns`, `on<event>`;
231
+ `onrowclick` runs beside `onRowClick`. The binding owns `columns`, `data`, and `sortMode`, and
232
+ leaves out editing, `rowReorder`, and `hierarchy`, which change rows it never persists.
192
233
  - `buildZeyosViewConfig(client, resource, opts)` resolves one schema to shared `ViewField[]`, a
193
234
  complete projection, and view options. `zeyosView(client, resource, CardView|TableView|KanbanView,
194
235
  opts)` returns `{view, load(), setSearch(), setFilters(), loadMore(), count, page, hasMore,
@@ -206,7 +247,11 @@ await records.saveView({ name: 'My open invoices', setDefault: true });
206
247
  scope's `userId` is intentionally not sent to the endpoint; the server session remains authority.
207
248
 
208
249
  Field mapping is schema-driven: entity/FK → async `zxselect`; enum/list → `optionlist` or native
209
- `select`; indexed `date` bigint → `date`; other date/time bigints → `datetime`; money/price/numeric
250
+ `select` with readable labels (a schema `{value, label}` as given, a stored constant in sentence
251
+ case — `INVOICED` → "Invoiced", `PARTLYPAID` → "Partly paid"); date-only bigints (indexed `date`,
252
+ `duedate`, `birthdate`, `datefrom`/`dateto` outside appointments and events, `datecancel`,
253
+ `lastbilling`) → `date`; other date/time bigints and `lastmodified` → `datetime`;
254
+ `formats: { field: 'date' }` overrides the guess; money/price/numeric
210
255
  → right-aligned `float`/number columns (amount columns use the row currency); boolean/checked →
211
256
  `toggle` and a check column; percent/progress → numeric percent; email/tel/url → typed text; arrays
212
257
  → `valuelist`; long text/JSON → `textarea`; short scalar text → `text`.
@@ -214,8 +259,13 @@ Field mapping is schema-driven: entity/FK → async `zxselect`; enum/list → `o
214
259
  ZeyOS queries always use `filters` (plural), not `filter`. Full-text input becomes `query`; sort is
215
260
  sent as signed server fields (`+field`/`-field`); projection supports aliases and dot joins; and
216
261
  search/filter/sort/pagination remain server-side. ZeyOS timestamps are integer Unix seconds, so
217
- forms convert them to/from `Date`. Use `dataFilterStateToFilters(state, defs)` before
218
- `tableBinding.setFilters(...)` when wiring a `DataFilter`.
262
+ forms convert them to/from `Date`. A date-only field stores midnight of a calendar day in the
263
+ instance's zone — a day, not an instant: tables show it with no time, and forms edit it as that day,
264
+ so neither the viewer's zone nor daylight saving moves it. Pass the instance's IANA zone as
265
+ `timeZone` (table, form, view; default: the runtime's own); `unixSecondsToCalendarDay(seconds, tz)`
266
+ and `calendarDayToUnixSeconds(date, tz)` do the same conversion for application code. Use
267
+ `dataFilterStateToFilters(state, defs)` before `tableBinding.setFilters(...)` when wiring a
268
+ `DataFilter`.
219
269
 
220
270
  ### Module icons and colours
221
271
 
@@ -282,19 +332,36 @@ accept.
282
332
  `aria-keyshortcuts`. The host binds the action and owns editing/modality/permission guards;
283
333
  a hint never installs a global handler. Dialog and Sheet footer descriptors accept it too.
284
334
  - **`buttonGroup([button(…), …])`** → a container that joins the buttons into one control.
335
+ - **Appearance** — `primary` is the screen's one solid accent and reads the
336
+ `--zx-color-accent-solid*` roles, so in dark it keeps the deep stop with white ink rather than a
337
+ bright slab. A `ghost` button's icon is drawn in `--zx-color-text-subtle` until hovered. A button
338
+ with `aria-pressed="true"` (a Toolbar item with `active`, a view toggle) is a mode, not a pick:
339
+ it takes the neutral `--zx-color-bg-current` fill and the text colour, never the accent.
285
340
  <!-- /doc -->
286
341
 
287
342
  <!-- doc:badge -->
288
343
  ### Badge — `badge(options)` / `badgeGroup(badges)`
289
344
 
290
345
  Factories (not classes) returning a status pill: a small, non-interactive `<span>` carrying a
291
- semantic colour, used for stages, counts, tags, and record-header metadata. A badge with no label
292
- but a `title` is named for assistive technology through `role="img"`; everything else is plain text
293
- and needs no ARIA.
346
+ semantic colour — or, with `tone`, a categorical one — used for stages, counts, tags, and
347
+ record-header metadata. A badge with no label but a `title` is named for assistive technology
348
+ through `role="img"`; everything else is plain text and needs no ARIA.
294
349
 
295
350
  - **Options** — `label`, `icon` (an icon name, rendered before the label), `kind:
296
- 'neutral'|'accent'|'success'|'warning'|'danger'|'info'`, `variant: 'soft'|'solid'|'outline'`,
297
- `size: 'md'|'sm'`, `dot` (a leading status dot instead of an icon), `title`.
351
+ 'neutral'|'accent'|'success'|'warning'|'danger'|'info'`, `tone:
352
+ 'blue'|'teal'|'green'|'amber'|'orange'|'red'|'pink'|'violet'|'slate'|null`, `variant:
353
+ 'soft'|'solid'|'outline'`, `size: 'md'|'sm'`, `dot` (a leading status dot instead of an icon),
354
+ `title`.
355
+ - **`kind` vs `tone`** — `kind` says how something is doing (status); `tone` says what it is (a
356
+ project, a label, a module) and draws from the `--zx-color-tone-*` palette, which is neither the
357
+ accent nor the status set, so a project tagged red never reads as an error. When `tone` is set it
358
+ decides the colour and `kind` is ignored for it; every `variant` still applies (soft: tinted,
359
+ borderless; solid: filled with the tone's ink; outline). The badge carries `data-tone`. Badges
360
+ are medium weight and sentence case.
361
+ - **Neutral and small** — the neutral soft fill is a wash of the text colour, not an opaque surface
362
+ step, so it stays visible on raised surfaces in the dark theme. `size: 'sm'` fits table cells
363
+ and board cards; a small badge with `dot` keeps the 12px text and a full spacing step between
364
+ dot and label.
298
365
  - **Methods** — none. `badge(options)` → `HTMLSpanElement`, `badgeGroup([badge(…), …])` → a wrapping
299
366
  inline row with its own spacing; you own both elements.
300
367
  - **Events** — none; a badge is not interactive.
@@ -319,6 +386,8 @@ a `MenuButton`, where choosing one activates the original control.
319
386
  `onclick`, from the row and from the overflow menu alike.
320
387
  - **Keyboard** — Tab reaches the toolbar once; ArrowLeft/ArrowRight move between controls and wrap;
321
388
  Home/End jump to the first and last control.
389
+ - **Pressed items** — an item with `active` renders `aria-pressed="true"` and takes the neutral
390
+ `--zx-color-bg-current` fill, never the accent.
322
391
  <!-- /doc -->
323
392
 
324
393
  <!-- doc:empty-state -->
@@ -413,7 +482,8 @@ shape is genuinely known. All three are factories returning elements.
413
482
 
414
483
  Two-state pressed button on a native `<button aria-pressed>`. The check indicator has a glyph in
415
484
  both states — an empty box when off, a check when on — so an unpressed or disabled button still
416
- reads as a two-state control.
485
+ reads as a two-state control. Pressed, the button goes neutral (the `--zx-color-bg-current` fill,
486
+ a stronger border, the text colour); only the check glyph, the mark, keeps the accent.
417
487
 
418
488
  - **Options** — `label: '' | [onLabel, offLabel]`, `checked: false`, `icon: true`,
419
489
  `disabled: false`.
@@ -428,6 +498,8 @@ reads as a two-state control.
428
498
  Switch (`role="switch"`, `aria-checked`).
429
499
 
430
500
  - **Options** — `checked: false`, `value: true`, `label: null`, `disabled: false`.
501
+ - **Label** — start-aligned and wrapping like text within the available width, the track level
502
+ with its first line.
431
503
  - **Methods** — `get()`, `set(checked, {silent})`, `toggle()`, `getValue()` (returns
432
504
  `options.value` when on, `false` when off), `enable()`/`disable()`.
433
505
  - **Events** — `change {checked, value}`.
@@ -456,17 +528,34 @@ permissions, recent-history/cache policy, and the route or command ultimately in
456
528
  `new Launcher(existingDialog, options)` enhances and later restores an existing `<dialog>`.
457
529
  - **Options** — `items`, abortable `sources: [{id, label?, minQuery?, when?, order?, load(query, {signal})}]`,
458
530
  `query`, `debounce: 250`, `minQuery: 0`, `maxResults: 100`, `placeholder`, `label`, `emptyText`,
459
- `loadingText`, `shortcut: 'mod+k'|false`, visible `hints` labels (or `false`), and `scope` to
460
- explicitly select an element whose nearest Zx theme scope owns the launcher.
461
- - **Items** — `{id, label, description?, keywords?, group?, icon?, badge?, value?, href?, target?,
462
- invoke?, pinned?, disabled?, kind?, current?, when?, groupOrder?, itemOrder?}`. `icon` accepts a
463
- bundled icon name, Node, or factory. Links stay native so modified and middle clicks work;
531
+ `loadingText`, `shortcut: 'mod+k'|false`, visible `hints` labels `{move, open, back, close}` (or
532
+ `false`), and `scope` to explicitly select an element whose nearest Zx theme scope owns the launcher.
533
+ - **Items** — `{id, label, description?, keywords?, group?, icon?, badge?, hint?, shortcut?, keys?,
534
+ value?, href?, target?, invoke?, pinned?, disabled?, kind?, current?, when?, groupOrder?,
535
+ itemOrder?, items?, placeholder?, order?}`. `icon` accepts a bundled icon name, Node, or
536
+ factory. Links stay native so modified and middle clicks work;
464
537
  non-link commands may provide an application-owned `invoke` callback. Malformed and executable
465
- URL schemes are stripped; a descriptor with no safe link or callback is disabled.
466
- - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `focus()`, `setQuery()`, `getQuery()`,
467
- `setItems()`, `setCurrent()`, `setSources()`, `destroy()`.
468
- - **Events** — cancelable `select {item, value, source, query}`, `query {query}`, `open`, `close`,
469
- and `error {source, error}`. Preventing `select` suppresses the supplied link or callback.
538
+ URL schemes are stripped; a descriptor with no safe link or callback is disabled. `hint` is muted
539
+ text at the end of the row (a current value, a project); `shortcut: ['⌘', 'K']` (or Button's
540
+ `{label, keys}`) draws key caps for display only, and `keys: 'Meta+K'` adds `aria-keyshortcuts`.
541
+ - **Pages** — an item with `items` opens a page instead of being selected: an array, or a loader
542
+ `(query, {signal}) => items | Promise<items>` that runs when the page opens and, debounced, as
543
+ its query changes (the loading and empty states show meanwhile; `order: 'source'` keeps the
544
+ loader's order instead of ranking it). Opening a page clears the query, shows a scope chip with
545
+ the item's label before the input — part of the combobox's name, `Search <label>` — and adds
546
+ "⌫ Back" to the footer; `placeholder` sets the page's placeholder. Page rows show a chevron.
547
+ Backspace on an empty query, ArrowLeft on an empty query, or the chip's × goes back one page,
548
+ restoring the query and highlighted row beneath; Escape closes from any page, and the next
549
+ opening starts on the root. Pages nest. `open({page})` opens straight into a page — a local page
550
+ id (at any depth of array pages, the path becoming the back stack) or a page item. A polite
551
+ status line announces each change ("Agents, 7 results").
552
+ - **Methods** — `open({page}?)`, `close()`, `toggle()`, `isOpen()`, `focus()`, `setQuery()`,
553
+ `getQuery()`, `back()`, `getPage()`, `setItems()` (the root's items), `setCurrent()`,
554
+ `setSources()`, `destroy()`.
555
+ - **Events** — cancelable `select {item, value, source, query, page}`, `query {query}`,
556
+ `page {page, path}` (after every page change; `page` is null on the root), `open`, `close`, and
557
+ `error {source, error, page}` (a failing page loader reports as source `page:<id>`). Preventing
558
+ `select` suppresses the supplied link or callback.
470
559
  - **Ranking** — local results are case/diacritic-insensitive and deterministic: exact label,
471
560
  label prefix, word prefix, label substring, identifier substring, then acronym prefix. Pinned
472
561
  items retain an explicit order ahead of otherwise equal results. A new async query aborts and
@@ -474,13 +563,18 @@ permissions, recent-history/cache policy, and the route or command ultimately in
474
563
  - **Keyboard** — Cmd+K on macOS or Ctrl+K elsewhere opens by default; the shortcut is ignored in
475
564
  editable controls, composition, and modal contexts. Application tiles use spatial arrow-key
476
565
  movement; Up/Down may continue into record groups. Home/End, Page Up/Down, and Enter operate the
477
- single listbox selection model; Escape closes and restores focus. `aria-current` marks the
478
- active destination independently from the keyboard's `aria-selected` option.
566
+ single listbox selection model; Escape closes and restores focus. Enter, or ArrowRight with the
567
+ caret at the end of the query, opens a page row; Backspace or ArrowLeft on an empty query goes
568
+ back. `aria-current` marks the active destination independently from the keyboard's
569
+ `aria-selected` option.
479
570
  - **Optional ZeyOS adapter** — import `zeyosLauncher()` or `buildZeyosLauncherConfig()` from
480
571
  `@zeyos/zx/zeyos`. The adapter receives the permitted module/menu catalogue, pins, current
481
572
  identifier, recent loader or cache, abortable search, destination resolvers, and application/
482
573
  record icon renderers explicitly. It maps the complete shell payload without reading globals or
483
574
  taking over application routing.
575
+ - **Appearance** — group labels are sentence-case captions. The application you are in
576
+ (`aria-current="page"`) takes the neutral `--zx-color-bg-current` fill and a semibold label; the
577
+ highlighted row is the lighter hover step. Neither uses the accent.
484
578
  <!-- /doc -->
485
579
 
486
580
  <!-- doc:app-icon -->
@@ -519,13 +613,20 @@ options rather than application globals.
519
613
  <!-- doc:avatar -->
520
614
  ### Avatar
521
615
 
522
- A fixed-geometry user image with deterministic initials fallback. Image loading or failure never
523
- changes layout, and the component is decorative by default so adjacent identity text is not
524
- announced twice.
525
-
526
- - **Options** — `src`, `name`, `initials`, `label`, `size: 'sm'|'md'|'lg'|<pixels>`,
527
- `shape: 'circle'|'rounded'|'square'`, `status: 'online'|'away'|'busy'|'offline'|null`, and
528
- `statusLabel`.
616
+ A fixed-geometry user image with a deterministic fallback: initials on a tint from the categorical
617
+ tone palette, picked by a stable hash of the name. Image loading or failure never changes layout,
618
+ and the component is decorative by default so adjacent identity text is not announced twice.
619
+
620
+ - **Options** — `src`, `name`, `initials`, `label`, `size: 'sm'|'md'|'lg'|<pixels>` (24 / 32 /
621
+ 40px at the default spacing unit), `shape: 'circle'|'rounded'|'square'`, `tone:
622
+ 'blue'|'teal'|'green'|'amber'|'orange'|'red'|'pink'|'violet'|'slate'|null`, `status:
623
+ 'online'|'away'|'busy'|'offline'|null`, and `statusLabel`.
624
+ - **Fallback** — no ring; the `--zx-color-tone-*-bg` fill with its `-fg` ink, initials at 0.4× the
625
+ size (never under 10px), weight 600, one initial below 24px. `tone: null` derives the tone with
626
+ `avatarTone(name)` (exported from the module): FNV-1a over the trimmed, NFC-normalised,
627
+ lower-cased name, mapped onto the eight chromatic tones, so a person keeps their colour on every
628
+ screen. `slate`, the neutral, is reserved for an avatar with no name and can be set explicitly.
629
+ The root carries `data-tone`. `avatarInitials(name, limit = 2)` returns the initials.
529
630
  - **Methods** — `set(values)`, `setStatus(status, label?)`, `hasImage()`, `destroy()`.
530
631
  - **Accessibility** — set `label` only when the avatar stands alone. Presence is supplementary
531
632
  status, not the account's accessible name; supply `statusLabel` when it conveys information.
@@ -563,10 +664,11 @@ A durable or transient file row composed from `EntityRef`, MIME/byte-size metada
563
664
  status, and accessible progress. It never starts a transfer, decides authorization, resolves a
564
665
  preview, or persists a file.
565
666
 
566
- - **Options** — `id`, `name`, `size` (bytes), `mime`, `subtitle`, `icon: 'file'`, additional
667
+ - **Options** — `id`, `name`, `size` (bytes), `mime`, `typeLabel: null`, `subtitle`,
668
+ `icon: 'file'`, additional
567
669
  `metadata`, safe native `link`, `actions`, status (`ready` | `temporary` | `waiting` |
568
670
  `uploading` | `processing` | `success` | `error`), `statusLabel`, `progress` (0–100), `indeterminate`,
569
- `density: 'sm'|'md'|'lg'`, `wrap`, `actionsLabel`, and `locale`.
671
+ `identitySize: 'sm'|'md'|'lg'`, `wrap`, `actionsLabel`, and `locale`.
570
672
  - **Methods** — `set(values)`, `setStatus(status,label?)`,
571
673
  `setProgress(percent|null,{indeterminate?})`, `getFile()`, `focus()`, and `destroy()`.
572
674
  - **Events** — cancelable `activate {id,file,href,event}` and
@@ -574,6 +676,11 @@ preview, or persists a file.
574
676
  suppresses callbacks and navigation rather than merely observing them.
575
677
  - **Progress** — determinate work exposes min/max/now and percentage text. Indeterminate work
576
678
  omits `aria-valuenow`; status changes are announced separately.
679
+ - **Type** — the MIME type is shown as a readable type ("PDF", "CSV", "Image", "Spreadsheet"),
680
+ falling back to the name's extension ("EXE") when it is unfamiliar and never to the raw string;
681
+ `typeLabel` overrides it and `''` hides it. The default `icon: 'file'` gives way to a glyph for
682
+ the MIME type (`file-pdf`, `file-image`, …) when the active icon set has one — Font Awesome, or
683
+ a glyph added with `registerIcons()`; any other `icon` is used as given.
577
684
  <!-- /doc -->
578
685
 
579
686
  <!-- doc:file-list -->
@@ -620,8 +727,12 @@ incremental host-controlled mutation. It deliberately preserves source order ins
620
727
  whether a feed is newest- or oldest-first.
621
728
 
622
729
  - **Options** — `items`, `groupBy` (property, resolver, or null), `groupLabel`,
623
- `label: 'Activity'`, `loading`, `loadingCount: 3`, `loadingText`, `emptyText`,
624
- `groupHeadingLevel`, and `itemHeadingLevel`.
730
+ `label: 'Activity'`, `variant: 'default'|'compact'`, `loading`, `loadingCount: 3`,
731
+ `loadingText`, `emptyText`, `groupHeadingLevel`, and `itemHeadingLevel`.
732
+ - **Compact** — `variant: 'compact'` (reflected as `data-variant`) keeps every entry on one line at
733
+ any width, for a summary card narrower than 30rem: actor and title read as one sentence that
734
+ wraps as text, the time keeps the trailing end instead of stacking under the title, an avatar
735
+ descriptor without a `size` is small, and metadata follows as a muted second line.
625
736
  - **Methods** — `setItems(items,{silent?})`, `getItems()`, `getItem(id)`,
626
737
  `appendItems(items,{silent?})`, `prependItems(items,{silent?})`,
627
738
  `updateItem(id,values,{silent?})`, `removeItem(id,{silent?})`, `setLoading()`, and `destroy()`.
@@ -647,6 +758,9 @@ out, or mutates the session.
647
758
  action without rebuilding the account surface.
648
759
  - **Accessibility** — compact mode is visually avatar-only but keeps the full account-menu name on
649
760
  its single native trigger; MenuButton retains focus return, typeahead, and arrow-key behavior.
761
+ - **Appearance** — the trigger is a flat row (avatar, name, secondary line, chevron) with the hover
762
+ fill, not a button: it takes back the border, glass fill, blur and shadow MenuButton's `.zx-btn`
763
+ would give it, whatever order the stylesheets load in.
650
764
  <!-- /doc -->
651
765
 
652
766
  <!-- doc:select -->
@@ -671,13 +785,33 @@ Unifies single-select, local filtering, and async loading.
671
785
  entries are narrowed locally against the same query and stay resolvable by `set()`. They survive
672
786
  `setItems()` and are shown below `minQuery`. Use them for choices with no row in the source —
673
787
  "Unassigned", "Everyone", "Private".
674
- - **Presets** — `Select.priority(target, opts)` renders the five-level, five-square ZeyOS priority
675
- scale; `Select.status(target, opts)` supplies shape- and semantic-color-distinct workflow states
676
- and accepts replacement `items`; `Select.permission(target, {groups, value})` pins Private and
788
+ - **Presets** — `Select.priority(target, opts)` renders the five-level ZeyOS priority scale (values
789
+ 0–4) as grey signal bars filled to the level; only the highest is coloured, a danger square with
790
+ "!". A "No priority" row (value null, message `priority.none`) heads the list and is the empty
791
+ control's placeholder; `clearable: false` removes it, as does a `required` `zxselect` field with
792
+ `preset: 'priority'`. `Select.status(target, opts)` supplies shape- and semantic-color-distinct workflow states
793
+ and accepts replacement `items` (each `{ID, name, indicator, tone, description?, shortcut?}`);
794
+ `Select.permission(target, {groups, value})` pins Private and
677
795
  Public above record-access groups. Its value is the tri-state ZeyOS stores (`false` private,
678
796
  `true` public, or a group ID), and `groups` takes an array or loader `(query)=>items`.
797
+ - **Glyphs** — `statusIcon(status, {tone?, indicator?, label?})` and `priorityIcon(level, {label?})`
798
+ return the exact glyphs the two presets draw (`.zx-status-icon`, `.zx-priority-icon`; inside a
799
+ Select they also carry `.zx-select__status-indicator` / `.zx-select__priority-icon`), for list
800
+ rows, board columns, filter chips, and headings. Status ids: `backlog` (dotted ring), `todo`
801
+ (empty ring), `in-progress` (ring with a centre dot), `in-review` or `review` (half filled),
802
+ `ready` (a third filled), `waiting` (two thirds filled), `done` (filled, check), `canceled` or
803
+ `cancelled` (filled, cross), `duplicate` (ring with a slash); a `Select.status` item object is
804
+ accepted too, and an unknown id draws a muted empty ring. Indicators: `dotted`, `ring`,
805
+ `progress`, `review`, `ready`, `waiting`, `done`, `canceled`, `duplicate`; tones: `muted`,
806
+ `neutral`, `warning`, `info`, `accent`, `success`, `danger`. Priority levels: `null` (no
807
+ priority, dashes), `0` lowest … `3` high (signal bars filled to the level), `4` highest (danger
808
+ square with "!"). Both are decorative (`aria-hidden`) — always pair them with the status or
809
+ priority as text, or a tooltip on the control holding them; `label` instead makes a standalone
810
+ glyph `role="img"` with that name.
679
811
  - **Entity preset** — `Select.entity(target, options)` renders icon/color, label, and secondary
680
- text using `iconKey`, `colorKey`, and `subtitleKey` (or `renderIcon`). `recent` pins a deduplicated
812
+ text using `iconKey`, `colorKey`, and `subtitleKey` (or `renderIcon`). `colorKey` takes a CSS
813
+ colour or a token reference (`var(--zx-color-tone-blue-fg)`); without one the glyph is a neutral
814
+ tile in muted ink. `recent` pins a deduplicated
681
815
  **Recent** section above ordinary/local/async results; `create: true|actionDescriptor` appends an
682
816
  application-owned command at the bottom without changing the selected ID. `recentLabel` and the
683
817
  ordinary Select options remain overridable. For schema-driven ZeyOS loading,
@@ -686,6 +820,13 @@ Unifies single-select, local filtering, and async loading.
686
820
  - **Keyboard** (APG combobox) — ArrowDown/Alt+ArrowDown open; arrows navigate and wrap; Home/End;
687
821
  Enter selects; Esc closes; Tab closes; printable characters filter (editable) or run a typeahead
688
822
  (readonly). `aria-activedescendant` tracks the active option.
823
+ - **Appearance** — the highlighted option is a neutral step; a chosen option is a pick and keeps
824
+ the accent tint and its check mark. Group labels are sentence-case captions. The focus ring
825
+ follows keyboard focus: a click on a read-only input (no `filter`) does not draw it
826
+ (`data-focus-origin="pointer"` on the root until the next key). Disabled, the whole control fades
827
+ as a disabled Field does, and not a second time inside one.
828
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the value reads as text: no border, fill,
829
+ chevron or clear button until hover; full field chrome while focused or open.
689
830
  <!-- /doc -->
690
831
 
691
832
  <!-- doc:sort-control -->
@@ -713,7 +854,10 @@ from `Select`, with its themed popover and keyboard behavior.
713
854
  Searchable multi-check list with optional async loading.
714
855
 
715
856
  - **Options** — `items`, `valueKey: 'ID'`, `labelKey: 'name'`, `checkedKey: 'on'`, `search: true`,
716
- `height: 280`, `defaultChecked: false`, `load: async ()=>items`.
857
+ `height: 280`, `defaultChecked: false`, `hideLabel: false` (leaves out the heading; the group
858
+ keeps its text as `aria-label`), `load: async ()=>items`.
859
+ - **In a Field** — field type `checklist` draws no heading of its own: the field's label names the
860
+ group (`aria-labelledby`).
717
861
  - **Methods** — `setItems()`, `getValues()`, `setValues(ids)`, `checkAll()`/`uncheckAll()`,
718
862
  `search(query)`, `reload()`.
719
863
  - **Events** — `change {values}`, `loaded`.
@@ -739,6 +883,11 @@ Field type: `number`.
739
883
  set (otherwise they keep their caret meaning), Enter commits, wheel steps while focused.
740
884
  - **Also exported** — `parseNumber(raw)` (accepts `.` and `,` as the decimal separator, ignores
741
885
  grouping, returns null for junk) and `snapNumber(value, {min, max, step, precision})`.
886
+ - **In a Field** — field type `number` fills the control column like a text input and takes the
887
+ field key as its `name`.
888
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the number reads as text: no border, no
889
+ fill, the value start-aligned with the quiet values around it, both steppers at the trailing end
890
+ and hidden (kept in the layout so nothing moves) until hover or focus; full chrome on focus.
742
891
  <!-- /doc -->
743
892
 
744
893
  <!-- doc:rating -->
@@ -770,7 +919,9 @@ from; everything Zx adds is chrome around it. Field type: `slider`.
770
919
  `showValue: true`, `showBounds: false` (draws `min` and `max` either side of the track),
771
920
  `showInput: false` (adds a number box for entering a value precisely instead of aiming at it),
772
921
  `unit: ''`, `marks: null` (numbers, or `{value, label}` objects), `disabled: false`,
773
- `readonly: false`, `size: 'md'|'sm'`, `formatValue: (value) => string`.
922
+ `readonly: false`, `size: 'md'|'sm'`, `formatValue: (value) => string`. Each mark sits under
923
+ the thumb's centre at its value; the outermost labels end flush with the track instead of
924
+ spilling out of the component.
774
925
  - **Methods** — `get()`, `set(value, {silent})`, `setRange(min, max, step?)`, `setLabel()`,
775
926
  `setReadonly()`, `focus()`, `enable()`/`disable()`. `value` is also a getter/setter.
776
927
  - **Events** — `input {value}` continuously while dragging, `change {value}` once the value is
@@ -805,7 +956,8 @@ to a hidden textarea when the async Clipboard API is unavailable or denied.
805
956
  that did not happen.
806
957
  - **Accessibility** — on success the glyph becomes a tick and a visually hidden `role="status"`
807
958
  speaks the confirmation, then both revert. The icon-only form carries the `title` as its
808
- accessible name.
959
+ accessible name. The status line is positioned inside the button, so it never adds scrollable
960
+ overflow to the page or to a scrolling pane.
809
961
 
810
962
  #### CopyInput
811
963
 
@@ -856,10 +1008,16 @@ Text date input with a calendar popover. `DateTimeBox(target, opts)` is a Datebo
856
1008
  tokens are the date-utility ones: `%d %m %Y %H %M`.
857
1009
 
858
1010
  - **Options** — `value: Date | number(unix) | string`, `format: '%d.%m.%Y'`, `time: false`, `min`,
859
- `max`, `placeholder`, `clearable: true`, `disabled`.
1011
+ `max`, `placeholder`, `clearable: true` (the clear button shows while there is a value and the
1012
+ box is enabled; without `clearable` the input keeps no room for it), `disabled`.
1013
+ - **In a Field** — field types `date`/`datetime` (and `daterange`) fill the control column like a
1014
+ text input and give way in a narrow two-column row; standalone, the box keeps its own width.
860
1015
  - **Methods** — `get(unit='date')` (`'seconds'` → unix), `set(value, {silent})` (accepts a Date;
861
1016
  pass Date objects, not display strings), `open()`/`close()`, `enable()`/`disable()`, `focus()`.
862
1017
  - **Events** — `change {date}`, `invalid {text}`, `open`, `close`.
1018
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the date reads as text: no border, no fill,
1019
+ the calendar button hidden until hover; full chrome on focus. An invalid value keeps its danger
1020
+ border. DateRangeBox behaves the same way.
863
1021
  <!-- /doc -->
864
1022
 
865
1023
  <!-- doc:date-range -->
@@ -877,7 +1035,7 @@ An inline two-month calendar for picking a start and an end day, plus the form-u
877
1035
 
878
1036
  #### DateRangeBox
879
1037
 
880
- - **Options** — everything DateRangePicker takes, plus `format: '%d.%m.%Y'`, `separator: ' – '`, `placeholder: null` (derived from the format when null), `clearable: true`. Typed text is parsed as `"<start><separator><end>"`; a lone date reads as a one-day range and a trailing separator keeps the range half open.
1038
+ - **Options** — everything DateRangePicker takes, plus `format: '%d.%m.%Y'`, `separator: ' – '`, `placeholder: null` (derived from the format when null), `clearable: true` (no clear button while disabled, and no room kept for one without it). Typed text is parsed as `"<start><separator><end>"`; a lone date reads as a one-day range and a trailing separator keeps the range half open.
881
1039
  - **Methods** — `get(unit = 'date')` (`'seconds'` returns `{start, end}` as Unix seconds), `set(range, {silent})` accepting `{start, end}` of Dates, Unix-seconds numbers, or formatted strings, `clear({silent})`, `open()`, `close()`, `enable()`, `disable()`, `focus()`, `destroy()`.
882
1040
  - **Events** — `change {start, end}`, `invalid {text}` when the typed value cannot be parsed (the text stays editable and the field takes `data-state="invalid"` plus `aria-invalid`), `open`, `close`.
883
1041
  <!-- /doc -->
@@ -893,6 +1051,8 @@ use `TimePicker` or `DateTimeBox`.
893
1051
  - **Methods** — `get(unit)`, `set(value, unit?, {silent})`, `enable()`/`disable()`.
894
1052
  - **Events** — `change {value}`, in `options.unit`.
895
1053
  - **Also exported** — the pure helpers `splitTime` and `joinTime`.
1054
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the segments (native inputs) and the sign
1055
+ button drop their border and fill until hover or focus; the sign keeps its colour.
896
1056
  <!-- /doc -->
897
1057
 
898
1058
  <!-- doc:message -->
@@ -908,8 +1068,24 @@ messages rather than the page, and uses the translucent glass surface tokens wit
908
1068
  `Message.progress(text, {scope?})` → `{ update(pct, text?), done(), fail(text?) }`. `scope` is
909
1069
  an element or selector whose nearest Zx theme scope owns that floating region; without it the
910
1070
  focused opener supplies the scope.
1071
+ - **Region lifecycle** — a floating region is created on the first toast for its scope and removed
1072
+ when its last toast closes. One whose host was emptied or left the document is replaced on the
1073
+ next toast rather than reused, and a scope that is itself detached falls back to the focused
1074
+ element's scope. `Message.disposeRegion(scope)` → boolean removes a scope's region at once,
1075
+ toasts and all, for a host torn down while toasts are still showing.
911
1076
  - **Per-message options** — `kind: 'info'`, `timeout`, `closable: true`, `maxVisible: 5` (extras
912
1077
  queue, and hovering pauses dismissal).
1078
+ - **Action** — `action: { label, onclick?, kind? }` renders one small button (`.zx-btn`,
1079
+ `data-size="sm"`, secondary unless `kind` is `primary`/`danger`/`ghost`) in a toast or an inline
1080
+ message; activating it runs `onclick(event)` and closes the message. A message with an action
1081
+ stays at least 8000ms (`timeout: 0` still persists). Every message holds its timer while the
1082
+ pointer is over it or focus is inside it (WCAG 2.2.1) and resumes once neither is; the region is
1083
+ not modal, so Tab moves between the action and the close button, and closing a message that holds
1084
+ focus returns focus to where it came from. The live region still announces the text.
1085
+ - **Leading** — `leading: Node | component` (an Avatar, say) replaces the kind icon. It is
1086
+ decorative; the text must name what it shows.
1087
+ - **Handle** — every `show()` (static or inline) and `info/success/warning/error()` return
1088
+ `{ close() }`; closing twice, or a queued message, is safe.
913
1089
  <!-- /doc -->
914
1090
 
915
1091
  <!-- doc:modal -->
@@ -919,7 +1095,9 @@ Thin overlay on the native `<dialog>` element. The surface uses the shared, read
919
1095
  panel material; the backdrop uses `--zx-color-overlay-backdrop`, and focus returns to the opener on close.
920
1096
 
921
1097
  - **Options** — `content`, `width: 'auto'`, `closable: true`, `lightDismiss: false`,
922
- `destroyOnClose: false`, `scope: null` (the opener's nearest Zx theme scope by default).
1098
+ `destroyOnClose: false`, `scope: null` (the opener's nearest Zx theme scope by default). The
1099
+ scope is resolved again on every `open()`; a `scope` element not yet in the document leaves the
1100
+ dialog detached until then, rather than parking it on `<body>` outside the theme.
923
1101
  - **Methods** — `open()`, `close(result?)`, `setContent()`, `isOpen()`.
924
1102
  - **Events** — `open`, `close {result}`, `cancel` (Esc; preventable with `event.preventDefault()`).
925
1103
  <!-- /doc -->
@@ -933,9 +1111,17 @@ Structured modal: a header with title and close button, a body, and footer butto
933
1111
  `buttons: [{ label, kind, action: 'close'|'cancel'|fn(dialog), autofocus, shortcut }]`, `closable: true`.
934
1112
  - **Methods** — `setTitle()`, `setContent()`, `setButtons()`,
935
1113
  `addView(key, { content, buttons? })`, `showView(key)`.
936
- - **Statics returning Promises** — `Dialog.alert({ title, message })`,
937
- `Dialog.confirm({ title, message, danger })` → boolean,
938
- `Dialog.prompt({ title, message, value })` → string|null.
1114
+ - **Statics returning Promises** — `Dialog.alert({ title, message, okLabel })`,
1115
+ `Dialog.confirm({ title, message, okLabel, cancelLabel, danger })` → boolean,
1116
+ `Dialog.prompt({ title, message, value, placeholder, okLabel, cancelLabel })` → string|null.
1117
+ The labels default to the translated "OK" and "Cancel"; name the action instead where you can
1118
+ ("Delete", "Rename").
1119
+ - **Type to confirm** — `Dialog.confirm({ ..., confirmText: 'Website Redesign' })` adds an input
1120
+ labelled "Type **Website Redesign** to confirm" (a real `<label>`; message
1121
+ `dialog.typeToConfirm`, `'Type %1 to confirm'`). Focus starts in the input; the confirm button
1122
+ stays disabled, and Enter does nothing, until the value matches exactly (case-sensitive, ignoring
1123
+ surrounding spaces). Escape, the close button, and Cancel resolve `false` as before. Use it for
1124
+ deletes that cannot be undone, with `danger: true` and an `okLabel` that names the action.
939
1125
  <!-- /doc -->
940
1126
 
941
1127
  <!-- doc:sheet -->
@@ -987,12 +1173,14 @@ When a Dock adopts it, the Sheet becomes an opaque layout surface instead.
987
1173
  `aria-valuenow`/`aria-valuemin`/`aria-valuemax`: arrow keys along the axis step it by 16px, Shift
988
1174
  by 64px. A docked sheet hides its handle — the dock's own divider resizes it there.
989
1175
  - **Custom properties** — `--zx-sheet-size` (written by `size`; the per-side default otherwise)
990
- and `--zx-sheet-radius` (inner corners of a top/bottom sheet, default `--zx-radius-xl`) are
1176
+ and `--zx-sheet-radius` (inner corners of a top/bottom sheet, default `--zx-radius-lg`) are
991
1177
  internal geometry, not published styling hooks — prefer the options. `--zx-overlay-blur` is a
992
1178
  tier-2 semantic token and is public: it sets the `'blur'` backdrop's radius, default `8px`.
993
1179
  - **Motion** — entry and exit both animate, via `@starting-style` and
994
1180
  `transition-behavior: allow-discrete` on `display`/`overlay`; without them the sheet appears and
995
- disappears instantly. All of it sits inside `prefers-reduced-motion: no-preference`.
1181
+ disappears instantly. The slide (and a docked track's collapse, and a stack's shift) runs on
1182
+ `--zx-ease-drawer`; the fade keeps `--zx-ease`. All of it sits inside
1183
+ `prefers-reduced-motion: no-preference`, and stops inside a `data-zx-motion="reduced"` region.
996
1184
  - **Keyboard and screen readers** — identical to Dialog: the native `<dialog>` supplies the role
997
1185
  and `aria-labelledby` points at the title. Escape closes when `closable`, focus moves to the
998
1186
  `[autofocus]` control or the first focusable descendant on open, and returns to the opener on
@@ -1049,6 +1237,10 @@ DOM child of the anchor's nearest Zx theme scope while the browser lifts it into
1049
1237
  `left*` and `right*` placements, `offset: 4`,
1050
1238
  `matchWidth: false`, `openOn: 'click'|'manual'`, `closeOnSelect: false`, `scope: null` (the
1051
1239
  anchor's nearest Zx theme scope by default).
1240
+ - **Hosting** — the host is resolved again on every `open()`. A dropdown created before its anchor
1241
+ is in the document keeps its panel detached until the first open instead of parking it on
1242
+ `<body>`, so an anchor inserted into a `[data-zx-theme]` region or a `.zx-scope` afterwards still
1243
+ gets its theme. Tooltip, MenuButton, and ContextMenu panels follow the same rule.
1052
1244
  - **Methods** — `open()`, `close()`, `toggle()`, `isOpen()`, `setContent()`, `getPanel()`.
1053
1245
  - **Events** — `open`, `close`.
1054
1246
  <!-- /doc -->
@@ -1068,13 +1260,22 @@ A button that opens a `role="menu"`.
1068
1260
  default.
1069
1261
  - `role: 'menuitemcheckbox'|'menuitemradio'` with `checked` exposes an APG checked action. The
1070
1262
  application owns state changes and calls `setItems()` with the new model.
1071
- - **Methods** — `setItems()`, `open()`, `close()`, `setLabel()`, `getTrigger()`, `getPanel()`,
1072
- `focusFirst()`, `focusLast()`.
1263
+ - **Methods** — `setItems()`, `open({returnFocus})`, `close()`, `setLabel()`, `getTrigger()`,
1264
+ `getPanel()`, `focusFirst({returnFocus})`, `focusLast({returnFocus})`.
1073
1265
  - **Events** — `select {value, item}`, `open`, `close`.
1266
+ - **Focus return** — choosing an item closes the menu and returns focus to the trigger before the
1267
+ `select` event and the item's `onselect` run, so a dialog they open returns to the trigger when
1268
+ it closes, and focus they move themselves stays where they put it. Escape returns focus the same
1269
+ way. `returnFocus` sets the destination for one opening: `true` (default) is the trigger, an
1270
+ element is that element — the list row a keyboard shortcut opened the menu from — and `false`
1271
+ leaves focus alone.
1074
1272
  - **Cancellation** — `select` is cancelable. Preventing it skips the item's `onselect` callback and
1075
- leaves the menu open, which is the composition seam used by `AccountMenu` and rail flyouts.
1273
+ leaves the menu open with focus back on the item, which is the composition seam used by
1274
+ `AccountMenu` and rail flyouts.
1076
1275
  - **Keyboard** — ArrowDown/Enter/Space open and focus the first item; ArrowUp opens and focuses the
1077
1276
  last; arrows cycle; Home/End; typeahead; Enter/Space select; Esc/Tab close.
1277
+ - **Appearance** — the highlighted item is a neutral step, a checked item keeps its accent check
1278
+ mark, and section headings are sentence-case captions. ContextMenu shares the same rules.
1078
1279
  <!-- /doc -->
1079
1280
 
1080
1281
  <!-- doc:context-menu -->
@@ -1150,13 +1351,28 @@ Sortable, selectable, sticky-header data table. `fr` widths fill the container w
1150
1351
  widths scroll horizontally, and multi-select adds a tri-state header checkbox plus Shift+click
1151
1352
  range select.
1152
1353
 
1153
- - **Options** — `columns: [{ id, label, sortable?, width ('120px'|'2fr'|'auto'), align,
1354
+ - **Options** — `columns: [{ id, label, ariaLabel?, sortable?, width ('120px'|'2fr'|'auto'), align,
1154
1355
  type?: 'text'|'number'|'currency'|'percent'|'unit', locale?, decimals?, currency?, unit?,
1155
1356
  render?: (row,i)=>Node|string, sortValue?, headerTitle? }]`, `data`, `rowId: 'ID'`,
1156
1357
  `sort: { id, dir }`, `sortMode: 'local'|'server'`, `selectable: false|'single'|'multi'`,
1157
- `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`, and
1158
- `hierarchy: false|{parentId, column?, expanded?}`. Locale, currency, unit, and decimals may be
1159
- row callbacks; values remain numbers in row data while display formatting is locale-aware.
1358
+ `stickyHeader: true`, `height`, `emptyText`, `rowClass`, `zebra: true`, `variant:
1359
+ 'default'|'plain'`, and `hierarchy: false|{parentId, column?, expanded?}`. Locale, currency,
1360
+ unit, and decimals may be row callbacks; values remain numbers in row data while display
1361
+ formatting is locale-aware.
1362
+ - **Appearance** — the header sits on the table's surface (no band) in the muted text colour at a
1363
+ medium weight, sentence case; the sorted column's label takes the text colour beside its arrow.
1364
+ Rows are divided by `--zx-color-border-subtle`, the header by the ordinary border, and every cell
1365
+ uses tabular figures. Checked rows of a multi-selection keep the accent tint (`--zx-color-bg-selected`);
1366
+ the selected row of a `selectable: 'single'` table is the current record of a master–detail list
1367
+ and takes the neutral `--zx-color-bg-current` fill. Sortable and plain headers set the same type,
1368
+ and the scroll region reserves no scrollbar gutter until the table actually scrolls.
1369
+ - **Columns in the DOM** — every header and body cell carries `data-column="<id>"` in every mode,
1370
+ read-only included, so a table can be styled per column. A column with `label: ''` (icons,
1371
+ actions) keeps a blank header named by `ariaLabel`, or by its id; `ariaLabel` also names the
1372
+ column in the chooser and its cell editors.
1373
+ - **`variant: 'plain'`** — for a table inside a card or panel that already draws the box: no outer
1374
+ frame, no header band, no zebra, hairline row dividers. Reflected as `data-variant="plain"` on the
1375
+ root; TableView passes it through `table: { variant: 'plain' }`.
1160
1376
  - **Methods** — `setData()`, `addData()`, `updateRow(id,row)`, `removeRow(id)`, `getRow(id)`,
1161
1377
  `getData()`, `empty()`, `setSort(id,dir)`, `getSort()`, `clearSort()`, `getSelection()`, `setSelection(ids)`,
1162
1378
  `clearSelection()`, `setLoading(bool)` (busy/skeleton state — dims rows, shows an indeterminate
@@ -1174,6 +1390,8 @@ range select.
1174
1390
  `'cell'` edits one cell, `'row'` opens every editable cell of the row and commits them as a unit.
1175
1391
  Editors are sized into the cell without changing row height, and an editing cell carries
1176
1392
  `data-editing="true"` (`data-invalid="true"` plus an `aria-live` message when a validator rejects).
1393
+ Text, number, and date editors select their content on entry, so typing replaces the value
1394
+ ("6" over "1.00" gives 6); a textarea keeps its caret.
1177
1395
  - **Editable columns** — extra column keys: `editable: false|true|'text'|'number'|'select'|'date'|
1178
1396
  'checkbox'|'textarea'|((row)=>boolean)` (`true` infers `'number'` for number/currency/percent/unit
1179
1397
  columns and `'text'` otherwise; a function is evaluated per row, so individual rows stay
@@ -1309,6 +1527,8 @@ portable state as TableView and KanbanView.
1309
1527
  `selectionTrigger:'card'`, Enter also toggles selection. Links, actions,
1310
1528
  selection controls, and selected text do not also activate the card. `recordaction` adds the
1311
1529
  resolved action to the shared event set.
1530
+ - **Appearance** — a selected card is a pick and keeps the accent: the `--zx-color-bg-selected`
1531
+ tint inside an accent outline. Metadata pairs are borderless tinted chips, not boxes in a box.
1312
1532
  <!-- /doc -->
1313
1533
 
1314
1534
  <!-- doc:kanban-view -->
@@ -1334,6 +1554,9 @@ cards, advisory work-in-progress limits, and equivalent pointer/programmatic/key
1334
1554
  `eyebrow-start`, `eyebrow-end`, and `title-prefix`. Unslotted metadata stays as quiet inline
1335
1555
  facts. Titles clamp visually to two lines while retaining their complete accessible text. Move
1336
1556
  and action controls reveal on hover/focus/selection/grab, and remain visible on touch devices.
1557
+ - **Assignees** — `assignees` faces (a name, names, or `{name, src?, initials?, tone?}`) are drawn
1558
+ like a small Avatar: initials on the tint `avatarTone(name)` picks, so a person has the same
1559
+ colour on the board as in every Avatar; a descriptor's own `tone` wins.
1337
1560
  - **Movement boundary** — `recordmove {record,id,from,to,column,swimlane,limitExceeded}` is
1338
1561
  cancelable and fires before a commit/request; either the component event or bubbling
1339
1562
  `zx-recordmove` mirror may veto it. `accept` is a hard eligibility gate; WIP is visual advice.
@@ -1344,7 +1567,8 @@ cards, advisory work-in-progress limits, and equivalent pointer/programmatic/key
1344
1567
  chooses a column, Up/Down chooses a bucket-relative position, and Alt+Up/Down chooses a swim
1345
1568
  lane. A polite live region announces every target, result, and reached/exceeded WIP limit.
1346
1569
  Collapse keeps focus on its control, including when a programmatic collapse hides the focused
1347
- card. Native drag is an enhancement.
1570
+ card. Native drag is an enhancement. The pointer-drag preview and a keyboard-grabbed card lift
1571
+ on `--zx-drag-shadow`.
1348
1572
  <!-- /doc -->
1349
1573
 
1350
1574
  <!-- doc:grid -->
@@ -1384,13 +1608,16 @@ code, so future adapters do not change the public `Chart` API.
1384
1608
  `setError()`, `clearError()`, `resize()`, `refreshTheme()`, `getInstance()`, `destroy()`.
1385
1609
  - **Events** — `ready {instance}`, `update {data, type}`, `select {datasetIndex, index, dataset,
1386
1610
  label, value, nativeEvent}`, and `error {error}`.
1387
- - **Adapter contract** — `adapter.create(canvas, {type, data, options, onSelect})` returns
1388
- `{update(spec), resize(), destroy(), instance?}`. Implementations must destroy engine resources
1389
- and must not retain the component after teardown.
1611
+ - **Adapter contract** — `adapter.create(canvas, {type, data, options, reducedMotion, onSelect})`
1612
+ returns `{update(spec), resize(), destroy(), instance?}`. Implementations must destroy engine
1613
+ resources and must not retain the component after teardown. `reducedMotion` is true under the
1614
+ system preference or inside a `data-zx-motion="reduced"` region, and an adapter must then turn its
1615
+ engine's animation off; Chart watches for the answer changing and updates the mounted engine.
1390
1616
  - **`ChartJsAdapter`** — construct it with a Chart.js constructor or namespace:
1391
1617
  `new ChartJsAdapter(window.Chart)`. The documentation pins Chart.js 4.5.1 as a development-only
1392
1618
  asset; `package.json` has no runtime dependencies and application builds explicitly choose how
1393
- the engine is loaded.
1619
+ the engine is loaded. Under reduced motion it sets `animation: false`, over any animation the
1620
+ application configured.
1394
1621
  - **Accessibility** — the canvas always has a name and optional description, while synchronized
1395
1622
  labels and dataset values live in a semantic table. `summary: 'hidden'` keeps it visually hidden
1396
1623
  for assistive technology; `'visible'` makes exact values part of the interface.
@@ -1431,6 +1658,9 @@ windowed loader.
1431
1658
  - **View layout** — agenda days keep every event in one vertically stacked content column. Month
1432
1659
  uses a continuous seven-day grid with full-height day boundaries and event lanes overlaid below
1433
1660
  the date labels; today is indicated with accent text instead of a decorative edge highlight.
1661
+ - **Appearance** — the view switch is one framed control with the current view on the neutral
1662
+ `--zx-color-bg-current` fill; weekday headings are sentence case; inner grid lines use
1663
+ `--zx-color-border-subtle`; the New button is the solid primary (`--zx-color-accent-solid`).
1434
1664
  - **Pointer editing** — a timed move preserves the point where the event was grabbed and snaps the
1435
1665
  resulting start to `slotDuration`. The lower grip stretches the event in its original day column,
1436
1666
  while pointer capture keeps either interaction active if the pointer leaves the event surface.
@@ -1559,7 +1789,12 @@ mutate the caller's data.
1559
1789
  double-click), `check {ids, node}`.
1560
1790
  - **Keyboard** — ↑/↓ move, → expands then descends, ← collapses then ascends, Home/End, Enter
1561
1791
  activates, Space selects or toggles the checkbox, `*` expands every sibling, typing jumps to the
1562
- next match.
1792
+ next match. A typed character's default action is prevented, so find-as-you-type and page
1793
+ shortcuts do not act on it too; typing into a control inside a row is left to that control.
1794
+ - **Appearance** — the selected node of a single-selection tree is where you are: the neutral
1795
+ `--zx-color-bg-current` fill, the text colour, a semibold label. A `selection: 'multi'` tree is a
1796
+ set of picks and keeps the accent tint. Icons, folders included, are drawn in
1797
+ `--zx-color-text-subtle`; badges are plain muted counts.
1563
1798
  <!-- /doc -->
1564
1799
 
1565
1800
  <!-- doc:finder -->
@@ -1580,6 +1815,9 @@ breadcrumb.
1580
1815
  - **Keyboard** — ↑/↓ move within the active column, → steps into the selected branch and lands on
1581
1816
  its first row, ← returns to the parent column and truncates the path, Home/End, Enter opens a
1582
1817
  leaf, typing jumps to the next match.
1818
+ - **Appearance** — the selected row of each column is the path you are on: the neutral
1819
+ `--zx-color-bg-current` fill, the text colour and a semibold label, with subtle icons and
1820
+ column dividers.
1583
1821
  <!-- /doc -->
1584
1822
 
1585
1823
  <!-- doc:form -->
@@ -1587,7 +1825,8 @@ breadcrumb.
1587
1825
 
1588
1826
  #### Form
1589
1827
 
1590
- - **Options** — `fieldsets: []`, `actions: [button descriptors]`, `novalidate: true`.
1828
+ - **Options** — `fieldsets: []`, `actions: [button descriptors]`, `novalidate: true`,
1829
+ `idPrefix: null` (handed to the fieldsets it builds; a fieldset's own wins).
1591
1830
  - **Methods** (proxied across the fieldsets) — `getValues()`, `setValues()`, `getField()`,
1592
1831
  `setValue()`, `getValue()`, `reset()`, `setHighlights()`, `clearHighlights()`, `addFieldset()`,
1593
1832
  `setActions()`, `submit()`.
@@ -1596,21 +1835,47 @@ breadcrumb.
1596
1835
 
1597
1836
  #### Fieldset
1598
1837
 
1599
- - **Options** — `title`, `columns: 1|2|3`, `fields: { id: Field-options }`.
1838
+ - **Options** — `title` (the legend: a quiet sentence-case caption, small, medium weight, muted —
1839
+ the fields are the content), `columns: 1|2|3`, `fields: { id: Field-options }`,
1840
+ `idPrefix: null`.
1841
+ - **Keys and ids** — a field key names the value, not an element: it is the field's `name` (and
1842
+ a native control's `name`) and its `data-field-id`, never a DOM id by itself. Control ids are
1843
+ `<idPrefix>-<key>`, with a prefix generated per instance unless `idPrefix` is given, so a form
1844
+ mounted twice repeats no id and a field `stage` never collides with a `#stage` elsewhere; the
1845
+ label, description, and message ids derive from it (`<id>-label`). Radio groups are named after
1846
+ the control id. An `id` in the field options that differs from the key is used as given.
1600
1847
  - **Methods** — `addField(id, opts)`, `getField`/`hasField`/`getFields`,
1601
1848
  `getValues`/`setValues`/`getValue`/`setValue`, `reset`, `clear`, `focus(id)`,
1602
1849
  `setHighlights`/`clearHighlights`.
1603
1850
 
1604
1851
  #### Field
1605
1852
 
1606
- - **Options** — `id`, `type: 'text'`, `label`, `description`, `value`, `placeholder`, `required`,
1607
- `disabled`, `options`, `layout: 'stack'|'inline'`, `props: {}`.
1853
+ - **Options** — `id` (the control's DOM id; generated when omitted), `name` (the field name and
1854
+ native control `name`; defaults to `id` on a standalone field), `type: 'text'`, `label`,
1855
+ `description`, `value`, `placeholder`, `required`, `disabled`, `options`,
1856
+ `layout: 'stack'|'inline'`, `props: {}`.
1857
+ - **Properties** — `controlId` (the control's DOM id), `name`.
1608
1858
  - **Built-in types** — `text`, `password`, `textarea`, `checkbox`, `int`, `float`, `select`,
1609
- `optionlist`, `hidden`, `html`.
1859
+ `optionlist`, `hidden`, `html` (display content as text: no input frame, a control's height,
1860
+ inset like the values around it in a quiet form).
1610
1861
  - **Methods** — `getValue`/`setValue`, `focus`, `reset`, `setDisabled`,
1611
1862
  `setHighlight(msg, kind)`, `clearHighlight`, `getInput`, `own(child)`, plus the statics
1612
1863
  `Field.register(type, adapter)` and `Field.has(type)`.
1613
1864
  - **Events** — `change {value}`, `invalid {message}`.
1865
+ - **Labels** — small, medium weight, in the muted text colour: quieter than the values they name.
1866
+ With `layout: 'inline'` the label lines up with the control's first line.
1867
+
1868
+ #### Quiet fields
1869
+
1870
+ Inside any element with `data-zx-fields="quiet"` — an inspector, a property list, an inline record
1871
+ — fields read as values rather than a wall of boxes. Native inputs, selects and textareas, and the
1872
+ composite controls (`Select`, `TagPicker`, `NumberField`, `Datebox`, `DateRangeBox`, `Timebox`)
1873
+ show no border, fill, chevron, stepper or calendar button until hovered (the hover fill and the
1874
+ affordances come back) and their full field chrome and focus ring while focused or open. A
1875
+ NumberField's value starts where a quiet text input's text starts, and `html` display content is
1876
+ inset to match. Disabled
1877
+ and read-only controls never light up; an invalid Datebox keeps its danger border. It is a context
1878
+ attribute, not an option: set it on the container.
1614
1879
  <!-- /doc -->
1615
1880
 
1616
1881
  <!-- doc:form-widgets -->
@@ -1643,8 +1908,10 @@ stays a separate policy decision.
1643
1908
  - **Events** — `input {value}`, `change {value}`.
1644
1909
 
1645
1910
  Tab indents rather than moving focus, which is what a code surface has to do and what makes it a
1646
- keyboard trap if nothing else is arranged — Escape then Tab leaves the field. A `code` Form field
1647
- adapter is registered by `registerFieldAdapters()`.
1911
+ keyboard trap if nothing else is arranged — so Escape releases the capture until the next
1912
+ keystroke in the editor: Escape then Tab (or Shift+Tab) leaves the field, and any other key,
1913
+ or leaving the editor, restores indentation. A `code` Form field adapter is registered by
1914
+ `registerFieldAdapters()`.
1648
1915
  <!-- /doc -->
1649
1916
 
1650
1917
  <!-- doc:questionnaire -->
@@ -1768,12 +2035,18 @@ shows a fixed set, and `ValueList` takes free text with no catalogue. Unknown ID
1768
2035
  yet. Field type: `tagpicker`.
1769
2036
 
1770
2037
  - **Options** — `items`, `values: []`, `valueKey: 'ID'`, `labelKey: 'name'`, `iconKey: 'icon'`,
1771
- `colorKey: 'color'`, `searchKeys`,
2038
+ `colorKey: 'color'`, `toneKey: 'tone'`, `searchKeys`,
1772
2039
  `filter: 'local'|(query)=>Promise<items>`, `minQuery: 0`, `debounce: 200`, `allowCreate: false`,
1773
2040
  `max` (maximum tags; further options become `aria-disabled`), `closeOnSelect: false`,
1774
2041
  `placeholder`, `listHeight: 260`, `disabled`, `readonly`, `renderItem`, `renderTag`.
1775
- - **Tag identity** — icon values accept a bundled icon name or Node; colors are scoped to the tag
1776
- and its option through `--zx-tag-color`. Custom `renderItem`/`renderTag` remain authoritative.
2042
+ - **Tag identity** — icon values accept a bundled icon name or Node; colors (a CSS colour or a token
2043
+ reference such as `var(--zx-color-tone-blue-fg)`) are scoped to the tag and its option through
2044
+ `--zx-tag-color`. `toneKey` names a categorical tone from the Badge palette (`blue`, `teal`,
2045
+ `green`, `amber`, `orange`, `red`, `pink`, `violet`, `slate`): the tag takes its fill and ink as a
2046
+ toned Badge does (`data-tone`), and a tone wins over a colour. Custom `renderItem`/`renderTag`
2047
+ remain authoritative.
2048
+ - **Read-only** — `readonly` (or `setReadonly(true)`) shows only the tags: no input, chevron, or
2049
+ remove buttons, so a full row of tags never wraps onto an empty line.
1777
2050
  - **Methods** — `getValues()`, `getItems()`, `setValues(values, {silent})`, `addValue()`,
1778
2051
  `removeValue()`, `clear()`, `reset()`, `setItems()`, `isFull()`, `open()`/`close()`, `focus()`,
1779
2052
  `setReadonly()`, `enable()`/`disable()`.
@@ -1782,6 +2055,8 @@ yet. Field type: `tagpicker`.
1782
2055
  - **Keyboard** — ↓ opens and moves, ↑ moves, Enter toggles the active option (or creates one from
1783
2056
  the query), Escape closes, Backspace on an empty input removes the last tag, Home/End jump
1784
2057
  within the list.
2058
+ - **Quiet fields** — inside `[data-zx-fields="quiet"]` the tags read as the value: no border, fill
2059
+ or chevron until hover; full field chrome while focused or open.
1785
2060
  <!-- /doc -->
1786
2061
 
1787
2062
  <!-- doc:groupbox -->
@@ -1827,15 +2102,22 @@ component count. `statTile()` is the convenience form of `new StatTile(null, opt
1827
2102
  returns the component, the way `tooltip()` does; call `.toElement()` for the node to place.
1828
2103
 
1829
2104
  - **Options** — `label`, `value`, `format: 'number'|'currency'|'percent'|'fileSize'|null|fn`,
1830
- `currency`, `locale`, `delta`, `deltaKind: 'positive'|'negative'|'neutral'`, `deltaFormat`,
1831
- `deltaLabel`, `trend: number[]`, `icon`, `kind`, `href`, `clickable`, `loading`, `msg`.
2105
+ `currency`, `locale`, `caption`, `delta`, `deltaKind: 'positive'|'negative'|'neutral'`,
2106
+ `deltaFormat`, `deltaLabel`, `trend: number[]`, `icon`, `kind`, `href`, `clickable`, `loading`,
2107
+ `msg`.
2108
+ - **Appearance** — the label is a sentence-case muted line (13px, medium); the value is the loud
2109
+ part (24px, weight 650, tabular figures); `caption` is one muted context line under it ("of
2110
+ 13,184"). `icon` is a muted glyph beside the label, never a coloured chip; `kind` tints only the
2111
+ sparkline. The tile is a column with the sparkline at its foot, so tiles in a row are the same
2112
+ height and line up whether or not each has a caption, a delta or a trend.
1832
2113
  - **Methods** — `update(options)`, `getAccessibleName()`.
1833
2114
  - **Events** — `click {event}`.
1834
2115
 
1835
2116
  - **A delta is never colour alone.** It carries its sign and a direction glyph, and `deltaKind`
1836
2117
  decides the colour rather than the sign — rising ticket counts are not a success, and a
1837
2118
  component that assumes "up is good" is wrong for half the metrics in any business.
1838
- - **The accessible name is label + value + spoken delta** ("up 2 vs. last week"). The visible
2119
+ - **The accessible name is label + value (with its caption) + spoken delta** ("Active items,
2120
+ 2,184 of 13,184, up 2 vs. last week"). The visible
1839
2121
  `+2` row and the sparkline are `aria-hidden`, because a sparkline nobody can read is
1840
2122
  decoration and saying the delta twice is worse than saying it once.
1841
2123
  - **The element follows the behaviour** — `href` renders an `<a>`, `onclick` without `href` a
@@ -1990,10 +2272,12 @@ detail side of a master–detail screen. Give the root a size of its own (a grid
1990
2272
 
1991
2273
  Four appearances, all square-cornered. `divided` (the default) sets flat blocks on a muted track,
1992
2274
  split by hairline dividers, above a bordered panel, with the active block lifted onto the panel
1993
- surface under an accent rule. `bracket` outlines folder tabs that fuse into that same panel, the
1994
- active tab bridging the seam. `line` underlines the active tab across a full-width rule, for page-
1995
- and section-level navigation. `segmented` renders a compact group on a muted fill with the active
1996
- tab raised onto a control surface, so the row reads as one control inside a toolbar or card header.
2275
+ surface. `bracket` outlines folder tabs that fuse into that same panel, the active tab bridging
2276
+ the seam. `line` underlines the active tab in the text colour across a full-width rule, for page-
2277
+ and section-level navigation. `segmented` renders a compact framed group with the active tab on
2278
+ the neutral `--zx-color-bg-current` fill, so the row reads as one control inside a toolbar or card
2279
+ header. The active tab is where you are, not a pick: every variant gives it the text colour and a
2280
+ semibold label, and none uses the accent. Tab badges are neutral counts, not accent bubbles.
1997
2281
 
1998
2282
  `divided` and `bracket` draw the panel box themselves; `line` and `segmented` leave the panel bare
1999
2283
  for the surrounding layout to frame. The boxed variants read `--zx-tabbox-radius` (default `0px`)
@@ -2001,7 +2285,8 @@ from the component root, so `el.style.setProperty('--zx-tabbox-radius', 'var(--z
2001
2285
  restores rounded corners without forking the stylesheet.
2002
2286
 
2003
2287
  - **Options** — `tabs: [{ name, title, content, icon?, closable?, disabled? }]`, `active`,
2004
- `variant: 'divided'|'bracket'|'line'|'segmented'`, `keepAlive: true`.
2288
+ `variant: 'divided'|'bracket'|'line'|'segmented'`, `keepAlive: true`, `label` (the tab list's
2289
+ accessible name — give one whenever a page has more than one tab list).
2005
2290
  - **Methods** — `addTab()`, `removeTab()`, `openTab(name)`, `getActive()`, `setTitle()`,
2006
2291
  `setBadge(name, text|null)`, `enableTab()`/`disableTab()`.
2007
2292
  - **Events** — `change {name, previous}` (preventable), `close {name}`.
@@ -2080,7 +2365,10 @@ Three properties are the reason to use this rather than assemble your own:
2080
2365
  rows out of twenty with nothing on screen saying why, and the badge is what prevents that.
2081
2366
  - **The count line is a live region**, and `setCount(null)` clears it. The previous number
2082
2367
  describes the previous filters; a stale count during a load is worse than none.
2083
- - **Tool buttons meet the 44 px target** in both densities.
2368
+ - **Tool buttons meet the 44 px target** at any spacing.
2369
+
2370
+ The current view and a pressed tool take the neutral `--zx-color-bg-current` fill — a mode the
2371
+ list is in, not a pick — never the accent tint.
2084
2372
 
2085
2373
  `bind(view)` is optional sugar for a `TableView` or `CardView`: it feature-detects rather than
2086
2374
  importing them, adopts the view's shape, and mounts a field chooser built with
@@ -2108,6 +2396,8 @@ treated as one empty page rather than zero pages.
2108
2396
  - **Semantics** — `<nav aria-label="Pagination">`; the current page carries `aria-current="page"`;
2109
2397
  previous/next are labelled icon buttons that go `disabled` at the ends; the ellipses are
2110
2398
  `aria-hidden` spans, not buttons.
2399
+ - **Appearance** — the current page is the neutral `--zx-color-bg-current` fill with a semibold
2400
+ figure: where you are, not a call to action.
2111
2401
  <!-- /doc -->
2112
2402
 
2113
2403
  <!-- doc:navigation-bar -->
@@ -2126,6 +2416,9 @@ bar and wrong for a phone bottom bar, where the navigation is the point. `overfl
2126
2416
  every item at every width; `overflowBelow` moves the threshold; `minVisible: n` pins the first `n`
2127
2417
  items and overflows the rest. At the default the collapse is a container query with no observer
2128
2418
  installed; any other threshold installs a `ResizeObserver`, which is the cost of asking for one.
2419
+
2420
+ The current item is the neutral `--zx-color-bg-current` fill with the text colour and a semibold
2421
+ label; item badges are neutral counts. Only the brand mark keeps the accent.
2129
2422
  <!-- /doc -->
2130
2423
 
2131
2424
  <!-- doc:app-sidebar -->
@@ -2136,14 +2429,16 @@ vertical rail, and horizontal rail layouts. It mounts exactly one presentation a
2136
2429
  preserving active and expanded branch state. Only the expanded vertical state reveals descendants
2137
2430
  inline; every rail state opens descendants toward the workspace in anchored flyouts.
2138
2431
 
2139
- - **Options** — `items`, `active`, `expanded`, `collapsed`, `collapsible`,
2140
- `orientation: 'vertical'|'horizontal'`, `side: 'left'|'right'|'top'|'bottom'`, `label`,
2432
+ - **Options** — `items`, `active`, `expanded`, `collapsed`, `collapsible` (`false` removes the
2433
+ minimise and expand buttons), `orientation: 'vertical'|'horizontal'`,
2434
+ `side: 'left'|'right'|'top'|'bottom'`, `label`,
2141
2435
  `header`, `footer`, `railHeader`, `railFooter`, `openDelay: 80`, `closeDelay: 160`, and
2142
2436
  `renderIcon(item, context)`. Icon values may also be names, nodes, components, or factories.
2143
2437
  - **Methods** — `setItems()`, `getItems()`, `setActive()`, `setExpanded()`, `getExpanded()`,
2144
2438
  `toggleBranch()`, `expand()`, `collapse()`, `setCollapsed()`, `isCollapsed()`,
2145
2439
  `setLayout({orientation, side})`, `openFlyout(id, {focus})`, `closeFlyout()`,
2146
- `closeAllFlyouts()`, `isFlyoutOpen()`, `focus()`, `destroy()`.
2440
+ `closeAllFlyouts()`, `isFlyoutOpen()`, `focus()`, `destroy()`. `getItems()` returns copies of
2441
+ the normalized tree; a leaf carries no `children` property.
2147
2442
  - **Events** — cancelable `select {item, id, value, href, event}`,
2148
2443
  `branchchange {item, id, expanded, ids}`, `collapsechange {collapsed}`, and
2149
2444
  `flyoutchange {item, id, open}`.
@@ -2151,11 +2446,17 @@ inline; every rail state opens descendants toward the workspace in anchored flyo
2151
2446
  an expand control. Switching layouts does not erase the saved vertical collapsed preference or
2152
2447
  expanded branch IDs. `AppSidebar` is the single public application-navigation component; use
2153
2448
  `collapsed: true` or `orientation: 'horizontal'` for its rail presentations.
2449
+ - **Current state** — the current page is neutral: the `--zx-color-bg-current` fill, the full
2450
+ text colour and a semibold label, in the tree, the rail and every flyout; hover is a lighter
2451
+ step of the same overlay. A closed branch (or a rail item) over the active route takes the
2452
+ semibold label or the text colour, never the accent. Nothing is docked to an edge.
2154
2453
  - **Pointer and focus** — hover or focus reveals rail descendants without stealing focus. Click,
2155
2454
  Enter, Space, and the arrow toward the workspace also open a flyout; Escape, outside activation,
2156
2455
  or selection closes it. Expanded navigation supports arrows plus Home/End across visible rows.
2157
2456
  - **Semantics** — persistent navigation uses `<nav>` and nested lists with native links and
2158
- disclosure buttons, not menu or menubar roles. Icon-only destinations retain an accessible name
2457
+ disclosure buttons, not menu or menubar roles. The `<nav>` is the only landmark: a root the
2458
+ sidebar creates is a plain `<div>`, and a caller's own root keeps its element and attributes.
2459
+ Icon-only destinations retain an accessible name
2159
2460
  and Tooltip. A parent with children is disclosure-only; add an Overview child when it also needs
2160
2461
  a destination. Malformed and executable URL schemes are stripped; an item with no remaining
2161
2462
  link or callback is disabled in every presentation.
@@ -2187,6 +2488,9 @@ inline; every rail state opens descendants toward the workspace in anchored flyo
2187
2488
  `%d %m %Y %y %H %M %S %a %A %b %B %s`), `clampDate`, `isSameDay`, `addDays`, `addMonths`,
2188
2489
  `getWeekStart`.
2189
2490
  - **Keyboard** — `focusTrap`, `rovingTabindex`, `typeahead`.
2491
+ - **Motion** — `prefersReducedMotion(element?)` → boolean, and
2492
+ `watchMotion(element, fn, {signal?, immediate?})` → disposer, for code that animates in script;
2493
+ see Theming.
2190
2494
  - **Utils** — `debounce`, `uid`, `deepMerge`, `isElement`, `clamp`, `toArray`.
2191
2495
  <!-- /doc -->
2192
2496
 
@@ -2203,7 +2507,8 @@ import { loadFontAwesome, useFontAwesome, registerIcons } from '/assets/zx.esm.j
2203
2507
 
2204
2508
  await loadFontAwesome({ kit: 'ae8320b210', style: 'duotone' }); // injects the kit, switches over
2205
2509
  useFontAwesome(); // page already carries Font Awesome; no network
2206
- registerIcons({ rocket: ['0 0 512 512', 'M…'] }); // extend the bundled inline set
2510
+ registerIcons({ rocket: ['0 0 512 512', 'M…'] }); // a filled glyph (Font Awesome paths)
2511
+ registerIcons({ anchor: ['0 0 24 24', 'M12 6v16…', 'stroke'] }); // an outline glyph (Lucide paths)
2207
2512
  ```
2208
2513
 
2209
2514
  - `loadFontAwesome(kit | {kit, style, family, fixedWidth, activate})` — injects the kit script once
@@ -2212,7 +2517,20 @@ registerIcons({ rocket: ['0 0 512 512', 'M…'] }); // extend the bundled inli
2212
2517
  domains configured in the Font Awesome account.
2213
2518
  - `useFontAwesome(opts)` / `useBuiltinIcons()` / `configureIcons(opts)` / `getIconConfig()` —
2214
2519
  switch or read the renderer without touching the network.
2215
- - `iconNames()` lists every inline glyph; `registerIcons(map)` adds `[viewBox, path]` entries.
2520
+ - `iconNames()` lists every inline glyph, bundled and registered; `icons` is the bundled table.
2521
+ - `registerIcons(map)` adds or replaces inline glyphs. `[viewBox, path]` is a filled shape — the
2522
+ form Font Awesome path data comes in. `[viewBox, path, 'stroke']` is an outline drawn with the
2523
+ bundled pen; its stroke scales with the view box, so a 24-unit outline set such as Lucide renders
2524
+ exactly like the bundled glyphs. Anything else throws `TypeError`.
2525
+
2526
+ **The bundled set is outline.** The inline glyphs are Lucide icons (https://lucide.dev, ISC licence;
2527
+ the notice is in `src/core/icons.js`) on a 24-unit grid: `fill="none"`, `stroke="currentColor"`,
2528
+ round caps and joins, a stroke of 1.75 units — about 1.2px at the default 16px, the weight of
2529
+ 13–14px interface text — and never thinner than one pixel, so 11–13px glyphs stay legible. The
2530
+ names are Zx's own (`gear`, `dots`, `warning`, `success`, `drag`, …), not Lucide's. Colour follows
2531
+ `currentColor`; muting an icon is the caller's choice. The paint sits on the `<svg>` as attributes,
2532
+ so CSS on `.zx-icon` overrides it — `fill: currentColor` turns an outline into its solid form, which
2533
+ is how a state such as a set star or a liked heart is shown without a second glyph.
2216
2534
 
2217
2535
  **Names decide their own renderer.** A bare name (`'check'`) follows the active provider, and the
2218
2536
  built-in names are translated to their Font Awesome counterparts (`x` → `fa-xmark`, `search` →
@@ -2229,7 +2547,8 @@ itself once per distinct name through `console.warn`. Pass `icon(name, { strict:
2229
2547
  old `RangeError`, at a call site that can afford to fail: a test, or a build-time check.
2230
2548
 
2231
2549
  Font Awesome elements are sized with `font-size` and occupy 1em; bundled SVGs use width/height
2232
- attributes. Both honour `label` (`null` ⇒ `aria-hidden`, otherwise `role="img"` + `aria-label`).
2550
+ attributes (default 16). Both honour `label` (`null` ⇒ `aria-hidden`, otherwise `role="img"` +
2551
+ `aria-label`).
2233
2552
  <!-- /doc -->
2234
2553
 
2235
2554
  <!-- doc:helpers -->
@@ -2333,15 +2652,49 @@ Registered elements: `<zx-toggle>`, `<zx-check-button>`, `<zx-select items='[…
2333
2652
  <!-- doc:tokens -->
2334
2653
  ### Design tokens
2335
2654
 
2336
- Two tiers of CSS custom properties. Dark mode and density are pure token/attribute swaps
2337
- (`[data-zx-theme="dark"]`, `[data-zx-density="compact"]`), and `tests/lint-tokens.js` forbids raw
2338
- colour literals and tier-1 references in component CSS.
2339
-
2340
- - **Tier 1** — the global palette in `styles/tokens/global.css`, never referenced by components:
2341
- `--zx-gray-0…950`, `--zx-green-*`, `--zx-red/amber/blue-*`, `--zx-space-1…8`,
2342
- `--zx-radius-sm/md/lg/full`, the type scale, shadows, and motion.
2655
+ Two tiers of CSS custom properties. Dark mode is a pure token/attribute swap
2656
+ (`[data-zx-theme="dark"]`), there is one geometry derived from `--zx-space-unit` and
2657
+ `--zx-radius`, and `tests/lint-tokens.js` forbids raw colour literals and tier-1 colour references
2658
+ in component CSS.
2659
+
2660
+ - **Tier 1** — the global palette and primitives in `styles/tokens/global.css`: `--zx-gray-0…950`,
2661
+ `--zx-emerald-*` (the brand ramp the default accent reads), the status ramps
2662
+ `--zx-green/red/amber/blue-*` (green is a yellow-green, deliberately apart from the brand), the
2663
+ tone ramps `--zx-tone-<hue>-300/500/700` — none of them referenced by components — plus the
2664
+ spacing scale (`--zx-space-unit`, `--zx-space-1…8`), the radius scale (`--zx-radius`,
2665
+ `--zx-radius-sm/md/lg/xl/full`), the type scale (`--zx-text-xs…3xl`, 12–30px), weights
2666
+ (`--zx-weight-medium` 500, `--zx-weight-semibold` 600), tracking (`--zx-tracking-tight`
2667
+ -0.01em, set on `.zx-scope`; `--zx-tracking-title` -0.015em), shadows, and motion
2668
+ (`--zx-dur-fast` 120ms, `--zx-dur-base` 180ms — both 0ms in a reduced-motion region — and the
2669
+ curves `--zx-ease`, `--zx-ease-spring`, `--zx-ease-drawer`).
2343
2670
  - **Tier 2** — the semantic set, the only tokens components may use: `--zx-color-*`,
2344
- `--zx-control-*`, `--zx-focus-ring`, `--zx-overlay-shadow`.
2671
+ `--zx-control-*`, `--zx-focus-ring`, `--zx-overlay-shadow`, `--zx-drag-shadow` (an item being
2672
+ dragged; its own value in dark, with a faint light rim). The roles that carry the visual
2673
+ language:
2674
+ - **Columns.** `--zx-color-bg-surface` is the working canvas; `--zx-color-bg-chrome` is the
2675
+ navigation and inspector tone beside it (one step below the canvas in light, between page and
2676
+ canvas in dark), so tone rather than a line separates them. Dividers inside a column use
2677
+ `--zx-color-border-subtle`; `--zx-color-border` is for frames.
2678
+ - **Current vs selected.** Where you *are* — the active navigation item, the current row, the
2679
+ open node, the active view — is neutral: `--zx-color-bg-current` (translucent text, about 8%,
2680
+ so it works on any column), text colour, weight 600. Hover is `--zx-color-bg-hover`, about half
2681
+ as strong. What you have *picked* keeps the accent tint `--zx-color-bg-selected`.
2682
+ - **Text.** `--zx-color-text`, `--zx-color-text-muted` (secondary text, 4.5:1 on every column and
2683
+ on a current row), `--zx-color-text-subtle` (icons and non-essential meta, 3:1),
2684
+ `--zx-color-text-placeholder`.
2685
+ - **Solid accent.** `--zx-color-accent-solid` (`-hover`, `-active`) with
2686
+ `--zx-color-on-accent-solid` is the one primary action per screen: the 600 stop with white ink in
2687
+ both themes. `--zx-color-accent` is the accent as text, marks, and focus (600 in light, 400 in
2688
+ dark) and pairs with `--zx-color-on-accent` when something is filled with it.
2689
+ - **Status.** `--zx-color-success/warning/danger/info` and their `-bg` fills mean state and
2690
+ nothing else; they ship with an icon or a word.
2691
+ - **Tones.** `--zx-color-tone-{blue,teal,green,amber,orange,red,pink,violet,slate}-bg` and `-fg`:
2692
+ categorical colour for tags, chips, icon tiles, and avatars — neither the accent nor a status. A
2693
+ soft fill (13% of the tone over the surface in light, 20% in dark) with ink at 4.5:1 or better.
2694
+ - **Chart series.** `--zx-color-chart-1…8`, assigned in order, never cycled: blue, orange, teal,
2695
+ amber, pink, green, violet, red. Neighbours stay apart under red-green colour-vision deficiency
2696
+ and every series clears 3:1 as a mark on the light and the dark surface; the list does not open
2697
+ on the accent.
2345
2698
  - **Component styling hooks** — a short, deliberately small set of component-level custom
2346
2699
  properties for restyling one component without touching its internals. Each falls back to the
2347
2700
  semantic token it replaces, so setting none of them changes nothing:
@@ -2350,4 +2703,13 @@ colour literals and tier-1 references in component CSS.
2350
2703
  Reach for the least specific override that works: a global semantic token first, one of these
2351
2704
  second, an internal selector never. Component CSS declares many other `--zx-<component>-*`
2352
2705
  properties — those are internal geometry, not API, and will change without notice.
2706
+
2707
+ **Base typography.** `.zx-scope` sets Inter with `--zx-tracking-tight` and the `cv11` feature (the
2708
+ single-storey a, chosen because it reads rounder and calmer at 13–14px; fonts without it ignore it).
2709
+
2710
+ **Quiet fields.** Put `data-zx-fields="quiet"` on a property panel, an inspector, or an inline
2711
+ record: native inputs, selects, and textareas inside it drop their border and fill until hover
2712
+ (`--zx-color-bg-hover`) or focus (the normal border, the control fill, and the focus ring), so values
2713
+ read as text rather than as a wall of boxes. Read-only and disabled fields stay quiet. The rules have
2714
+ zero specificity, so a component's own input styling still wins.
2353
2715
  <!-- /doc -->