@slotted/styles 0.0.0-stage → 0.1.0-beta.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 (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,1427 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── A frame of chosen values, with a field that adds more ───────────────
4
+ *
5
+ * The family is two subtrees, as `select`'s and `combobox`'s are: the
6
+ * **control**, where the consumer wrote it, and the **popup**, portalled to
7
+ * the end of the document so it escapes every clipping ancestor between them.
8
+ * Nothing selects one from the other, so `data-ui='multi-select'` is written
9
+ * on both roots — the contract records it under `portalledIdentity`.
10
+ *
11
+ * ┌─ root ─ the frame ─────────────────────────────────────────┐
12
+ * │ ┌─ control ─ role="grid" ───────────────┐ │
13
+ * │ │ ┌─ chips ─ role="row" ─ wraps ──────┐ │ counter │
14
+ * │ │ │ tag ▸ tag ▸ tag │ │ clear trigger │
15
+ * │ │ │ field ▸ input ─ role="combobox" │ │ ▸ arrow
16
+ * │ │ └───────────────────────────────────┘ │ │
17
+ * │ └───────────────────────────────────────┘ │
18
+ * └────────────────────────────────────────────────────────────┘
19
+ *
20
+ * ┌─ positioner ─ the layer ───────────────────────────────────┐
21
+ * │ ┌─ content ─ role="listbox" ─ scrolls ───────────────┐ │
22
+ * │ │ chosen ─ every value, however the row overflowed │ │
23
+ * │ │ group ▸ group-label │ │
24
+ * │ │ item ▸ icon │ label ▸ match │ indicator │ │
25
+ * │ │ description │ │
26
+ * │ │ separator · empty · status · placeholder │ │
27
+ * │ └────────────────────────────────────────────────────┘ │
28
+ * └────────────────────────────────────────────────────────────┘
29
+ *
30
+ * ── What this sheet does not decide ──────────────────────────────────
31
+ *
32
+ * **Nothing about a chip.** The chips are `tag`'s, worn as `data-ui` beside
33
+ * this family's own parts, so the pill, the small step, and the remove
34
+ * control floored at `--slotted-target-min` are all stated once in
35
+ * `tag/tag.css`. This sheet decides only where the row is and how it wraps.
36
+ *
37
+ * **Nothing about the mark on a matched span.** That is
38
+ * `highlight/highlight.css`, worn as `data-ui='highlight'` beside the `match`
39
+ * part, because every list a reader types into says the same thing about the
40
+ * same span. `combobox` recorded the metric behind the decision.
41
+ *
42
+ * **The popup.** The bounded scrolling surface, the three columns, the
43
+ * subgrid, the check in the last column, the highlight on `data-current` and
44
+ * the two-line label clamp are `select`'s decisions, taken in an earlier
45
+ * session. They are repeated here rather than shared because the three
46
+ * families have three public identifiers, and a stylesheet that painted
47
+ * `[data-ui='select']` from a multi-select would make one family's selector
48
+ * match another's DOM. The duplication is the price of that boundary.
49
+ *
50
+ * ── What this sheet decides on its own ───────────────────────────────
51
+ *
52
+ * **The control grows by rows, and by nothing else.** It is the first family
53
+ * here whose height is not a token: an empty control is exactly the height of
54
+ * a text field beside it on the same form, and every wrapped row of chips
55
+ * adds one chip's height and one gap. Three rows is where it stops growing
56
+ * and starts scrolling — past that it has stopped being a field.
57
+ *
58
+ * **The chips wrap inside the frame, beside the caret.** `select` puts its
59
+ * row *above* its control because its control is a `button` and a button
60
+ * cannot contain the remove button a chip needs. This control is a `div`
61
+ * holding a real `input`, so the chips sit where every surveyed
62
+ * implementation draws them, and `Backspace` can reach them.
63
+ *
64
+ * **Under 120 pixels there is no chip.** A chip narrower than a word is a
65
+ * shape, so the counter answers instead. The width is watched by
66
+ * `createWidthGate` and reported as `data-narrow`, not by a container query:
67
+ * `container-type: inline-size` would stop the frame's own width depending on
68
+ * its contents, which is exactly how a control that is not `fullWidth` is
69
+ * measured. `select` recorded the same finding.
70
+ */
71
+
72
+ /* ── The frame ────────────────────────────────────────────────────────── */
73
+
74
+ [data-ui='multi-select'][data-part='root'] {
75
+ --_font-size: var(--slotted-control-font-size-md, 13px);
76
+ --_height: var(--slotted-control-height-md, 34px);
77
+ --_padding-inline: var(--slotted-control-padding-inline-md, 13px);
78
+ --_gap: var(--slotted-control-gap-md, 8px);
79
+ /** The floor under the glyph scale at the dense step; a family token so a theme can retune it. */
80
+ --_icon-floor: var(--slotted-multi-select-icon-size-min, 14px);
81
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
82
+
83
+ /* `text-field`'s arithmetic, because these are the same buttons in the same
84
+ frame and a second answer would drift from the first. */
85
+ --_action-size: var(
86
+ --slotted-multi-select-action-size,
87
+ min(calc(var(--_icon-size) + 2 * var(--slotted-space-2, 0.25rem)), var(--_inner-size))
88
+ );
89
+
90
+ /**
91
+ * The air above and below a row of chips: a space step, kept at every
92
+ * density. It is the chip that yields, below — not the air — so a control
93
+ * holding one row of chips is the height of an empty one beside it and the
94
+ * chip never touches the frame.
95
+ *
96
+ * The first answer sized the row at the target minimum and let the air
97
+ * give way, which stood the chip edge to edge inside a `dense` frame and
98
+ * two pixels taller than an `sm` one; it was recorded as the honest price
99
+ * of a chip at the tightest step, and the reviewer refused it at
100
+ * `tags-input`'s release gate. The geometry below is `tags-input.css`'s.
101
+ */
102
+ --_row-air: var(--slotted-space-1, 0.125rem);
103
+ /**
104
+ * One row's height is the height the foundation gives the inside of a
105
+ * control at this size step, `--slotted-control-inner-size-*`: the chip
106
+ * takes it through the property the row writes below, so a `dense` frame
107
+ * of 26 pixels holds an 18-pixel chip with four pixels of air a side, and
108
+ * three rows plus their gaps divide the cap below with no sliver of a
109
+ * fourth. The type stays 12px; the chip's line and inset are derived from
110
+ * the height in `tag.css`, and the remove control keeps its 24-pixel target
111
+ * through `tag`'s own halo.
112
+ */
113
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
114
+ --_row-size: var(--_inner-size);
115
+ --_row-gap: var(--slotted-space-2, 0.25rem);
116
+
117
+ align-items: center;
118
+ background-color: var(--slotted-multi-select-background, var(--slotted-control-surface, Field));
119
+ border: var(--slotted-control-border-width, 1px) solid
120
+ var(--slotted-multi-select-border-color, var(--slotted-control-border-color, ButtonBorder));
121
+ border-radius: var(--slotted-multi-select-radius, var(--slotted-control-radius, 4px));
122
+ box-shadow: var(--slotted-multi-select-shadow, var(--slotted-control-shadow, none));
123
+ box-sizing: border-box;
124
+ color: var(--slotted-multi-select-color, var(--slotted-text, CanvasText));
125
+ display: flex;
126
+ font-family: var(--slotted-control-font-family, system-ui, sans-serif);
127
+ font-size: var(--_font-size);
128
+ gap: var(--slotted-space-1, 0.125rem);
129
+ inline-size: 100%;
130
+ min-block-size: var(--_height);
131
+ min-inline-size: 0;
132
+ padding-inline-start: var(--_padding-inline);
133
+ /* The end padding is the buttons' gutter rather than the frame's, so the
134
+ glyph — not the button's box — ends where the text begins on the other
135
+ side. `text-field` states the same arithmetic. */
136
+ padding-inline-end: max(
137
+ 0px,
138
+ calc(var(--_padding-inline) - (var(--_action-size) - var(--_icon-size)) / 2)
139
+ );
140
+ transition:
141
+ background-color var(--slotted-control-transition-duration)
142
+ var(--slotted-control-transition-easing),
143
+ border-color var(--slotted-control-transition-duration)
144
+ var(--slotted-control-transition-easing),
145
+ box-shadow var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
146
+ }
147
+
148
+ [data-ui='multi-select'][data-part='root']:not([data-full-width]) {
149
+ inline-size: auto;
150
+ }
151
+
152
+ /**
153
+ * The grid. It holds one row and gives up every visual decision to it: the
154
+ * role is what the layout-grid pattern asks for, and the geometry belongs to
155
+ * the row that wraps.
156
+ */
157
+ [data-ui='multi-select'] > [data-part='control'] {
158
+ display: flex;
159
+ flex: 1 1 auto;
160
+ min-inline-size: 0;
161
+ /* The frame stretches to the row; the row is what has a height. */
162
+ align-self: stretch;
163
+ }
164
+
165
+ /**
166
+ * The distance between the values and the controls that act on them. The
167
+ * frame's own gap is the tight one the action buttons want between each
168
+ * other, and a chip has a hard edge where an input's text has none: the last
169
+ * chip stood two pixels from the clear button while the chips stood four
170
+ * pixels from each other, so the boundary between what is chosen and what
171
+ * acts on it was narrower than the rhythm inside it. Whichever part comes
172
+ * first after the row takes the row's own gap.
173
+ */
174
+ [data-ui='multi-select'] > [data-part='control'] + * {
175
+ margin-inline-start: var(--_row-gap);
176
+ }
177
+
178
+ /**
179
+ * The row. One DOM row that wraps onto as many visual lines as it needs,
180
+ * which is what keeps `ArrowDown` and `ArrowUp` free for the popup: there is
181
+ * no cell above or below any cell, so the grid answers neither key.
182
+ */
183
+ [data-ui='multi-select'] [data-part='chips'] {
184
+ /**
185
+ * `safe`, because this box scrolls. Centring a scroll container's content
186
+ * pushes the first line above the scrollport when there is more than fits,
187
+ * and the browser gives no way to scroll back to it: the ninth chip was on
188
+ * screen and the first three were unreachable at 240 pixels. `safe` falls
189
+ * back to `start` exactly when the content overflows, which is the one case
190
+ * where centring is wrong.
191
+ */
192
+ align-content: safe center;
193
+ align-items: center;
194
+ /* The cap below counts rows, gaps and the row's own air, so it has to be a
195
+ border box: against a content box the same number left four pixels of a
196
+ fourth row showing under the third. */
197
+ box-sizing: border-box;
198
+ column-gap: var(--_row-gap);
199
+ display: flex;
200
+ flex: 1 1 auto;
201
+ flex-wrap: wrap;
202
+ min-inline-size: 0;
203
+ padding-block: var(--_row-air);
204
+ /**
205
+ * The row scrolls, and a box that scrolls on one axis clips on both. A
206
+ * chip's remove control takes the rest of its own target from a transparent
207
+ * halo that stands outside the chip (TG2), so the row keeps two pixels of
208
+ * inline padding for it and takes them back with a negative margin: the
209
+ * chips stand exactly where the frame's gutter puts them, and the halo of
210
+ * the first and last is inside the scrollport rather than against its edge.
211
+ */
212
+ padding-inline: var(--_row-air);
213
+ margin-inline: calc(-1 * var(--_row-air));
214
+ row-gap: var(--_row-gap);
215
+ /**
216
+ * Three rows, then it scrolls. Three *whole* rows: a chip drawn to half its
217
+ * height is not an affordance, it is a chip that looks broken. Every chip
218
+ * stays in the DOM either way, which is what the feature guide asks of an
219
+ * overflow — the set is summarised, never hidden.
220
+ */
221
+ max-block-size: calc(3 * var(--_row-size) + 2 * var(--_row-gap) + 2 * var(--_row-air));
222
+ overflow-y: auto;
223
+ overscroll-behavior: contain;
224
+ /**
225
+ * The row scrolls and does not draw a scrollbar. A form control has one
226
+ * frame and a bar inside that frame reads as a panel that got into a field:
227
+ * it takes width from the chips, moves them when it appears, and paints a
228
+ * second edge inside the control's own. The bar is refused, not the
229
+ * scrolling — the wheel, the trackpad and every arrow key still reach the
230
+ * rows above and below, because a chip is focusable and the browser scrolls
231
+ * focus into view.
232
+ *
233
+ * What says there is more is not the bar. It is the counter beside the row,
234
+ * the row's own `aria-label` carrying the count, and the band at the top of
235
+ * the popup, which holds every chosen value whatever this row did with them.
236
+ * A reader who cannot see all the chips has three places that say how many
237
+ * there are; a reader who could see a scrollbar would have four, and one of
238
+ * them would be inside a field.
239
+ */
240
+ scrollbar-width: none;
241
+ }
242
+
243
+ [data-ui='multi-select'] [data-part='chips']::-webkit-scrollbar {
244
+ display: none;
245
+ }
246
+
247
+ /**
248
+ * One line high, whatever is in it. For a control in a table row or a filter
249
+ * bar, where the frame cannot be allowed to grow: what does not fit is
250
+ * collapsed by `createCollapse` into the counting chip below, and the whole
251
+ * set is still reachable in the popup's `chosen` band.
252
+ */
253
+ [data-ui='multi-select'][data-overflow='collapse'] [data-part='chips'] {
254
+ flex-wrap: nowrap;
255
+ max-block-size: none;
256
+ overflow: hidden;
257
+ }
258
+
259
+ /**
260
+ * A chip being edited. The grid pattern's own answer to a cell holding
261
+ * editable content is a text field inside the cell, so that is what it is —
262
+ * and it gives up every mark of a field, because the chip around it is
263
+ * already the box. It takes the chip's own type and ink so the word does not
264
+ * move when editing begins.
265
+ *
266
+ * Selected by element rather than by part: the field belongs to the chip and
267
+ * not to this family, so it has no `data-part` of its own to be selected by,
268
+ * and the query's own input is told apart by the part it does have.
269
+ */
270
+ [data-ui='multi-select'] [data-part='chips'] input:not([data-part='input']) {
271
+ background-color: transparent;
272
+ border: 0;
273
+ color: inherit;
274
+ font: inherit;
275
+ min-inline-size: 0;
276
+ outline: none;
277
+ padding: 0;
278
+ }
279
+
280
+ /**
281
+ * A chip never shrinks. Flexbox shrinks before it wraps when it is allowed
282
+ * to, and at 240 pixels that turned a run of chips into `B.. ×` beside a
283
+ * remove button with nothing left to remove. A chip either fits on the line
284
+ * or goes to the next one; the only chip that gives way is one wider than the
285
+ * whole row, which is capped here and truncated by `tag.css`.
286
+ */
287
+ [data-ui='multi-select'] [data-part='chips'] > :not([data-part='field']) {
288
+ flex: 0 0 auto;
289
+ max-inline-size: 100%;
290
+ /* The row's own height, so three chips plus their gaps divide the cap the
291
+ row above states and no fourth row shows as a sliver under the third. */
292
+ min-block-size: var(--_row-size);
293
+ min-inline-size: 0;
294
+ }
295
+
296
+ /**
297
+ * The chip takes the height the inside of this control has, through the
298
+ * property `tag` reads for it. Set on the row, inherited by every chip, and
299
+ * no rule of this sheet names `[data-ui='tag']`: the host stands beside the
300
+ * tag sheet and never reaches into it.
301
+ */
302
+ [data-ui='multi-select'] [data-part='chips'] {
303
+ --slotted-control-inner-size: var(--_inner-size);
304
+ }
305
+
306
+ /**
307
+ * The cell that holds the caret. It is the last cell of the row, so it wraps
308
+ * with the chips rather than being pushed to a line of its own, and it asks
309
+ * for a word's worth of width so the reader always has somewhere to type.
310
+ *
311
+ * It asks rather than demands: the basis is a word and the floor is a couple
312
+ * of characters, because a cell that insisted on a word sent itself to a line
313
+ * of its own the moment two chips were in front of it, and a control 240
314
+ * pixels wide holding two values then stood two rows tall with one row empty.
315
+ */
316
+ [data-ui='multi-select'] [data-part='field'] {
317
+ align-items: center;
318
+ display: flex;
319
+ flex: 1 1 var(--slotted-multi-select-field-basis, 4rem);
320
+ min-block-size: var(--_row-size);
321
+ min-inline-size: var(--slotted-multi-select-field-min, 2.5rem);
322
+ }
323
+
324
+ /**
325
+ * The control's text. It gives up its own frame entirely and keeps
326
+ * everything that makes it a text control: the caret, the selection, IME
327
+ * composition, autofill and every native attribute the consumer wrote.
328
+ */
329
+ [data-ui='multi-select'] [data-part='input'] {
330
+ background-color: transparent;
331
+ border: 0;
332
+ color: inherit;
333
+ flex: 1 1 auto;
334
+ font: inherit;
335
+ /* The frame draws the ring for it, so two rings never nest. */
336
+ outline: none;
337
+ padding: 0;
338
+ /* The intrinsic width of an `input` is about twenty characters, and a flex
339
+ item refuses to shrink below it without this. */
340
+ min-inline-size: 0;
341
+ text-overflow: ellipsis;
342
+ }
343
+
344
+ [data-ui='multi-select'] [data-part='input']::placeholder {
345
+ color: var(--slotted-multi-select-placeholder-color, var(--slotted-text-muted, GrayText));
346
+ text-overflow: ellipsis;
347
+ }
348
+
349
+ /**
350
+ * Autofill. Chrome paints its own ground over the control's, and a
351
+ * hundred-pixel inset shadow is the only way to reach it. The signal is kept
352
+ * rather than erased, in the accent surface the theme owns — the repair
353
+ * `text-field` makes, for the same reason.
354
+ */
355
+ [data-ui='multi-select'] [data-part='input']:autofill {
356
+ box-shadow: inset 0 0 0 100px
357
+ var(--slotted-multi-select-autofill-background, var(--slotted-tone-accent-surface));
358
+ -webkit-text-fill-color: var(--slotted-multi-select-color, var(--slotted-text, CanvasText));
359
+ }
360
+
361
+ /**
362
+ * A chip that holds focus. The cell takes the ring rather than the remove
363
+ * button inside it — the departure the contract records against the grid
364
+ * pattern — so the ring is drawn around the value, which is the thing
365
+ * `Delete` acts on. It follows the chip's own radius by being an outline on
366
+ * the pill itself.
367
+ *
368
+ * The ring is drawn one width inside the chip, not at the theme's offset
369
+ * outside it. The row around the chip scrolls, or clips under
370
+ * `overflow='collapse'`, and either way it cuts at its padding edge, two
371
+ * pixels past the chip; a ring two pixels further out lost its outer half on
372
+ * every side, at every density. One width inside is `listbox`'s answer for an
373
+ * option in a scrolling list: the ring covers the chip's own edge and reads
374
+ * as the chip's edge turned to the ring colour, whole, with the text clear
375
+ * of it even at `dense`, where the chip has no inset to spare. `tag` draws
376
+ * its own inner ring two widths in to keep a sliver of ground outside it;
377
+ * here that sliver would cost the text its leading.
378
+ */
379
+ [data-ui='multi-select'] [data-part='chips'] > :not([data-part='field']):focus-visible {
380
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
381
+ outline-offset: calc(-1 * var(--slotted-focus-ring-width, 2px));
382
+ }
383
+
384
+ /**
385
+ * The chip that counts what did not fit, under `overflow='collapse'`. It is a
386
+ * chip and not a number beside them, so the row reads as one run of chips
387
+ * whose last one happens to be a count, and `createCollapse` measures it the
388
+ * way it measures every other unit.
389
+ */
390
+ [data-ui='multi-select'] [data-part='chips'] > [data-count] {
391
+ flex: none;
392
+ }
393
+
394
+ /**
395
+ * The count of what is held, against the limit that holds it. It is drawn
396
+ * only where it answers something — while `max` is set, or while the frame is
397
+ * too narrow for a chip — because a counter with nothing to count against is
398
+ * furniture.
399
+ */
400
+ [data-ui='multi-select'] > [data-part='counter'] {
401
+ text-box: auto; /* refine the alignment */
402
+ align-self: center;
403
+ color: var(--slotted-multi-select-counter-color, var(--slotted-text-muted, GrayText));
404
+ /* It gives way before the buttons do: under 120 pixels the counter, the
405
+ clear button and the trigger needed 127 pixels of a 112-pixel frame, and
406
+ the trigger ran past the border. The grid's aria-label carries the count
407
+ whole. */
408
+ flex: 0 1 auto;
409
+ font-size: var(--slotted-font-size-xs, 0.75rem);
410
+ min-inline-size: 0;
411
+ overflow: hidden;
412
+ text-overflow: ellipsis;
413
+ font-variant-numeric: tabular-nums;
414
+ line-height: var(--slotted-line-height-sm, 1.45);
415
+ padding-inline: var(--slotted-space-1, 0.125rem);
416
+ /* Its own bidi run, ordered by its own content: "3 of 5" is a European
417
+ number, a Latin word and another number, and in a right-to-left document
418
+ the neutral spaces resolve to the paragraph and the runs swap. The repair
419
+ `status`, `empty` and `select`'s summary all make. */
420
+ unicode-bidi: plaintext;
421
+ white-space: nowrap;
422
+ }
423
+
424
+ /**
425
+ * A full control says so where the count is, in the accent the family already
426
+ * uses for a chosen value. Not a warning tone: reaching the limit is the
427
+ * reader arriving somewhere, not going wrong.
428
+ */
429
+ [data-ui='multi-select'][data-complete] > [data-part='counter'] {
430
+ color: var(--slotted-multi-select-counter-complete-color, var(--slotted-tone-accent-text));
431
+ font-weight: var(--slotted-font-weight-medium, 500);
432
+ }
433
+
434
+ /* Too narrow for a chip: the count is the whole answer. */
435
+ [data-ui='multi-select'][data-narrow] [data-part='chips'] > :not([data-part='field']) {
436
+ display: none;
437
+ }
438
+
439
+ /* The row takes only the room the count and the buttons leave, so the count
440
+ is what shortens when even that room runs out. */
441
+ [data-ui='multi-select'][data-narrow] > [data-part='control'] {
442
+ flex-basis: 0;
443
+ }
444
+
445
+ /* ── The two buttons inside the frame ─────────────────────────────────── */
446
+
447
+ [data-ui='multi-select'] > :is([data-part='clear'], [data-part='trigger']) {
448
+ align-items: center;
449
+ align-self: center;
450
+ appearance: none;
451
+ background-color: transparent;
452
+ block-size: var(--_action-size);
453
+ border: 0;
454
+ border-radius: var(--slotted-multi-select-radius, var(--slotted-control-radius, 4px));
455
+ color: var(--slotted-multi-select-action-color, var(--slotted-text-muted, GrayText));
456
+ cursor: pointer;
457
+ display: inline-flex;
458
+ flex: 0 0 auto;
459
+ inline-size: var(--_action-size);
460
+ justify-content: center;
461
+ /* The user agent pads a button, and inside a box sized to its glyph that
462
+ padding squeezed the glyph to twelve pixels. */
463
+ padding: 0;
464
+ /* The containing block of the hit area below. */
465
+ position: relative;
466
+ touch-action: manipulation;
467
+ transition:
468
+ background-color var(--slotted-control-transition-duration)
469
+ var(--slotted-control-transition-easing),
470
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
471
+ -webkit-tap-highlight-color: transparent;
472
+ }
473
+
474
+ /**
475
+ * The hit area, recovered outside the paint. At `sm` with `dense` the whole
476
+ * control is 24 pixels tall — the WCAG 2.2 minimum — so nothing drawn inside
477
+ * it can also be 24.
478
+ */
479
+ [data-ui='multi-select'] > :is([data-part='clear'], [data-part='trigger'])::after {
480
+ content: '';
481
+ inset: min(0px, calc((100% - var(--slotted-target-min, 24px)) / 2));
482
+ position: absolute;
483
+ }
484
+
485
+ /* On a finger the target is the comfortable size in both axes and the box
486
+ is the one a mouse sees: the ground and the focus ring stay square, and
487
+ the hit area above grows to 44 pixels around them. Side by side, neither
488
+ button reaches across the edge the two share. */
489
+ @media (pointer: coarse) {
490
+ [data-ui='multi-select'] > :is([data-part='clear'], [data-part='trigger'])::after {
491
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
492
+ }
493
+
494
+ [data-ui='multi-select']
495
+ > [data-part='clear']:has(+ [data-part='trigger']:not([hidden]))::after {
496
+ inset-inline-end: 0;
497
+ }
498
+
499
+ [data-ui='multi-select'] > [data-part='clear']:not([hidden]) + [data-part='trigger']::after {
500
+ inset-inline-start: 0;
501
+ }
502
+ }
503
+
504
+ /* The cross and the chevron are `Glyph` elements the binding draws, sized
505
+ here to the control's icon step. */
506
+ [data-ui='multi-select'] > :is([data-part='clear'], [data-part='trigger']) {
507
+ --slotted-icon-size: var(--_icon-size);
508
+ }
509
+
510
+ /**
511
+ * The chevron, drawn by its own part rather than by the button, because the
512
+ * button is what a reader presses and the arrow is what turns over. The
513
+ * library's own `Glyph` in an `icon` box; a character is never an icon.
514
+ */
515
+ [data-ui='multi-select'] [data-part='arrow'] {
516
+ align-items: center;
517
+ block-size: var(--_icon-size);
518
+ display: flex;
519
+ inline-size: var(--_icon-size);
520
+ justify-content: center;
521
+ }
522
+
523
+ [data-ui='multi-select'] [data-part='arrow'] > [data-ui='icon'] {
524
+ transition: rotate var(--slotted-control-transition-duration)
525
+ var(--slotted-control-transition-easing);
526
+ }
527
+
528
+ [data-ui='multi-select'][data-open] [data-part='arrow'] > [data-ui='icon'] {
529
+ rotate: 180deg;
530
+ }
531
+
532
+ /**
533
+ * The ring, where the chevron stands. `select`'s decision and `select`'s
534
+ * reasons: while an answer is travelling the control shows the ring rather
535
+ * than the chevron — the chevron says the popup opens, and it does not open
536
+ * onto anything yet — and the arc is the one `spinner/spinner.css` draws for
537
+ * every family that waits.
538
+ */
539
+ [data-ui='multi-select'] [data-part='busy'] {
540
+ --slotted-spinner-size: var(--_icon-size);
541
+
542
+ align-self: center;
543
+ color: var(--slotted-multi-select-arrow-color, var(--slotted-text-muted, GrayText));
544
+ flex: none;
545
+ }
546
+
547
+ /* ── Size ─────────────────────────────────────────────────────────────── */
548
+
549
+ [data-ui='multi-select'][data-part='root'][data-size='sm'] {
550
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
551
+ --_height: var(--slotted-control-height-sm, 30px);
552
+ --_inner-size: var(--slotted-control-inner-size-sm, 22px);
553
+ --_padding-inline: var(--slotted-control-padding-inline-sm, 10px);
554
+ --_gap: var(--slotted-control-gap-sm, 6px);
555
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-sm, 14px));
556
+ }
557
+
558
+ [data-ui='multi-select'][data-part='root'][data-size='md'] {
559
+ --_font-size: var(--slotted-control-font-size-md, 13px);
560
+ --_height: var(--slotted-control-height-md, 34px);
561
+ --_padding-inline: var(--slotted-control-padding-inline-md, 13px);
562
+ --_gap: var(--slotted-control-gap-md, 8px);
563
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-md, 16px));
564
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
565
+ }
566
+
567
+ [data-ui='multi-select'][data-part='root'][data-size='lg'] {
568
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
569
+ --_height: var(--slotted-control-height-lg, 38px);
570
+ --_inner-size: var(--slotted-control-inner-size-lg, 28px);
571
+ --_padding-inline: var(--slotted-control-padding-inline-lg, 16px);
572
+ --_gap: var(--slotted-control-gap-lg, 8px);
573
+ --_icon-size: max(var(--_icon-floor), var(--slotted-control-icon-size-lg, 18px));
574
+ }
575
+
576
+ /* ── Variant ──────────────────────────────────────────────────────────── */
577
+
578
+ [data-ui='multi-select'][data-part='root'][data-variant='subtle'] {
579
+ background-color: var(
580
+ --slotted-multi-select-subtle-background,
581
+ var(--slotted-surface-sunken, Canvas)
582
+ );
583
+ border-color: var(
584
+ --slotted-multi-select-subtle-border-color,
585
+ var(--slotted-border-subtle, ButtonBorder)
586
+ );
587
+ }
588
+
589
+ /* No edge, no lift, no ground and no corner of its own: the cell it sits in
590
+ is the box, the row is the ground, a cell has no corner, and the resting
591
+ shadow would draw the cell where there is none. */
592
+ [data-ui='multi-select'][data-part='root'][data-variant='plain'] {
593
+ background-color: transparent;
594
+ border-color: transparent;
595
+ border-radius: var(--slotted-radius-none, 0px);
596
+ box-shadow: none;
597
+ }
598
+
599
+ @media (forced-colors: active) {
600
+ [data-ui='multi-select'][data-part='root'][data-variant='plain'] {
601
+ border-color: ButtonBorder;
602
+ }
603
+ }
604
+
605
+ /* ── States on the control ────────────────────────────────────────────── */
606
+
607
+ [data-ui='multi-select'][data-part='root']:hover:not([data-disabled]):not([data-readonly]):not(
608
+ [data-invalid]
609
+ ) {
610
+ border-color: var(--slotted-multi-select-hover-border-color, var(--slotted-tone-accent-border));
611
+ }
612
+
613
+ [data-ui='multi-select'][data-part='root'][data-variant='subtle']:hover:not([data-disabled]):not(
614
+ [data-readonly]
615
+ ):not([data-invalid]) {
616
+ background-color: var(
617
+ --slotted-multi-select-subtle-hover-background,
618
+ var(--slotted-tone-secondary-subtle-hover)
619
+ );
620
+ }
621
+
622
+ [data-ui='multi-select'][data-part='root'][data-variant='plain']:hover:not([data-disabled]):not(
623
+ [data-readonly]
624
+ ):not([data-invalid]) {
625
+ background-color: var(
626
+ --slotted-multi-select-plain-hover-background,
627
+ var(--slotted-tone-secondary-subtle-hover)
628
+ );
629
+ /* The accent edge the other variants take would draw the cell's outline. */
630
+ border-color: transparent;
631
+ }
632
+
633
+ /**
634
+ * The frame carries the ring for whatever inside it holds focus — the input
635
+ * or a chip — because the frame is the control a reader is operating.
636
+ * `:focus-within` rather than `:has(:focus-visible)`: a chip is focused by
637
+ * script, after a key, and a browser does not always match `:focus-visible`
638
+ * on an element script moved focus to.
639
+ */
640
+ [data-ui='multi-select'][data-part='root']:has([data-part='input']:focus-visible),
641
+ [data-ui='multi-select'][data-part='root']:has(
642
+ [data-part='chips'] > :not([data-part='field']):focus-visible
643
+ ) {
644
+ box-shadow: var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
645
+ var(--slotted-focus-ring-color, Highlight);
646
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
647
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
648
+ }
649
+
650
+ /**
651
+ * The ring of a control whose box is not its own is drawn inside that box,
652
+ * the way the inner controls draw theirs inside the frame: two pixels past
653
+ * the control is two pixels over the next row's rule. No lift either — a
654
+ * shadow that appears only under focus is a cell that changes size when it
655
+ * is used.
656
+ */
657
+ [data-ui='multi-select'][data-part='root'][data-variant='plain']:has(
658
+ [data-part='input']:focus-visible
659
+ ),
660
+ [data-ui='multi-select'][data-part='root'][data-variant='plain']:has(
661
+ [data-part='chips'] > :not([data-part='field']):focus-visible
662
+ ) {
663
+ box-shadow: none;
664
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
665
+ }
666
+
667
+ /**
668
+ * A chip already wears the ring, and the frame around it wearing a second one
669
+ * is two rings for one focus. The frame keeps its lift and gives up the
670
+ * outline while the ring is inside it.
671
+ */
672
+ [data-ui='multi-select'][data-part='root']:has(
673
+ [data-part='chips'] > :not([data-part='field']):focus-visible
674
+ ) {
675
+ outline: none;
676
+ }
677
+
678
+ [data-ui='multi-select'][data-part='root'][data-open]:not([data-disabled]) {
679
+ border-color: var(--slotted-multi-select-open-border-color, var(--slotted-tone-accent-border));
680
+ }
681
+
682
+ [data-ui='multi-select'][data-part='root'][data-invalid] {
683
+ --slotted-focus-ring-color: var(
684
+ --slotted-multi-select-invalid-focus-ring-color,
685
+ var(--slotted-focus-ring-color-invalid, Highlight)
686
+ );
687
+ --_invalid-edge-width: var(
688
+ --slotted-multi-select-invalid-edge-width,
689
+ var(--slotted-control-invalid-edge-width, 1px)
690
+ );
691
+ --_invalid-edge-color: var(
692
+ --slotted-multi-select-invalid-border-color,
693
+ var(--slotted-tone-danger-border)
694
+ );
695
+ --_invalid-edge: inset 0 0 0 var(--_invalid-edge-width) var(--_invalid-edge-color);
696
+
697
+ border-color: var(
698
+ --slotted-multi-select-invalid-border-color,
699
+ var(--slotted-tone-danger-border)
700
+ );
701
+ /*
702
+ * Width as well as colour (WCAG 1.4.1), the edge `text-field` draws: in
703
+ * `high-contrast` the danger border is the resting edge's black. The line
704
+ * is a shadow inside the border, so neither box nor text moves, and it
705
+ * takes the resting lift's place.
706
+ */
707
+ box-shadow: var(--_invalid-edge);
708
+ }
709
+
710
+ [data-ui='multi-select'][data-part='root'][data-invalid]:has([data-part='input']:focus-visible),
711
+ [data-ui='multi-select'][data-part='root'][data-invalid]:has(
712
+ [data-part='chips'] > :not([data-part='field']):focus-visible
713
+ ) {
714
+ box-shadow:
715
+ var(--_invalid-edge),
716
+ var(--slotted-control-shadow-focus-geometry, 0 1px 4px -1px)
717
+ var(--slotted-focus-ring-color, Highlight);
718
+ }
719
+
720
+ [data-ui='multi-select'][data-part='root'][data-variant='plain'][data-invalid]:has(
721
+ [data-part='input']:focus-visible
722
+ ),
723
+ [data-ui='multi-select'][data-part='root'][data-variant='plain'][data-invalid]:has(
724
+ [data-part='chips'] > :not([data-part='field']):focus-visible
725
+ ) {
726
+ /* The ring lies inside the box here, over the border and an edge as wide as
727
+ it; the edge reaches past the ring. */
728
+ box-shadow: inset 0 0 0
729
+ calc(
730
+ var(--slotted-focus-ring-offset, 2px) - var(--slotted-control-border-width, 1px) +
731
+ var(--_invalid-edge-width)
732
+ )
733
+ var(--_invalid-edge-color);
734
+ }
735
+
736
+ /*
737
+ * Forced colours remove every box shadow, and the edge with them. The
738
+ * width comes back as an outline inside the border, and a focused invalid
739
+ * control draws its ring double, which a valid ring is not.
740
+ */
741
+ @media (forced-colors: active) {
742
+ [data-ui='multi-select'][data-part='root'][data-invalid] {
743
+ outline: var(--_invalid-edge-width) solid CanvasText;
744
+ outline-offset: calc(
745
+ -1 * (var(--slotted-control-border-width, 1px) + var(--_invalid-edge-width))
746
+ );
747
+ }
748
+
749
+ [data-ui='multi-select'][data-part='root'][data-invalid]:has(
750
+ [data-part='input']:focus-visible
751
+ ) {
752
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
753
+ var(--slotted-focus-ring-color, Highlight);
754
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
755
+ }
756
+
757
+ [data-ui='multi-select'][data-part='root'][data-variant='plain'][data-invalid]:has(
758
+ [data-part='input']:focus-visible
759
+ ) {
760
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
761
+ var(--slotted-focus-ring-color, Highlight);
762
+ outline-offset: calc(
763
+ -1 * (var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width))
764
+ );
765
+ }
766
+
767
+ [data-ui='multi-select'][data-part='root'][data-invalid]:has(
768
+ [data-part='chips'] > :not([data-part='field']):focus-visible
769
+ ),
770
+ [data-ui='multi-select'][data-part='root'][data-variant='plain'][data-invalid]:has(
771
+ [data-part='chips'] > :not([data-part='field']):focus-visible
772
+ ) {
773
+ outline: var(--_invalid-edge-width) solid CanvasText;
774
+ outline-offset: calc(
775
+ -1 * (var(--slotted-control-border-width, 1px) + var(--_invalid-edge-width))
776
+ );
777
+ }
778
+ }
779
+
780
+ /**
781
+ * Read-only keeps the chips, the caret, the selection and the copy, and
782
+ * withholds the popup and every remove control. The native `readonly` gives
783
+ * the text half; the rest is the family's, because neither pattern has a
784
+ * readonly for a combobox.
785
+ */
786
+ [data-ui='multi-select'][data-part='root'][data-readonly] {
787
+ background-color: var(
788
+ --slotted-multi-select-readonly-background,
789
+ var(--slotted-tone-secondary-subtle-hover)
790
+ );
791
+ }
792
+
793
+ [data-ui='multi-select'][data-part='root'][data-readonly] [data-part='arrow'] {
794
+ opacity: 0.5;
795
+ }
796
+
797
+ /**
798
+ * `:disabled` on the input is matched beside `data-disabled` on the frame,
799
+ * because a disabled `fieldset` disables every control inside it and the
800
+ * control has no way to say so.
801
+ */
802
+ [data-ui='multi-select'][data-part='root'][data-disabled],
803
+ [data-ui='multi-select'][data-part='root']:has([data-part='input']:disabled) {
804
+ background-color: var(
805
+ --slotted-multi-select-disabled-background,
806
+ var(--slotted-disabled-background)
807
+ );
808
+ border-color: var(--slotted-multi-select-disabled-border-color, var(--slotted-disabled-border));
809
+ box-shadow: none;
810
+ color: var(--slotted-multi-select-disabled-color, var(--slotted-disabled-foreground));
811
+ cursor: not-allowed;
812
+ }
813
+
814
+ [data-ui='multi-select']
815
+ > :is([data-part='clear'], [data-part='trigger']):hover:not(:disabled):not([data-disabled]) {
816
+ background-color: var(
817
+ --slotted-multi-select-action-hover-background,
818
+ var(--slotted-tone-secondary-subtle-hover)
819
+ );
820
+ }
821
+
822
+ [data-ui='multi-select']
823
+ > :is([data-part='clear'], [data-part='trigger']):active:not(:disabled):not([data-disabled]) {
824
+ background-color: var(
825
+ --slotted-multi-select-action-active-background,
826
+ var(--slotted-tone-secondary-subtle-active)
827
+ );
828
+ }
829
+
830
+ /**
831
+ * Each button carries its own ring rather than borrowing the frame's.
832
+ * Tabbing from the input to the clear button has to move something visible,
833
+ * and a ring around the whole frame would sit still while focus moved inside
834
+ * it.
835
+ */
836
+ [data-ui='multi-select'] > :is([data-part='clear'], [data-part='trigger']):focus-visible {
837
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
838
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
839
+ }
840
+
841
+ /* ── The layer ────────────────────────────────────────────────────────── */
842
+
843
+ [data-ui='multi-select'][data-part='positioner'] {
844
+ --_surface: var(--slotted-multi-select-surface, var(--slotted-surface-raised, #ffffff));
845
+ --_text: var(--slotted-multi-select-content-text, var(--slotted-text, #0f172a));
846
+ --_muted: var(--slotted-multi-select-content-text-muted, var(--slotted-text-muted, #5b6b7f));
847
+ --_border-color: var(
848
+ --slotted-multi-select-content-border-color,
849
+ var(--slotted-border-subtle, #e2e8f0)
850
+ );
851
+ --_border-width: var(--slotted-border-width-thin, 1px);
852
+ --_radius: var(--slotted-multi-select-content-radius, var(--slotted-radius-lg, 8px));
853
+ --_shadow: var(--slotted-multi-select-content-shadow, var(--slotted-shadow-md));
854
+
855
+ /**
856
+ * The stronger step of the neutral tone, for the reason `select.css` writes
857
+ * out in full: in the dark scheme `--slotted-tone-secondary-subtle-hover`
858
+ * is `#18181b` where the raised popup is `#1c1c20`, so the usual highlight
859
+ * resolves *darker* than its own ground and reads as a hole. The `-active`
860
+ * step is the only member of the pair that lifts in all three schemes.
861
+ */
862
+ --_highlight: var(
863
+ --slotted-multi-select-highlight,
864
+ var(--slotted-tone-secondary-subtle-active)
865
+ );
866
+ --_highlight-active: var(
867
+ --slotted-multi-select-highlight-active,
868
+ var(--slotted-tone-accent-surface)
869
+ );
870
+ --_selected-color: var(--slotted-multi-select-selected-color, var(--slotted-tone-accent-text));
871
+
872
+ --_inset-block: var(
873
+ --slotted-multi-select-content-padding-block,
874
+ var(--slotted-space-2, 0.25rem)
875
+ );
876
+ --_inset-inline: var(
877
+ --slotted-multi-select-content-padding-inline,
878
+ var(--slotted-space-2, 0.25rem)
879
+ );
880
+ --_item-inset: var(--slotted-control-padding-inline-md, 0.8125rem);
881
+ --_item-height: var(--slotted-control-height-md, 34px);
882
+ --_item-radius: var(--slotted-multi-select-item-radius, var(--slotted-control-radius));
883
+ --_content-font-size: var(--slotted-control-font-size-md, 13px);
884
+ --_secondary-font-size: var(--slotted-control-font-size-sm, 12px);
885
+ --_content-icon-size: var(--slotted-control-icon-size-md, 16px);
886
+ --_column-gap: var(--slotted-control-gap-md, 0.5rem);
887
+
888
+ --_measure: var(--slotted-multi-select-measure, 24rem);
889
+
890
+ /* Only the block inset: the positioner writes `left` and never `right`, and
891
+ `inset-inline-start` would resolve to `right` in a right-to-left document
892
+ and win over the measured value. */
893
+ position: absolute;
894
+ inset-block-start: 0;
895
+
896
+ z-index: calc(var(--slotted-z-index-popover, 1100) + var(--slotted-layer-depth, 0));
897
+ isolation: isolate;
898
+
899
+ box-sizing: border-box;
900
+ pointer-events: none;
901
+
902
+ opacity: 0;
903
+ /**
904
+ * The layer arrives along the axis it opens on — from one space step above
905
+ * its place when it opens downward, from one step below when it opens
906
+ * upward — and never along the inline axis. A `scale` from the corner the
907
+ * positioner named as origin moved the far edge by four per cent of the
908
+ * width, which read as an entrance from the left.
909
+ */
910
+ --_enter-distance: var(--slotted-space-2, 0.25rem);
911
+ translate: 0 calc(-1 * var(--_enter-distance));
912
+ transition:
913
+ opacity var(--slotted-duration-short) var(--slotted-easing-exit),
914
+ translate var(--slotted-duration-short) var(--slotted-easing-exit);
915
+ }
916
+
917
+ [data-ui='multi-select'][data-part='positioner'][data-side='top'] {
918
+ translate: 0 var(--_enter-distance);
919
+ }
920
+
921
+ /* A lateral side has no block direction to arrive from, so it only fades. */
922
+ [data-ui='multi-select'][data-part='positioner']:is([data-side='left'], [data-side='right']) {
923
+ translate: 0 0;
924
+ }
925
+
926
+ /**
927
+ * The entrance is a keyframe animation and not a transition. A transition
928
+ * starts from the style the browser computed last, and the positioner writes
929
+ * `data-side` in the same style update as the family opens the layer, so a
930
+ * transition set out from the closed style of the side the layer had before
931
+ * it had one. A keyframe states where it starts. The exit stays a transition:
932
+ * it leaves from wherever the layer is, on the closed rule's durations.
933
+ */
934
+ [data-ui='multi-select'][data-part='positioner'][data-open] {
935
+ opacity: 1;
936
+ translate: 0 0;
937
+ transition: none;
938
+ }
939
+
940
+ /**
941
+ * Only a layer the positioner placed arrives: it writes `data-positioned`
942
+ * with the side, and a layer written open in static markup — the harness, a
943
+ * docs snippet — is simply there, and is photographed where it stands.
944
+ */
945
+ [data-ui='multi-select'][data-part='positioner'][data-open][data-positioned] {
946
+ animation: slotted-multi-select-enter-down var(--slotted-duration-medium)
947
+ var(--slotted-easing-enter);
948
+ }
949
+
950
+ [data-ui='multi-select'][data-part='positioner'][data-open][data-positioned][data-side='top'] {
951
+ animation-name: slotted-multi-select-enter-up;
952
+ }
953
+
954
+ [data-ui='multi-select'][data-part='positioner'][data-open][data-positioned]:is(
955
+ [data-side='left'],
956
+ [data-side='right']
957
+ ) {
958
+ animation-name: slotted-multi-select-fade;
959
+ }
960
+
961
+ @keyframes slotted-multi-select-enter-down {
962
+ from {
963
+ opacity: 0;
964
+ translate: 0 calc(-1 * var(--_enter-distance));
965
+ }
966
+ }
967
+
968
+ @keyframes slotted-multi-select-enter-up {
969
+ from {
970
+ opacity: 0;
971
+ translate: 0 var(--_enter-distance);
972
+ }
973
+ }
974
+
975
+ @keyframes slotted-multi-select-fade {
976
+ from {
977
+ opacity: 0;
978
+ }
979
+ }
980
+
981
+ /* Stillness keeps the fade and drops the movement. */
982
+ @media (prefers-reduced-motion: reduce) {
983
+ [data-ui='multi-select'][data-part='positioner'],
984
+ [data-ui='multi-select'][data-part='positioner'][data-side='top'],
985
+ [data-ui='multi-select'][data-part='positioner'][data-side='left'],
986
+ [data-ui='multi-select'][data-part='positioner'][data-side='right'] {
987
+ translate: 0 0;
988
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
989
+ }
990
+
991
+ [data-ui='multi-select'][data-part='positioner'][data-open][data-positioned],
992
+ [data-ui='multi-select'][data-part='positioner'][data-open][data-positioned][data-side='top'],
993
+ [data-ui='multi-select'][data-part='positioner'][data-open][data-positioned][data-side='left'],
994
+ [data-ui='multi-select'][data-part='positioner'][data-open][data-positioned][data-side='right'] {
995
+ animation-name: slotted-multi-select-fade;
996
+ }
997
+ }
998
+
999
+ [data-ui='multi-select'][data-part='positioner'][data-detached] {
1000
+ visibility: hidden;
1001
+ }
1002
+
1003
+ [data-ui='multi-select'][data-part='positioner'][data-width='anchor'] {
1004
+ inline-size: var(--slotted-positioner-anchor-width, auto);
1005
+ }
1006
+
1007
+ [data-ui='multi-select'][data-part='positioner'][data-width='content'] {
1008
+ inline-size: max-content;
1009
+ max-inline-size: min(var(--_measure), var(--slotted-positioner-available-width, 100vw));
1010
+ min-inline-size: var(--slotted-positioner-anchor-width, auto);
1011
+ }
1012
+
1013
+ [data-ui='multi-select'][data-part='positioner'][data-size='sm'] {
1014
+ --_item-inset: var(--slotted-control-padding-inline-sm, 0.625rem);
1015
+ --_item-height: var(--slotted-control-height-sm, 30px);
1016
+ --_content-font-size: var(--slotted-control-font-size-sm, 12px);
1017
+ --_secondary-font-size: var(--slotted-font-size-xs, 0.75rem);
1018
+ --_content-icon-size: var(--slotted-control-icon-size-sm, 14px);
1019
+ --_column-gap: var(--slotted-control-gap-sm, 0.375rem);
1020
+ }
1021
+
1022
+ [data-ui='multi-select'][data-part='positioner'][data-size='lg'] {
1023
+ --_item-inset: var(--slotted-control-padding-inline-lg, 1rem);
1024
+ --_item-height: var(--slotted-control-height-lg, 38px);
1025
+ --_content-font-size: var(--slotted-control-font-size-lg, 14px);
1026
+ --_secondary-font-size: var(--slotted-control-font-size-sm, 12px);
1027
+ --_content-icon-size: var(--slotted-control-icon-size-lg, 18px);
1028
+ --_column-gap: var(--slotted-control-gap-lg, 0.5rem);
1029
+ }
1030
+
1031
+ /* ── The surface ──────────────────────────────────────────────────────── */
1032
+
1033
+ [data-ui='multi-select'] > [data-part='content'] {
1034
+ background: var(--_surface);
1035
+ border: var(--_border-width) solid var(--_border-color);
1036
+ border-radius: var(--_radius);
1037
+ box-shadow: var(--_shadow);
1038
+ box-sizing: border-box;
1039
+ color: var(--_text);
1040
+ display: grid;
1041
+ font-family: var(--slotted-font-family-sans);
1042
+ font-size: var(--_content-font-size);
1043
+ grid-template-columns: auto minmax(0, 1fr) auto;
1044
+ max-block-size: calc(
1045
+ var(--slotted-positioner-available-height, 100vh) - var(--slotted-space-5, 0.75rem)
1046
+ );
1047
+ outline: none;
1048
+ overflow-y: auto;
1049
+ overscroll-behavior: contain;
1050
+ padding-block: var(--_inset-block);
1051
+ padding-inline: var(--_inset-inline);
1052
+ pointer-events: auto;
1053
+ }
1054
+
1055
+ /**
1056
+ * The surface never takes a ring. Focus stays on the control for the whole
1057
+ * interaction — the highlight is `aria-activedescendant`, not focus — so a
1058
+ * ring here would draw a box around something nobody is operating.
1059
+ */
1060
+ [data-ui='multi-select'] > [data-part='content']:focus-visible {
1061
+ outline: none;
1062
+ }
1063
+
1064
+ /**
1065
+ * Every chosen value, whatever the row in the control did with them. It is
1066
+ * what makes `overflow='collapse'` legal against the feature guide: the `+N`
1067
+ * chip in the control summarises, and this band is where the whole set stays
1068
+ * reachable. It is drawn only while something is chosen.
1069
+ *
1070
+ * Its chips carry no remove control, and the reason is keyboard reach rather
1071
+ * than tidiness: focus never enters this layer, so a control in here would be
1072
+ * reachable by pointer and by nothing else. Taking a value back is the
1073
+ * option's own row, which the list has open beside it, and the chips in the
1074
+ * control, which are focusable. `select` recorded the same decision.
1075
+ */
1076
+ [data-ui='multi-select'] [data-part='chosen'] {
1077
+ background: var(--_surface);
1078
+ border-block-end: var(--_border-width) solid var(--_border-color);
1079
+ box-sizing: border-box;
1080
+ display: flex;
1081
+ flex-wrap: wrap;
1082
+ gap: var(--slotted-space-2, 0.25rem);
1083
+ grid-column: 1 / -1;
1084
+ /* Its own padding stands where the surface's would, so nothing shows above
1085
+ it as the list scrolls under. */
1086
+ inset-block-start: calc(-1 * var(--_inset-block));
1087
+ margin-block: calc(-1 * var(--_inset-block)) var(--_inset-block);
1088
+ margin-inline: calc(-1 * var(--_inset-inline));
1089
+ /* Two whole rows of chips, the gap between them, and the air above the
1090
+ first. What says there is more is the scrollbar and the rule under it. */
1091
+ max-block-size: calc(
1092
+ 2 * var(--slotted-target-min, 24px) + var(--slotted-space-2, 0.25rem) + var(--_inset-block)
1093
+ );
1094
+ min-inline-size: 0;
1095
+ overflow-y: auto;
1096
+ overscroll-behavior: contain;
1097
+ padding-block: var(--_inset-block);
1098
+ padding-inline: var(--_inset-inline);
1099
+ position: sticky;
1100
+ z-index: 1;
1101
+ }
1102
+
1103
+ /* ── Groups ───────────────────────────────────────────────────────────── */
1104
+
1105
+ [data-ui='multi-select'] [data-part='group'] {
1106
+ display: grid;
1107
+ grid-column: 1 / -1;
1108
+ grid-template-columns: subgrid;
1109
+ }
1110
+
1111
+ /**
1112
+ * An option the query rejected, and a group whose options all went. Both
1113
+ * carry `hidden` — Angular hides each rejected option rather than leaving the
1114
+ * list, and both bindings hide an emptied group — and `display: grid` on
1115
+ * either beats the user agent's own `[hidden] { display: none }`, because an
1116
+ * author rule wins over the user agent sheet. Without this the search
1117
+ * filtered nothing the reader could see.
1118
+ */
1119
+ [data-ui='multi-select'] [data-part='item'][hidden],
1120
+ [data-ui='multi-select'] [data-part='group'][hidden] {
1121
+ display: none;
1122
+ }
1123
+
1124
+ [data-ui='multi-select'] [data-part='group-label'] {
1125
+ color: var(--_muted);
1126
+ font-size: var(--_secondary-font-size);
1127
+ font-weight: var(--slotted-font-weight-semibold, 600);
1128
+ grid-column: 1 / -1;
1129
+ line-height: var(--slotted-line-height-sm, 1.45);
1130
+ padding-block: var(--slotted-space-2, 0.25rem);
1131
+ padding-inline: var(--_item-inset);
1132
+ }
1133
+
1134
+ /* ── Options ──────────────────────────────────────────────────────────── */
1135
+
1136
+ [data-ui='multi-select'] [data-part='item'] {
1137
+ align-content: center;
1138
+ border-radius: var(--_item-radius);
1139
+ box-sizing: border-box;
1140
+ /* A row that adds a value to the set is a press, and this popup is not a
1141
+ platform menu: `select` and `combobox` draw the same cursor on the same
1142
+ row, and the three say one thing about one gesture. */
1143
+ cursor: pointer;
1144
+ display: grid;
1145
+ grid-column: 1 / -1;
1146
+ grid-template-columns: subgrid;
1147
+ line-height: var(--slotted-line-height-md, 1.5);
1148
+ min-block-size: var(--_item-height);
1149
+ padding-block: var(--slotted-space-2, 0.25rem);
1150
+ padding-inline: var(--_item-inset);
1151
+ row-gap: var(--slotted-space-1, 0.125rem);
1152
+ user-select: none;
1153
+ transition:
1154
+ background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
1155
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
1156
+ color var(--slotted-control-transition-duration, var(--slotted-duration-short))
1157
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
1158
+ }
1159
+
1160
+ [data-ui='multi-select'] [data-part='item'][data-current] {
1161
+ background-color: var(--_highlight);
1162
+ /**
1163
+ * A ring as well as a ground. Nothing on this surface holds focus — DOM
1164
+ * focus is on the input for the whole interaction — so there is no focus
1165
+ * ring anywhere to say where the reader is, and the APG's own select-only
1166
+ * example marks its current option with an outline for that reason. An
1167
+ * inset shadow rather than an outline, so it follows the option's own
1168
+ * radius and never overflows the surface's rounded edge.
1169
+ */
1170
+ box-shadow: inset 0 0 0 var(--slotted-border-width-thin, 1px)
1171
+ var(--slotted-multi-select-highlight-ring, var(--slotted-tone-accent-border));
1172
+ }
1173
+
1174
+ [data-ui='multi-select'] [data-part='item']:not([data-disabled]):active {
1175
+ background-color: var(--_highlight-active);
1176
+ }
1177
+
1178
+ [data-ui='multi-select'] [data-part='item'][data-selected] {
1179
+ color: var(--_selected-color);
1180
+ }
1181
+
1182
+ /**
1183
+ * Two chosen neighbours are one block. Every item keeps its own radius, so
1184
+ * two selected siblings painted with the same ground met at a waist — four
1185
+ * rounded corners inside the block — and a multiple selection read as a
1186
+ * column of separate chips. The second gives up its top corners and the first
1187
+ * its bottom ones, so the block is rounded only where the selection begins
1188
+ * and ends. `select` and `combobox` answer the same way, and
1189
+ * `selection-block.test.mjs` holds all of them to it.
1190
+ */
1191
+ [data-ui='multi-select'] [data-part='item'][data-selected] + [data-part='item'][data-selected] {
1192
+ border-start-end-radius: 0;
1193
+ border-start-start-radius: 0;
1194
+ }
1195
+
1196
+ [data-ui='multi-select']
1197
+ [data-part='item'][data-selected]:has(+ [data-part='item'][data-selected]) {
1198
+ border-end-end-radius: 0;
1199
+ border-end-start-radius: 0;
1200
+ }
1201
+
1202
+ [data-ui='multi-select'] [data-part='item'][data-selected] > [data-part='label'] {
1203
+ font-weight: var(--slotted-font-weight-medium, 500);
1204
+ }
1205
+
1206
+ [data-ui='multi-select'] [data-part='item'][data-disabled] {
1207
+ color: var(--slotted-disabled-foreground, #64748b);
1208
+ cursor: not-allowed;
1209
+ }
1210
+
1211
+ /**
1212
+ * The highlight can land on an option nobody can choose — a disabled one, or
1213
+ * any unchosen one while the control is full — because it keeps its place in
1214
+ * the collection and the pattern requires it to be announced.
1215
+ *
1216
+ * **The ground stays the highlight's.** Painting the theme's disabled surface
1217
+ * here was the first answer and it is wrong in the dark scheme, for the same
1218
+ * reason the highlight itself was: `--slotted-disabled-background` is
1219
+ * `#18181b` where the raised popup is `#1c1c20`, so the cursor would *sink*
1220
+ * on a disabled row and rise on every other one. A cursor lifts, always. What
1221
+ * the row cannot do is said by the ink and by the ring, which both step down
1222
+ * to the disabled pair.
1223
+ */
1224
+ [data-ui='multi-select'] [data-part='item'][data-disabled][data-current] {
1225
+ box-shadow: inset 0 0 0 var(--slotted-border-width-thin, 1px)
1226
+ var(--slotted-disabled-border, #cbd5e1);
1227
+ color: var(--slotted-disabled-foreground, #64748b);
1228
+ }
1229
+
1230
+ /* ── The cells ────────────────────────────────────────────────────────── */
1231
+
1232
+ [data-ui='multi-select'] [data-part='item'] > :is([data-part='indicator'], [data-part='icon']) {
1233
+ block-size: 1lh;
1234
+ }
1235
+
1236
+ /* Column one holds content and paints none: a `Glyph` in an `Icon` box, or
1237
+ artwork of the consumer's own, centred and sized from the column. */
1238
+ [data-ui='multi-select'] [data-part='item'] > [data-part='icon'] {
1239
+ --slotted-icon-size: var(--_content-icon-size);
1240
+
1241
+ align-items: center;
1242
+ display: flex;
1243
+ grid-column: 1;
1244
+ grid-row: 1;
1245
+ inline-size: var(--_content-icon-size);
1246
+ justify-content: center;
1247
+ justify-self: center;
1248
+ margin-inline-end: var(--_column-gap);
1249
+ }
1250
+
1251
+ [data-ui='multi-select'] [data-part='item'] > [data-part='label'] {
1252
+ grid-column: 2;
1253
+ grid-row: 1;
1254
+ min-inline-size: 0;
1255
+ display: -webkit-box;
1256
+ -webkit-box-orient: vertical;
1257
+ -webkit-line-clamp: 2;
1258
+ overflow: hidden;
1259
+ overflow-wrap: anywhere;
1260
+ }
1261
+
1262
+ [data-ui='multi-select'] [data-part='item'] > [data-part='description'] {
1263
+ color: var(--_muted);
1264
+ font-size: var(--_secondary-font-size);
1265
+ grid-column: 2;
1266
+ grid-row: 2;
1267
+ line-height: var(--slotted-line-height-sm, 1.45);
1268
+ min-inline-size: 0;
1269
+ overflow-wrap: anywhere;
1270
+ }
1271
+
1272
+ [data-ui='multi-select'] [data-part='item'][data-disabled] > [data-part='description'] {
1273
+ color: inherit;
1274
+ }
1275
+
1276
+ /* The tick is a `Glyph` the binding draws, at the content's icon step. */
1277
+ [data-ui='multi-select'] [data-part='item'] > [data-part='indicator'] {
1278
+ --slotted-icon-size: var(--_content-icon-size);
1279
+
1280
+ align-items: center;
1281
+ display: flex;
1282
+ grid-column: 3;
1283
+ grid-row: 1;
1284
+ inline-size: var(--_content-icon-size);
1285
+ justify-content: center;
1286
+ justify-self: end;
1287
+ margin-inline-start: var(--_column-gap);
1288
+ opacity: 0;
1289
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-standard);
1290
+ }
1291
+
1292
+ [data-ui='multi-select'] [data-part='item'][data-selected] > [data-part='indicator'] {
1293
+ opacity: 1;
1294
+ }
1295
+
1296
+ /**
1297
+ * The offer to add what was typed. It is an option — the same part, the same
1298
+ * columns, the same keys — told apart by `data-create`, and its mark is the
1299
+ * plus rather than the check: the check says "this is in the set" and nothing
1300
+ * is in the set yet. The binding draws the plus; the rule only shows it.
1301
+ */
1302
+ [data-ui='multi-select'] [data-part='item'][data-create] > [data-part='indicator'] {
1303
+ opacity: 1;
1304
+ }
1305
+
1306
+ /**
1307
+ * The offer's label is the family's own sentence with the reader's query
1308
+ * quoted inside it, which is why it takes its own bidi run where an option's
1309
+ * label — the consumer's content — does not. `Add “berr”` in a right-to-left
1310
+ * document put the opening quote after the closing one, because the quotes
1311
+ * are neutral characters and resolved to the paragraph rather than to the
1312
+ * Latin word between them. The repair `status`, `empty` and the counter all
1313
+ * make, for the same reason.
1314
+ */
1315
+ [data-ui='multi-select'] [data-part='item'][data-create] > [data-part='label'] {
1316
+ unicode-bidi: plaintext;
1317
+ }
1318
+
1319
+ /* ── Separator, empty, status ─────────────────────────────────────────── */
1320
+
1321
+ [data-ui='multi-select'] [data-part='separator'] {
1322
+ background-color: var(--slotted-multi-select-separator-color, var(--_border-color));
1323
+ block-size: var(--_border-width);
1324
+ border: 0;
1325
+ grid-column: 1 / -1;
1326
+ margin-block: var(--slotted-space-2, 0.25rem);
1327
+ margin-inline: 0;
1328
+ }
1329
+
1330
+ /**
1331
+ * A query with no answers, and a query whose answer is still travelling. Both
1332
+ * keep the surface's measure rather than collapsing, so nothing jumps when
1333
+ * the results land.
1334
+ */
1335
+ [data-ui='multi-select'] :is([data-part='empty'], [data-part='status']) {
1336
+ color: var(--_muted);
1337
+ /* Its own bidi run, ordered by its own content: "3 results" is a European
1338
+ number, a neutral space and a Latin word, and in a right-to-left document
1339
+ the space resolves to the paragraph and the two runs swap. */
1340
+ unicode-bidi: plaintext;
1341
+ grid-column: 1 / -1;
1342
+ /**
1343
+ * Wide enough that a one-word message does not draw a popup narrower than a
1344
+ * word, and never wider than the popup it is in — as two declarations
1345
+ * rather than as one `min()`.
1346
+ *
1347
+ * `min-inline-size: min(12rem, 100%)` was the first answer and it breaks on
1348
+ * a popup sized to its content. A percentage resolves against an indefinite
1349
+ * width as nothing while the browser is computing that width, so the `min()`
1350
+ * collapsed to zero, the surface sized itself to the widest thing left — the
1351
+ * three waiting bars — and the message then took its twelve rems inside a
1352
+ * box narrower than that and hung out of both sides of it, centred. Stating
1353
+ * the floor and the ceiling separately means the floor is counted while the
1354
+ * surface is being measured and the ceiling is applied once it has a width.
1355
+ *
1356
+ * The box is a border box so the ceiling counts the message's own gutters;
1357
+ * against a content box `100%` would let the padding push it out again.
1358
+ */
1359
+ box-sizing: border-box;
1360
+ min-inline-size: 12rem;
1361
+ max-inline-size: 100%;
1362
+ padding-block: var(--slotted-space-5, 0.75rem);
1363
+ padding-inline: var(--_item-inset);
1364
+ text-align: center;
1365
+ }
1366
+
1367
+ /**
1368
+ * The status row is also the live region that announces the count, so it is
1369
+ * often present with nothing to draw. A row with no text takes no space.
1370
+ */
1371
+ [data-ui='multi-select'] [data-part='status']:empty {
1372
+ display: none;
1373
+ }
1374
+
1375
+ /**
1376
+ * The wait, as the shape of the list that is coming: rows the height of an
1377
+ * option rather than one sentence, so the surface a reader is looking at
1378
+ * while the answer travels is the surface they will go on looking at. The
1379
+ * bars are `skeleton/skeleton.css`'s, and they are hidden from assistive
1380
+ * technology because the `status` row beside them is the live region.
1381
+ */
1382
+ [data-ui='multi-select'] [data-part='placeholder'] {
1383
+ display: grid;
1384
+ grid-column: 1 / -1;
1385
+ padding-block: var(--slotted-space-2, 0.25rem);
1386
+ padding-inline: var(--_item-inset);
1387
+ }
1388
+
1389
+ [data-ui='multi-select'] [data-part='placeholder'] > * {
1390
+ align-items: center;
1391
+ display: flex;
1392
+ min-block-size: var(--_item-height);
1393
+ }
1394
+
1395
+ [data-ui='multi-select'] [data-part='placeholder'] > [data-ui='skeleton'] {
1396
+ align-self: center;
1397
+ }
1398
+
1399
+ /* Three ragged widths: three identical bars read as a table, and a list of
1400
+ labels is ragged. */
1401
+ [data-ui='multi-select'] [data-part='placeholder'] > :nth-child(2) [data-ui='skeleton'] {
1402
+ inline-size: 78%;
1403
+ }
1404
+
1405
+ [data-ui='multi-select'] [data-part='placeholder'] > :nth-child(3) [data-ui='skeleton'] {
1406
+ inline-size: 56%;
1407
+ }
1408
+
1409
+ [data-ui='multi-select'] > [data-part='content'][data-loading] {
1410
+ cursor: progress;
1411
+ }
1412
+
1413
+ /* ── Forced colours ───────────────────────────────────────────────────── */
1414
+
1415
+ @media (forced-colors: active) {
1416
+ [data-ui='multi-select'] [data-part='item'][data-current] {
1417
+ background-color: Highlight;
1418
+ color: HighlightText;
1419
+ forced-color-adjust: none;
1420
+ }
1421
+
1422
+ [data-ui='multi-select'] [data-part='item'][data-disabled][data-current] {
1423
+ background-color: Highlight;
1424
+ color: GrayText;
1425
+ }
1426
+ }
1427
+ }