@sableui/react 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/AGENTS.md +157 -0
  2. package/CHANGELOG.md +50 -0
  3. package/CONTRIBUTING.md +221 -0
  4. package/DESIGN.md +548 -0
  5. package/LICENSE +21 -0
  6. package/README.md +194 -2
  7. package/THIRD_PARTY_NOTICES.md +44 -0
  8. package/bin/sable-lint.mjs +0 -0
  9. package/bin/sable.mjs +1223 -0
  10. package/dist/_contract-CYScOdf5.d.ts +150 -0
  11. package/dist/chunk-3IWZYBGH.js +398 -0
  12. package/dist/chunk-5Z5KLOAU.js +167 -0
  13. package/dist/chunk-7MG4SHYX.js +15 -0
  14. package/dist/chunk-ADLLDWLW.js +15 -0
  15. package/dist/chunk-EP55KSYA.js +14 -0
  16. package/dist/chunk-J23ELQQZ.js +9396 -0
  17. package/dist/chunk-JI7WJP2L.js +286 -0
  18. package/dist/chunk-K7RUHUGC.js +14 -0
  19. package/dist/chunk-KO3NAEBF.js +15 -0
  20. package/dist/chunk-L4IEIQQA.js +14 -0
  21. package/dist/chunk-L6Y46C62.js +270 -0
  22. package/dist/chunk-NEQNXI2G.js +25 -0
  23. package/dist/chunk-PAOZBZME.js +40 -0
  24. package/dist/chunk-RWP4XBSK.js +139 -0
  25. package/dist/chunk-SRQWQUTW.js +63 -0
  26. package/dist/chunk-YXBOYWBN.js +15 -0
  27. package/dist/foundations/index.d.ts +88 -0
  28. package/dist/foundations/index.js +792 -0
  29. package/dist/icon/index.d.ts +60 -0
  30. package/dist/icon/index.js +2 -0
  31. package/dist/icon-sets/brands.d.ts +44 -0
  32. package/dist/icon-sets/brands.js +147 -0
  33. package/dist/icon-sets/reicon.d.ts +8 -0
  34. package/dist/icon-sets/reicon.js +1 -0
  35. package/dist/index.d.ts +7013 -0
  36. package/dist/index.js +15462 -0
  37. package/dist/ios/chrome.css +83 -0
  38. package/dist/ios/frame.css +5 -0
  39. package/dist/ios/index.d.ts +70 -0
  40. package/dist/ios/index.js +313 -0
  41. package/dist/ios/preview.d.ts +64 -0
  42. package/dist/ios/preview.js +133 -0
  43. package/dist/ios/styles.css +77 -0
  44. package/dist/layout/index.d.ts +280 -0
  45. package/dist/layout/index.js +2 -0
  46. package/dist/meta-6dUzgC6M.d.ts +205 -0
  47. package/dist/patterns/index.d.ts +36 -0
  48. package/dist/patterns/index.js +934 -0
  49. package/dist/presets/index.d.ts +37 -0
  50. package/dist/presets/index.js +117 -0
  51. package/dist/registry-CyL28OzJ.d.ts +28 -0
  52. package/dist/styles.css +1893 -0
  53. package/dist/theme-provider-BWcizhAt.d.ts +105 -0
  54. package/dist/themes/atelier/atelier.css +213 -0
  55. package/dist/themes/atelier/index.d.ts +9 -0
  56. package/dist/themes/atelier/index.js +1 -0
  57. package/dist/themes/base/base.css +443 -0
  58. package/dist/themes/base/index.d.ts +9 -0
  59. package/dist/themes/base/index.js +1 -0
  60. package/dist/themes/ember/ember.css +270 -0
  61. package/dist/themes/ember/index.d.ts +9 -0
  62. package/dist/themes/ember/index.js +1 -0
  63. package/dist/themes/index.d.ts +10 -0
  64. package/dist/themes/index.js +8 -0
  65. package/dist/themes/meridian/index.d.ts +9 -0
  66. package/dist/themes/meridian/index.js +1 -0
  67. package/dist/themes/meridian/meridian.css +255 -0
  68. package/dist/themes/poppy/index.d.ts +6 -0
  69. package/dist/themes/poppy/index.js +1 -0
  70. package/dist/themes/poppy/poppy.css +174 -0
  71. package/dist/themes/prism/index.d.ts +6 -0
  72. package/dist/themes/prism/index.js +1 -0
  73. package/dist/themes/prism/prism.css +183 -0
  74. package/dist/themes/verdant/index.d.ts +9 -0
  75. package/dist/themes/verdant/index.js +1 -0
  76. package/dist/themes/verdant/verdant.css +210 -0
  77. package/dist/themes.css +30 -0
  78. package/llms/components/accordion.md +93 -0
  79. package/llms/components/app-header.md +70 -0
  80. package/llms/components/app-shell.md +117 -0
  81. package/llms/components/attachment.md +105 -0
  82. package/llms/components/avatar.md +141 -0
  83. package/llms/components/badge.md +117 -0
  84. package/llms/components/banner.md +119 -0
  85. package/llms/components/bottom-nav.md +80 -0
  86. package/llms/components/breadcrumbs.md +75 -0
  87. package/llms/components/browser.md +67 -0
  88. package/llms/components/bubble.md +119 -0
  89. package/llms/components/button.md +199 -0
  90. package/llms/components/calendar.md +75 -0
  91. package/llms/components/card.md +135 -0
  92. package/llms/components/carousel.md +87 -0
  93. package/llms/components/chat-composer.md +126 -0
  94. package/llms/components/checkbox-card.md +98 -0
  95. package/llms/components/checkbox.md +90 -0
  96. package/llms/components/code-block.md +81 -0
  97. package/llms/components/code.md +62 -0
  98. package/llms/components/collapsible.md +83 -0
  99. package/llms/components/color-field.md +100 -0
  100. package/llms/components/combobox.md +119 -0
  101. package/llms/components/command-menu.md +158 -0
  102. package/llms/components/context-card.md +73 -0
  103. package/llms/components/context-menu.md +79 -0
  104. package/llms/components/data-table.md +99 -0
  105. package/llms/components/date-picker.md +97 -0
  106. package/llms/components/description.md +96 -0
  107. package/llms/components/destructive-action-modal.md +76 -0
  108. package/llms/components/dialog.md +127 -0
  109. package/llms/components/dots-menu.md +64 -0
  110. package/llms/components/empty-state.md +113 -0
  111. package/llms/components/entity.md +67 -0
  112. package/llms/components/feedback.md +63 -0
  113. package/llms/components/fieldset.md +106 -0
  114. package/llms/components/file-dropzone.md +103 -0
  115. package/llms/components/form.md +111 -0
  116. package/llms/components/gliding-highlight.md +84 -0
  117. package/llms/components/inline-status.md +62 -0
  118. package/llms/components/input.md +127 -0
  119. package/llms/components/kbd.md +57 -0
  120. package/llms/components/label.md +73 -0
  121. package/llms/components/marker.md +99 -0
  122. package/llms/components/marquee.md +66 -0
  123. package/llms/components/menu.md +122 -0
  124. package/llms/components/message-scroller.md +110 -0
  125. package/llms/components/message.md +114 -0
  126. package/llms/components/meter.md +124 -0
  127. package/llms/components/middle-truncate.md +62 -0
  128. package/llms/components/note.md +81 -0
  129. package/llms/components/notification-badge.md +67 -0
  130. package/llms/components/number-field.md +70 -0
  131. package/llms/components/otp-field.md +101 -0
  132. package/llms/components/page-header.md +76 -0
  133. package/llms/components/page-loader.md +67 -0
  134. package/llms/components/pagination.md +92 -0
  135. package/llms/components/panel-group.md +120 -0
  136. package/llms/components/panel-section.md +122 -0
  137. package/llms/components/phone.md +70 -0
  138. package/llms/components/pill.md +73 -0
  139. package/llms/components/popover.md +79 -0
  140. package/llms/components/progress.md +98 -0
  141. package/llms/components/radio-card.md +90 -0
  142. package/llms/components/radio.md +86 -0
  143. package/llms/components/relative-time.md +60 -0
  144. package/llms/components/scroller.md +88 -0
  145. package/llms/components/scrub-field.md +97 -0
  146. package/llms/components/search-input.md +84 -0
  147. package/llms/components/select.md +106 -0
  148. package/llms/components/separator.md +70 -0
  149. package/llms/components/settings-menu.md +139 -0
  150. package/llms/components/sheet.md +97 -0
  151. package/llms/components/show-more.md +59 -0
  152. package/llms/components/side-nav.md +106 -0
  153. package/llms/components/sidebar.md +87 -0
  154. package/llms/components/skeleton.md +62 -0
  155. package/llms/components/slider.md +114 -0
  156. package/llms/components/spinner.md +87 -0
  157. package/llms/components/status-bar.md +91 -0
  158. package/llms/components/status-dot.md +70 -0
  159. package/llms/components/stepper.md +125 -0
  160. package/llms/components/streaming-text.md +86 -0
  161. package/llms/components/switch.md +87 -0
  162. package/llms/components/table.md +112 -0
  163. package/llms/components/tabs.md +84 -0
  164. package/llms/components/textarea.md +88 -0
  165. package/llms/components/theme-selector.md +95 -0
  166. package/llms/components/theme-switcher.md +71 -0
  167. package/llms/components/thinking-orb.md +91 -0
  168. package/llms/components/toast.md +130 -0
  169. package/llms/components/toggle.md +95 -0
  170. package/llms/components/toolbar.md +168 -0
  171. package/llms/components/tooltip.md +93 -0
  172. package/llms/components/tree.md +227 -0
  173. package/llms/components/video.md +66 -0
  174. package/llms/components/workspace.md +138 -0
  175. package/llms/foundations/accessibility.md +60 -0
  176. package/llms/foundations/motion.md +96 -0
  177. package/llms/foundations/scrolling.md +41 -0
  178. package/llms/foundations/shape.md +73 -0
  179. package/llms/foundations/spacing.md +82 -0
  180. package/llms/foundations/surfaces.md +69 -0
  181. package/llms/foundations/typography.md +120 -0
  182. package/llms/layout/aspect-box.md +37 -0
  183. package/llms/layout/bleed.md +31 -0
  184. package/llms/layout/center.md +37 -0
  185. package/llms/layout/columns.md +51 -0
  186. package/llms/layout/grid-overlay.md +40 -0
  187. package/llms/layout/grid.md +47 -0
  188. package/llms/layout/page.md +39 -0
  189. package/llms/layout/region.md +49 -0
  190. package/llms/layout/row.md +40 -0
  191. package/llms/layout/safe-area.md +37 -0
  192. package/llms/layout/spacer.md +39 -0
  193. package/llms/layout/split-pane.md +41 -0
  194. package/llms/layout/stack.md +39 -0
  195. package/llms/patterns/auth-page.md +68 -0
  196. package/llms/patterns/brand-landing-page.md +423 -0
  197. package/llms/patterns/chat-panel.md +210 -0
  198. package/llms/patterns/dashboard-page.md +170 -0
  199. package/llms/patterns/feed-page.md +103 -0
  200. package/llms/patterns/landing-page.md +122 -0
  201. package/llms/patterns/list-detail-page.md +100 -0
  202. package/llms/patterns/settings-page.md +121 -0
  203. package/llms/reference/token-intent.md +324 -0
  204. package/llms/reference/utilities.md +129 -0
  205. package/llms/tasks/adding-a-component.md +113 -0
  206. package/llms/tasks/auditing-an-app.md +216 -0
  207. package/llms/tasks/building-a-page.md +109 -0
  208. package/llms/tasks/building-a-tool.md +141 -0
  209. package/llms/tasks/building-an-ios-prototype.md +105 -0
  210. package/llms/tasks/building-with-sable.md +73 -0
  211. package/llms/tasks/choosing-a-component.md +109 -0
  212. package/llms/tasks/converting-an-app.md +139 -0
  213. package/llms/tasks/creating-a-preset.md +43 -0
  214. package/llms/tasks/creating-design-assets.md +96 -0
  215. package/llms/tasks/designing-a-surface.md +115 -0
  216. package/llms/tasks/developing-taste.md +157 -0
  217. package/llms/tasks/getting-started.md +63 -0
  218. package/llms/tasks/learning-from-builds.md +74 -0
  219. package/llms/tasks/reviewing-a-design.md +114 -0
  220. package/llms/tasks/reviewing-a-journey.md +84 -0
  221. package/llms/tasks/reviewing-motion.md +92 -0
  222. package/llms/tasks/using-presets.md +87 -0
  223. package/llms/taste/asset-plan.template.json +53 -0
  224. package/llms/taste/composition-follows-content.md +20 -0
  225. package/llms/taste/hierarchy-is-a-relationship.md +22 -0
  226. package/llms/taste/lessons.json +57 -0
  227. package/llms/taste/motion-needs-a-reason.md +21 -0
  228. package/llms/taste/surface-states-travel-together.md +20 -0
  229. package/llms-full.txt +14906 -0
  230. package/llms.txt +405 -0
  231. package/package.json +185 -3
  232. package/registry.json +18812 -0
  233. package/scripts/agent-setup.mjs +54 -0
  234. package/scripts/design-atlas.mjs +221 -0
  235. package/scripts/design-directions.mjs +123 -0
  236. package/scripts/design-memory.mjs +166 -0
  237. package/scripts/design-preferences.mjs +227 -0
  238. package/scripts/design-presets.mjs +48 -0
  239. package/scripts/grade.mjs +509 -0
  240. package/scripts/lint-rules.mjs +1065 -0
  241. package/scripts/review-evidence.mjs +120 -0
  242. package/scripts/skeleton.mjs +248 -0
  243. package/skills/entry/SKILL.md +34 -0
  244. package/skills/sable/SKILL.md +187 -0
  245. package/skills/sable/agents/openai.yaml +6 -0
  246. package/targets/ios.json +982 -0
  247. package/third-party/reicon-LICENSE.txt +20 -0
@@ -0,0 +1,324 @@
1
+ ## Token intent
2
+
3
+ Which token to reach for, and what it is most often confused with. Generated from `src/themes/token-docs.ts`; the full data is `registry.json → tokens`.
4
+
5
+ ### semantic (44)
6
+
7
+ - **`background`** — The page. The furthest-back surface in the page register. *Use for:* The document body, and any surface meant to read as 'the page itself'. Never anything that floats: background is the BOTTOM of the surface stack (background < card < popover in every dark mode), so a floating panel painted with it reads as a hole beneath the cards it hovers over.
8
+ - **Not** `popover` for anything floating, `card` for a bounded block on the page. `canvas`. `canvas` is the *work* surface in a tool — the artboard being edited — which is not the page behind the UI. Using `background` for an artboard makes the thing being edited indistinguishable from the chrome around it.
9
+ - **`foreground`** — The default ink, for `background`. *Use for:* Body text and any content sitting directly on the page.
10
+ - **Not** any other surface's ink. On `card`, `popover`, `panel` or a tone, use that surface's own `-foreground`.
11
+ - **`card`** — A raised content container on the page. *Use for:* A bounded block of related content on a page: an article teaser, a settings group, a stat panel.
12
+ - **Not** `popover`. A card sits *in* the document flow; a popover floats above it and needs its own elevation and border treatment. And never inside a docked panel — use `panel` surfaces and `PanelSection`; a card in a 240px panel spends its border and inset on nothing. Never rely on `card` as a FILL to separate something from the page: a preset's `card` can equal `background`, because a card separates by its border. A track, well or pad that must read with no border is `muted`.
13
+ - **`card-foreground`** — Ink paired with `card`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `card`. Use them as a pair — `bg-card text-card-foreground` — so a theme can retune the surface and the ink together.
14
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
15
+ - **`popover`** — A floating surface: menu, popover, tooltip body, select list, dialog, and any persistent floating panel (an inspector, a toolbar over a canvas). *Use for:* Anything rendered in a layer above the page that needs its own background, dismissible or not. The test is elevation: if it floats over content (`shadow-md` or higher, fixed or absolutely positioned above the page), it is popover. Pair with `text-popover-foreground`.
16
+ - **Not** `card` and `background`. The distinction is elevation, not shape or lifetime: a panel that stays open is still floating. `background` is the page ground and `card` sits on it; both read as sunk under a floating panel. In light mode several presets make all three equal, so a floating surface also needs its edge (ring or border) and shadow, never the fill alone.
17
+ - **`popover-foreground`** — Ink paired with `popover`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `popover`. Use them as a pair — `bg-popover text-popover-foreground` — so a theme can retune the surface and the ink together.
18
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
19
+ - **`muted`** — A recessed surface. Quieter than the page, not interactive. *Use for:* An inset well: a code block background, a disabled field, a read-only region, a table's zebra stripe, a segmented-control track. On the page it always differs from `background`. Inside any OTHER surface (a card, a popover, a floating panel) use the `sable-well` utility instead: it derives the well from the surface it sits in, so it is the same visible step everywhere, whereas `muted` can land within a step of a popover in some dark modes. Never compensate with an outline on every well — that turns a panel into a stack of boxes.
20
+ - **Not** `secondary` and `accent`. All three are near-identical in most themes, which is exactly why the rule is by ROLE: `muted` is *recessed and inert*, `secondary` is a *control fill*, `accent` is a *transient state*. Pick by what the surface does, never by which looks right — they diverge in other themes.
21
+ - **`muted-foreground`** — De-emphasised ink. The single most-used token after `foreground`. *Use for:* Secondary text: captions, helper text, placeholder-adjacent labels, a property name next to its value, timestamps.
22
+ - **Not** `foreground` at reduced opacity. Opacity on text compounds unpredictably against whatever is behind it and can fall below the contrast floor; this token is a tuned value that does not.
23
+ - **`border`** — The default hairline. Tuned to be subtle. *Use for:* Input outlines, badge edges, card borders — chrome that should not draw the eye.
24
+ - **Not** `border-strong`. Reach for `border-strong` when the line carries *structure* (a grid rule, a table divider, a section separator) and for `border` when it merely bounds a control.
25
+ - **`border-strong`** — A structural divider. Visibly stronger than `border`. *Use for:* Grid lines, table rules, separators between major sections.
26
+ - **Not** `border`. If removing the line would make the layout ambiguous, it is structural and belongs here.
27
+ - **`input`** — The outline of a form control specifically. *Use for:* Text inputs, textareas, selects, and the resting edge of a checkbox or radio.
28
+ - **Not** `border`. They are often the same value, but a theme is entitled to make form controls read differently from other chrome, and only this token gives it the hook.
29
+ - **`ring`** — The focus indicator colour. *Use for:* Only for `focus-visible`. Nothing else.
30
+ - **Not** `primary`. Focus must remain distinguishable from selection; if the ring is the same colour as a selected state, a keyboard user cannot tell where focus is.
31
+ - **`primary`** — The one high-emphasis action colour. *Use for:* The single most important action on a surface — one per view. A submit button, the primary CTA.
32
+ - **Not** `accent`. `primary` says *do this*; `accent` says *this is the current one*. Two primary buttons on one screen means neither is primary.
33
+ - **`primary-foreground`** — Ink paired with `primary`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `primary`. Use them as a pair — `bg-primary text-primary-foreground` — so a theme can retune the surface and the ink together.
34
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
35
+ - **`secondary`** — A lower-emphasis control fill. *Use for:* A secondary button, a segmented control's resting fill, a neutral chip.
36
+ - **Not** `muted`. `secondary` is for something you can *press*; `muted` is for a surface that is inert. See the `muted` note — role, not appearance.
37
+ - **`secondary-foreground`** — Ink paired with `secondary`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `secondary`. Use them as a pair — `bg-secondary text-secondary-foreground` — so a theme can retune the surface and the ink together.
38
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
39
+ - **`accent`** — A transient state fill: hover, selected, current. *Use for:* The fill behind a hovered menu item, a selected list row, the active segment of a switcher.
40
+ - **Not** `primary` for selection, and `muted` for hover. `accent` is the state layer; the other two are emphasis and recession respectively.
41
+ - **`accent-foreground`** — Ink paired with `accent`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `accent`. Use them as a pair — `bg-accent text-accent-foreground` — so a theme can retune the surface and the ink together.
42
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
43
+ - **`destructive`** — Irreversible or data-losing actions. *Use for:* Delete, revoke, remove, discard — and the error state of a form field.
44
+ - **Not** `tone-warning`. Destructive means *this destroys something*; warning means *be careful*. A confirmation dialog's action is destructive; the banner explaining it is a warning.
45
+ - **`destructive-foreground`** — Ink paired with `destructive`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `destructive`. Use them as a pair — `bg-destructive text-destructive-foreground` — so a theme can retune the surface and the ink together.
46
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
47
+ - **`success`** — A confirmed, completed, healthy state. *Use for:* A passing check, a saved indicator, a healthy status dot.
48
+ - **Not** `tone-info`. Success asserts an outcome; info asserts a fact. 'Saved' is success, '3 items' is info.
49
+ - **`success-foreground`** — Ink paired with `success`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `success`. Use them as a pair — `bg-success text-success-foreground` — so a theme can retune the surface and the ink together.
50
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
51
+ - **`tone-standard`** — Domain tone: the baseline classification in a set. Part of the `tone-*` family, which exists so a product can classify content without inventing colours per feature. *Use for:* The default category of a classified item — the one that means 'nothing special'.
52
+ - **Not** `muted`. A tone is *semantic classification* that a reader is meant to decode; `muted` is de-emphasis. If a legend would help the user, it is a tone.
53
+ - **`tone-collectible`** — Domain tone: a scarce or owned item. *Use for:* Marking an item as collectible/owned in a classified set.
54
+ - **Not** `primary`. A tone classifies the *thing*; primary drives an *action*. A collectible badge is not a button.
55
+ - **`tone-edition`** — Domain tone: an edition or release. *Use for:* Marking editioned content in a classified set.
56
+ - **Not** `tone-collectible`. Distinct classifications must stay visually distinct.
57
+ - **`tone-info`** — Domain tone: neutral, factual information. *Use for:* An informational note or banner, a neutral count, a 'did you know' surface.
58
+ - **Not** `success`. Info states a fact; success states an outcome. Also not `primary` — an info banner is not an action.
59
+ - **`tone-warning`** — Domain tone: proceed with care, nothing is broken yet. *Use for:* A caution banner, a nearing-limit indicator, a soloed/overridden state in a tool.
60
+ - **Not** `destructive`. Warning is *advisory*; destructive is *consequential*. If nothing is lost by continuing, it is a warning.
61
+ - **`highlight`** — A marker on content the user is meant to notice — search hits, diffs, annotations. *Use for:* Highlighting matched text, a changed line, a called-out passage.
62
+ - **Not** `accent`. `accent` is UI state that follows the pointer or selection; `highlight` marks the *content* and persists independently of interaction.
63
+ - **`tone-standard-foreground`** — Ink paired with `tone-standard`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-standard`. Use them as a pair — `bg-tone-standard text-tone-standard-foreground` — so a theme can retune the surface and the ink together.
64
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
65
+ - **`tone-collectible-foreground`** — Ink paired with `tone-collectible`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-collectible`. Use them as a pair — `bg-tone-collectible text-tone-collectible-foreground` — so a theme can retune the surface and the ink together.
66
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
67
+ - **`tone-edition-foreground`** — Ink paired with `tone-edition`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-edition`. Use them as a pair — `bg-tone-edition text-tone-edition-foreground` — so a theme can retune the surface and the ink together.
68
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
69
+ - **`tone-info-foreground`** — Ink paired with `tone-info`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-info`. Use them as a pair — `bg-tone-info text-tone-info-foreground` — so a theme can retune the surface and the ink together.
70
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
71
+ - **`tone-warning-foreground`** — Ink paired with `tone-warning`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `tone-warning`. Use them as a pair — `bg-tone-warning text-tone-warning-foreground` — so a theme can retune the surface and the ink together.
72
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
73
+ - **`highlight-foreground`** — Ink paired with `highlight`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `highlight`. Use them as a pair — `bg-highlight text-highlight-foreground` — so a theme can retune the surface and the ink together.
74
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
75
+ - **`overlay`** — A scrim over the page behind a modal, and the wash behind media chrome. *Use for:* Dialog and sheet backdrops, letterboxing, a pill sitting on top of an image.
76
+ - **Not** `bg-black/50`. Pure black is never right — it crushes whatever is behind it, and a theme cannot tune it. This token is the sanctioned answer, which is why `raw-black-white` is a lint rule.
77
+ - **`overlay-foreground`** — Ink paired with `overlay`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `overlay`. Use them as a pair — `bg-overlay text-overlay-foreground` — so a theme can retune the surface and the ink together.
78
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
79
+ - **`sidebar`** — The app sidebar's own surface. Heads a small family (`sidebar-*`) that lets navigation chrome diverge from the page without overriding page tokens. *Use for:* The `Sidebar` / `SideNav` background.
80
+ - **Not** `card`. A sidebar is persistent chrome, not content — a theme frequently wants it a different tone from the cards inside the page it frames.
81
+ - **`sidebar-foreground`** — Ink paired with `sidebar`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `sidebar`. Use them as a pair — `bg-sidebar text-sidebar-foreground` — so a theme can retune the surface and the ink together.
82
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
83
+ - **`sidebar-primary`** — The high-emphasis colour *within* the sidebar. *Use for:* The active nav item's fill, when the sidebar has its own tonal world.
84
+ - **Not** `primary`. Page primary on a sidebar surface may not clear contrast; this token is tuned against `sidebar`.
85
+ - **`sidebar-primary-foreground`** — Ink paired with `sidebar-primary`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `sidebar-primary`. Use them as a pair — `bg-sidebar-primary text-sidebar-primary-foreground` — so a theme can retune the surface and the ink together.
86
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
87
+ - **`sidebar-accent`** — The state layer within the sidebar — hover, current. *Use for:* Hovered or current nav items.
88
+ - **Not** `accent`, for the same contrast reason as `sidebar-primary`.
89
+ - **`sidebar-accent-foreground`** — Ink paired with `sidebar-accent`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `sidebar-accent`. Use them as a pair — `bg-sidebar-accent text-sidebar-accent-foreground` — so a theme can retune the surface and the ink together.
90
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
91
+ - **`sidebar-border`** — Hairlines inside the sidebar. *Use for:* Dividers between nav sections, and the sidebar's own trailing edge.
92
+ - **Not** `border`, which is tuned against page surfaces.
93
+ - **`sidebar-ring`** — Focus indicator inside the sidebar. *Use for:* `focus-visible` on nav items.
94
+ - **Not** `ring`, which is tuned against page surfaces.
95
+
96
+ ### typography (25)
97
+
98
+ - **`display-4xl`** — The largest type: display-lg through display-4xl. Marketing-scale, one per view at most. *Use for:* A landing page hero, a section opener on a brand surface.
99
+ - **Not** `heading-1` for page titles. Display is a *statement*; headings are *structure*. A settings page titled in display-3xl looks like an advert.
100
+ - **`display-3xl`** — The largest type: display-lg through display-4xl. Marketing-scale, one per view at most. *Use for:* A landing page hero, a section opener on a brand surface.
101
+ - **Not** `heading-1` for page titles. Display is a *statement*; headings are *structure*. A settings page titled in display-3xl looks like an advert.
102
+ - **`display-2xl`** — The largest type: display-lg through display-4xl. Marketing-scale, one per view at most. *Use for:* A landing page hero, a section opener on a brand surface.
103
+ - **Not** `heading-1` for page titles. Display is a *statement*; headings are *structure*. A settings page titled in display-3xl looks like an advert.
104
+ - **`display-xl`** — The largest type: display-lg through display-4xl. Marketing-scale, one per view at most. *Use for:* A landing page hero, a section opener on a brand surface.
105
+ - **Not** `heading-1` for page titles. Display is a *statement*; headings are *structure*. A settings page titled in display-3xl looks like an advert.
106
+ - **`display-lg`** — The largest type: display-lg through display-4xl. Marketing-scale, one per view at most. *Use for:* A landing page hero, a section opener on a brand surface.
107
+ - **Not** `heading-1` for page titles. Display is a *statement*; headings are *structure*. A settings page titled in display-3xl looks like an advert.
108
+ - **`heading-1`** — Structural document headings, heading-1 through heading-4. *Use for:* The real outline of a page. Pick the level by depth in the document, not by desired size.
109
+ - **Not** `display` for size, or `title` for structure. If it belongs in a table of contents, it is a heading.
110
+ - **`heading-2`** — Structural document headings, heading-1 through heading-4. *Use for:* The real outline of a page. Pick the level by depth in the document, not by desired size.
111
+ - **Not** `display` for size, or `title` for structure. If it belongs in a table of contents, it is a heading.
112
+ - **`heading-3`** — Structural document headings, heading-1 through heading-4. *Use for:* The real outline of a page. Pick the level by depth in the document, not by desired size.
113
+ - **Not** `display` for size, or `title` for structure. If it belongs in a table of contents, it is a heading.
114
+ - **`heading-4`** — Structural document headings, heading-1 through heading-4. *Use for:* The real outline of a page. Pick the level by depth in the document, not by desired size.
115
+ - **Not** `display` for size, or `title` for structure. If it belongs in a table of contents, it is a heading.
116
+ - **`title-lg`** — The title of a *component*, not of a document. *Use for:* A card's heading, a dialog title, a section label inside a panel.
117
+ - **Not** `heading-*`. A card title is not part of the page outline, and using a heading tier for it produces a document structure that lies to a screen reader's heading navigation.
118
+ - **`title-sm`** — The title of a *component*, not of a document. *Use for:* A card's heading, a dialog title, a section label inside a panel.
119
+ - **Not** `heading-*`. A card title is not part of the page outline, and using a heading tier for it produces a document structure that lies to a screen reader's heading navigation.
120
+ - **`body-lg`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
121
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
122
+ - **`body-md`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
123
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
124
+ - **`body-sm`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
125
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
126
+ - **`body-xs`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
127
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
128
+ - **`label-lg`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
129
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
130
+ - **`label-md`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
131
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
132
+ - **`label-sm`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
133
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
134
+ - **`label-xs`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
135
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
136
+ - **`caption`** — The smallest page-register tier. Metadata beneath something. *Use for:* Image captions, timestamps, footnotes, a card's secondary line.
137
+ - **Not** `text-overline`. Caption sits *below* and explains; overline sits *above* and categorises.
138
+ - **`mono-xl`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
139
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
140
+ - **`mono-lg`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
141
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
142
+ - **`mono-md`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
143
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
144
+ - **`mono-caption`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
145
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
146
+ - **`mono-sm`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
147
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
148
+
149
+ ### radii (7)
150
+
151
+ - **`none`** — Square. A deliberate 0. *Use for:* Edge-to-edge surfaces, table cells, anything whose corners meet another element's corners.
152
+ - **Not** omitting a radius class. Stating `none` records the intent; omitting it means the next person cannot tell square-on-purpose from square-by-accident.
153
+ - **`xs`** — The tightest real corner. Reads as 'crisp', not 'rounded'. *Use for:* Tool-chrome controls, tree rows, small swatches — anywhere the radius should be felt but not seen. The workspace register runs ~5px on essentially everything.
154
+ - **Not** `sm`. In a 24px-tall control an `sm` radius starts to read as a lozenge.
155
+ - **`sm`** — The default control corner. *Use for:* Inputs, buttons in the page register, badges, small cards.
156
+ - **`md`** — A container corner. *Use for:* Cards, panels, popovers, menus — surfaces that hold other things.
157
+ - **Not** `sm`. A container should be softer than the controls inside it, or the nesting reads inverted.
158
+ - **`lg`** — A prominent container corner. *Use for:* Dialogs, sheets, hero media, feature cards.
159
+ - **`xl`** — The softest rectangular corner. A styling statement. *Use for:* Marketing surfaces, large imagery, a theme that wants a soft character.
160
+ - **Not** `full`. `xl` is still a rectangle with soft corners; `full` is a capsule. Using `xl` on a small element makes it look like a failed capsule.
161
+ - **`full`** — A capsule or circle — radius exceeds half the height. *Use for:* Avatars, status dots, notification badges, pills, and page-register actions (the 'round action in a tight container' contrast that says *this is a control*).
162
+ - **Not** any fixed radius, for circles. And NOT in tool chrome: neither Figma nor Paper has a single pill in its chrome, which is why the compact density scope flattens `--button-radius` to ~5px.
163
+
164
+ ### layout (11)
165
+
166
+ - **`rhythm-unit`** — The vertical rhythm base — two spacing steps (8px at the default scale). Every vertical measure is a multiple of it, and it follows the density control with the spacing scale. *Use for:* Through `Stack`/`Row`/`Grid` gap numbers, which are rhythm units rather than pixels. A gap of 2 is 16px at the default scale, and grows or tightens with density.
167
+ - **Not** arbitrary vertical spacing. The rhythm is what makes unrelated sections of a page feel like one document.
168
+ - **`grid-columns`** — The column count of the content grid — 12. *Use for:* `Grid`/`Columns`, WITHIN a region, for genuinely multi-column content.
169
+ - **Not** using a column span as the page's reading width. A span is not a measure — see `measure-prose`.
170
+ - **`grid-gutter`** — The gap between grid columns. *Use for:* `Grid`/`Columns` spacing.
171
+ - **`page-inset`** — The page's outer breathing room, left and right. *Use for:* The gutter between content and the viewport edge, at every breakpoint.
172
+ - **Not** a global max-width. Sable caps width per REGION by role, never once for the whole page.
173
+ - **`measure-prose`** — The maximum comfortable line length for prose, in `ch`. *Use for:* Any long-form text column.
174
+ - **Not** a layout width. Measure and width are different things: a wide region can still hold a narrow measure, and capping the region instead makes everything else in it too narrow.
175
+ - **`region-form`** — The region cap for a form — narrow, because fields should not run long. *Use for:* Settings pages, auth screens, any column of inputs.
176
+ - **Not** `region-content`. A 700px-wide text input is harder to use, not more generous.
177
+ - **`region-feed`** — The region cap for a scrolling list of items. *Use for:* Feeds, timelines, search results.
178
+ - **`region-content`** — The default region cap for mixed page content. *Use for:* Article bodies with media, documentation, most product pages.
179
+ - **`region-wide`** — A wide region for dense or tabular content. *Use for:* Data tables, dashboards, galleries — content that genuinely needs the room.
180
+ - **`region-full`** — No cap. Edge to edge. *Use for:* Hero imagery, full-bleed sections, and a workspace, which is always edge-to-edge.
181
+ - **`region-ultrawide (legacy — prefer region-full)`** — Deprecated. Kept so existing usage keeps resolving. *Use for:* Nothing new. Use `region-full`.
182
+ - **Not** `region-full`, which is what this became.
183
+
184
+ ### typographyOverrides (60)
185
+
186
+ - **`type-display-4xl-size`** — The largest type: display-lg through display-4xl. Marketing-scale, one per view at most. *Use for:* A landing page hero, a section opener on a brand surface.
187
+ - **Not** `heading-1` for page titles. Display is a *statement*; headings are *structure*. A settings page titled in display-3xl looks like an advert.
188
+ - **`type-title-lg-size`** — The title of a *component*, not of a document. *Use for:* A card's heading, a dialog title, a section label inside a panel.
189
+ - **Not** `heading-*`. A card title is not part of the page outline, and using a heading tier for it produces a document structure that lies to a screen reader's heading navigation.
190
+ - **`type-title-sm-size`** — The title of a *component*, not of a document. *Use for:* A card's heading, a dialog title, a section label inside a panel.
191
+ - **Not** `heading-*`. A card title is not part of the page outline, and using a heading tier for it produces a document structure that lies to a screen reader's heading navigation.
192
+ - **`type-body-lg-size`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
193
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
194
+ - **`type-body-lg-weight`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
195
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
196
+ - **`type-body-lg-leading`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
197
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
198
+ - **`type-body-lg-tracking`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
199
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
200
+ - **`type-body-md-size`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
201
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
202
+ - **`type-body-md-weight`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
203
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
204
+ - **`type-body-md-leading`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
205
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
206
+ - **`type-body-md-tracking`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
207
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
208
+ - **`type-body-sm-size`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
209
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
210
+ - **`type-body-sm-weight`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
211
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
212
+ - **`type-body-sm-leading`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
213
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
214
+ - **`type-body-sm-tracking`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
215
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
216
+ - **`type-body-xs-size`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
217
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
218
+ - **`type-body-xs-weight`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
219
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
220
+ - **`type-body-xs-leading`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
221
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
222
+ - **`type-body-xs-tracking`** — The reading tier: body-lg through body-xs. Prose and long-form content. *Use for:* Paragraphs, descriptions, help text, anything a person reads a sentence of.
223
+ - **Not** `label-*`. Body is for reading, label is for naming. A form's help text is body; the field's name is a label. Getting this wrong is why some UIs feel like a wall of controls.
224
+ - **`type-label-lg-size`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
225
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
226
+ - **`type-label-lg-weight`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
227
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
228
+ - **`type-label-lg-leading`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
229
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
230
+ - **`type-label-lg-tracking`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
231
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
232
+ - **`type-label-md-size`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
233
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
234
+ - **`type-label-md-weight`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
235
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
236
+ - **`type-label-md-leading`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
237
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
238
+ - **`type-label-md-tracking`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
239
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
240
+ - **`type-label-sm-size`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
241
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
242
+ - **`type-label-sm-weight`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
243
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
244
+ - **`type-label-sm-leading`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
245
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
246
+ - **`type-label-sm-tracking`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
247
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
248
+ - **`type-label-xs-size`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
249
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
250
+ - **`type-label-xs-weight`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
251
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
252
+ - **`type-label-xs-leading`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
253
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
254
+ - **`type-label-xs-tracking`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
255
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
256
+ - **`type-label-xs-transform`** — The naming tier: label-lg through label-xs. Short, denser, slightly heavier. *Use for:* Field labels, table column headers, button text, tab titles, chip text.
257
+ - **Not** `body-*` at the same size. Labels carry more weight and tighter leading because they are scanned, not read.
258
+ - **`type-caption-size`** — The smallest page-register tier. Metadata beneath something. *Use for:* Image captions, timestamps, footnotes, a card's secondary line.
259
+ - **Not** `text-overline`. Caption sits *below* and explains; overline sits *above* and categorises.
260
+ - **`type-caption-weight`** — The smallest page-register tier. Metadata beneath something. *Use for:* Image captions, timestamps, footnotes, a card's secondary line.
261
+ - **Not** `text-overline`. Caption sits *below* and explains; overline sits *above* and categorises.
262
+ - **`type-caption-leading`** — The smallest page-register tier. Metadata beneath something. *Use for:* Image captions, timestamps, footnotes, a card's secondary line.
263
+ - **Not** `text-overline`. Caption sits *below* and explains; overline sits *above* and categorises.
264
+ - **`type-caption-tracking`** — The smallest page-register tier. Metadata beneath something. *Use for:* Image captions, timestamps, footnotes, a card's secondary line.
265
+ - **Not** `text-overline`. Caption sits *below* and explains; overline sits *above* and categorises.
266
+ - **`type-mono-xl-size`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
267
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
268
+ - **`type-mono-xl-weight`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
269
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
270
+ - **`type-mono-xl-leading`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
271
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
272
+ - **`type-mono-xl-tracking`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
273
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
274
+ - **`type-mono-lg-size`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
275
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
276
+ - **`type-mono-lg-weight`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
277
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
278
+ - **`type-mono-lg-leading`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
279
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
280
+ - **`type-mono-lg-tracking`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
281
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
282
+ - **`type-mono-md-size`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
283
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
284
+ - **`type-mono-md-weight`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
285
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
286
+ - **`type-mono-md-leading`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
287
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
288
+ - **`type-mono-md-tracking`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
289
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
290
+ - **`type-mono-caption-size`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
291
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
292
+ - **`type-mono-caption-weight`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
293
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
294
+ - **`type-mono-caption-leading`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
295
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
296
+ - **`type-mono-caption-tracking`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
297
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
298
+ - **`type-mono-sm-size`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
299
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
300
+ - **`type-mono-sm-weight`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
301
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
302
+ - **`type-mono-sm-leading`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
303
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
304
+ - **`type-mono-sm-tracking`** — The monospace family: mono-xl through mono-sm, plus mono-caption. *Use for:* Code, identifiers, hashes, keyboard keys, and any figure where digits must align in a column.
305
+ - **Not** the sans tiers for numeric data. Proportional digits make a column of numbers ragged; that is what `text-ui-num` solves in chrome and mono solves on a page.
306
+
307
+ ### derived (8)
308
+
309
+ - **`canvas`** — The WORK surface — the artboard, the document being edited. *Use for:* Behind the thing the tool operates on. `WorkspaceCanvas` uses it.
310
+ - **Not** `background`. The thing being edited is not the page behind the UI, and collapsing the two makes the document indistinguishable from its chrome.
311
+ - **`canvas-foreground`** — Ink paired with `canvas`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `canvas`. Use them as a pair — `bg-canvas text-canvas-foreground` — so a theme can retune the surface and the ink together.
312
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
313
+ - **`panel`** — Docked chrome: the surface of a side panel or an inspector. *Use for:* `Panel`, `WorkspaceBar`, `StatusBar` backgrounds.
314
+ - **Not** `card`. Panels are chrome and sit in a deliberately compressed tonal band — see DESIGN.md → Registers. A card's elevation language is wrong here.
315
+ - **`panel-foreground`** — Ink paired with `panel`. Evaluate the rendered contrast together; never choose it independently. *Use for:* Whenever the background is `panel`. Use them as a pair — `bg-panel text-panel-foreground` — so a theme can retune the surface and the ink together.
316
+ - **Not** `foreground`. That is the ink for `background` only; on any other surface it is a contrast gamble that happens to pass in the default theme and fails in another.
317
+ - **`panel-raised`** — One step up from `panel`: a selected row, a hover fill, a nested well. *Use for:* Tree row selection, hovered panel rows, and the light square of a checkerboard.
318
+ - **Not** `accent`. In a workspace the tonal band is only a few units wide; `accent` is a page-register step and reads as a shout in chrome.
319
+ - **`hairline`** — The 1px structural line that carries all separation in tool chrome. *Use for:* Between panel sections, under a toolbar, above a status bar.
320
+ - **Not** a bigger tonal step. This is one of the two rules the workspace register deliberately reverses: on a page prefer space or tone over a line, in chrome the hairline *is* the structure.
321
+ - **`hairline-strong`** — A hairline that must be seen — an input edge, a swatch ring. *Use for:* Bounding a control whose fill is arbitrary (a colour swatch) or user content.
322
+ - **Not** `hairline`, when the line would otherwise disappear against what it bounds.
323
+ - **`border-floating`** — The edge of a floating surface. Fainter than `border`: the shadow carries the elevation. *Use for:* Every floating surface — popover, menu, select, tooltip, dialog, sheet, toast, a floating panel — as `border-border-floating` or `ring-border-floating`, together with `bg-popover` and a `shadow-md`+ shadow. Derived for every theme (a faint alpha of the foreground); a theme may tune it with `--theme-border-floating`.
324
+ - **Not** `border`. A full hairline plus a shadow stacks into a heavy outline around anything that floats. `border` is for resting edges: inputs, cards, badges.
@@ -0,0 +1,129 @@
1
+ ## Utilities
2
+
3
+ The `@utility` and `@custom-variant` layer in `styles.css`: what each one is, when to reach for it, and what you would hand-write without it. Generated from `src/utilities/utility-docs.ts`; the full data is `registry.json → utilities`; one entry at a time with `sable utility <name>`.
4
+
5
+ - **Typography:** The paired `text-*` page tiers (`text-heading-1`, `text-body-md`, `text-label-sm`, `text-mono-md`, …) are documented as `registry.json → tokens.typography` (named without the `text-` prefix). `sable token typography`.
6
+ - **Motion:** The `motion-*` and `animate-*` recipes are listed in `registry.json → tokens.motion.recipes`, with the slot model in `registry.json → motionLayer`. `sable docs motion`.
7
+
8
+ ### relative surface (6)
9
+
10
+ - **`sable-surface`** — Paints an element as a semantic surface (card by default) and DERIVES its hover and active fills from it — background, ink, edge and both state colours from one role. *Use for:* A bespoke container whose rows or buttons need hover/selected fills that sit the right step off the container in every theme. Pick the role with a modifier (`sable-surface-muted`, `-popover`, `-accent`); descendants using `sable-surface-interactive` or `sable-well` then compute against it.
11
+ - **Instead of** `bg-card text-card-foreground border-border` plus a hand-mixed hover like `hover:bg-[color-mix(in_oklab,var(--foreground)_8%,var(--card))]`. The utility computes the same mix from whatever role you pick, so it stays one visible step in every preset and mode. Use a component (`Card`, `Panel`) first when one fits.
12
+ - `<div className="sable-surface sable-frame sable-frame-r-lg sable-frame-p-4">…</div>`
13
+ - **`sable-surface-muted`** — Switches a `sable-surface` to the `muted` role: recessed background, `foreground` ink, `border` edge. Sets the surface variables only; it does not paint on its own. *Use for:* With `sable-surface`, for a quiet, recessed container whose interactive rows still need derived hover/selected fills.
14
+ - **Instead of** `bg-muted` alone, which leaves descendants' hover fills computed against the wrong surface. For a small recessed area INSIDE another surface (a track, a pad) use `sable-well` instead.
15
+ - `<div className="sable-surface sable-surface-muted sable-frame sable-frame-r-lg sable-frame-p-4">…</div>`
16
+ - **`sable-surface-popover`** — Switches the surface context to the `popover` role (popover fill and ink, `border-floating` edge). Sets the variables only, so it can also be used ALONE to declare 'descendants sit on a popover'. *Use for:* On a hand-composed floating panel (an inspector, a toolbar over a canvas) so `sable-well` and `sable-surface-interactive` inside it derive from the popover rather than the page. Alone, alongside `bg-popover text-popover-foreground`, it is purely that context hint; with `sable-surface` it also paints.
17
+ - **Instead of** leaving the context unset. Then `sable-well` falls back to `--background`, which inside a popover in some dark modes lands within a step of the panel: the bug that tempts an outline on every well.
18
+ - `<aside className="sable-surface-popover bg-popover text-popover-foreground rounded-lg ring-1 ring-border-floating shadow-lg">…</aside>`
19
+ - **`sable-surface-accent`** — Switches a `sable-surface` to the `accent` role: the transient-state fill with its paired ink. Sets the variables only. *Use for:* With `sable-surface`, for a container that is itself in a highlighted/current state and still hosts interactive rows whose hover must step off it.
20
+ - **Instead of** `bg-accent text-accent-foreground` on a container whose children then hover against the page colour. Accent is a STATE, not a decoration — don't use it to make a static section stand out.
21
+ - `<div className="sable-surface sable-surface-accent sable-frame sable-frame-r-md sable-frame-p-2">…</div>`
22
+ - **`sable-surface-interactive`** — Hover / highlighted and pressed / selected fills for a row or button, derived from the surface it sits on. Hover = the surface's hover step (8% ink); active, selected or checked = its active step (12% ink inside a `sable-surface`, `accent` outside one). *Use for:* A clickable row, list item or bespoke button inside a surface, where the hover colour must be one consistent step off THAT surface. Matches `:hover`, `[data-highlighted]` / `[data-active]` and `:active`, `[data-selected]` / `[data-checked]` / `aria-selected` / `aria-checked`, so Base UI items work without extra variants. Pair with `motion-interactive` for the transition.
23
+ - **Instead of** `hover:bg-accent`, `hover:bg-muted` or a hand-written `color-mix(in oklab, var(--foreground) 8%, …)` hover. A fixed hover token is right on one surface and invisible or loud on the next; this one is computed from where the row actually is. Reach for `ListItem`, `Menu` or `Button variant="ghost"` first when they fit.
24
+ - `<button className="sable-surface-interactive sable-frame-inner motion-interactive px-3 py-2 text-start text-body-sm">Rename</button>`
25
+ - **`sable-well`** — A recessed area ON whatever surface it sits in: 7% ink mixed into the inherited surface (`--sable-surface-bg`, the page background by default), so it is the same visible step on the page, in a card or in a popover. *Use for:* A segmented-control track, a colour or XY pad, a preview well, a readout block inside a panel. Sable's Card (default), Popover, Dialog and Sheet declare their surface, so a well inside them derives from them automatically; on a hand-composed panel, put `sable-surface-popover` (or set `--sable-surface-bg`) on the panel yourself. Whatever sits ON the well takes the surface's own colour back (`bg-popover` inside a popover). A well takes no outline.
26
+ - **Instead of** `bg-muted`, which is fixed: inside a popover it can sit within a step of the panel in some dark modes. And never a hand-mixed `color-mix(… var(--foreground) …)` or an outline on every well to compensate — that turns a panel into a stack of boxes. For a segmented control itself, use `ToggleGroup`; the well is for what no component draws.
27
+ - `<div className="sable-well rounded-md p-1">…</div>`
28
+
29
+ ### frame (6)
30
+
31
+ - **`sable-frame`** — A bordered, padded, rounded container that computes the radius its children need: `--sable-frame-inner-radius` = radius − padding − border width, floored at `radius-sm`. *Use for:* A container whose children have their own rounded backgrounds (rows, tiles, a nested preview), so the corners nest concentrically and keep doing so when the radius ladder is rescaled. Defaults: `radius-lg`, `--spacing(4)` padding (1rem at the default scale, and it follows the density control), a 1px edge from the surface (`--sable-surface-edge`, else `border`). Size it with `sable-frame-r-*` / `sable-frame-p-*`; children take `sable-frame-inner`.
32
+ - **Instead of** `rounded-lg p-4 border` on the parent plus a guessed `rounded-sm` on each child — the guess is concentric only at the default ladder. For a one-off nested corner without a frame, `rounded-concentric-*`.
33
+ - `<div className="sable-frame sable-surface sable-frame-r-lg sable-frame-p-4"><div className="sable-frame-inner sable-well p-3">…</div></div>`
34
+ - **`sable-frame-popover`** — The popup frame recipe: `radius-md`, `--spacing(1)` padding, the `border-floating` edge and `shadow-lg`, applied over `sable-frame`. *Use for:* A hand-composed floating list or panel that must match Sable's menus (with `sable-frame sable-surface sable-surface-popover`); rows inside take `sable-frame-inner`.
35
+ - **Instead of** Copying a Menu's class string. And use `Menu`, `Select`, `Popover` or `Combobox` first — they own focus and dismissal; this is only for floating chrome no component covers.
36
+ - `<div className="sable-frame sable-surface sable-surface-popover sable-frame-popover">…</div>`
37
+ - **`sable-frame-borderless`** — Sets a `sable-frame`'s border width to 0 while keeping the inner-radius math correct. *Use for:* A frame that separates by fill or space rather than an edge (a well, a tinted group) but still needs concentric children.
38
+ - **Instead of** `border-0` on a `sable-frame`, which removes the border but leaves 1px in the inner-radius calculation.
39
+ - `<div className="sable-frame sable-frame-borderless sable-well sable-frame-r-md sable-frame-p-1">…</div>`
40
+ - **`sable-frame-r-*`** — Sets a `sable-frame`'s outer radius from the radius ladder (`sable-frame-r-lg`) or an arbitrary value (`sable-frame-r-[14px]`). *Use for:* Whenever a frame should not be the default `radius-lg`. The inner radius follows automatically.
41
+ - **Instead of** `rounded-*` on a `sable-frame` — that changes the painted corner but not `--sable-frame-inner-radius`, so the children stop nesting.
42
+ - `<div className="sable-frame sable-frame-r-md sable-frame-p-2">…</div>`
43
+ - **`sable-frame-p-*`** — Sets a `sable-frame`'s padding on the spacing scale (`sable-frame-p-2` = `--spacing(2)`), feeding the inner-radius math. On the scale, so the density control moves it. *Use for:* Whenever a frame's inset is not the default `--spacing(4)`.
44
+ - **Instead of** `p-*` on a `sable-frame` — the padding changes but the inner radius is still computed from the default `--spacing(4)`, so the children's corners are wrong.
45
+ - `<div className="sable-frame sable-frame-r-lg sable-frame-p-2">…</div>`
46
+ - **`sable-frame-inner`** — Takes the radius its `sable-frame` ancestor computed (outer − inset − border), never below `radius-sm`: concentric when the child sits near the frame edge (a popover row), the theme's small radius when it sits deep inside a padded card (where the subtraction would leave a near-square row). Falls back to `radius-sm` outside a frame. *Use for:* On every rounded child of a `sable-frame`: rows, tiles, an inner well, a nested image.
47
+ - **Instead of** A hardcoded `rounded-sm` on the child, which drifts the moment a theme or the customizer rescales radii.
48
+ - `<div className="sable-frame sable-frame-r-lg sable-frame-p-2"><button className="sable-frame-inner sable-surface-interactive px-3 py-2">…</button></div>`
49
+
50
+ ### geometry (4)
51
+
52
+ - **`rounded-concentric-*`** — A border radius computed as an outer radius minus an inset: `rounded-concentric-<radius>/<spacing>`, clamped at zero. `rounded-concentric-md/1` = `radius-md` − `spacing-1`. *Use for:* A one-off rounded element nested inside another — a tab trigger inside a tab list, a thumb inside a track, a row inside a well — where a `sable-frame` would be overkill. The outer radius and the inset are the parent's.
53
+ - **Instead of** Picking the next ladder step down by eye (`rounded-sm` inside `rounded-md p-1`). That is concentric only at the default ladder; the utility recomputes from live tokens. Registered in `cn()` as a radius, so it correctly overrides a component's own `rounded-*`.
54
+ - `<div className="sable-well rounded-md p-1"><button className="rounded-concentric-md/1 px-2 py-1">…</button></div>`
55
+ - **`popup-inset`** — The padding of a popup list, derived from the popup's radius: a third of `radius-md`, at least `--spacing(1)`. A rounder popup gets more room, and the floor is on the spacing scale, so the density control moves it. *Use for:* On a hand-composed rounded-md popup that holds a list of rows. Sable's Menu, Select, Combobox and ContextMenu already use it.
56
+ - **Instead of** `p-1`. A fixed inset at a larger radius preset leaves the rows' corners approaching half their height, so every row turns into a pill.
57
+ - `<div className="rounded-md bg-popover popup-inset shadow-md">…</div>`
58
+ - **`rounded-popup-item`** — The row radius that nests inside a `popup-inset` popup: `radius-md` minus the popup inset, clamped at zero. *Use for:* On every row inside a `popup-inset` container.
59
+ - **Instead of** `rounded-sm` on menu rows, which matches only the default ladder (12px popup, 4px inset, 8px rows).
60
+ - `<div role="option" className="rounded-popup-item px-2 py-1.5 sable-highlighted:bg-accent">…</div>`
61
+ - **`popup-bleed`** — A negative inline margin of exactly one `popup-inset`, so a separator inside a padded popup runs edge to edge at every radius and density. *Use for:* On a hand-composed separator (a 1px rule) inside a `popup-inset` popup. Sable's MenuSeparator, ContextMenuSeparator and SelectSeparator already use it.
62
+ - **Instead of** `-mx-1`, which matches the inset only at the default ladder and stops short of both edges once a rounder preset or a roomier density grows it. design-lint flags it (`popup-separator-bleed`).
63
+ - `<div role="separator" className="popup-bleed my-1 h-px bg-border" />`
64
+
65
+ ### state variant (5)
66
+
67
+ - **`sable-highlighted:`** — A variant (`sable-highlighted:`) matching the keyboard/pointer-highlighted item: `[data-highlighted]`, `[data-active]`, `[data-active-item]`. *Use for:* Styling the current item in a list, menu, combobox or listbox — Base UI and ARIA-style composites mark it differently, this matches all of them.
68
+ - **Instead of** `data-[highlighted]:bg-accent data-[active]:bg-accent …` repeated per attribute, or `hover:` alone (which misses keyboard navigation).
69
+ - `<div role="option" className="sable-highlighted:bg-accent sable-highlighted:text-accent-foreground">…</div>`
70
+ - **`sable-selected:`** — A variant (`sable-selected:`) matching a selected or checked element: `[data-selected]`, `[data-checked]`, `[aria-selected=true]`, `[aria-checked=true]`. *Use for:* The chosen row, the pressed segment, the checked option — whichever attribute the primitive happens to set.
71
+ - **Instead of** Separate `data-[selected]:` / `data-[checked]:` / `aria-selected:` classes, one of which is always forgotten.
72
+ - `<div role="option" className="sable-selected:bg-accent sable-selected:text-accent-foreground">…</div>`
73
+ - **`sable-open:`** — A variant (`sable-open:`) matching an open trigger or disclosure: `[data-open]`, `[data-popup-open]`, `[data-panel-open]`, `[aria-expanded=true]`. *Use for:* Rotating a chevron, holding a trigger's pressed look while its menu or panel is open.
74
+ - **Instead of** `data-[popup-open]:` on one trigger and `aria-expanded:` on the next.
75
+ - `<button className="motion-interactive sable-open:bg-accent sable-open:text-accent-foreground">…</button>`
76
+ - **`sable-disabled:`** — A variant (`sable-disabled:`) matching a disabled element: `:disabled`, `[disabled]`, `[data-disabled]`, `[aria-disabled=true]`. *Use for:* Dimming or locking a bespoke element regardless of whether it is natively or ARIA-disabled.
77
+ - **Instead of** `disabled:` alone, which misses `aria-disabled` and Base UI's `data-disabled` — the cases that stay focusable.
78
+ - `<div className="sable-disabled:pointer-events-none sable-disabled:opacity-50">…</div>`
79
+ - **`sable-invalid:`** — A variant (`sable-invalid:`) matching an invalid field: `[aria-invalid=true]`, `[data-invalid]`. *Use for:* The error edge or ink on a bespoke field wrapper. Use `Field` / `Input` first; they already style it.
80
+ - **Instead of** `aria-invalid:` or `data-[invalid]:` alone — Base UI's Field sets one, native validation wiring the other.
81
+ - `<div className="ring-1 ring-border sable-invalid:ring-destructive">…</div>`
82
+
83
+ ### scroll (2)
84
+
85
+ - **`scrollbar-quiet`** — A thin floating scrollbar: no stepper arrows, a 4px thumb inset from the edges, invisible at rest and fading in while the region is scrolled, hovered or focused. *Use for:* Every VERTICAL scroll container (anything with `overflow-y: auto` or `scroll`). Add `useScrollActivity` so the thumb also appears on touch drags and programmatic scrolls. Required by design-lint's `unmanaged-overflow`.
86
+ - **Instead of** A scroll container with no policy, which on Windows draws stepper arrows hard against the padding edge and out of any rounded corner. Never set `scrollbar-width` yourself — in Chromium it disables this utility.
87
+ - `<div className="max-h-80 overflow-y-auto scrollbar-quiet">…</div>`
88
+ - **`scrollbar-hidden`** — Scrolls without ever drawing a bar. *Use for:* HORIZONTAL strips whose edge fade or paging controls carry the 'there is more' affordance (it is `Scroller`'s horizontal default), a canvas panned by drag. You should be able to name what carries the affordance instead.
89
+ - **Instead of** `scrollbar-quiet` on a vertical list. A vertical region with no bar and no fade tells a reader nothing about how far through they are.
90
+ - `<div className="flex gap-3 overflow-x-auto scrollbar-hidden">…</div>`
91
+
92
+ ### text (8)
93
+
94
+ - **`text-overline`** — The eyebrow / overline / kicker tier: small, letter-spaced, uppercase by default (a theme may set sentence case via `--type-overline-transform`). *Use for:* A section eyebrow above a heading, a stat's label, a badge-style category line. Sits ABOVE and categorises.
95
+ - **Instead of** `text-label-xs uppercase tracking-wider` assembled by hand, or `text-caption`, which sits BELOW and explains. In tool chrome, panel headers use `text-ui-section` instead.
96
+ - `<p className="text-overline text-muted-foreground">Latest news</p>`
97
+ - **`text-ui-md`** — Workspace-register chrome text, the default tier: ~12px at weight ~500 on 16px leading. *Use for:* Labels and values in tool chrome — panels, inspectors, toolbars, trees — inside the workspace register.
98
+ - **Instead of** `text-label-sm` or `text-body-sm` in a dense panel (the page register's weights read as shouting at this size), or a size class plus a weight class composed by hand.
99
+ - `<span className="text-ui-md text-panel-foreground">Opacity</span>`
100
+ - **`text-ui-sm`** — Workspace chrome text, the compact tier: ~11px at weight ~500. *Use for:* Secondary chrome: property names in a grid, status-bar items, tree rows in a compact density.
101
+ - **Instead of** `text-label-xs`, a page-register tier tuned for different weights and leading.
102
+ - `<span className="text-ui-sm text-muted-foreground">W</span>`
103
+ - **`text-ui-body`** — The reading tier inside chrome: `text-ui-md`'s size at regular weight. *Use for:* Descriptions, help text and empty-state copy inside a panel or inspector.
104
+ - **Instead of** `text-ui-md`, whose medium weight makes a sentence read as a label; or `text-body-sm`, which is page-register.
105
+ - `<p className="text-ui-body text-muted-foreground">Select a layer to edit its properties.</p>`
106
+ - **`text-ui-section`** — Panel section headers in tool chrome: `text-ui-sm`'s size, a little heavier and tracked — not uppercase. *Use for:* A `PanelSection` header or any group title inside a dense panel.
107
+ - **Instead of** `text-overline`, whose all-caps reads louder than the content it labels at this density.
108
+ - `<h3 className="text-ui-section text-panel-foreground">Layout</h3>`
109
+ - **`text-ui-micro`** — The smallest chrome tier: ~10px at weight ~450. Use sparingly. *Use for:* Chrome annotations — a unit suffix, a shortcut hint, a ruler tick label.
110
+ - **Instead of** `text-[10px]`, which bypasses the theme and density hooks.
111
+ - `<kbd className="text-ui-micro text-muted-foreground">⌘K</kbd>`
112
+ - **`text-ui-num`** — Numeric values in chrome: mono, tabular figures, at the `text-ui-sm` size. *Use for:* Values in a property grid, a scrub field's readout, coordinates — any number that changes while you watch.
113
+ - **Instead of** `text-ui-sm` (proportional digits make a column jitter while a slider drags) or `text-mono-sm`, which is page-register.
114
+ - `<span className="text-ui-num">128</span>`
115
+ - **`stream-tail`** — The soft trailing edge of a live text stream: a slight blur plus a fading mask, so the newest token reads as 'more is coming' rather than a sentence that has ended. Static. *Use for:* On the last run of text while a model or source is still streaming; remove it when the stream finishes. `StreamingText` applies it for you.
116
+ - **Instead of** An animated fade or a hand-written `mask-image` gradient. Use `StreamingText` first; this is for a stream rendered without it.
117
+ - `<span className="stream-tail">{latestChunk}</span>`
118
+
119
+ ### helper (3)
120
+
121
+ - **`hit-target`** — Grows an element's tap target to 44px with a transparent `::after`, without moving its layout by a pixel. `--hit-target-size` overrides the size. *Use for:* A deliberately small mark — a tag's remove ×, a tiny disclosure, a toast close — that must still meet the WCAG touch floor. Shrink it (`[--hit-target-size:32px]`) where targets sit side by side, or the later one steals the taps.
122
+ - **Instead of** Padding the glyph up (moves it off the row's grid and pushes siblings) or a bigger icon. Never on an `<input>` or other replaced element — pseudo-elements silently do nothing there; wrap it in a label.
123
+ - `<button className="hit-target text-muted-foreground" aria-label="Remove"><Icon name="xmark" /></button>`
124
+ - **`link-underline`** — An underline drawn from the font's own metrics (position and thickness `from-font`) with ink skipping descenders, so it retunes when a theme swaps typeface. *Use for:* Inline links in running text. Pair with `motion-interactive` for a colour change on hover.
125
+ - **Instead of** `underline underline-offset-4 decoration-2` — tuned to one face and wrong for the next theme's. Animating thickness or offset is not supported; a growing underline is a separate pseudo-element.
126
+ - `<a href="/terms" className="link-underline motion-interactive text-primary">terms</a>`
127
+ - **`checkerboard`** — The transparency checkerboard, painted from `--panel` and a derived darker step so it reads in both modes. `--checkerboard-size` (default 16px) and `--checkerboard-alt` tune it. *Use for:* Behind anything that can be translucent: a colour swatch with alpha, an image or artboard preview, an alpha slider track.
128
+ - **Instead of** A hand-rolled `repeating-conic-gradient` with two hardcoded greys. Deliberately not named `bg-checkerboard`, so `cn()` never merges it away against a real `bg-*` colour — you can layer a colour over it.
129
+ - `<div className="checkerboard [--checkerboard-size:8px] size-6 rounded-sm" />`