@slotted/styles 0.0.0-stage → 0.1.0-beta.1

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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/package.json +276 -3
  4. package/src/accordion/accordion.css +96 -0
  5. package/src/accordion/accordion.tokens.json +14 -0
  6. package/src/action-bar/action-bar.css +455 -0
  7. package/src/action-bar/action-bar.tokens.json +50 -0
  8. package/src/action-sheet/action-sheet.css +462 -0
  9. package/src/action-sheet/action-sheet.tokens.json +59 -0
  10. package/src/alert/alert.css +764 -0
  11. package/src/alert/alert.tokens.json +77 -0
  12. package/src/app-shell/app-shell.css +1899 -0
  13. package/src/app-shell/app-shell.tokens.json +88 -0
  14. package/src/aspect-ratio/aspect-ratio.css +131 -0
  15. package/src/aspect-ratio/aspect-ratio.tokens.json +1 -0
  16. package/src/attachment/attachment.css +906 -0
  17. package/src/attachment/attachment.tokens.json +85 -0
  18. package/src/avatar/avatar.css +598 -0
  19. package/src/avatar/avatar.tokens.json +64 -0
  20. package/src/avatar-group/avatar-group.css +268 -0
  21. package/src/avatar-group/avatar-group.tokens.json +16 -0
  22. package/src/badge/badge.css +462 -0
  23. package/src/badge/badge.tokens.json +58 -0
  24. package/src/breadcrumb/breadcrumb.css +441 -0
  25. package/src/breadcrumb/breadcrumb.tokens.json +50 -0
  26. package/src/button/button.css +754 -0
  27. package/src/button/button.tokens.json +72 -0
  28. package/src/button-group/button-group.css +455 -0
  29. package/src/button-group/button-group.tokens.json +11 -0
  30. package/src/calendar/calendar.css +846 -0
  31. package/src/calendar/calendar.tokens.json +67 -0
  32. package/src/card/card.css +602 -0
  33. package/src/card/card.tokens.json +68 -0
  34. package/src/carousel/carousel.css +860 -0
  35. package/src/carousel/carousel.tokens.json +62 -0
  36. package/src/cascader/cascader.css +1320 -0
  37. package/src/cascader/cascader.tokens.json +103 -0
  38. package/src/chart/chart.css +1473 -0
  39. package/src/chart/chart.tokens.json +107 -0
  40. package/src/checkbox/checkbox.css +1011 -0
  41. package/src/checkbox/checkbox.tokens.json +102 -0
  42. package/src/checkbox-group/checkbox-group.css +70 -0
  43. package/src/checkbox-group/checkbox-group.tokens.json +6 -0
  44. package/src/code-block/code-block.css +555 -0
  45. package/src/code-block/code-block.tokens.json +69 -0
  46. package/src/collapsible/collapsible.css +546 -0
  47. package/src/collapsible/collapsible.tokens.json +55 -0
  48. package/src/color-picker/color-picker.css +607 -0
  49. package/src/color-picker/color-picker.tokens.json +58 -0
  50. package/src/column-manager/column-manager.css +452 -0
  51. package/src/column-manager/column-manager.tokens.json +43 -0
  52. package/src/combobox/combobox.css +1048 -0
  53. package/src/combobox/combobox.tokens.json +112 -0
  54. package/src/command-palette/command-palette.css +1000 -0
  55. package/src/command-palette/command-palette.tokens.json +75 -0
  56. package/src/container/container.css +198 -0
  57. package/src/container/container.tokens.json +18 -0
  58. package/src/context-menu/context-menu.css +104 -0
  59. package/src/context-menu/context-menu.tokens.json +11 -0
  60. package/src/currency-field/currency-field.css +787 -0
  61. package/src/currency-field/currency-field.tokens.json +80 -0
  62. package/src/dashboard-grid/dashboard-grid.css +748 -0
  63. package/src/dashboard-grid/dashboard-grid.tokens.json +67 -0
  64. package/src/data-grid/data-grid.css +2484 -0
  65. package/src/data-grid/data-grid.tokens.json +100 -0
  66. package/src/data-list/data-list.css +455 -0
  67. package/src/data-list/data-list.tokens.json +35 -0
  68. package/src/date-field/date-field.css +560 -0
  69. package/src/date-field/date-field.tokens.json +80 -0
  70. package/src/date-picker/date-picker.css +420 -0
  71. package/src/date-picker/date-picker.tokens.json +49 -0
  72. package/src/date-range-picker/date-range-picker.css +983 -0
  73. package/src/date-range-picker/date-range-picker.tokens.json +109 -0
  74. package/src/datetime-field/datetime-field.css +850 -0
  75. package/src/datetime-field/datetime-field.tokens.json +91 -0
  76. package/src/datetime-picker/datetime-picker.css +843 -0
  77. package/src/datetime-picker/datetime-picker.tokens.json +78 -0
  78. package/src/datetime-range-picker/datetime-range-picker.css +1375 -0
  79. package/src/datetime-range-picker/datetime-range-picker.tokens.json +135 -0
  80. package/src/description-list/description-list.css +675 -0
  81. package/src/description-list/description-list.tokens.json +66 -0
  82. package/src/detail-panel/detail-panel.css +329 -0
  83. package/src/detail-panel/detail-panel.tokens.json +33 -0
  84. package/src/dialog/dialog.css +968 -0
  85. package/src/dialog/dialog.tokens.json +66 -0
  86. package/src/divider/divider.css +131 -0
  87. package/src/divider/divider.tokens.json +18 -0
  88. package/src/drawer/drawer.css +1303 -0
  89. package/src/drawer/drawer.tokens.json +75 -0
  90. package/src/entity-picker/entity-picker.css +1367 -0
  91. package/src/entity-picker/entity-picker.tokens.json +118 -0
  92. package/src/event-calendar/event-calendar.css +2148 -0
  93. package/src/event-calendar/event-calendar.tokens.json +87 -0
  94. package/src/field/field.css +707 -0
  95. package/src/field/field.tokens.json +58 -0
  96. package/src/field-group/field-group.css +226 -0
  97. package/src/field-group/field-group.tokens.json +33 -0
  98. package/src/filter-bar/filter-bar.css +1306 -0
  99. package/src/filter-bar/filter-bar.tokens.json +78 -0
  100. package/src/form/form.css +218 -0
  101. package/src/form/form.tokens.json +32 -0
  102. package/src/fullscreen/fullscreen.css +376 -0
  103. package/src/fullscreen/fullscreen.tokens.json +50 -0
  104. package/src/grid/grid.css +173 -0
  105. package/src/grid/grid.tokens.json +20 -0
  106. package/src/highlight/highlight.css +135 -0
  107. package/src/highlight/highlight.tokens.json +16 -0
  108. package/src/hover-card/hover-card.css +93 -0
  109. package/src/hover-card/hover-card.tokens.json +17 -0
  110. package/src/icon/icon.css +136 -0
  111. package/src/icon/icon.tokens.json +14 -0
  112. package/src/image/image.css +449 -0
  113. package/src/image/image.tokens.json +27 -0
  114. package/src/import-export-center/import-export-center.css +1175 -0
  115. package/src/import-export-center/import-export-center.tokens.json +84 -0
  116. package/src/inline/inline.css +149 -0
  117. package/src/inline/inline.tokens.json +14 -0
  118. package/src/inline-message/inline-message.css +396 -0
  119. package/src/inline-message/inline-message.tokens.json +19 -0
  120. package/src/kbd/kbd.css +490 -0
  121. package/src/kbd/kbd.tokens.json +40 -0
  122. package/src/link/link.css +237 -0
  123. package/src/link/link.tokens.json +25 -0
  124. package/src/list/list.css +240 -0
  125. package/src/list/list.tokens.json +42 -0
  126. package/src/list-item/list-item.css +937 -0
  127. package/src/list-item/list-item.tokens.json +114 -0
  128. package/src/listbox/listbox.css +674 -0
  129. package/src/listbox/listbox.tokens.json +72 -0
  130. package/src/loading-overlay/loading-overlay.css +730 -0
  131. package/src/loading-overlay/loading-overlay.tokens.json +48 -0
  132. package/src/markdown/markdown.css +168 -0
  133. package/src/markdown/markdown.tokens.json +18 -0
  134. package/src/menu/menu.css +817 -0
  135. package/src/menu/menu.tokens.json +78 -0
  136. package/src/menubar/menubar.css +170 -0
  137. package/src/menubar/menubar.tokens.json +32 -0
  138. package/src/message/message.css +1453 -0
  139. package/src/message/message.tokens.json +85 -0
  140. package/src/message-scroller/message-scroller.css +371 -0
  141. package/src/message-scroller/message-scroller.tokens.json +47 -0
  142. package/src/multi-select/multi-select.css +1427 -0
  143. package/src/multi-select/multi-select.tokens.json +114 -0
  144. package/src/navigation-menu/navigation-menu.css +1029 -0
  145. package/src/navigation-menu/navigation-menu.tokens.json +70 -0
  146. package/src/notification-center/notification-center.css +856 -0
  147. package/src/notification-center/notification-center.tokens.json +84 -0
  148. package/src/number-field/number-field.css +794 -0
  149. package/src/number-field/number-field.tokens.json +80 -0
  150. package/src/otp-input/otp-input.css +525 -0
  151. package/src/otp-input/otp-input.tokens.json +69 -0
  152. package/src/page/page.css +179 -0
  153. package/src/page/page.tokens.json +20 -0
  154. package/src/page-header/page-header.css +1181 -0
  155. package/src/page-header/page-header.tokens.json +71 -0
  156. package/src/pagination/pagination.css +500 -0
  157. package/src/pagination/pagination.tokens.json +56 -0
  158. package/src/popover/popover.css +526 -0
  159. package/src/popover/popover.tokens.json +60 -0
  160. package/src/progress-indicator/progress-indicator.css +1584 -0
  161. package/src/progress-indicator/progress-indicator.tokens.json +50 -0
  162. package/src/prompt-composer/prompt-composer.css +680 -0
  163. package/src/prompt-composer/prompt-composer.tokens.json +99 -0
  164. package/src/prose/prose.css +733 -0
  165. package/src/prose/prose.tokens.json +79 -0
  166. package/src/radio-group/radio-group.css +778 -0
  167. package/src/radio-group/radio-group.tokens.json +103 -0
  168. package/src/rating/rating.css +1071 -0
  169. package/src/rating/rating.tokens.json +88 -0
  170. package/src/result-state/result-state.css +980 -0
  171. package/src/result-state/result-state.tokens.json +63 -0
  172. package/src/runtime-properties.mjs +321 -0
  173. package/src/saved-views/saved-views.css +394 -0
  174. package/src/saved-views/saved-views.tokens.json +41 -0
  175. package/src/scroll-area/scroll-area.css +481 -0
  176. package/src/scroll-area/scroll-area.tokens.json +15 -0
  177. package/src/section/section.css +177 -0
  178. package/src/section/section.tokens.json +39 -0
  179. package/src/segmented-control/segmented-control.css +564 -0
  180. package/src/segmented-control/segmented-control.tokens.json +67 -0
  181. package/src/select/select.css +1593 -0
  182. package/src/select/select.tokens.json +110 -0
  183. package/src/side-navigation/side-navigation.css +1096 -0
  184. package/src/side-navigation/side-navigation.tokens.json +87 -0
  185. package/src/skeleton/skeleton.css +431 -0
  186. package/src/skeleton/skeleton.tokens.json +29 -0
  187. package/src/slider/slider.css +792 -0
  188. package/src/slider/slider.tokens.json +75 -0
  189. package/src/sparkline/sparkline.css +282 -0
  190. package/src/sparkline/sparkline.tokens.json +45 -0
  191. package/src/spinner/spinner.css +224 -0
  192. package/src/spinner/spinner.tokens.json +8 -0
  193. package/src/split-button/split-button.css +253 -0
  194. package/src/split-button/split-button.tokens.json +12 -0
  195. package/src/splitter/splitter.css +336 -0
  196. package/src/splitter/splitter.tokens.json +24 -0
  197. package/src/stack/stack.css +128 -0
  198. package/src/stack/stack.tokens.json +14 -0
  199. package/src/stat-card/stat-card.css +641 -0
  200. package/src/stat-card/stat-card.tokens.json +95 -0
  201. package/src/status-indicator/status-indicator.css +481 -0
  202. package/src/status-indicator/status-indicator.tokens.json +38 -0
  203. package/src/stepper/stepper.css +629 -0
  204. package/src/stepper/stepper.tokens.json +72 -0
  205. package/src/switch/switch.css +703 -0
  206. package/src/switch/switch.tokens.json +75 -0
  207. package/src/table/table.css +1482 -0
  208. package/src/table/table.tokens.json +69 -0
  209. package/src/table-of-contents/table-of-contents.css +533 -0
  210. package/src/table-of-contents/table-of-contents.tokens.json +53 -0
  211. package/src/tabs/tabs.css +998 -0
  212. package/src/tabs/tabs.tokens.json +82 -0
  213. package/src/tag/tag.css +768 -0
  214. package/src/tag/tag.tokens.json +85 -0
  215. package/src/tags-input/tags-input.css +757 -0
  216. package/src/tags-input/tags-input.tokens.json +84 -0
  217. package/src/text-field/text-field.css +900 -0
  218. package/src/text-field/text-field.tokens.json +87 -0
  219. package/src/textarea/textarea.css +365 -0
  220. package/src/textarea/textarea.tokens.json +59 -0
  221. package/src/time-field/time-field.css +719 -0
  222. package/src/time-field/time-field.tokens.json +86 -0
  223. package/src/time-picker/time-picker.css +665 -0
  224. package/src/time-picker/time-picker.tokens.json +66 -0
  225. package/src/time-range-picker/time-range-picker.css +1287 -0
  226. package/src/time-range-picker/time-range-picker.tokens.json +134 -0
  227. package/src/timeline/timeline.css +1596 -0
  228. package/src/timeline/timeline.tokens.json +96 -0
  229. package/src/toast/toast.css +943 -0
  230. package/src/toast/toast.tokens.json +90 -0
  231. package/src/toggle/toggle.css +290 -0
  232. package/src/toggle/toggle.tokens.json +54 -0
  233. package/src/toggle-group/toggle-group.css +463 -0
  234. package/src/toggle-group/toggle-group.tokens.json +54 -0
  235. package/src/toolbar/toolbar.css +434 -0
  236. package/src/toolbar/toolbar.tokens.json +49 -0
  237. package/src/tooltip/tooltip.css +368 -0
  238. package/src/tooltip/tooltip.tokens.json +39 -0
  239. package/src/top-navigation/top-navigation.css +1287 -0
  240. package/src/top-navigation/top-navigation.tokens.json +91 -0
  241. package/src/tour/tour.css +680 -0
  242. package/src/tour/tour.tokens.json +67 -0
  243. package/src/transfer/transfer.css +571 -0
  244. package/src/transfer/transfer.tokens.json +49 -0
  245. package/src/tree/tree.css +1000 -0
  246. package/src/tree/tree.tokens.json +80 -0
  247. package/src/typography/typography.css +211 -0
  248. package/src/typography/typography.tokens.json +31 -0
  249. package/src/upload/upload.css +1706 -0
  250. package/src/upload/upload.tokens.json +123 -0
  251. package/src/visually-hidden/visually-hidden.css +52 -0
  252. package/src/visually-hidden/visually-hidden.tokens.json +1 -0
  253. package/src/wizard/wizard.css +740 -0
  254. package/src/wizard/wizard.tokens.json +63 -0
@@ -0,0 +1,794 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── One number, and the two buttons that move it ───────────────────────
4
+ *
5
+ * A frame around an `input`, rather than the `input` itself — the shape
6
+ * `text-field` established, and for the same WAI-ARIA reason: a `spinbutton`,
7
+ * like a `textbox`, may hold only non-interactive presentational children, so
8
+ * the two steppers are siblings of the control inside a frame with no role.
9
+ *
10
+ * The control is `type="text"`, never `type="number"`: the HTML value
11
+ * sanitization algorithm empties a number input whenever the text is not a
12
+ * valid floating-point number, so no grouping separator, no decimal comma and
13
+ * no currency affix survives it.
14
+ *
15
+ * Every rule keys on `data-ui` and `data-part`. The field around the control
16
+ * mirrors its state onto the frame rather than styling through, so a control
17
+ * used outside a field looks the same as one inside it.
18
+ */
19
+ [data-ui='number-field'][data-part='root'] {
20
+ /**
21
+ * Type, height, inline padding, gap and glyph size all come from the
22
+ * control scales — the only ones the theme restates per density — so an
23
+ * amount in a dense table shrinks whole rather than in parts.
24
+ */
25
+ --_font-size: var(--slotted-control-font-size-md, 13px);
26
+ --_height: var(--slotted-control-height-md, 34px);
27
+ --_padding-inline: var(--slotted-control-padding-inline-md, 13px);
28
+ --_gap: var(--slotted-control-gap-md, 8px);
29
+ /** The floor under the glyph scale at the dense step (SL3); a family token so a theme can retune it. */
30
+ --_icon-floor: var(--slotted-number-field-icon-size-min, 14px);
31
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
32
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
33
+
34
+ /**
35
+ * The stepper's cross measurement — the column's width when the two halves
36
+ * are stacked, and each button's width when they are split.
37
+ *
38
+ * It is the glyph plus a step of air on each side — the box every inner
39
+ * control in the library draws (R2) — capped at the inner size of a
40
+ * control at this step (`--slotted-control-inner-size-*`) so the column
41
+ * stays square-ish, and floored at the WCAG 2.2 minimum so the inline axis
42
+ * is never short.
43
+ * The block axis is answered per arrangement further down.
44
+ */
45
+ --_stepper-size: var(
46
+ --slotted-number-field-stepper-size,
47
+ max(
48
+ var(--slotted-target-min, 24px),
49
+ min(calc(var(--_icon-size) + 2 * var(--slotted-space-2, 0.25rem)), var(--_inner-size))
50
+ )
51
+ );
52
+
53
+ /**
54
+ * The split buttons are the inner controls `text-field` draws: the glyph
55
+ * plus a step of air, capped at the inner size, inset concentrically — the
56
+ * air beside a button equals the air above and below it, and its corner is
57
+ * the frame's less that air (docs/history/designs/2026-10-01-number-field-reui-dossier.md § 3).
58
+ */
59
+ --_action-size: var(
60
+ --slotted-number-field-action-size,
61
+ min(calc(var(--_icon-size) + 2 * var(--slotted-space-2, 0.25rem)), var(--_inner-size))
62
+ );
63
+ --_inset: max(
64
+ 0px,
65
+ calc(
66
+ (var(--_height) - 2 * var(--slotted-control-border-width, 1px) - var(--_action-size)) / 2
67
+ )
68
+ );
69
+ --_action-radius: max(
70
+ var(--slotted-radius-xs, 2px),
71
+ calc(var(--slotted-number-field-radius, var(--slotted-control-radius, 4px)) - var(--_inset))
72
+ );
73
+
74
+ /**
75
+ * The stacked column is inset by the smallest step rather than by
76
+ * `--_inset`: two halves share the frame's height, and the larger air
77
+ * left each half 12 pixels tall at `md`.
78
+ */
79
+ --_column-inset: var(--slotted-space-1, 2px);
80
+ --_column-radius: max(
81
+ var(--slotted-radius-xs, 2px),
82
+ calc(
83
+ var(--slotted-number-field-radius, var(--slotted-control-radius, 4px)) -
84
+ var(--_column-inset)
85
+ )
86
+ );
87
+
88
+ align-items: center;
89
+ background-color: var(--slotted-number-field-background, var(--slotted-control-surface, Field));
90
+ /**
91
+ * The control step of the border scale rather than the strong one: a
92
+ * separator between two surfaces and the edge of a control are the same
93
+ * ratio and not the same weight.
94
+ */
95
+ border: var(--slotted-control-border-width, 1px) solid
96
+ var(--slotted-number-field-border-color, var(--slotted-control-border-color, ButtonBorder));
97
+ border-radius: var(--slotted-number-field-radius, var(--slotted-control-radius, 4px));
98
+ box-shadow: var(--slotted-number-field-shadow, var(--slotted-control-shadow, none));
99
+ box-sizing: border-box;
100
+ color: var(--slotted-number-field-color, var(--slotted-text, CanvasText));
101
+ display: flex;
102
+ font-family: var(--slotted-control-font-family, system-ui, sans-serif);
103
+ font-size: var(--_font-size);
104
+ gap: var(--_gap);
105
+ inline-size: 100%;
106
+ /* A control in a grid track narrower than its intrinsic width has to shrink. */
107
+ min-inline-size: 0;
108
+ /* A minimum rather than a height: the frame is the taller of the control
109
+ scale and whatever a consumer put in an adornment. */
110
+ min-block-size: var(--_height);
111
+ padding-inline: var(--_padding-inline);
112
+ transition:
113
+ background-color var(--slotted-control-transition-duration)
114
+ var(--slotted-control-transition-easing),
115
+ border-color var(--slotted-control-transition-duration)
116
+ var(--slotted-control-transition-easing),
117
+ box-shadow var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
118
+ }
119
+
120
+ /**
121
+ * The control. It gives up its own frame entirely and keeps everything that
122
+ * makes it a text control: the value, the caret, the selection and every
123
+ * native attribute a consumer wrote.
124
+ *
125
+ * The figures are tabular without an axis to ask for it. A number field whose
126
+ * digits change width as they are typed over is a number field whose value
127
+ * moves under the reader's eye, and no other family has a stronger claim on
128
+ * the setting.
129
+ */
130
+ [data-ui='number-field'] > [data-part='input'] {
131
+ background-color: transparent;
132
+ /* The frame's border box, not its content box: on a coarse pointer the frame
133
+ is the touch target's height and the control it holds is too. Stretched
134
+ rather than a percentage of the frame, which is indefinite wherever the
135
+ frame is not stretched itself — a search field in a bar measured 17
136
+ pixels. The control paints no ground, so the frame's edge still shows. */
137
+ align-self: stretch;
138
+ margin-block: calc(var(--slotted-control-border-width, 1px) * -1);
139
+ border: 0;
140
+ color: inherit;
141
+ flex: 1 1 auto;
142
+ font: inherit;
143
+ font-variant-numeric: tabular-nums;
144
+ /* The frame draws the ring for it, so two rings never nest. */
145
+ outline: none;
146
+ padding: 0;
147
+ /* The intrinsic width of an `input` is about twenty characters, and a flex
148
+ item refuses to shrink below it without this. */
149
+ min-inline-size: 0;
150
+ text-overflow: ellipsis;
151
+ /**
152
+ * The value is its own bidi run, ordered by its own content.
153
+ *
154
+ * A number has no strong character in it — digits, a sign and two
155
+ * separators are all weak or neutral — so in a right-to-left document the
156
+ * whole run took the paragraph's direction and `-1.480,00` printed as
157
+ * `1.480,00-`, with the sign after the amount. The right-to-left capture
158
+ * showed it in the `content` scene.
159
+ *
160
+ * `plaintext` resolves a run with no strong character to left-to-right,
161
+ * which is the order every numbering system writes a number in, while the
162
+ * element's own direction still decides which end of the frame the value is
163
+ * aligned to. It is the same declaration the adornments carry, for the same
164
+ * reason.
165
+ */
166
+ unicode-bidi: plaintext;
167
+ }
168
+
169
+ /**
170
+ * The one place this family names a side.
171
+ *
172
+ * `plaintext` above pins the value's run to left-to-right, and `text-align`
173
+ * resolves `start` against that pinned direction rather than against the
174
+ * document's — so a right-to-left form drew the amount against the stepper
175
+ * column with the reading edge empty. There is no logical value that says
176
+ * "the document's start edge, whatever the run inside is doing", so the
177
+ * alignment is named once, under a guard that is itself the direction.
178
+ */
179
+ [data-ui='number-field']:dir(rtl) > [data-part='input'] {
180
+ text-align: right;
181
+ }
182
+
183
+ [data-ui='number-field'] > [data-part='input']::placeholder {
184
+ color: var(--slotted-number-field-placeholder-color, var(--slotted-text-muted, GrayText));
185
+ text-overflow: ellipsis;
186
+ }
187
+
188
+ /* ── Size ─────────────────────────────────────────────────────────────
189
+ *
190
+ * Three steps, matching the field the control sits in and the sibling text
191
+ * control beside it. One library, one scale.
192
+ */
193
+
194
+ [data-ui='number-field'][data-part='root'][data-size='sm'] {
195
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
196
+ --_height: var(--slotted-control-height-sm, 30px);
197
+ --_padding-inline: var(--slotted-control-padding-inline-sm, 10px);
198
+ --_gap: var(--slotted-control-gap-sm, 6px);
199
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-sm, 14px));
200
+ --_inner-size: var(--slotted-control-inner-size-sm, 22px);
201
+ }
202
+
203
+ [data-ui='number-field'][data-part='root'][data-size='md'] {
204
+ --_font-size: var(--slotted-control-font-size-md, 13px);
205
+ --_height: var(--slotted-control-height-md, 34px);
206
+ --_padding-inline: var(--slotted-control-padding-inline-md, 13px);
207
+ --_gap: var(--slotted-control-gap-md, 8px);
208
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
209
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
210
+ }
211
+
212
+ [data-ui='number-field'][data-part='root'][data-size='lg'] {
213
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
214
+ --_height: var(--slotted-control-height-lg, 38px);
215
+ --_padding-inline: var(--slotted-control-padding-inline-lg, 16px);
216
+ --_gap: var(--slotted-control-gap-lg, 8px);
217
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-lg, 18px));
218
+ --_inner-size: var(--slotted-control-inner-size-lg, 28px);
219
+ }
220
+
221
+ /* ── Variant ──────────────────────────────────────────────────────────
222
+ *
223
+ * The three `text-field` ships, unchanged: `outline` takes the ground it is
224
+ * placed on, `subtle` sinks into the page, and `plain` shows nothing at rest,
225
+ * which is what a dense table needs.
226
+ */
227
+
228
+ [data-ui='number-field'][data-part='root'][data-variant='subtle'] {
229
+ background-color: var(
230
+ --slotted-number-field-subtle-background,
231
+ var(--slotted-surface-sunken, Canvas)
232
+ );
233
+ border-color: var(
234
+ --slotted-number-field-subtle-border-color,
235
+ var(--slotted-border-subtle, ButtonBorder)
236
+ );
237
+ }
238
+
239
+ /* No edge, no lift, no ground and no corner of its own: the cell it sits in
240
+ is the box, the row is the ground, a cell has no corner, and the resting
241
+ shadow would draw the cell where there is none. */
242
+ [data-ui='number-field'][data-part='root'][data-variant='plain'] {
243
+ background-color: transparent;
244
+ border-color: transparent;
245
+ border-radius: var(--slotted-radius-none, 0px);
246
+ box-shadow: none;
247
+ }
248
+
249
+ /**
250
+ * The one rule the theme cannot write for the family. A transparent border
251
+ * has no system colour to be remapped to, so the `plain` control would lose
252
+ * its only edge exactly where an edge matters most.
253
+ */
254
+ @media (forced-colors: active) {
255
+ [data-ui='number-field'][data-part='root'][data-variant='plain'] {
256
+ border-color: ButtonBorder;
257
+ }
258
+ }
259
+
260
+ /* ── Adornments ───────────────────────────────────────────────────────
261
+ *
262
+ * A symbol beside the number rather than inside it. A symbol that belongs
263
+ * inside the value — a currency, a percent sign, a unit — is `format`'s job
264
+ * and arrives through `Intl` as part of the string the control holds.
265
+ */
266
+ [data-ui='number-field'] > [data-part='start'],
267
+ [data-ui='number-field'] > [data-part='end'] {
268
+ align-items: center;
269
+ color: var(--slotted-number-field-adornment-color, var(--slotted-text-muted, GrayText));
270
+ display: inline-flex;
271
+ /* Never squeezed: the value gives up width first, because the value is the
272
+ part that can scroll. */
273
+ flex: 0 0 auto;
274
+ gap: var(--slotted-space-2, 0.25rem);
275
+ line-height: 1;
276
+ /**
277
+ * Its own bidi run, ordered by its own content, so `R$` reads as itself in
278
+ * a right-to-left document instead of resolving to the paragraph.
279
+ */
280
+ unicode-bidi: plaintext;
281
+
282
+ --slotted-icon-size: var(--_icon-size);
283
+ }
284
+
285
+ [data-ui='number-field'] > [data-part='start'] > svg,
286
+ [data-ui='number-field'] > [data-part='end'] > svg {
287
+ block-size: var(--_icon-size);
288
+ inline-size: var(--_icon-size);
289
+ }
290
+
291
+ /**
292
+ * Optical alignment between the two kinds of adornment: a glyph fills the
293
+ * whole box and its artwork does not, while a character fills only its own
294
+ * advance width. An adornment that draws no glyph takes the margin the glyph
295
+ * box already has, on both sides — the measurement `text-field` derived.
296
+ */
297
+ [data-ui='number-field'] > [data-part='start']:not(:has(svg, img, [data-ui='icon'])),
298
+ [data-ui='number-field'] > [data-part='end']:not(:has(svg, img, [data-ui='icon'])) {
299
+ padding-inline: calc(var(--_icon-size) / 8);
300
+ }
301
+
302
+ /**
303
+ * A mark that moves the number when it is dragged sideways — the design
304
+ * tools' gesture. The adornment holding it stands the frame's full height,
305
+ * so the target is the frame's height rather than the mark's.
306
+ *
307
+ * Hover lifts the mark from the adornments' quiet ink to the value's: the
308
+ * mark is not a button, it has no ground to change, and the cursor alone
309
+ * said nothing a reader would look at.
310
+ */
311
+ [data-ui='number-field']
312
+ > :is([data-part='start'], [data-part='end']):has(> [data-part='scrub']) {
313
+ align-self: stretch;
314
+ }
315
+
316
+ [data-ui='number-field'] [data-part='scrub'] {
317
+ align-items: center;
318
+ align-self: stretch;
319
+ cursor: ew-resize;
320
+ display: inline-flex;
321
+ gap: var(--slotted-space-2, 0.25rem);
322
+ /* The drag is the gesture; a pan or a text selection would take it. */
323
+ touch-action: none;
324
+ transition: color var(--slotted-control-transition-duration)
325
+ var(--slotted-control-transition-easing);
326
+ user-select: none;
327
+ -webkit-user-select: none;
328
+ }
329
+
330
+ [data-ui='number-field']:not([data-disabled]):not([data-readonly])
331
+ [data-part='scrub']:is(:hover, [data-scrubbing]) {
332
+ color: var(--slotted-number-field-scrub-active-color, var(--slotted-text, CanvasText));
333
+ }
334
+
335
+ [data-ui='number-field']:is([data-disabled], [data-readonly]) [data-part='scrub'] {
336
+ cursor: default;
337
+ touch-action: auto;
338
+ }
339
+
340
+ /* ── The two steppers ─────────────────────────────────────────────────
341
+ *
342
+ * Real buttons, out of the tab order — the APG's "the text field is usually
343
+ * the only focusable component" — with real names and `aria-controls`, so a
344
+ * screen-reader cursor still reaches them. Their glyphs are the `Glyph`
345
+ * element the binding renders: a character is never an icon.
346
+ */
347
+ [data-ui='number-field'] > [data-part='stepper'] > button,
348
+ [data-ui='number-field'] > [data-part='decrement'],
349
+ [data-ui='number-field'] > [data-part='increment'] {
350
+ align-items: center;
351
+ appearance: none;
352
+ background-color: transparent;
353
+ border: 0;
354
+ color: var(--slotted-number-field-stepper-color, var(--slotted-text-muted, GrayText));
355
+ cursor: pointer;
356
+ display: inline-flex;
357
+ flex: 0 0 auto;
358
+ justify-content: center;
359
+ padding: 0;
360
+ /* The containing block of the hit area recovered below. */
361
+ position: relative;
362
+ /* No tap delay waiting for a second tap the button cannot use. */
363
+ touch-action: manipulation;
364
+ transition:
365
+ background-color var(--slotted-control-transition-duration)
366
+ var(--slotted-control-transition-easing),
367
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
368
+ -webkit-tap-highlight-color: transparent;
369
+ }
370
+
371
+ /* The `icon` box inside each button is drawn at the control's own icon
372
+ step rather than at the icon family's default. */
373
+ [data-ui='number-field'] [data-part='decrement'],
374
+ [data-ui='number-field'] [data-part='increment'] {
375
+ --slotted-icon-size: var(--_icon-size);
376
+ }
377
+
378
+ /**
379
+ * Stacked, the marks are chevrons: two halves of one column, pointing at the
380
+ * direction the value moves. The binding draws the down chevron in both, and
381
+ * the up one is that artwork turned half a turn — the glyph scale does not
382
+ * carry two drawings of one shape.
383
+ *
384
+ * Split, they are the minus and the plus — one pair on one grid. A chevron
385
+ * pointing along the inline axis would read as "previous" and "next" and
386
+ * would have to be mirrored in a right-to-left document; less and more do not
387
+ * change with the reading direction.
388
+ */
389
+ [data-ui='number-field'][data-stepper='stacked'] [data-part='increment'] > [data-ui='icon'] {
390
+ rotate: 0.5turn;
391
+ }
392
+
393
+ /* ── Stacked: one column at the inline end ────────────────────────────
394
+ *
395
+ * The column stands `--_column-inset` inside the frame's edge, in the block
396
+ * axis and at the inline end, with no rule of its own: each half paints a
397
+ * ground concentric with the frame when it is hovered, pressed or at its
398
+ * bound, and that ground is the only edge a half needs. The two lines the
399
+ * column drew before separated nothing — the value, the column and its
400
+ * halves are one surface. The paint gives the inset back to the pointer:
401
+ * each half's hit area reaches the frame below.
402
+ */
403
+ [data-ui='number-field'][data-part='root'][data-stepper='stacked'] {
404
+ padding-inline-end: 0;
405
+ }
406
+
407
+ [data-ui='number-field'] > [data-part='stepper'] {
408
+ align-self: stretch;
409
+ box-sizing: content-box;
410
+ display: flex;
411
+ flex: 0 0 auto;
412
+ flex-direction: column;
413
+ inline-size: var(--_stepper-size);
414
+ padding-block: var(--_column-inset);
415
+ padding-inline-end: var(--_column-inset);
416
+ }
417
+
418
+ [data-ui='number-field'] > [data-part='stepper'] > button {
419
+ /* Two halves of the column, whatever the frame's height turns out to be. */
420
+ block-size: 50%;
421
+ border-radius: var(--_column-radius);
422
+ inline-size: 100%;
423
+ }
424
+
425
+ [data-ui='number-field']
426
+ > [data-part='stepper']
427
+ > :is([data-part='increment'], [data-part='decrement'])::after {
428
+ content: '';
429
+ inset-inline: 0 calc(var(--_column-inset) * -1);
430
+ position: absolute;
431
+ }
432
+
433
+ [data-ui='number-field'] > [data-part='stepper'] > [data-part='increment']::after {
434
+ inset-block: calc(var(--_column-inset) * -1) 0;
435
+ }
436
+
437
+ [data-ui='number-field'] > [data-part='stepper'] > [data-part='decrement']::after {
438
+ inset-block: 0 calc(var(--_column-inset) * -1);
439
+ }
440
+
441
+ /**
442
+ * The glyph box is the control's own, bounded by the half that holds it.
443
+ *
444
+ * A chevron's artwork occupies about a quarter of its box in the block axis,
445
+ * so a box shrunk to leave visible air left a mark three pixels tall beside a
446
+ * minus and a plus drawn at full size — the two arrangements read as two
447
+ * different families. The box now fills the half and the artwork supplies its
448
+ * own air; `min()` keeps it inside the half at the tightest step, where the
449
+ * whole control is 24 pixels tall.
450
+ *
451
+ * Size containment keeps the drawing out of the column's intrinsic height.
452
+ * While the frame measures its content the half has no height yet, so the
453
+ * percentage falls back to the `<svg>`'s own square, and two of them plus the
454
+ * divider stood a pixel taller than the value and grew the frame.
455
+ */
456
+ [data-ui='number-field'] > [data-part='stepper'] > button > [data-ui='icon'] {
457
+ block-size: min(var(--_icon-size), 100%);
458
+ contain: size;
459
+ }
460
+
461
+ /* ── Split: one button at each end ────────────────────────────────────
462
+ *
463
+ * The quantity picker's arrangement. Each button is a square inner control
464
+ * inset concentrically, and the frame's padding is that inset on both
465
+ * sides; the value takes the gap as its own inset and sits centred between
466
+ * the two buttons, which is where a quantity belongs. No rule divides a
467
+ * button from the value: the ground it paints is its edge.
468
+ */
469
+ [data-ui='number-field'][data-part='root'][data-stepper='split'] {
470
+ gap: 0;
471
+ padding-inline: var(--_inset);
472
+ }
473
+
474
+ [data-ui='number-field'][data-stepper='split'] > [data-part='decrement'],
475
+ [data-ui='number-field'][data-stepper='split'] > [data-part='increment'] {
476
+ block-size: var(--_action-size);
477
+ border-radius: var(--_action-radius);
478
+ inline-size: var(--_action-size);
479
+ }
480
+
481
+ [data-ui='number-field'][data-stepper='split'] > [data-part='input'] {
482
+ padding-inline: var(--_gap);
483
+ text-align: center;
484
+ }
485
+
486
+ [data-ui='number-field'][data-stepper='split'] > [data-part='start'] {
487
+ padding-inline-start: var(--_gap);
488
+ }
489
+
490
+ [data-ui='number-field'][data-stepper='split'] > [data-part='end'] {
491
+ padding-inline-end: var(--_gap);
492
+ }
493
+
494
+ /**
495
+ * The hit area, recovered outside the paint. At `sm` with `dense` the whole
496
+ * control is 24 pixels tall, so a button bounded by the frame cannot also be
497
+ * 24 in both axes. This grows the target to the minimum and leaves a larger
498
+ * one alone — the correction `docs/token-foundation.md` leaves to the family.
499
+ *
500
+ * Only the split arrangement takes it. Two stacked halves cannot both grow
501
+ * to 24 pixels inside a 34-pixel control without meeting in the middle, and a
502
+ * boundary strip that belongs to whichever button paints last is worse than a
503
+ * small target. That shortfall is the arrangement's own, and `split` is the
504
+ * answer to it.
505
+ */
506
+ [data-ui='number-field'][data-stepper='split'] > [data-part='decrement']::after,
507
+ [data-ui='number-field'][data-stepper='split'] > [data-part='increment']::after {
508
+ content: '';
509
+ inset: min(0px, calc((100% - var(--slotted-target-min, 24px)) / 2));
510
+ position: absolute;
511
+ }
512
+
513
+ /**
514
+ * On a finger the target is the comfortable size. The paint stays the inset
515
+ * square a mouse sees; only the hit area grows.
516
+ *
517
+ * Split, each button's hit area reaches 44 in both axes.
518
+ *
519
+ * Stacked, the two halves of one column cannot each be 44 tall inside a
520
+ * 44-pixel frame. Each keeps its own half of the column in the block axis —
521
+ * reaching the frame's edge, as on a fine pointer — and reaches 44 in the
522
+ * inline axis, over the value's edge and past the frame's.
523
+ */
524
+ @media (pointer: coarse) {
525
+ [data-ui='number-field'][data-stepper='split']
526
+ > :is([data-part='decrement'], [data-part='increment'])::after {
527
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
528
+ }
529
+
530
+ [data-ui='number-field']
531
+ > [data-part='stepper']
532
+ > :is([data-part='increment'], [data-part='decrement'])::after {
533
+ inset-inline: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
534
+ }
535
+ }
536
+
537
+ /* ── Stepper states ───────────────────────────────────────────────────
538
+ *
539
+ * Written strongest last, and each weaker one guarded against the stronger
540
+ * ones it can meet, which is the order `STATE_PRECEDENCE` sets.
541
+ */
542
+
543
+ [data-ui='number-field'] [data-part='decrement']:hover:not(:disabled),
544
+ [data-ui='number-field'] [data-part='increment']:hover:not(:disabled) {
545
+ background-color: var(
546
+ --slotted-number-field-stepper-hover-background,
547
+ var(--slotted-tone-secondary-subtle-hover)
548
+ );
549
+ }
550
+
551
+ [data-ui='number-field'] [data-part='decrement']:active:not(:disabled),
552
+ [data-ui='number-field'] [data-part='increment']:active:not(:disabled) {
553
+ background-color: var(
554
+ --slotted-number-field-stepper-active-background,
555
+ var(--slotted-tone-secondary-subtle-active)
556
+ );
557
+ }
558
+
559
+ /**
560
+ * A stepper at its bound is disabled rather than silently inert, so its name
561
+ * still announces and its appearance says why nothing happens. It is also
562
+ * what a disabled or read-only control's steppers look like: the family keeps
563
+ * drawing them, because a number field without its stepper is a different
564
+ * component and a disabled form still has to say what it holds.
565
+ */
566
+ [data-ui='number-field'] [data-part='decrement']:disabled,
567
+ [data-ui='number-field'] [data-part='increment']:disabled {
568
+ /**
569
+ * The ground as well as the ink. The disabled foreground and the resting
570
+ * stepper colour are two steps of one neutral and differ by about a shade,
571
+ * so a stepper at its bound painted only in ink read as available — the
572
+ * capture of the `stepper` scene showed a decrement at nought that nothing
573
+ * distinguished from the one above it. The switched-off segment says it in
574
+ * the ground, which is how the frame says the same word.
575
+ */
576
+ background-color: var(
577
+ --slotted-number-field-stepper-disabled-background,
578
+ var(--slotted-disabled-background)
579
+ );
580
+ color: var(--slotted-number-field-stepper-disabled-color, var(--slotted-disabled-foreground));
581
+ cursor: not-allowed;
582
+ }
583
+
584
+ /**
585
+ * The steppers are out of the tab order, so this answers the pointer-lock
586
+ * path on touch and a screen-reader cursor activating one directly. It sits
587
+ * inside the frame's own edge, so the ring never doubles the border.
588
+ */
589
+ [data-ui='number-field'] [data-part='decrement']:focus-visible,
590
+ [data-ui='number-field'] [data-part='increment']:focus-visible {
591
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
592
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
593
+ }
594
+
595
+ /* ── Frame states ─────────────────────────────────────────────────────── */
596
+
597
+ [data-ui='number-field'][data-part='root']:hover:not([data-disabled]):not([data-readonly]):not(
598
+ [data-invalid]
599
+ ) {
600
+ border-color: var(--slotted-number-field-hover-border-color, var(--slotted-tone-accent-border));
601
+ }
602
+
603
+ [data-ui='number-field'][data-part='root'][data-variant='subtle']:hover:not([data-disabled]):not(
604
+ [data-readonly]
605
+ ):not([data-invalid]) {
606
+ background-color: var(
607
+ --slotted-number-field-subtle-hover-background,
608
+ var(--slotted-tone-secondary-subtle-hover)
609
+ );
610
+ }
611
+
612
+ [data-ui='number-field'][data-part='root'][data-variant='plain']:hover:not([data-disabled]):not(
613
+ [data-readonly]
614
+ ):not([data-invalid]) {
615
+ background-color: var(
616
+ --slotted-number-field-plain-hover-background,
617
+ var(--slotted-tone-secondary-subtle-hover)
618
+ );
619
+ /* The accent edge the other variants take would draw the cell's outline. */
620
+ border-color: transparent;
621
+ }
622
+
623
+ /**
624
+ * The ring belongs to the frame and answers the control's focus: a browser
625
+ * matches `:focus-visible` on a text input for a pointer press as well as for
626
+ * a Tab, which is right for a control whose caret is the thing that moved.
627
+ */
628
+ [data-ui='number-field'][data-part='root']:has(> [data-part='input']:focus-visible) {
629
+ /* The resting shadow, recoloured: the lift under the control and the ring
630
+ around it are one colour, and the state decides which colour that is. */
631
+ box-shadow: var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
632
+ var(--slotted-focus-ring-color, Highlight);
633
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
634
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
635
+ }
636
+
637
+ /**
638
+ * The ring of a control whose box is not its own is drawn inside that box,
639
+ * the way the inner controls draw theirs inside the frame: two pixels past
640
+ * the control is two pixels over the next row's rule. No lift either — a
641
+ * shadow that appears only under focus is a cell that changes size when it
642
+ * is used.
643
+ */
644
+ [data-ui='number-field'][data-part='root'][data-variant='plain']:has(
645
+ > [data-part='input']:focus-visible
646
+ ) {
647
+ box-shadow: none;
648
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
649
+ }
650
+
651
+ /**
652
+ * Invalid outranks hover and is written after it for that reason. The ring is
653
+ * retargeted rather than repainted: every rule that draws focus reads
654
+ * `--slotted-focus-ring-color`, so pointing that one name at the invalid
655
+ * colour on the element moves the outline, the shadow and both steppers'
656
+ * rings at once.
657
+ */
658
+ [data-ui='number-field'][data-part='root'][data-invalid] {
659
+ --slotted-focus-ring-color: var(
660
+ --slotted-number-field-invalid-focus-ring-color,
661
+ var(--slotted-focus-ring-color-invalid, Highlight)
662
+ );
663
+ --_invalid-edge-width: var(
664
+ --slotted-number-field-invalid-edge-width,
665
+ var(--slotted-control-invalid-edge-width, 1px)
666
+ );
667
+ --_invalid-edge-color: var(
668
+ --slotted-number-field-invalid-border-color,
669
+ var(--slotted-tone-danger-border)
670
+ );
671
+ --_invalid-edge: inset 0 0 0 var(--_invalid-edge-width) var(--_invalid-edge-color);
672
+
673
+ border-color: var(
674
+ --slotted-number-field-invalid-border-color,
675
+ var(--slotted-tone-danger-border)
676
+ );
677
+ /*
678
+ * Width as well as colour (WCAG 1.4.1), the edge `text-field` draws: in
679
+ * `high-contrast` the danger border is the resting edge's black. The line
680
+ * is a shadow inside the border, so neither box nor text moves, and it
681
+ * takes the resting lift's place.
682
+ */
683
+ box-shadow: var(--_invalid-edge);
684
+ }
685
+
686
+ [data-ui='number-field'][data-part='root'][data-invalid]:has(
687
+ > [data-part='input']:focus-visible
688
+ ) {
689
+ box-shadow:
690
+ var(--_invalid-edge),
691
+ var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
692
+ var(--slotted-focus-ring-color, Highlight);
693
+ }
694
+
695
+ [data-ui='number-field'][data-part='root'][data-variant='plain'][data-invalid]:has(
696
+ > [data-part='input']:focus-visible
697
+ ) {
698
+ /* The ring lies inside the box here, over the border and an edge as wide as
699
+ it; the edge reaches past the ring. */
700
+ box-shadow: inset 0 0 0
701
+ calc(
702
+ var(--slotted-focus-ring-offset, 2px) - var(--slotted-control-border-width, 1px) +
703
+ var(--_invalid-edge-width)
704
+ )
705
+ var(--_invalid-edge-color);
706
+ }
707
+
708
+ /*
709
+ * Forced colours remove every box shadow, and the edge with them. The
710
+ * width comes back as an outline inside the border, and a focused invalid
711
+ * control draws its ring double, which a valid ring is not.
712
+ */
713
+ @media (forced-colors: active) {
714
+ [data-ui='number-field'][data-part='root'][data-invalid] {
715
+ outline: var(--_invalid-edge-width) solid CanvasText;
716
+ outline-offset: calc(
717
+ -1 * (var(--slotted-control-border-width, 1px) + var(--_invalid-edge-width))
718
+ );
719
+ }
720
+
721
+ [data-ui='number-field'][data-part='root'][data-invalid]:has(
722
+ > [data-part='input']:focus-visible
723
+ ) {
724
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
725
+ var(--slotted-focus-ring-color, Highlight);
726
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
727
+ }
728
+
729
+ [data-ui='number-field'][data-part='root'][data-variant='plain'][data-invalid]:has(
730
+ > [data-part='input']:focus-visible
731
+ ) {
732
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
733
+ var(--slotted-focus-ring-color, Highlight);
734
+ outline-offset: calc(
735
+ -1 * (var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width))
736
+ );
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Read-only stays focusable, selectable and legible, and must not be
742
+ * mistakable for disabled: the value is still real and still submitted. The
743
+ * ground says "not yours to change" and the ink says nothing at all.
744
+ */
745
+ [data-ui='number-field'][data-part='root'][data-readonly] {
746
+ background-color: var(
747
+ --slotted-number-field-readonly-background,
748
+ var(--slotted-tone-secondary-subtle-hover)
749
+ );
750
+ /**
751
+ * The edge as well as the ground. In the dark scheme the subtle ground sits
752
+ * within a couple of steps of the surface, and the state's capture showed a
753
+ * read-only control that nothing distinguished from the one above it —
754
+ * worse than looking like disabled, which is the failure the rule was
755
+ * written against. The control's edge stepping down to the separator weight
756
+ * is visible in every scheme, and it is not the disabled edge: disabled
757
+ * keeps its own border and mutes the ink, while read-only keeps the ink at
758
+ * full strength because the value is real and still submitted.
759
+ */
760
+ border-color: var(
761
+ --slotted-number-field-readonly-border-color,
762
+ var(--slotted-border-subtle, ButtonBorder)
763
+ );
764
+ cursor: default;
765
+ }
766
+
767
+ /**
768
+ * `:disabled` on the control is matched beside `data-disabled` on the frame,
769
+ * because a control can be disabled by something that never told it so: a
770
+ * disabled `fieldset` disables every control inside it, and the control has
771
+ * no way to know.
772
+ */
773
+ [data-ui='number-field'][data-part='root'][data-disabled],
774
+ [data-ui='number-field'][data-part='root']:has(> [data-part='input']:disabled) {
775
+ background-color: var(
776
+ --slotted-number-field-disabled-background,
777
+ var(--slotted-disabled-background)
778
+ );
779
+ border-color: var(--slotted-number-field-disabled-border-color, var(--slotted-disabled-border));
780
+ /* Nothing a person cannot reach is lifted off the page. */
781
+ box-shadow: none;
782
+ color: var(--slotted-number-field-disabled-color, var(--slotted-disabled-foreground));
783
+ cursor: not-allowed;
784
+ }
785
+
786
+ /**
787
+ * Muted to the colour the theme measures rather than faded by opacity: a
788
+ * value nobody can read is a field nobody can check.
789
+ */
790
+ [data-ui='number-field'][data-part='root'][data-disabled] > [data-part='start'],
791
+ [data-ui='number-field'][data-part='root'][data-disabled] > [data-part='end'] {
792
+ color: var(--slotted-disabled-foreground);
793
+ }
794
+ }