@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,1303 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * A panel that comes in from an edge, and either covers the page or stands
4
+ * beside it.
5
+ *
6
+ * Two boxes, and the second one is the whole family:
7
+ *
8
+ * ┌─ root ──────────────────────────────────────────────┐
9
+ * │ ┌─ backdrop ────────────────────────────────────────┐│
10
+ * │ │ ┌─ panel ────────────────────┐││
11
+ * │ │ │ ⁞ header │││
12
+ * │ │ │ ⁞ ─────────────────────── │││
13
+ * │ │ │ ⁞ body (scrolls) │││
14
+ * │ │ │ ⁞ ─────────────────────── │││
15
+ * │ │ │ ⁞ footer │││
16
+ * │ │ └─┴──────────────────────────┘││
17
+ * │ └───────────────────────┬───────────────────────────┘│
18
+ * └─────────────────────────┼────────────────────────────┘
19
+ * resizer
20
+ *
21
+ * `dialog` needs a third box between its layer and its panel because its root
22
+ * declares `container-type: inline-size`, which applies `contain: layout`,
23
+ * which makes a scrim positioned inside a scrolling root part of what
24
+ * scrolls. This root declares no container and never scrolls: the panel is
25
+ * pinned to an edge and bounded to the layer by construction, so `backdrop`
26
+ * and `panel` are two cells of one one-cell grid and nothing is positioned.
27
+ *
28
+ * The resizer is a four-pixel gutter whose pointer reach is
29
+ * `--slotted-target-min`, the WCAG 2.2 floor, drawn by a transparent
30
+ * pseudo-element centred on it; it is a row of the panel's grid on a block
31
+ * side and an overlay on its inner edge on an inline side. The section that draws it says why the two
32
+ * differ; in one sentence, every rule in this panel runs across the inline
33
+ * axis, and a column would cut all of them.
34
+ *
35
+ * Every part is reached with a child combinator. A drawer opened from inside
36
+ * a drawer is the ordinary case, and a descendant rule would paint the inner
37
+ * panel with the outer panel's geometry.
38
+ */
39
+ [data-ui='drawer'] {
40
+ /**
41
+ * The panel's resting measure, per size step. Five named steps rather than
42
+ * the pixel string Ant and MUI both take: a theme retunes a step and cannot
43
+ * retune `width: 400px`, and a family that accepted one would be carrying a
44
+ * second scale of its own.
45
+ */
46
+ --_step: var(--slotted-drawer-extent-md, 24rem);
47
+ /**
48
+ * What the panel actually measures. `--slotted-drawer-extent` is the one
49
+ * name for it, and it is read from two places: an application setting one
50
+ * measure for every drawer writes it on `[data-ui='drawer']`, and the
51
+ * binding writes the length the reader dragged to as an inline custom
52
+ * property on this element. Both are lengths, so the cascade decides
53
+ * between them the way it decides everything else.
54
+ */
55
+ --_extent: var(--slotted-drawer-extent, var(--_step));
56
+ /** The panel's own inset, which every row reads. */
57
+ --_padding-inline: var(--slotted-space-7, 1.25rem);
58
+ --_padding-block: var(--slotted-space-6, 1rem);
59
+ --_gap: var(--slotted-space-5, 0.75rem);
60
+ --_rule-size: var(--slotted-border-width-thin, 1px);
61
+ /**
62
+ * The title is on the type scale rather than on the control type scale: a
63
+ * control's label is 12 to 14 pixels because it lives inside a button, and
64
+ * a drawer's title is a heading set like every other heading in the library.
65
+ */
66
+ --_title-font-size: var(--slotted-font-size-lg, 1rem);
67
+ --_description-font-size: var(--slotted-font-size-md, 0.875rem);
68
+ --_body-font-size: var(--slotted-font-size-md, 0.875rem);
69
+ --_icon-size: var(--slotted-control-icon-size-md, 16px);
70
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
71
+ /**
72
+ * The close control's box: an inner control, not a button (R2), so the
73
+ * glyph plus a step of air on each side, floored at the WCAG 2.2 minimum
74
+ * so the target holds at every density.
75
+ */
76
+ --_close-size: max(
77
+ var(--slotted-target-min, 24px),
78
+ min(calc(var(--_icon-size) + 2 * var(--slotted-space-2, 0.25rem)), var(--_inner-size))
79
+ );
80
+ /**
81
+ * The separator is a gutter, not a handle with a grip on it: four pixels
82
+ * of nothing at rest, a two-pixel line under the hand, and a reach the
83
+ * pointer finds without the eye being told where it is.
84
+ *
85
+ * `--_resizer-reach` is the WCAG 2.2 floor and it is centred on the
86
+ * gutter, so it takes ten pixels from each side rather than twenty from
87
+ * the panel — which is what keeps it off the header of a sheet.
88
+ */
89
+ /**
90
+ * One rem, and not the four pixels it was.
91
+ *
92
+ * Four was right for a gutter that drew nothing: the picture was the edge,
93
+ * and the target came from the reach alone. It is wrong for a gutter that
94
+ * carries a grip — the grip does not fit — and it was wrong for the focus
95
+ * ring too: a two-pixel ring inset into a four-pixel box leaves nothing
96
+ * between its own edges, so a focused separator read as a solid accent
97
+ * stripe rather than as a ring around something. The capture is what
98
+ * showed it.
99
+ *
100
+ * It is `splitter`'s track, because the two families draw one picture and a
101
+ * picture is a geometry as much as a colour. The panel's rows already read
102
+ * this through `max(--_padding-inline, --_resizer-thickness)`, so widening
103
+ * it moves nothing else.
104
+ */
105
+ --_resizer-thickness: var(--slotted-space-6, 1rem);
106
+ --_resizer-reach: var(--slotted-target-min, 24px);
107
+ --_resizer-line: var(--slotted-border-width-thick, 2px);
108
+ --_radius: var(--slotted-radius-lg, 8px);
109
+
110
+ /**
111
+ * Two timings, because two distances.
112
+ *
113
+ * `--_slide` is the panel crossing the whole layer from outside it, which
114
+ * is the longest travel any family in the library animates — `dialog`'s
115
+ * panel moves eight pixels and this one moves the width of itself, so the
116
+ * duration it borrowed from that family read as a snap. It takes the top of
117
+ * the motion scale arriving and one step down leaving, because a reader who
118
+ * has decided to close is waiting.
119
+ *
120
+ * `--_move` is any measure that changes — an inline drawer opening, and a
121
+ * separator released onto a snap point. One step below the slide.
122
+ *
123
+ * It used to carry the scrim as well, on the argument that the scrim should
124
+ * have settled by the time the panel lands. It was reported as a scrim that
125
+ * simply appeared, and the correction is `dialog`'s, which had the same
126
+ * complaint and answered it with measurement: on the entrance curve the ink
127
+ * was at 57.6 per cent by the second frame and 83 by the fourth, while the
128
+ * standard curve over the long step spends the frames where they can be
129
+ * seen — 10.8 per cent at 25 milliseconds and 55 at 100. The scrim's own
130
+ * pair is on the backdrop rules below, read from that family rather than
131
+ * invented again here.
132
+ *
133
+ * Both are custom properties rather than pairs of rules because of the
134
+ * cascade: the side rules that set the exit offset carry the same
135
+ * specificity as a `[data-open]` rule would, and the last one written would
136
+ * decide. A property is resolved on the element instead, and the three
137
+ * states below read as what they are.
138
+ */
139
+ --_slide: var(--slotted-duration-long) var(--slotted-easing-enter);
140
+ --_move: var(--slotted-duration-medium) var(--slotted-easing-enter);
141
+ /**
142
+ * `allow-discrete` is the whole of it. Without it the rule that hides a
143
+ * closed drawer takes effect at the first frame of the exit and the
144
+ * milliseconds `presence` is holding the node mounted for show nothing;
145
+ * with it, the flip to `hidden` happens at the end of the duration instead.
146
+ * `mount="active"` never reaches the closed state — the layer is gone — so
147
+ * this is what `eager`, `lazy` and every inline drawer need in order to
148
+ * animate at all.
149
+ */
150
+ /**
151
+ * The same step the exit takes, because this only has to outlast the
152
+ * movement and `presence` reads the longest transition on this element to
153
+ * know how long to hold the node. A step longer left the panel sitting off
154
+ * screen, inert and invisible, for a hundred and fifty milliseconds after
155
+ * it had finished leaving — which is a hundred and fifty milliseconds
156
+ * before `closed` fires.
157
+ */
158
+ --_reveal: content-visibility var(--slotted-duration-medium) allow-discrete;
159
+
160
+ /**
161
+ * The repository has no global reset, so a family that gives a box an
162
+ * explicit measure states this itself. Without it the panel's own edge is
163
+ * a pixel *outside* the extent a consumer asked for — and under `inline`
164
+ * the root clips at that extent, so the edge that separates the drawer
165
+ * from the page beside it disappeared entirely in the first capture.
166
+ */
167
+ box-sizing: border-box;
168
+ display: grid;
169
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
170
+
171
+ color: var(--slotted-drawer-color, var(--slotted-text, CanvasText));
172
+ font-family: var(--slotted-control-font-family, var(--slotted-font-family-sans));
173
+
174
+ content-visibility: visible;
175
+ transition: var(--_reveal);
176
+ }
177
+
178
+ /** Leaving is shorter than arriving: a reader who has decided is waiting. */
179
+ [data-ui='drawer']:not([data-open]) {
180
+ --_slide: var(--slotted-duration-medium) var(--slotted-easing-exit);
181
+ --_move: var(--slotted-duration-medium) var(--slotted-easing-exit);
182
+ }
183
+
184
+ /**
185
+ * While the pointer is down nothing eases. An extent that transitioned toward
186
+ * every pointer position would lag the hand by the whole duration, and a
187
+ * bottom sheet the reader is pulling has to follow it.
188
+ */
189
+ [data-ui='drawer'][data-dragging] {
190
+ --_move: var(--slotted-duration-instant) var(--slotted-easing-standard);
191
+ }
192
+
193
+ [data-ui='drawer'][data-size='sm'] {
194
+ --_step: var(--slotted-drawer-extent-sm, 18rem);
195
+ --_padding-inline: var(--slotted-space-6, 1rem);
196
+ }
197
+
198
+ [data-ui='drawer'][data-size='lg'] {
199
+ --_step: var(--slotted-drawer-extent-lg, 30rem);
200
+ }
201
+
202
+ [data-ui='drawer'][data-size='xl'] {
203
+ --_step: var(--slotted-drawer-extent-xl, 40rem);
204
+ --_padding-inline: var(--slotted-space-8, 1.5rem);
205
+ --_title-font-size: var(--slotted-font-size-xl, 1.125rem);
206
+ }
207
+
208
+ /** The step a drawer that is a page takes: the whole layer, and no measure. */
209
+ [data-ui='drawer'][data-size='full'] {
210
+ --_step: 100%;
211
+ }
212
+
213
+ /* ── The layer, and the box that is not one ───────────────────────────── */
214
+
215
+ /**
216
+ * The one z-index in the family, and it is a sum. `dismissable-layer` writes
217
+ * `--slotted-layer-depth` on this element as it opens, so a drawer opened
218
+ * over a dialog is one step above it on the single stack the library keeps
219
+ * rather than a number this stylesheet guessed.
220
+ */
221
+ [data-ui='drawer'][data-mode='overlay'] {
222
+ position: fixed;
223
+ inset: 0;
224
+ z-index: calc(var(--slotted-z-index-overlay, 1000) + var(--slotted-layer-depth, 0));
225
+ }
226
+
227
+ /**
228
+ * Under `inline` there is no layer: the root is a box in the consumer's own
229
+ * layout and its own measure is what opens and closes. `overflow: clip` is
230
+ * what lets the panel keep its measure while the box around it collapses — a
231
+ * panel that reflowed as it closed would rewrap its text on the way out.
232
+ *
233
+ * The sibling content reflows because the consumer's grid says so, which is
234
+ * what replaces Angular Material's `push` and MUI's `persistent`. Neither is
235
+ * a mode here, because a family writing a margin on an element it does not
236
+ * own is a family deciding someone else's layout.
237
+ */
238
+ [data-ui='drawer'][data-mode='inline'] {
239
+ overflow: clip;
240
+ }
241
+
242
+ [data-ui='drawer'][data-mode='inline'][data-side='start'],
243
+ [data-ui='drawer'][data-mode='inline'][data-side='end'] {
244
+ inline-size: 0;
245
+ transition:
246
+ inline-size var(--_move),
247
+ var(--_reveal);
248
+ }
249
+
250
+ [data-ui='drawer'][data-mode='inline'][data-side='top'],
251
+ [data-ui='drawer'][data-mode='inline'][data-side='bottom'] {
252
+ block-size: 0;
253
+ transition:
254
+ block-size var(--_move),
255
+ var(--_reveal);
256
+ }
257
+
258
+ /**
259
+ * The step is what the drawer asks for; the column is what it gets. An
260
+ * `sm` drawer is eighteen rems, and in a two-hundred-and-forty-pixel layout
261
+ * that is the whole of the room and thirty-two pixels more — the content
262
+ * beside it was left with a column narrower than one of its own words.
263
+ * `max-inline-size` is the same clamp the overlay panel already carries as
264
+ * `min(…, 100%)`; it resolves wherever the consumer's column has a size to
265
+ * resolve against, and is inert where the column is sized by the drawer
266
+ * itself.
267
+ *
268
+ * It is written on `[data-open]` alone. Closing, the root's own measure runs
269
+ * to nought, and a cap against a box on its way to nought is what would
270
+ * rewrap the panel on the way out.
271
+ */
272
+ [data-ui='drawer'][data-mode='inline'][data-open][data-side='start'],
273
+ [data-ui='drawer'][data-mode='inline'][data-open][data-side='end'] {
274
+ inline-size: var(--_extent);
275
+ max-inline-size: 100%;
276
+ }
277
+
278
+ [data-ui='drawer'][data-mode='inline'][data-open][data-side='top'],
279
+ [data-ui='drawer'][data-mode='inline'][data-open][data-side='bottom'] {
280
+ block-size: var(--_extent);
281
+ max-block-size: 100%;
282
+ }
283
+
284
+ /**
285
+ * Mounted and closed. `eager` and `lazy` leave the layer in the document and
286
+ * an inline drawer never leaves it at all, so a panel the eye reads as closed
287
+ * would otherwise keep its controls in the tab sequence — the defect MUI's
288
+ * `persistent` variant ships. The rule keys on `inert`, which the binding
289
+ * writes while the panel is closed or exiting, and not on the absence of
290
+ * `data-open`: the entering frame has no `data-open` either, and a hidden
291
+ * panel in that frame refuses the focus scope's initial focus.
292
+ */
293
+ [data-ui='drawer'][inert] {
294
+ content-visibility: hidden;
295
+ pointer-events: none;
296
+ }
297
+
298
+ /* ── The scrim ────────────────────────────────────────────────────────── */
299
+
300
+ /**
301
+ * `--slotted-scrim` is a foundation token the theme restates per scheme and
302
+ * remaps to `Canvas` under forced colours, where it becomes an opaque ground.
303
+ * That is why the panel below carries an edge of its own: every shadow token
304
+ * is `none` in that mode, and a panel separated only by elevation would sit on
305
+ * an opaque ground with nothing marking where it starts.
306
+ */
307
+ [data-ui='drawer'] > [data-part='backdrop'] {
308
+ grid-area: 1 / 1;
309
+ background: var(--slotted-drawer-scrim, var(--slotted-scrim, Canvas));
310
+ opacity: 0;
311
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
312
+ }
313
+
314
+ /**
315
+ * The arrival is the long step on the standard curve, and it is `dialog`'s
316
+ * pair rather than a second answer to the same question — the two halves of
317
+ * one layer arrive together, and `packages/styles/src/scrim-motion.test.mjs`
318
+ * reads one family's declaration and holds the other to it.
319
+ *
320
+ * Leaving is the short step on the exit curve: a reader who has decided to
321
+ * close is waiting, and the page behind is back before the panel has finished
322
+ * going.
323
+ */
324
+ [data-ui='drawer'][data-open] > [data-part='backdrop'] {
325
+ opacity: 1;
326
+ transition: opacity var(--slotted-duration-long) var(--slotted-easing-standard);
327
+ }
328
+
329
+ /* ── The panel ────────────────────────────────────────────────────────── */
330
+
331
+ /**
332
+ * Three rows and, when a resizer is rendered, one track beside them. The
333
+ * middle row is bounded, which is what keeps the actions on screen when the
334
+ * body is long — the failure Vaul, MUI and Angular Material all leave to each
335
+ * consumer's own CSS, because none of the three ships a layout.
336
+ */
337
+ [data-ui='drawer'] > [data-part='panel'] {
338
+ grid-area: 1 / 1;
339
+ box-sizing: border-box;
340
+ /**
341
+ * The measure eases, which is the whole of what a snap point feels like: a
342
+ * release with no transition puts the panel on its stop between two frames
343
+ * and reads as a jump. `--_move` is `instant` while the pointer is down, so
344
+ * the panel still follows the hand, and returns to the enter step the
345
+ * moment the hand leaves.
346
+ */
347
+ transition:
348
+ inline-size var(--_move),
349
+ block-size var(--_move);
350
+ /* The containing block for the separator, on the two sides it lies over. */
351
+ position: relative;
352
+ display: grid;
353
+ grid-template-columns: minmax(0, 1fr);
354
+ min-inline-size: 0;
355
+ min-block-size: 0;
356
+
357
+ /**
358
+ * The inset each row keeps on each edge. A side widens the one its
359
+ * separator lies over, so the words never start under the reader's hand,
360
+ * and `max()` means a size step whose inset is already larger keeps it.
361
+ */
362
+ --_row-padding-inline-start: var(--_padding-inline);
363
+ --_row-padding-inline-end: var(--_padding-inline);
364
+
365
+ background: var(--slotted-drawer-surface, var(--slotted-surface-raised, Canvas));
366
+ /**
367
+ * `--slotted-border-subtle` and not `--slotted-border-strong`.
368
+ *
369
+ * The strong step is the line between two surfaces and the foundation holds
370
+ * it to 3:1 against each of them. Read as one, the panel's edge measured
371
+ * 3.51 against the panel in the dark scheme — a light grey stripe down a
372
+ * near-black surface, brighter than anything else the drawer paints, and
373
+ * the first thing the eye lands on when the panel arrives (DR4).
374
+ *
375
+ * The edge is not that line. What separates the panel from the page is the
376
+ * scrim in front of the page and the elevation the panel is lifted by; the
377
+ * edge is a detail inside the panel's own surface, which is the subtle step
378
+ * — 1.14 against the panel in `dark`, 1.24 in `light`, and the same black
379
+ * as every other line in `high-contrast`, where the query below thickens it
380
+ * to a system border anyway.
381
+ *
382
+ * The boundary that is left is measured rather than assumed: the panel
383
+ * stands 2.48 against the scrim in `dark` and 3.32 in `light`, which is the
384
+ * separation between two surfaces the foundation asks the strong step to
385
+ * carry — and here it is carried by the surfaces themselves. The elevation
386
+ * adds nothing in `dark`, where every shadow token is black on black over a
387
+ * scrim that is already black; it is the scrim, not the shadow, that darkens
388
+ * what lies behind. A theme that wants a heavier edge states
389
+ * `--slotted-drawer-border-color`.
390
+ */
391
+ border-color: var(--slotted-drawer-border-color, var(--slotted-border-subtle, ButtonBorder));
392
+ border-style: solid;
393
+ border-width: 0;
394
+ /* So the leading corners clip the row that scrolls under them. */
395
+ overflow: hidden;
396
+ }
397
+
398
+ /**
399
+ * The exit, and the only physical value in the family.
400
+ *
401
+ * Grid alignment is flow-relative, so the panel lands on the right edge in
402
+ * both directions with no rule of its own. `translate` is not: it takes a
403
+ * physical axis, and a start-side panel leaves toward the left of a
404
+ * left-to-right document and toward the right of a mirrored one.
405
+ */
406
+ [data-ui='drawer'][data-mode='overlay'] > [data-part='panel'] {
407
+ box-shadow: var(--slotted-shadow-lg);
408
+ translate: 0 0;
409
+ /* The measure is restated here, or this rule would replace the base one:
410
+ `transition` is one property and the more specific declaration wins it
411
+ whole. */
412
+ transition:
413
+ translate var(--_slide),
414
+ inline-size var(--_move),
415
+ block-size var(--_move);
416
+ }
417
+
418
+ [data-ui='drawer'][data-mode='overlay']:not([data-open]) > [data-part='panel'] {
419
+ translate: var(--_shift-x, 0) var(--_shift-y, 0);
420
+ }
421
+
422
+ /**
423
+ * While the pointer is down the measure is not eased at all — and *not
424
+ * eased* rather than eased for nought milliseconds, which is still a
425
+ * transition the engine starts and still an animation `getAnimations()`
426
+ * reports. The value under the hand is the reader's own: it is written once
427
+ * per `pointermove` and it animates nothing.
428
+ *
429
+ * R7 forbids a transition on a layout property, and this obeys it rather
430
+ * than excepting itself from it. What R7 governs is an *entrance* — a box
431
+ * that arrives by growing costs a layout on every frame of an animation
432
+ * nobody asked for, and `transform` and `opacity` say the same thing for
433
+ * free. A drag is the other case: the width is the value the reader is
434
+ * holding, there is no keyframe between two of their positions, and a
435
+ * transition here would put the panel behind the hand by the whole duration.
436
+ * The slide stays, because a panel released mid-gesture still has an exit.
437
+ */
438
+ [data-ui='drawer'][data-dragging] > [data-part='panel'] {
439
+ transition: none;
440
+ }
441
+
442
+ [data-ui='drawer'][data-dragging][data-mode='overlay'] > [data-part='panel'] {
443
+ transition: translate var(--_slide);
444
+ }
445
+
446
+ [data-ui='drawer'][data-dragging][data-mode='inline'][data-side='start'],
447
+ [data-ui='drawer'][data-dragging][data-mode='inline'][data-side='end'],
448
+ [data-ui='drawer'][data-dragging][data-mode='inline'][data-side='top'],
449
+ [data-ui='drawer'][data-dragging][data-mode='inline'][data-side='bottom'] {
450
+ transition: var(--_reveal);
451
+ }
452
+
453
+ /**
454
+ * Reduced motion drops the slide and keeps the scrim's fade: the panel is
455
+ * simply there, behind a scrim that still says something opened. The
456
+ * closed panel sits at rest because the layer hides it whole, above.
457
+ */
458
+ @media (prefers-reduced-motion: reduce) {
459
+ [data-ui='drawer'][data-mode='overlay'] > [data-part='panel'] {
460
+ translate: 0 0;
461
+ transition:
462
+ inline-size var(--_move),
463
+ block-size var(--_move);
464
+ }
465
+ }
466
+
467
+ /* ── Where the panel sits, and where it goes when it leaves ───────────── */
468
+
469
+ [data-ui='drawer'][data-side='start'] > [data-part='panel'],
470
+ [data-ui='drawer'][data-side='end'] > [data-part='panel'] {
471
+ align-self: stretch;
472
+ inline-size: min(var(--_extent), 100%);
473
+ grid-template-rows: auto minmax(0, 1fr) auto;
474
+ }
475
+
476
+ [data-ui='drawer'][data-side='top'] > [data-part='panel'],
477
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] {
478
+ justify-self: stretch;
479
+ block-size: min(var(--_extent), 100%);
480
+ }
481
+
482
+ [data-ui='drawer'][data-side='start'] > [data-part='panel'] {
483
+ justify-self: start;
484
+ --_shift-x: -100%;
485
+ /* The resizer lies over this edge, so the rows keep clear of it. */
486
+ --_row-padding-inline-end: max(var(--_padding-inline), var(--_resizer-thickness));
487
+ border-inline-end-width: var(--_rule-size);
488
+ }
489
+
490
+ [data-ui='drawer'][data-side='end'] > [data-part='panel'] {
491
+ justify-self: end;
492
+ --_shift-x: 100%;
493
+ --_row-padding-inline-start: max(var(--_padding-inline), var(--_resizer-thickness));
494
+ border-inline-start-width: var(--_rule-size);
495
+ }
496
+
497
+ /**
498
+ * A sheet keeps its leading corners and a side drawer keeps none. An
499
+ * edge-anchored panel that spans the whole block extent reads as a wall rather
500
+ * than as a card, which is what Fluent, Ant, MUI and Angular Material all
501
+ * draw; a sheet coming up from the bottom is the one case where the two
502
+ * corners the reader can see are what say it is a sheet.
503
+ *
504
+ * On a block side the resizer is a row of its own — see the section below for
505
+ * why it is a row here and an overlay on an inline side — so the four rows are
506
+ * placed explicitly rather than left to fall in DOM order. A panel with no
507
+ * resizer would otherwise put its body in an `auto` track and lose the one
508
+ * declaration that keeps the footer on screen.
509
+ */
510
+ [data-ui='drawer'][data-side='top'] > [data-part='panel'] {
511
+ align-self: start;
512
+ grid-template-rows: auto minmax(0, 1fr) auto auto;
513
+ --_shift-y: -100%;
514
+ border-block-end-width: var(--_rule-size);
515
+ border-end-start-radius: var(--slotted-drawer-radius, var(--_radius));
516
+ border-end-end-radius: var(--slotted-drawer-radius, var(--_radius));
517
+ }
518
+
519
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] {
520
+ align-self: end;
521
+ grid-template-rows: auto auto minmax(0, 1fr) auto;
522
+ --_shift-y: 100%;
523
+ border-block-start-width: var(--_rule-size);
524
+ border-start-start-radius: var(--slotted-drawer-radius, var(--_radius));
525
+ border-start-end-radius: var(--slotted-drawer-radius, var(--_radius));
526
+ }
527
+
528
+ /**
529
+ * A sheet's separator takes the first row, so the three that follow move down
530
+ * one. `top` needs no such rule: its separator is the fourth row and the
531
+ * three above it are already where the base puts them.
532
+ */
533
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] > [data-part='header'] {
534
+ grid-row: 2;
535
+ }
536
+
537
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] > [data-part='body'] {
538
+ grid-row: 3;
539
+ }
540
+
541
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] > [data-part='footer'] {
542
+ grid-row: 4;
543
+ }
544
+
545
+ /* ── The sheet ────────────────────────────────────────────────────────── */
546
+
547
+ /**
548
+ * A sheet on a wide layer is a card over the page, not a strip across it:
549
+ * forty rems, Material 3's 640 dp, centred, with the scrim on both sides. The
550
+ * edge then shows on three sides, so the panel draws it on three; on a phone
551
+ * the two side edges sit on the screen's own and read as nothing.
552
+ */
553
+ [data-ui='drawer'][data-mode='overlay'][data-side='top'] > [data-part='panel'],
554
+ [data-ui='drawer'][data-mode='overlay'][data-side='bottom'] > [data-part='panel'] {
555
+ justify-self: center;
556
+ inline-size: min(
557
+ 100%,
558
+ var(--slotted-drawer-sheet-inline-size, var(--slotted-drawer-extent-xl, 40rem))
559
+ );
560
+ border-inline-width: var(--_rule-size);
561
+ }
562
+
563
+ /**
564
+ * `size="content"`: the panel measures what it holds, which is what every
565
+ * native sheet does and no named step can. `auto` on a block side and
566
+ * `max-content` on an inline one, capped four rems short of the layer so a
567
+ * strip of scrim is always left for the press outside.
568
+ *
569
+ * The cap reads `--slotted-drawer-extent` first. Once the reader has moved the
570
+ * sheet the binding writes that length, and a sheet dragged to the whole layer
571
+ * is the whole layer; unset, the cap is the strip.
572
+ *
573
+ * `interpolate-size` is what lets a height of `auto` transition — content that
574
+ * arrives while the sheet is open, and a release back onto the content stop. A
575
+ * browser without it jumps, and nothing else changes.
576
+ */
577
+ [data-ui='drawer'][data-size='content'] {
578
+ --_step: auto;
579
+ --_content-cap: min(
580
+ var(--slotted-drawer-extent, calc(100% - var(--slotted-space-12, 4rem))),
581
+ 100%
582
+ );
583
+ interpolate-size: allow-keywords;
584
+ }
585
+
586
+ [data-ui='drawer'][data-size='content'][data-side='start'],
587
+ [data-ui='drawer'][data-size='content'][data-side='end'] {
588
+ --_step: max-content;
589
+ }
590
+
591
+ [data-ui='drawer'][data-size='content'][data-side='start'] > [data-part='panel'],
592
+ [data-ui='drawer'][data-size='content'][data-side='end'] > [data-part='panel'] {
593
+ inline-size: var(--_extent);
594
+ max-inline-size: var(--_content-cap);
595
+ }
596
+
597
+ [data-ui='drawer'][data-size='content'][data-side='top'] > [data-part='panel'],
598
+ [data-ui='drawer'][data-size='content'][data-side='bottom'] > [data-part='panel'] {
599
+ block-size: var(--_extent);
600
+ max-block-size: var(--_content-cap);
601
+ }
602
+
603
+ /**
604
+ * The virtual keyboard. `observeKeyboardInset` in `@slotted/core/measure` measures what the
605
+ * keyboard covers and the binding writes it here while the layer is open; the
606
+ * cell the panel sits in shrinks by it, so a sheet lands above the keys and a
607
+ * side panel's footer does too, and every `100%` cap shrinks with the cell.
608
+ */
609
+ [data-ui='drawer'][data-mode='overlay'] {
610
+ padding-block-end: var(--slotted-keyboard-inset, 0px);
611
+ }
612
+
613
+ /**
614
+ * The scrim a sheet leaves off below the stop the consumer named
615
+ * (`scrimFrom`): the map behind a sheet at its lowest stop is the page, and a
616
+ * press on it reaches it. The opacity keeps the transition the scrim already
617
+ * has, so crossing the stop fades rather than blinks.
618
+ */
619
+ [data-ui='drawer'][data-undimmed] > [data-part='backdrop'] {
620
+ opacity: 0;
621
+ pointer-events: none;
622
+ }
623
+
624
+ /* The layer is fixed over the whole viewport, so the scrim alone letting the
625
+ press through leaves the root to catch it; the panel takes it back. */
626
+ [data-ui='drawer'][data-undimmed] {
627
+ pointer-events: none;
628
+ }
629
+
630
+ [data-ui='drawer'][data-undimmed] > [data-part='panel'] {
631
+ pointer-events: auto;
632
+ }
633
+
634
+ /**
635
+ * A sheet with no footer: the body is the last row and the home indicator
636
+ * sits over its end. The inset is padding inside the scrollport, so the last
637
+ * line scrolls clear of the indicator rather than stopping under it.
638
+ */
639
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] > [data-part='body']:last-child {
640
+ padding-block-end: calc(var(--_padding-block) + env(safe-area-inset-bottom, 0px));
641
+ }
642
+
643
+ /** `start` is the left edge in one direction and the right edge in the other. */
644
+ [data-ui='drawer'][data-side='start']:dir(rtl) > [data-part='panel'] {
645
+ --_shift-x: 100%;
646
+ }
647
+
648
+ [data-ui='drawer'][data-side='end']:dir(rtl) > [data-part='panel'] {
649
+ --_shift-x: -100%;
650
+ }
651
+
652
+ /**
653
+ * `full` is the panel covering the layer, so its inner edge separates it from
654
+ * nothing and the hairline it would draw sits on the edge of the screen —
655
+ * which on a phone reads as a stray line under the browser chrome. `dialog`
656
+ * removed the same line from its own full-screen step for the same reason.
657
+ *
658
+ * A step the layer is too narrow to honour is clamped by `min(…, 100%)` and
659
+ * still draws its edge. CSS cannot compare a resolved length to its
660
+ * container, and the four container queries that would answer it — one per
661
+ * step, because a container condition cannot hold a custom property — buy one
662
+ * hairline at the extreme edge of the viewport.
663
+ */
664
+ [data-ui='drawer'][data-size='full'] > [data-part='panel'] {
665
+ border-width: 0;
666
+ border-radius: 0;
667
+ }
668
+
669
+ /**
670
+ * Under `inline` the panel keeps the measure the root is collapsing away
671
+ * from, which is what `overflow: clip` on the root is for. `min(…, 100%)`
672
+ * would resolve against a box that is on its way to nought.
673
+ */
674
+ [data-ui='drawer'][data-mode='inline'][data-side='start'] > [data-part='panel'],
675
+ [data-ui='drawer'][data-mode='inline'][data-side='end'] > [data-part='panel'] {
676
+ inline-size: var(--_extent);
677
+ }
678
+
679
+ [data-ui='drawer'][data-mode='inline'][data-side='top'] > [data-part='panel'],
680
+ [data-ui='drawer'][data-mode='inline'][data-side='bottom'] > [data-part='panel'] {
681
+ block-size: var(--_extent);
682
+ }
683
+
684
+ /**
685
+ * Open, the root is a settled measure and the panel takes it rather than the
686
+ * step it asked for: a root the column clamped would otherwise clip a panel
687
+ * that is still drawing itself at the full step, and a panel cut in half is
688
+ * a worse picture than one that wrapped. The cap is on `[data-open]` for the
689
+ * reason the root's is.
690
+ */
691
+ [data-ui='drawer'][data-mode='inline'][data-open][data-side='start'] > [data-part='panel'],
692
+ [data-ui='drawer'][data-mode='inline'][data-open][data-side='end'] > [data-part='panel'] {
693
+ max-inline-size: 100%;
694
+ }
695
+
696
+ [data-ui='drawer'][data-mode='inline'][data-open][data-side='top'] > [data-part='panel'],
697
+ [data-ui='drawer'][data-mode='inline'][data-open][data-side='bottom'] > [data-part='panel'] {
698
+ max-block-size: 100%;
699
+ }
700
+
701
+ /* ── The separator the reader moves ───────────────────────────────────── */
702
+
703
+ /**
704
+ * A focusable `separator`, which is the widget form of the role, from the
705
+ * WAI-ARIA Window Splitter pattern. Ant's resizer and Vaul's snap dragging
706
+ * are both pointer-only; this one is a target a keyboard can reach, which is
707
+ * what `docs/component-features.md` §2.8 asks of every resize.
708
+ *
709
+ * **It draws something at rest, and that is a correction.** It used to be
710
+ * four pixels of nothing until a hand arrived, and then a two-pixel line in
711
+ * the accent border — no grip, no dots, no mark — on the argument that a
712
+ * separator drawing anything at rest is a decoration on an edge nobody is
713
+ * touching. Both halves of that were wrong. A resizable edge nobody can see
714
+ * is a capability nobody finds, and it was reported as exactly that; and the
715
+ * accent is the one colour in the library that means "the keyboard is here",
716
+ * so spending it on hover left nothing to say at focus.
717
+ *
718
+ * The picture is `splitter`'s, which is the family that settled it: two
719
+ * hairlines in the strong border step with a grip between them, running the
720
+ * whole gutter under the hand, and the accent reserved for the focus ring.
721
+ * SAP's own splitter is where both families read it. The two are written
722
+ * twice rather than shared, because `packages/styles/src/identity.test.mjs`
723
+ * forbids one family reaching into another's rules — about thirty lines, and
724
+ * the cost of two families that can move apart.
725
+ *
726
+ * **A sheet is held differently, so it draws differently.** On a block side
727
+ * the lines are not painted at all and the grip becomes a pill above the
728
+ * header, which is where a hand looks for a sheet. The old gutter put the
729
+ * gesture on the bottom edge of the screen, which is nowhere a thumb goes
730
+ * looking.
731
+ *
732
+ * **The pointer reach is not the picture.** `::before` is a transparent box
733
+ * of `--slotted-target-min`, centred on the gutter, so the target holds at
734
+ * the WCAG 2.2 floor while the picture stays thin. Centred rather than run
735
+ * inward: inward it would take twenty pixels of the panel and, on a sheet,
736
+ * sit over the header.
737
+ *
738
+ * **It is a row on a block side and an overlay on an inline side**, and the
739
+ * asymmetry has one cause: every rule in this panel runs across the inline
740
+ * axis. A resizer row above the header or below the footer crosses none of
741
+ * them, while a resizer *column* would cut all of them — the first capture
742
+ * showed exactly that, a rule under the header stopping short of the panel's
743
+ * own edge, which reads as an unfinished line rather than as a track. A line
744
+ * cannot cross an opaque box, so on an inline side the separator lies over
745
+ * the panel instead and the rows keep clear of it with
746
+ * `--_row-padding-inline-*`.
747
+ *
748
+ * The cost is written rather than hidden: on `side="start"` the separator
749
+ * sits at the panel's inline-end edge, which is where a left-to-right
750
+ * document also puts the body's scrollbar. The wheel still scrolls and the
751
+ * scrollbar's inner half is still reachable, and every desktop application
752
+ * with a resizable sidebar has the same overlap.
753
+ *
754
+ * `touch-action: none` so a drag is not read as a scroll on the way past.
755
+ */
756
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='resizer'] {
757
+ display: flex;
758
+ align-items: center;
759
+ justify-content: center;
760
+ gap: min(var(--slotted-space-9, 2rem), 12%);
761
+ background: transparent;
762
+ touch-action: none;
763
+
764
+ /**
765
+ * The two steps, and the whole of the colour decision — the same one
766
+ * `splitter` makes, because the two are one picture in two families.
767
+ *
768
+ * The strong border step at rest, not the subtle one and not the accent.
769
+ * The accent is the one colour in the library that means "the keyboard is
770
+ * here", and this family used to spend it on hover as well: a two-pixel
771
+ * accent line appearing under the hand on an edge that drew nothing at all
772
+ * before it. Both halves of that were wrong. An edge nobody can see is a
773
+ * capability nobody finds, and an accent spent on hover has nothing left to
774
+ * say at focus.
775
+ */
776
+ --_rest: var(--slotted-drawer-resizer-line-color, var(--slotted-border-strong, ButtonBorder));
777
+ --_active: var(
778
+ --slotted-drawer-resizer-line-active-color,
779
+ var(--slotted-text-muted, ButtonText)
780
+ );
781
+ }
782
+
783
+ /* Under the hand the gap closes and the marks read as one. */
784
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='resizer']:not([data-disabled]):hover,
785
+ [data-ui='drawer']
786
+ > [data-part='panel']
787
+ > [data-part='resizer']:not([data-disabled]):focus-visible,
788
+ [data-ui='drawer'][data-dragging] > [data-part='panel'] > [data-part='resizer'] {
789
+ gap: var(--slotted-space-4, 0.5rem);
790
+ }
791
+
792
+ [data-ui='drawer'][data-side='start'] > [data-part='panel'] > [data-part='resizer'],
793
+ [data-ui='drawer'][data-side='end'] > [data-part='panel'] > [data-part='resizer'] {
794
+ flex-direction: column;
795
+ }
796
+
797
+ /**
798
+ * The reach. Transparent, centred on the gutter and above the panel's own
799
+ * rows, which is what makes a four-pixel picture a twenty-four-pixel target.
800
+ */
801
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='resizer']::before {
802
+ content: '';
803
+ position: absolute;
804
+ --_reach-overhang: calc((var(--_resizer-reach) - var(--_resizer-thickness)) / -2);
805
+ }
806
+
807
+ [data-ui='drawer'][data-side='start'] > [data-part='panel'] > [data-part='resizer']::before,
808
+ [data-ui='drawer'][data-side='end'] > [data-part='panel'] > [data-part='resizer']::before {
809
+ inset-block: 0;
810
+ inset-inline: var(--_reach-overhang);
811
+ }
812
+
813
+ [data-ui='drawer'][data-side='top'] > [data-part='panel'] > [data-part='resizer']::before,
814
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] > [data-part='resizer']::before {
815
+ inset-inline: 0;
816
+ inset-block: var(--_reach-overhang);
817
+ }
818
+
819
+ [data-ui='drawer'][data-side='start'] > [data-part='panel'] > [data-part='resizer'],
820
+ [data-ui='drawer'][data-side='end'] > [data-part='panel'] > [data-part='resizer'] {
821
+ position: absolute;
822
+ inset-block: 0;
823
+ inline-size: var(--_resizer-thickness);
824
+ cursor: col-resize;
825
+ }
826
+
827
+ [data-ui='drawer'][data-side='start'] > [data-part='panel'] > [data-part='resizer'] {
828
+ inset-inline-end: 0;
829
+ }
830
+
831
+ [data-ui='drawer'][data-side='end'] > [data-part='panel'] > [data-part='resizer'] {
832
+ inset-inline-start: 0;
833
+ }
834
+
835
+ [data-ui='drawer'][data-side='top'] > [data-part='panel'] > [data-part='resizer'],
836
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] > [data-part='resizer'] {
837
+ position: relative;
838
+ block-size: var(--_resizer-thickness);
839
+ cursor: row-resize;
840
+ }
841
+
842
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] > [data-part='resizer'] {
843
+ grid-row: 1;
844
+ }
845
+
846
+ [data-ui='drawer'][data-side='top'] > [data-part='panel'] > [data-part='resizer'] {
847
+ grid-row: 4;
848
+ }
849
+
850
+ /**
851
+ * The lines, on an inline side only. A hairline each side of the grip, fading
852
+ * to transparent away from it, running the whole gutter once a hand is on
853
+ * them. `splitter`'s picture, and its arithmetic too: a resting length of
854
+ * `min(4rem, 20%)` leaves slack at every gutter length, and it is that slack
855
+ * the hand claims.
856
+ *
857
+ * A sheet paints none of them — see the grabber below.
858
+ */
859
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='resizer'] > [data-part='line'] {
860
+ flex: 0 1 min(var(--slotted-space-12, 4rem), 20%);
861
+ min-block-size: 0;
862
+ min-inline-size: 0;
863
+ inline-size: var(--slotted-border-width-thin, 1px);
864
+ border-radius: var(--slotted-radius-sm, 2px);
865
+ transition:
866
+ flex-basis var(--slotted-control-transition-duration, var(--slotted-duration-short))
867
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
868
+ background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
869
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
870
+ }
871
+
872
+ [data-ui='drawer'][data-side='start']
873
+ > [data-part='panel']
874
+ > [data-part='resizer']
875
+ > [data-part='line']:first-child,
876
+ [data-ui='drawer'][data-side='end']
877
+ > [data-part='panel']
878
+ > [data-part='resizer']
879
+ > [data-part='line']:first-child {
880
+ background: linear-gradient(to bottom, transparent, var(--_rest));
881
+ }
882
+
883
+ [data-ui='drawer'][data-side='start']
884
+ > [data-part='panel']
885
+ > [data-part='resizer']
886
+ > [data-part='line']:last-child,
887
+ [data-ui='drawer'][data-side='end']
888
+ > [data-part='panel']
889
+ > [data-part='resizer']
890
+ > [data-part='line']:last-child {
891
+ background: linear-gradient(to top, transparent, var(--_rest));
892
+ }
893
+
894
+ [data-ui='drawer']
895
+ > [data-part='panel']
896
+ > [data-part='resizer']:not([data-disabled]):hover
897
+ > [data-part='line'],
898
+ [data-ui='drawer']
899
+ > [data-part='panel']
900
+ > [data-part='resizer']:not([data-disabled]):focus-visible
901
+ > [data-part='line'],
902
+ [data-ui='drawer'][data-dragging]
903
+ > [data-part='panel']
904
+ > [data-part='resizer']
905
+ > [data-part='line'] {
906
+ flex-basis: 100%;
907
+ background: var(--_active);
908
+ }
909
+
910
+ /**
911
+ * The grip, and the two pictures it takes.
912
+ *
913
+ * On an inline side it is `splitter`'s dot matrix: a mark on an edge, small
914
+ * enough to read as a texture rather than as a control.
915
+ *
916
+ * On a block side it is a pill, and the whole separator becomes it — the
917
+ * grabber a hand looks for at the top of a sheet. The lines are not painted
918
+ * there because a sheet is not held at its corners; it is held in the middle,
919
+ * which is where every mobile sheet in the field puts its mark.
920
+ */
921
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='resizer'] > [data-part='grip'] {
922
+ flex: none;
923
+ transition:
924
+ background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
925
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
926
+ color var(--slotted-control-transition-duration, var(--slotted-duration-short))
927
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
928
+ }
929
+
930
+ [data-ui='drawer'][data-side='start']
931
+ > [data-part='panel']
932
+ > [data-part='resizer']
933
+ > [data-part='grip'],
934
+ [data-ui='drawer'][data-side='end']
935
+ > [data-part='panel']
936
+ > [data-part='resizer']
937
+ > [data-part='grip'] {
938
+ color: var(--_rest);
939
+ background-image: radial-gradient(circle at center, currentColor 40%, transparent 42%);
940
+ background-size: var(--slotted-space-2, 0.25rem) var(--slotted-space-2, 0.25rem);
941
+ inline-size: calc(2 * var(--slotted-space-2, 0.25rem));
942
+ block-size: calc(3 * var(--slotted-space-2, 0.25rem));
943
+ }
944
+
945
+ /* The grabber. Four by thirty-six, full radius, centred above the header. */
946
+ [data-ui='drawer'][data-side='top']
947
+ > [data-part='panel']
948
+ > [data-part='resizer']
949
+ > [data-part='grip'],
950
+ [data-ui='drawer'][data-side='bottom']
951
+ > [data-part='panel']
952
+ > [data-part='resizer']
953
+ > [data-part='grip'] {
954
+ background-color: var(--_rest);
955
+ border-radius: var(--slotted-radius-full, 9999px);
956
+ block-size: var(--slotted-space-2, 0.25rem);
957
+ inline-size: var(--slotted-space-11, 3rem);
958
+ }
959
+
960
+ /* A sheet's lines are not painted: the pill is the whole picture. */
961
+ [data-ui='drawer'][data-side='top']
962
+ > [data-part='panel']
963
+ > [data-part='resizer']
964
+ > [data-part='line'],
965
+ [data-ui='drawer'][data-side='bottom']
966
+ > [data-part='panel']
967
+ > [data-part='resizer']
968
+ > [data-part='line'] {
969
+ display: none;
970
+ }
971
+
972
+ /**
973
+ * The dot matrix recolours its ink and nothing else: its dots are
974
+ * `currentColor` over a transparent box, and a `background-color` here filled
975
+ * the box — the grip became a solid block under the hand, under hover and
976
+ * under focus. `splitter`, which draws the same picture, only ever changed
977
+ * `color`. The pill on a block side is a filled box, so it recolours its fill.
978
+ */
979
+ [data-ui='drawer']
980
+ > [data-part='panel']
981
+ > [data-part='resizer']:not([data-disabled]):hover
982
+ > [data-part='grip'],
983
+ [data-ui='drawer']
984
+ > [data-part='panel']
985
+ > [data-part='resizer']:not([data-disabled]):focus-visible
986
+ > [data-part='grip'],
987
+ [data-ui='drawer'][data-dragging]
988
+ > [data-part='panel']
989
+ > [data-part='resizer']
990
+ > [data-part='grip'] {
991
+ color: var(--_active);
992
+ }
993
+
994
+ [data-ui='drawer'][data-side='top']
995
+ > [data-part='panel']
996
+ > [data-part='resizer']:not([data-disabled]):hover
997
+ > [data-part='grip'],
998
+ [data-ui='drawer'][data-side='bottom']
999
+ > [data-part='panel']
1000
+ > [data-part='resizer']:not([data-disabled]):hover
1001
+ > [data-part='grip'],
1002
+ [data-ui='drawer'][data-side='top']
1003
+ > [data-part='panel']
1004
+ > [data-part='resizer']:not([data-disabled]):focus-visible
1005
+ > [data-part='grip'],
1006
+ [data-ui='drawer'][data-side='bottom']
1007
+ > [data-part='panel']
1008
+ > [data-part='resizer']:not([data-disabled]):focus-visible
1009
+ > [data-part='grip'],
1010
+ [data-ui='drawer'][data-dragging][data-side='top']
1011
+ > [data-part='panel']
1012
+ > [data-part='resizer']
1013
+ > [data-part='grip'],
1014
+ [data-ui='drawer'][data-dragging][data-side='bottom']
1015
+ > [data-part='panel']
1016
+ > [data-part='resizer']
1017
+ > [data-part='grip'] {
1018
+ background-color: var(--_active);
1019
+ }
1020
+
1021
+ /**
1022
+ * Hover and press are held back for `disabled`, which this separator does not
1023
+ * have today. The guard is the shared rule from
1024
+ * `packages/styles/src/states.test.mjs`, and it is written here so that a
1025
+ * disabled resize, if one ever arrives, cannot ship without it.
1026
+ */
1027
+ /**
1028
+ * The ring stays on the gutter and it fills it: a two-pixel ring inset into
1029
+ * a four-pixel box leaves nothing between the two edges, so a focused
1030
+ * separator is a four-pixel bar and a hovered one is the two-pixel line down
1031
+ * its middle. The two pictures differ, which is all the ring has to do here.
1032
+ *
1033
+ * It cannot be drawn on the reach instead. `packages/styles/src/fallbacks.test.mjs`
1034
+ * forbids a system colour inside `::before` or `::after`, and the ring's own
1035
+ * last resort is `Highlight`.
1036
+ */
1037
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='resizer']:focus-visible {
1038
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
1039
+ outline-offset: calc(-1 * var(--slotted-focus-ring-offset, 2px));
1040
+ }
1041
+
1042
+ /**
1043
+ * The one place this family asks the compositor for anything, and only while
1044
+ * the pointer is down. `will-change` on a box that is not moving costs a
1045
+ * layer for nothing; on the box the hand is dragging it is the difference
1046
+ * between a resize the engine prepares for and one it discovers each frame.
1047
+ * It is written on the panel because the panel is what changes size.
1048
+ */
1049
+ [data-ui='drawer'][data-dragging][data-side='start'] > [data-part='panel'],
1050
+ [data-ui='drawer'][data-dragging][data-side='end'] > [data-part='panel'] {
1051
+ will-change: inline-size;
1052
+ }
1053
+
1054
+ [data-ui='drawer'][data-dragging][data-side='top'] > [data-part='panel'],
1055
+ [data-ui='drawer'][data-dragging][data-side='bottom'] > [data-part='panel'] {
1056
+ will-change: block-size;
1057
+ }
1058
+
1059
+ /* ── The header ───────────────────────────────────────────────────────── */
1060
+
1061
+ /**
1062
+ * Two columns: the words and the close button. The button keeps a column of
1063
+ * its own, so a title that wraps to two lines grows the row instead of
1064
+ * pushing it off the line.
1065
+ *
1066
+ * The header draws no rule of its own. Every rule in the panel belongs to the
1067
+ * row *below* it, which is what makes the count right for every combination a
1068
+ * consumer can write: a header, a body and a footer draw two rules; a header
1069
+ * and a footer alone draw one; and a panel that is only a body draws none.
1070
+ */
1071
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='header'] {
1072
+ grid-row: 1;
1073
+ display: grid;
1074
+ grid-template-columns: minmax(0, 1fr) auto;
1075
+ align-items: start;
1076
+ column-gap: var(--_gap);
1077
+ row-gap: var(--slotted-space-2, 0.25rem);
1078
+ padding-block: var(--_padding-block);
1079
+ padding-inline: var(--_row-padding-inline-start) var(--_row-padding-inline-end);
1080
+ }
1081
+
1082
+ /**
1083
+ * `overflow-wrap: anywhere` and a zero minimum are the pair that answer a
1084
+ * hostile string. A ninety-character unbroken token in a title widens the
1085
+ * panel past its own measure, and a panel pinned to an edge then pushes its
1086
+ * inner edge across the layer.
1087
+ */
1088
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='header'] > [data-part='title'] {
1089
+ grid-column: 1;
1090
+ grid-row: 1;
1091
+ margin: 0;
1092
+ min-inline-size: 0;
1093
+ overflow-wrap: anywhere;
1094
+ font-family: var(
1095
+ --slotted-drawer-title-font-family,
1096
+ var(
1097
+ --slotted-font-family-heading,
1098
+ var(--slotted-font-family-sans, ui-sans-serif, system-ui, sans-serif)
1099
+ )
1100
+ );
1101
+ font-size: var(--_title-font-size);
1102
+ font-weight: var(--slotted-font-weight-semibold, 600);
1103
+ line-height: var(--slotted-line-height-lg, 1.5);
1104
+ color: var(--slotted-drawer-title-color, var(--slotted-text, CanvasText));
1105
+ }
1106
+
1107
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='header'] > [data-part='description'] {
1108
+ grid-column: 1;
1109
+ grid-row: 2;
1110
+ margin: 0;
1111
+ min-inline-size: 0;
1112
+ overflow-wrap: anywhere;
1113
+ font-size: var(--_description-font-size);
1114
+ line-height: var(--slotted-line-height-md, 1.5);
1115
+ color: var(--slotted-drawer-description-color, var(--slotted-text-muted, GrayText));
1116
+ }
1117
+
1118
+ /* ── The close button ─────────────────────────────────────────────────── */
1119
+
1120
+ /**
1121
+ * An inner control, not a button (R2): the box is the glyph plus a step of
1122
+ * air on each side, floored at the WCAG 2.2 minimum, and the glyph is
1123
+ * centred in it. It was the control height before, a thirty-four pixel
1124
+ * square beside a sixteen pixel mark. The glyph is the binding's `Glyph` in
1125
+ * an `icon` box, sized here through the icon token so the box and the
1126
+ * formula above read one length.
1127
+ */
1128
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='header'] > [data-part='close'] {
1129
+ --slotted-icon-size: var(--_icon-size);
1130
+
1131
+ grid-column: 2;
1132
+ grid-row: 1 / span 2;
1133
+ align-self: start;
1134
+ display: inline-grid;
1135
+ place-items: center;
1136
+ block-size: var(--_close-size);
1137
+ inline-size: var(--_close-size);
1138
+ /* The box overhangs the panel's edge by its own air, so the mark aligns
1139
+ with the title. */
1140
+ margin-inline-end: calc(-1 * (var(--_close-size) - var(--_icon-size)) / 2);
1141
+ padding: 0;
1142
+ appearance: none;
1143
+ background-color: transparent;
1144
+ border: 0;
1145
+ border-radius: var(--slotted-control-radius, var(--slotted-radius-sm, 4px));
1146
+ color: var(--slotted-drawer-close-color, var(--slotted-text-muted, GrayText));
1147
+ cursor: pointer;
1148
+ transition:
1149
+ background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
1150
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
1151
+ color var(--slotted-control-transition-duration, var(--slotted-duration-short))
1152
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
1153
+ }
1154
+
1155
+ /* On a finger the target is the comfortable size in both axes and the box
1156
+ is the one a mouse sees: a transparent `::before` centred on the control
1157
+ reaches 44 pixels, and the ground and the ring stay square. */
1158
+ @media (pointer: coarse) {
1159
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='header'] > [data-part='close'] {
1160
+ position: relative;
1161
+ }
1162
+
1163
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='header'] > [data-part='close']::before {
1164
+ content: '';
1165
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
1166
+ position: absolute;
1167
+ }
1168
+ }
1169
+
1170
+ [data-ui='drawer']
1171
+ > [data-part='panel']
1172
+ > [data-part='header']
1173
+ > [data-part='close']:not(:disabled):hover {
1174
+ /* The ground moves under the pointer and nothing else does (R2). */
1175
+ background-color: var(
1176
+ --slotted-drawer-close-hover-background,
1177
+ var(--slotted-tone-secondary-subtle-hover)
1178
+ );
1179
+ }
1180
+
1181
+ [data-ui='drawer']
1182
+ > [data-part='panel']
1183
+ > [data-part='header']
1184
+ > [data-part='close']:not(:disabled):active {
1185
+ background-color: var(
1186
+ --slotted-drawer-close-active-background,
1187
+ var(--slotted-tone-secondary-subtle-active)
1188
+ );
1189
+ }
1190
+
1191
+ [data-ui='drawer']
1192
+ > [data-part='panel']
1193
+ > [data-part='header']
1194
+ > [data-part='close']:focus-visible {
1195
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
1196
+ outline-offset: calc(-1 * var(--slotted-focus-ring-offset, 2px));
1197
+ }
1198
+
1199
+ /* ── The row that scrolls ─────────────────────────────────────────────── */
1200
+
1201
+ /**
1202
+ * `overscroll-behavior: contain` is what keeps a wheel that reaches the end of
1203
+ * this box from scrolling the page behind it. The scroll lock in
1204
+ * `dismissable-layer` covers the modal case; this covers the other two, where
1205
+ * the page behind is not locked at all.
1206
+ */
1207
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='body'] {
1208
+ grid-row: 2;
1209
+ min-block-size: 0;
1210
+ min-inline-size: 0;
1211
+ overflow-x: hidden;
1212
+ overflow-y: auto;
1213
+ overflow-inline: hidden;
1214
+ overflow-block: auto;
1215
+ overscroll-behavior: contain;
1216
+ padding-block: var(--_padding-block);
1217
+ padding-inline: var(--_row-padding-inline-start) var(--_row-padding-inline-end);
1218
+ font-size: var(--_body-font-size);
1219
+ line-height: var(--slotted-line-height-md, 1.5);
1220
+ color: var(--slotted-drawer-body-color, var(--slotted-text, CanvasText));
1221
+ }
1222
+
1223
+ /* Content that brings its own inset — a page, a list on the panel's edges. */
1224
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='body'][data-inset='none'] {
1225
+ padding: 0;
1226
+ }
1227
+
1228
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='body']:not(:first-child) {
1229
+ border-block-start: var(--_rule-size) solid
1230
+ var(--slotted-drawer-rule-color, var(--slotted-border-subtle, ButtonBorder));
1231
+ }
1232
+
1233
+ /**
1234
+ * Content that is not there yet. Only Ant among the surveyed implementations
1235
+ * has a state for it, so elsewhere a consumer renders a spinner conditionally
1236
+ * and the panel jumps to size when the data lands. The row keeps a floor here
1237
+ * instead, and the header, the close button, Escape and the resizer all stay
1238
+ * operable: a loading drawer a reader cannot leave is the worse failure.
1239
+ */
1240
+ [data-ui='drawer'][data-loading] > [data-part='panel'] > [data-part='body'] {
1241
+ min-block-size: var(--slotted-space-12, 4rem);
1242
+ }
1243
+
1244
+ /* ── The actions ──────────────────────────────────────────────────────── */
1245
+
1246
+ /**
1247
+ * `flex-wrap` rather than a grid: two actions that do not fit on one line
1248
+ * stack, and the primary one stays last in both readings. This row is the
1249
+ * reason the middle one is bounded — an unbounded panel pushes it past the
1250
+ * fold, which is the whole failure the three-row grid exists to prevent.
1251
+ */
1252
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='footer'] {
1253
+ grid-row: 3;
1254
+ display: flex;
1255
+ flex-wrap: wrap;
1256
+ align-items: center;
1257
+ justify-content: flex-end;
1258
+ gap: var(--_gap);
1259
+ padding-block: var(--_padding-block);
1260
+ padding-inline: var(--_row-padding-inline-start) var(--_row-padding-inline-end);
1261
+ background: var(--slotted-drawer-footer-surface, var(--slotted-surface-sunken, Canvas));
1262
+ }
1263
+
1264
+ /**
1265
+ * The edge of the device, on a sheet.
1266
+ *
1267
+ * A bottom sheet's footer sits where the home indicator and the gesture bar
1268
+ * live, and a row of actions under them is a row the reader taps twice: once
1269
+ * on the system and once on the button. `env()` resolves to nought wherever
1270
+ * there is no inset, so this costs nothing on a desktop and there is no query
1271
+ * to keep in step.
1272
+ *
1273
+ * Only `bottom`, and only the footer. A sheet from the top has the status bar
1274
+ * above it and the header already clears it; the body scrolls, so an inset on
1275
+ * it would be padding inside a scrollport rather than under it.
1276
+ */
1277
+ [data-ui='drawer'][data-side='bottom'] > [data-part='panel'] > [data-part='footer'] {
1278
+ padding-block-end: calc(
1279
+ var(--_footer-padding-block, var(--_padding-block)) + env(safe-area-inset-bottom, 0px)
1280
+ );
1281
+ }
1282
+
1283
+ [data-ui='drawer'] > [data-part='panel'] > [data-part='footer']:not(:first-child) {
1284
+ border-block-start: var(--_rule-size) solid
1285
+ var(--slotted-drawer-rule-color, var(--slotted-border-subtle, ButtonBorder));
1286
+ }
1287
+
1288
+ /* ── Forced colours ───────────────────────────────────────────────────── */
1289
+
1290
+ /**
1291
+ * The theme remaps every token this family reads, so the only rule here is the
1292
+ * one the theme cannot reach: `--slotted-scrim` becomes `Canvas`, an opaque
1293
+ * ground, and the panel on it needs an edge the shadow no longer draws. The
1294
+ * border above already states one; this thickens it to the weight a system
1295
+ * border is read at, and gives the panel the three edges its side does not.
1296
+ */
1297
+ @media (forced-colors: active) {
1298
+ [data-ui='drawer'] > [data-part='panel'] {
1299
+ border-width: var(--slotted-border-width-thick, 2px);
1300
+ border-color: CanvasText;
1301
+ }
1302
+ }
1303
+ }