@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,968 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * A window over the page, and the scrim that says the page is out of reach.
4
+ *
5
+ * Three boxes, and the third one is not decoration:
6
+ *
7
+ * ┌─ root ───────────────────────────────────────────────┐
8
+ * │ ┌─ backdrop ─────────────────────────────────────────┐│
9
+ * │ │ ┌─ viewport ───────────────────────────────────────┐│
10
+ * │ │ │ ┌─ content ────────────┐ ││
11
+ * │ │ │ │ header │ ││
12
+ * │ │ │ ├──────────────────────┤ ││
13
+ * │ │ │ │ body (scrolls)│ ││
14
+ * │ │ │ ├──────────────────────┤ ││
15
+ * │ │ │ │ footer │ ││
16
+ * │ │ │ └──────────────────────┘ ││
17
+ * │ │ └──────────────────────────────────────────────────┘│
18
+ * │ └────────────────────────────────────────────────────┘│
19
+ * └──────────────────────────────────────────────────────┘
20
+ *
21
+ * `root` declares `container-type: inline-size`, which applies `contain:
22
+ * layout`, which makes it the containing block for every positioned
23
+ * descendant. A scrim positioned inside a root that scrolls is therefore part
24
+ * of what scrolls, and under `scroll="viewport"` it slides off the top of the
25
+ * layer and the page reappears behind the panel. `backdrop` and `viewport`
26
+ * are two cells of one one-cell grid instead: they stack with no positioning
27
+ * at all, and only the second of them ever scrolls.
28
+ *
29
+ * `content` is a three-row grid whose middle row is bounded. That one
30
+ * declaration is what keeps the actions on screen when the body is long —
31
+ * the failure Radix and React Aria leave to each consumer's own CSS, because
32
+ * neither ships a layout.
33
+ *
34
+ * Every part is reached with a child combinator. A dialog opened from inside
35
+ * a dialog is the ordinary case, and a descendant rule would paint the inner
36
+ * panel's header with the outer panel's tone.
37
+ */
38
+ [data-ui='dialog'] {
39
+ /**
40
+ * The panel's measure, per size step. Five named steps rather than the
41
+ * pixel string Material, Carbon and Ant all take: a theme can retune a
42
+ * step and cannot retune `width: '600px'`, and a family that accepted one
43
+ * would be carrying a second scale of its own.
44
+ *
45
+ * `--slotted-dialog-measure` is the way out for the panel the five steps do
46
+ * not hold — a two-column form at 46rem, a media viewer at the width of its
47
+ * own image. It is a family token rather than a prop, so it is set per
48
+ * instance with a style attribute, per region with a rule, or once for an
49
+ * application in the theme; and because every step reads it, an explicit
50
+ * measure wins over the step whichever step is asked for.
51
+ */
52
+ --_measure: var(--slotted-dialog-measure, 32rem);
53
+ /** The air between the panel and the edge of the layer. */
54
+ --_layer-padding: var(--slotted-space-8, 1.5rem);
55
+ /** The panel's own inset, which every row reads. */
56
+ --_padding-inline: var(--slotted-space-8, 1.5rem);
57
+ /**
58
+ * Three insets, not one number used three times, because the three rows
59
+ * hold three different things.
60
+ *
61
+ * The **body** is prose, and prose is what the generous inset is for: it is
62
+ * the one row whose air is read as air.
63
+ *
64
+ * The **header** is a title block whose own line height already carries
65
+ * some. A step down from the body keeps the title from floating and closes
66
+ * the gap between the description and the first line below the rule, which
67
+ * was two full insets stacked — forty pixels of nothing between a sentence
68
+ * and the paragraph it introduces.
69
+ *
70
+ * The **footer** is a row of controls, and a control carries its own height
71
+ * and its own optical padding. The same inset that reads as air around a
72
+ * paragraph reads as a slab around a button: twenty above and below a
73
+ * thirty-four pixel control is a seventy-four pixel bar for one line.
74
+ */
75
+ --_padding-block: var(--slotted-space-7, 1.25rem);
76
+ --_header-padding-block: var(--slotted-space-6, 1rem);
77
+ --_footer-padding-block: var(--slotted-space-5, 0.75rem);
78
+ --_gap: var(--slotted-space-5, 0.75rem);
79
+ --_rule-size: var(--slotted-border-width-thin, 1px);
80
+ /**
81
+ * The title is on the type scale rather than on the control type scale.
82
+ * A control's label is 12 to 14 pixels because it lives inside a button; a
83
+ * dialog's title is a heading, and the outline it belongs to is set like
84
+ * every other heading in the library. Density still moves the geometry,
85
+ * which is what "density changes size without changing meaning" asks for.
86
+ */
87
+ --_title-font-size: var(--slotted-font-size-xl, 1.125rem);
88
+ /**
89
+ * 1.5 rather than the heading step's 1.45, because the title's line is the
90
+ * one length every framed row above the footer adds up from. At 1.45 the
91
+ * sixteen-pixel title of `sm` drew a 23.2 pixel line and put the header at
92
+ * 114.19 and the panel at 167.19; at 1.5 both title steps land on a whole
93
+ * pixel, 24 and 27 (dossier `2026-09-29-alert-dialog-reui-dossier.md`).
94
+ */
95
+ --_title-line-height: var(--slotted-line-height-md, 1.5);
96
+ --_description-font-size: var(--slotted-font-size-md, 0.875rem);
97
+ --_body-font-size: var(--slotted-font-size-md, 0.875rem);
98
+ --_icon-size: var(--slotted-control-icon-size-lg, 18px);
99
+ --_inner-size: var(--slotted-control-inner-size-lg, 28px);
100
+ /**
101
+ * The close control's box: an inner control, not a button (R2), so the
102
+ * glyph plus a step of air on each side, floored at the WCAG 2.2 minimum
103
+ * so the target holds at every density.
104
+ */
105
+ --_close-size: max(
106
+ var(--slotted-target-min, 24px),
107
+ min(calc(var(--_icon-size) + 2 * var(--slotted-space-2, 0.25rem)), var(--_inner-size))
108
+ );
109
+ /**
110
+ * The mark of an alert dialog is the title's own size (R5), and eight
111
+ * pixels from it. It was a step and a half above the title before, which
112
+ * put a twenty-four-pixel glyph beside an eighteen-pixel headline and read
113
+ * as a picture standing next to the words rather than as part of them.
114
+ */
115
+ --_status-size: var(--_title-font-size);
116
+ --_mark-gap: var(--slotted-space-4, 0.5rem);
117
+ --_tone-color: var(--slotted-tone-danger-text, CanvasText);
118
+
119
+ /**
120
+ * The one z-index in the family, and it is a sum. `dismissable-layer`
121
+ * writes `--slotted-layer-depth` on this element as it opens, so a dialog
122
+ * opened over a dialog is one step above it on the single stack the
123
+ * library keeps rather than a number this stylesheet guessed.
124
+ */
125
+ position: fixed;
126
+ inset: 0;
127
+ z-index: calc(var(--slotted-z-index-overlay, 1000) + var(--slotted-layer-depth, 0));
128
+
129
+ display: grid;
130
+ grid-template: minmax(0, 1fr) / minmax(0, 1fr);
131
+
132
+ container-type: inline-size;
133
+ container-name: slotted-dialog;
134
+
135
+ color: var(--slotted-dialog-color, var(--slotted-text, CanvasText));
136
+ font-family: var(--slotted-control-font-family, var(--slotted-font-family-sans));
137
+
138
+ /**
139
+ * `allow-discrete` is the whole of it. Without this line the rule below
140
+ * hides the layer at the first frame of the exit and the two hundred
141
+ * milliseconds `presence` is holding the node mounted for show nothing;
142
+ * with it, the flip to `hidden` happens at the end of the duration instead.
143
+ * `mount="active"` never reaches the closed state — the layer is gone — so
144
+ * this is what `eager` and `lazy` need to animate at all.
145
+ */
146
+ content-visibility: visible;
147
+ transition: content-visibility var(--slotted-duration-medium) allow-discrete;
148
+ }
149
+
150
+ /**
151
+ * Mounted and closed. `eager` and `lazy` leave the layer in the document, and
152
+ * a layer left visible would cover the page it is not on. The rule keys on
153
+ * `inert`, which the binding writes while the layer is closed or exiting,
154
+ * and not on the absence of `data-open`: the entering frame has no
155
+ * `data-open` either, and a hidden layer in that frame refuses the focus
156
+ * scope's initial focus.
157
+ */
158
+ [data-ui='dialog'][inert] {
159
+ content-visibility: hidden;
160
+ pointer-events: none;
161
+ }
162
+
163
+ /**
164
+ * A non-modal layer leaves the page reachable, and that includes the pointer
165
+ * and the wheel. The layer is fixed over the whole viewport, so it lets the
166
+ * pointer through and paints no scrim: the scrim says the page is out of
167
+ * reach, which is what a non-modal layer is not. The panel alone takes the
168
+ * pointer, and a press on the page is the outside press the dismissal policy
169
+ * answers.
170
+ */
171
+ [data-ui='dialog']:not([data-modal]) {
172
+ pointer-events: none;
173
+ }
174
+
175
+ [data-ui='dialog']:not([data-modal]) > [data-part='backdrop'] {
176
+ visibility: hidden;
177
+ }
178
+
179
+ [data-ui='dialog']:not([data-modal]) > [data-part='viewport'] > [data-part='content'] {
180
+ pointer-events: auto;
181
+ }
182
+
183
+ [data-ui='dialog'][data-size='sm'] {
184
+ --_measure: var(--slotted-dialog-measure, 24rem);
185
+ --_padding-inline: var(--slotted-space-7, 1.25rem);
186
+ --_padding-block: var(--slotted-space-6, 1rem);
187
+ --_header-padding-block: var(--slotted-space-5, 0.75rem);
188
+ --_footer-padding-block: var(--slotted-space-4, 0.5rem);
189
+ --_title-font-size: var(--slotted-font-size-lg, 1rem);
190
+ }
191
+
192
+ [data-ui='dialog'][data-size='lg'] {
193
+ --_measure: var(--slotted-dialog-measure, 42rem);
194
+ }
195
+
196
+ [data-ui='dialog'][data-size='xl'] {
197
+ --_measure: var(--slotted-dialog-measure, 56rem);
198
+ }
199
+
200
+ /**
201
+ * `full` is the step a dialog that is a document takes: the panel spends the
202
+ * whole layer minus its padding, and the measure stops capping it.
203
+ */
204
+ [data-ui='dialog'][data-size='full'] {
205
+ --_measure: var(--slotted-dialog-measure, none);
206
+ }
207
+
208
+ /* ── The scrim ────────────────────────────────────────────────────────── */
209
+
210
+ /**
211
+ * `--slotted-scrim` is a foundation token the theme restates per scheme and
212
+ * remaps to `Canvas` under forced colours, where it becomes an opaque ground.
213
+ * That is why the panel below carries a border of its own: every shadow token
214
+ * is `none` in that mode, and a panel separated only by elevation would sit
215
+ * on an opaque ground with nothing to mark its edge.
216
+ */
217
+ [data-ui='dialog'] > [data-part='backdrop'] {
218
+ grid-area: 1 / 1;
219
+ background: var(--slotted-dialog-scrim, var(--slotted-scrim, Canvas));
220
+ opacity: 0;
221
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
222
+ }
223
+
224
+ /**
225
+ * The arrival is the long step on the standard curve, and both halves of
226
+ * that are a correction (DG4). On the entrance curve the scrim was at
227
+ * 57.6 per cent of its ink by the second frame of the transition and at
228
+ * 83 per cent by the fourth — a curve written for something that travels
229
+ * and decelerates into place, spent on something that only changes ink —
230
+ * and it was reported as a scrim that simply appeared. The standard curve
231
+ * over the long step spends the frames where they can be seen: measured on
232
+ * the same page, 10.8 per cent at 25 milliseconds and 55 per cent at 100.
233
+ *
234
+ * The panel below arrives over the same duration and the same curve, so the
235
+ * two halves of one layer arrive together.
236
+ */
237
+ [data-ui='dialog'][data-open] > [data-part='backdrop'] {
238
+ opacity: 1;
239
+ transition: opacity var(--slotted-duration-long) var(--slotted-easing-standard);
240
+ }
241
+
242
+ /* ── The box the panel sits in ────────────────────────────────────────── */
243
+
244
+ /**
245
+ * `safe center` rather than `center`. A panel taller than the layer, centred
246
+ * by the unsafe keyword, overflows equally at both ends — and the end that
247
+ * carries the title is the one that leaves the screen. `safe` aligns to the
248
+ * start edge as soon as the item cannot fit, which is the only reading that
249
+ * keeps the title reachable at 200% zoom and in a short window.
250
+ */
251
+ [data-ui='dialog'] > [data-part='viewport'] {
252
+ grid-area: 1 / 1;
253
+ display: grid;
254
+ grid-template-columns: minmax(0, 1fr);
255
+ align-content: safe center;
256
+ justify-items: center;
257
+ padding: var(--_layer-padding);
258
+ /**
259
+ * The last resort, in both scroll modes. Under `scroll="content"` the panel
260
+ * is bounded to the layer and its body is what scrolls — but a panel with
261
+ * no body, or one whose header and footer alone are taller than the layer,
262
+ * has no row that can shrink, and `auto` rows do not. The first capture
263
+ * showed exactly that: a title of three lines and a description of three
264
+ * more pushed the actions past the bottom edge of a short layer, and the
265
+ * only thing between the reader and an unreachable button was this line.
266
+ */
267
+ overflow: auto;
268
+ overscroll-behavior: contain;
269
+ }
270
+
271
+ [data-ui='dialog'][data-placement='top'] > [data-part='viewport'] {
272
+ align-content: safe start;
273
+ }
274
+
275
+ /**
276
+ * Under `scroll="viewport"` the panel is unbounded and this box is what
277
+ * scrolls it, which is the rule below on `content`. The scrim does not move
278
+ * with it, because it is the other cell of the grid above rather than a child
279
+ * of this box.
280
+ */
281
+ /* ── The panel ────────────────────────────────────────────────────────── */
282
+
283
+ [data-ui='dialog'] > [data-part='viewport'] > [data-part='content'] {
284
+ display: grid;
285
+ grid-template-rows: auto minmax(0, 1fr) auto;
286
+ inline-size: 100%;
287
+ max-inline-size: var(--_measure);
288
+ /* Bounded, so the middle row is the one that scrolls and the footer stays. */
289
+ max-block-size: 100%;
290
+
291
+ background: var(--slotted-dialog-surface, var(--slotted-surface-raised, Canvas));
292
+ /**
293
+ * The subtle step, and the reason is what is actually doing the separating.
294
+ *
295
+ * The panel is told apart from the scrim by its **fill**, not by its edge:
296
+ * in the dark scheme the panel is `#1c1c20` over a backdrop that composites
297
+ * to about `#070708`, and in the light scheme it is white over about
298
+ * `#93979f`. The border only finishes the edge, so it takes the step for a
299
+ * line inside a surface rather than the step for a line between two.
300
+ *
301
+ * It was the strong step for one iteration, on the reading that a shadow of
302
+ * black on black leaves nothing in the dark scheme. That is true of the
303
+ * shadow and not of the panel: the strong step is `#71717a` against a
304
+ * `#1c1c20` panel, a jump of eighty-five per channel, and it read as a
305
+ * drawn stroke around every dialog.
306
+ *
307
+ * Forced colours is where an edge does have to carry the separation, since
308
+ * `--slotted-scrim` becomes an opaque `Canvas` there and every shadow token
309
+ * becomes `none`. The rule at the end of this file is that case, and it
310
+ * thickens the border to say so.
311
+ */
312
+ border: var(--_rule-size) solid
313
+ var(--slotted-dialog-border-color, var(--slotted-border-subtle, ButtonBorder));
314
+ border-radius: var(--slotted-dialog-radius, var(--slotted-radius-lg, 8px));
315
+ box-shadow: var(--slotted-shadow-lg);
316
+ /* So the radius clips the row that scrolls under it. */
317
+ overflow: hidden;
318
+
319
+ /**
320
+ * The panel arrives by changing its ink and by nothing else.
321
+ *
322
+ * It grew into place for one iteration, from `0.98`, and R7 refuses that:
323
+ * a box with a title, a body and a row of buttons in it resamples every
324
+ * letter it holds while it is being scaled. Measured on the way in, the
325
+ * title's own box was up to 6.82 pixels narrower than at rest and off the
326
+ * pixel grid in thirteen frames of twenty-eight;
327
+ * `packages/styles/src/text-scale.test.mjs` holds that rule.
328
+ *
329
+ * Then it travelled four pixels instead, and the same defect was reported
330
+ * again (DG4). A box the browser is moving is a composited layer, and a
331
+ * layer moved by a fraction of a pixel has its text resampled for every
332
+ * frame it is between two of them: measured, eleven of the twelve frames
333
+ * of the movement landed on a fractional position — 32.305, 32.973,
334
+ * 33.326 — and the reader sees the words lose their edges and get them
335
+ * back. A centred panel has nothing for the movement to be *from* either,
336
+ * which is what the first draft of this rule said before the travel was
337
+ * written into it.
338
+ *
339
+ * So the entrance is `opacity` alone, at the same duration and on the same
340
+ * curve as the scrim behind it. Nothing about the geometry changes while
341
+ * the panel arrives, and there is no frame in which the type is drawn at a
342
+ * position it does not keep.
343
+ */
344
+ opacity: 0;
345
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
346
+ }
347
+
348
+ /**
349
+ * Arriving is longer than leaving, and on a different curve. A dialog
350
+ * appearing is a state change the eye has to follow, because the page it
351
+ * covers is still visible behind it; a reader who has decided to close is
352
+ * waiting, so the exit is the short duration.
353
+ *
354
+ * The panel placed at the block start arrived by sliding twelve pixels from
355
+ * the edge it sits on, and it is on the same rule now for the same reason:
356
+ * the type inside it is what the slide was resampling (DG4).
357
+ */
358
+ [data-ui='dialog'][data-open] > [data-part='viewport'] > [data-part='content'] {
359
+ opacity: 1;
360
+ transition: opacity var(--slotted-duration-long) var(--slotted-easing-standard);
361
+ }
362
+
363
+ /**
364
+ * Reduced motion has nothing left to drop here: neither the panel nor the
365
+ * scrim moves, and a fade is what the preference keeps. The theme collapses
366
+ * every duration token under this query, so the arrival becomes instant
367
+ * without a rule of its own — `motion.test.mjs` holds that for every family.
368
+ * The rule the family did carry was the one that stopped the panel
369
+ * travelling, and there is no travel to stop (DG4).
370
+ */
371
+
372
+ [data-ui='dialog'][data-scroll='viewport'] > [data-part='viewport'] > [data-part='content'] {
373
+ max-block-size: none;
374
+ }
375
+
376
+ /* ── The header ───────────────────────────────────────────────────────── */
377
+
378
+ /**
379
+ * Four columns: the mark, the words, the full-screen toggle and the close
380
+ * button. Each mark and each control keeps a column of its own, so a title
381
+ * that wraps to two lines grows the row instead of pushing any of them off
382
+ * the line. The toggle's track is empty and therefore nought wide unless the
383
+ * layer renders it, which is why the column gap is nought.
384
+ *
385
+ * The header draws no rule of its own. Every rule in the panel belongs to the
386
+ * row *below* it, which is what makes the count right for every combination a
387
+ * consumer can write: a header, a body and a footer draw two rules; a header
388
+ * and a footer alone draw one; and a panel that is only a body draws none.
389
+ * The first capture of an alert dialog — a header and a footer, with no body
390
+ * between them — showed the header's bottom rule and the footer's top rule
391
+ * stacked into one line of two pixels.
392
+ *
393
+ * The rules are drawn whether the body scrolls or not. `scroll-state()`
394
+ * container queries would let the stylesheet know, and they are limited
395
+ * availability — the same ground on which `collapsible` declined
396
+ * `interpolate-size`. The cost is a rule over a footer that sometimes has
397
+ * nothing above it to separate.
398
+ */
399
+ [data-ui='dialog'] > [data-part='viewport'] > [data-part='content'] > [data-part='header'] {
400
+ display: grid;
401
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
402
+ align-items: start;
403
+ /* The distance from the mark to the words belongs to the mark, and the
404
+ distance from the words to the close button to the button: a column gap
405
+ is drawn beside an empty track too, so a dialog with no mark would start
406
+ its title one gap in from the inset. */
407
+ column-gap: 0;
408
+ row-gap: var(--slotted-space-2, 0.25rem);
409
+ padding-block: var(--_header-padding-block);
410
+ padding-inline: var(--_padding-inline);
411
+ }
412
+
413
+ /**
414
+ * Everything in the header lands in the words column unless the family placed
415
+ * it somewhere else. The four rules below place the mark, the title, the
416
+ * description and the close button; this is what happens to the fifth thing —
417
+ * a badge beside the title, a breadcrumb, a line the consumer wrote.
418
+ *
419
+ * Without it those children auto-placed into whichever cell was free, which is
420
+ * column one whenever no icon was rendered: a consumer's own element landed
421
+ * where the mark would have gone, and moved as soon as a tone was set. Where
422
+ * it goes is now a rule rather than an accident of the grid.
423
+ */
424
+ [data-ui='dialog']
425
+ > [data-part='viewport']
426
+ > [data-part='content']
427
+ > [data-part='header']
428
+ > *:not(
429
+ [data-part='icon'],
430
+ [data-part='title'],
431
+ [data-part='description'],
432
+ [data-part='fullscreen'],
433
+ [data-part='close']
434
+ ) {
435
+ grid-column: 2;
436
+ /**
437
+ * `start`, not the grid's own `stretch`. A badge stretched across the words
438
+ * column paints its ground the full width and centres its label in it,
439
+ * which is what the first capture showed. The five placed parts are excluded
440
+ * by name rather than by `:not([data-part])`, because what a consumer puts
441
+ * here is usually another family and that carries a `data-part` of its own.
442
+ */
443
+ justify-self: start;
444
+ }
445
+
446
+ /* ── The mark an alert dialog carries ─────────────────────────────────── */
447
+
448
+ /**
449
+ * Real geometry: the binding draws the tone's own `Glyph` inside an `icon`
450
+ * box, and the slot gives it the tone's text colour and the mark's size.
451
+ * WCAG 2.2 SC 1.4.1 asks that colour not be the only carrier, and a
452
+ * destructive confirmation is the place that matters most: under forced
453
+ * colours every tone becomes one system colour and the silhouette is all
454
+ * that is left.
455
+ */
456
+ [data-ui='dialog']
457
+ > [data-part='viewport']
458
+ > [data-part='content']
459
+ > [data-part='header']
460
+ > [data-part='icon'] {
461
+ --slotted-icon-size: var(--_status-size);
462
+
463
+ grid-column: 1;
464
+ grid-row: 1 / span 2;
465
+ align-self: start;
466
+ display: inline-grid;
467
+ place-items: center;
468
+ color: var(--_tone-color);
469
+ block-size: var(--_status-size);
470
+ inline-size: var(--_status-size);
471
+ flex: none;
472
+ /* The mark sits on the title's first line rather than on its box: half
473
+ the difference between the title's line and the mark, in the title's own
474
+ type, because the header's line is not the title's. */
475
+ margin-block-start: calc(
476
+ (var(--_title-font-size) * var(--_title-line-height) - var(--_status-size)) / 2
477
+ );
478
+ margin-inline-end: var(--_mark-gap);
479
+ }
480
+
481
+ [data-ui='dialog'][data-tone='accent'] {
482
+ --_tone-color: var(--slotted-tone-accent-text, CanvasText);
483
+ }
484
+
485
+ [data-ui='dialog'][data-tone='secondary'] {
486
+ --_tone-color: var(--slotted-tone-secondary-text, CanvasText);
487
+ }
488
+
489
+ [data-ui='dialog'][data-tone='success'] {
490
+ --_tone-color: var(--slotted-tone-success-text, CanvasText);
491
+ }
492
+
493
+ [data-ui='dialog'][data-tone='warning'] {
494
+ --_tone-color: var(--slotted-tone-warning-text, CanvasText);
495
+ }
496
+
497
+ [data-ui='dialog'][data-tone='danger'] {
498
+ --_tone-color: var(--slotted-tone-danger-text, CanvasText);
499
+ }
500
+
501
+ /* ── The words ────────────────────────────────────────────────────────── */
502
+
503
+ /**
504
+ * `overflow-wrap: anywhere` and a zero minimum are the pair that answer a
505
+ * hostile string. A ninety-character unbroken token in a title widens the
506
+ * panel past the layer, and the panel is centred, so both of its edges leave
507
+ * the screen at once.
508
+ */
509
+ [data-ui='dialog']
510
+ > [data-part='viewport']
511
+ > [data-part='content']
512
+ > [data-part='header']
513
+ > [data-part='title'] {
514
+ grid-column: 2;
515
+ grid-row: 1;
516
+ margin: 0;
517
+ min-inline-size: 0;
518
+ overflow-wrap: anywhere;
519
+ font-family: var(
520
+ --slotted-dialog-title-font-family,
521
+ var(
522
+ --slotted-font-family-heading,
523
+ var(--slotted-font-family-sans, ui-sans-serif, system-ui, sans-serif)
524
+ )
525
+ );
526
+ font-size: var(--_title-font-size);
527
+ font-weight: var(--slotted-font-weight-semibold, 600);
528
+ line-height: var(--_title-line-height);
529
+ color: var(--slotted-dialog-title-color, var(--slotted-text, CanvasText));
530
+ }
531
+
532
+ [data-ui='dialog']
533
+ > [data-part='viewport']
534
+ > [data-part='content']
535
+ > [data-part='header']
536
+ > [data-part='description'] {
537
+ grid-column: 2;
538
+ grid-row: 2;
539
+ margin: 0;
540
+ min-inline-size: 0;
541
+ overflow-wrap: anywhere;
542
+ font-size: var(--_description-font-size);
543
+ line-height: var(--slotted-line-height-md, 1.5);
544
+ color: var(--slotted-dialog-description-color, var(--slotted-text-muted, GrayText));
545
+ }
546
+
547
+ /**
548
+ * The name of the thing the dialog acts on. A confirmation says which
549
+ * project or which member it removes, and a consumer marks that name with
550
+ * `strong`; left to the user agent it was bold in the muted colour, heavier
551
+ * than the title and no easier to find. It takes the title's colour and
552
+ * weight instead, which is what both product confirmations in the survey do
553
+ * with a span of their own.
554
+ */
555
+ [data-ui='dialog']
556
+ > [data-part='viewport']
557
+ > [data-part='content']
558
+ > [data-part='header']
559
+ > [data-part='description']
560
+ :is(strong, b) {
561
+ font-weight: var(--slotted-font-weight-semibold, 600);
562
+ color: var(--slotted-dialog-title-color, var(--slotted-text, CanvasText));
563
+ }
564
+
565
+ /* ── The close button ─────────────────────────────────────────────────── */
566
+
567
+ /**
568
+ * An inner control, not a button (R2): the box is the glyph plus a step of
569
+ * air on each side, floored at the WCAG 2.2 minimum, and the glyph is
570
+ * centred in it. It was the control height before, a thirty-four pixel
571
+ * square beside an eighteen pixel mark. The glyph is the binding's `Glyph`
572
+ * in an `icon` box, sized here through the icon token so the box and the
573
+ * formula above read one length.
574
+ */
575
+ [data-ui='dialog']
576
+ > [data-part='viewport']
577
+ > [data-part='content']
578
+ > [data-part='header']
579
+ > [data-part='close'] {
580
+ --slotted-icon-size: var(--_icon-size);
581
+
582
+ grid-column: 4;
583
+ grid-row: 1 / span 2;
584
+ align-self: start;
585
+ display: inline-grid;
586
+ place-items: center;
587
+ block-size: var(--_close-size);
588
+ inline-size: var(--_close-size);
589
+ /* The box overhangs the panel's edge by its own air, so the mark aligns
590
+ with the title. */
591
+ margin-inline-end: calc(-1 * (var(--_close-size) - var(--_icon-size)) / 2);
592
+ margin-inline-start: var(--_gap);
593
+ padding: 0;
594
+ appearance: none;
595
+ background-color: transparent;
596
+ border: 0;
597
+ border-radius: var(--slotted-control-radius, var(--slotted-radius-sm, 4px));
598
+ color: var(--slotted-dialog-close-color, var(--slotted-text-muted, GrayText));
599
+ cursor: pointer;
600
+ transition:
601
+ background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
602
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
603
+ color var(--slotted-control-transition-duration, var(--slotted-duration-short))
604
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
605
+ }
606
+
607
+ [data-ui='dialog']
608
+ > [data-part='viewport']
609
+ > [data-part='content']
610
+ > [data-part='header']
611
+ > [data-part='close']:not(:disabled):hover {
612
+ /* The ground moves under the pointer and nothing else does (R2). */
613
+ background-color: var(
614
+ --slotted-dialog-close-hover-background,
615
+ var(--slotted-tone-secondary-subtle-hover)
616
+ );
617
+ }
618
+
619
+ [data-ui='dialog']
620
+ > [data-part='viewport']
621
+ > [data-part='content']
622
+ > [data-part='header']
623
+ > [data-part='close']:not(:disabled):active {
624
+ background-color: var(
625
+ --slotted-dialog-close-active-background,
626
+ var(--slotted-tone-secondary-subtle-active)
627
+ );
628
+ }
629
+
630
+ [data-ui='dialog']
631
+ > [data-part='viewport']
632
+ > [data-part='content']
633
+ > [data-part='header']
634
+ > [data-part='close']:focus-visible {
635
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
636
+ outline-offset: calc(-1 * var(--slotted-focus-ring-offset, 2px));
637
+ }
638
+
639
+ /* ── The full-screen toggle ───────────────────────────────────────────── */
640
+
641
+ /**
642
+ * The reader's own control for taking the layer, and the same inner control
643
+ * the close is (R2): the box is the glyph plus a step of air on each side,
644
+ * floored at the WCAG 2.2 minimum, and the glyph is centred in it. The
645
+ * geometry is not chosen here — it is `--_close-size`, the one formula this
646
+ * family already states, so the two controls in the corner are one row of
647
+ * one size.
648
+ *
649
+ * It sits in the column before the close, which is where a window's controls
650
+ * put it, and it carries no end overhang: the overhang belongs to whichever
651
+ * control stands against the panel's inset, and that is the close. Its
652
+ * glyph is the binding's, chosen from the state: the mark says what the
653
+ * press does next, so it is the opposite of the state the panel is in.
654
+ */
655
+ [data-ui='dialog']
656
+ > [data-part='viewport']
657
+ > [data-part='content']
658
+ > [data-part='header']
659
+ > [data-part='fullscreen'] {
660
+ --slotted-icon-size: var(--_icon-size);
661
+
662
+ grid-column: 3;
663
+ grid-row: 1 / span 2;
664
+ align-self: start;
665
+ display: inline-grid;
666
+ place-items: center;
667
+ block-size: var(--_close-size);
668
+ inline-size: var(--_close-size);
669
+ margin-inline-start: var(--_gap);
670
+ padding: 0;
671
+ appearance: none;
672
+ background-color: transparent;
673
+ border: 0;
674
+ border-radius: var(--slotted-control-radius, var(--slotted-radius-sm, 4px));
675
+ color: var(--slotted-dialog-close-color, var(--slotted-text-muted, GrayText));
676
+ cursor: pointer;
677
+ transition:
678
+ background-color var(--slotted-control-transition-duration, var(--slotted-duration-short))
679
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard)),
680
+ color var(--slotted-control-transition-duration, var(--slotted-duration-short))
681
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
682
+ }
683
+
684
+ /**
685
+ * The two controls in the corner are neighbours, so the air between them is
686
+ * their own: each box already carries half the difference between it and its
687
+ * glyph on every side, and a second gap between them would set them further
688
+ * apart than either is from the words.
689
+ */
690
+ [data-ui='dialog']
691
+ > [data-part='viewport']
692
+ > [data-part='content']
693
+ > [data-part='header']
694
+ > [data-part='fullscreen']
695
+ + [data-part='close'] {
696
+ margin-inline-start: 0;
697
+ }
698
+
699
+ [data-ui='dialog']
700
+ > [data-part='viewport']
701
+ > [data-part='content']
702
+ > [data-part='header']
703
+ > [data-part='fullscreen']:not(:disabled):hover {
704
+ /* The ground moves under the pointer and nothing else does (R2). */
705
+ background-color: var(
706
+ --slotted-dialog-close-hover-background,
707
+ var(--slotted-tone-secondary-subtle-hover)
708
+ );
709
+ }
710
+
711
+ [data-ui='dialog']
712
+ > [data-part='viewport']
713
+ > [data-part='content']
714
+ > [data-part='header']
715
+ > [data-part='fullscreen']:not(:disabled):active {
716
+ background-color: var(
717
+ --slotted-dialog-close-active-background,
718
+ var(--slotted-tone-secondary-subtle-active)
719
+ );
720
+ }
721
+
722
+ [data-ui='dialog']
723
+ > [data-part='viewport']
724
+ > [data-part='content']
725
+ > [data-part='header']
726
+ > [data-part='fullscreen']:focus-visible {
727
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
728
+ outline-offset: calc(-1 * var(--slotted-focus-ring-offset, 2px));
729
+ }
730
+
731
+ /**
732
+ * On a finger the target is the comfortable size in both axes and the box
733
+ * is the one a mouse sees: a transparent `::before` centred on each control
734
+ * reaches 44 pixels, and the ground and the ring stay square. The two
735
+ * controls touch, so neither reaches across the edge they share.
736
+ */
737
+ @media (pointer: coarse) {
738
+ [data-ui='dialog']
739
+ > [data-part='viewport']
740
+ > [data-part='content']
741
+ > [data-part='header']
742
+ > :is([data-part='close'], [data-part='fullscreen']) {
743
+ position: relative;
744
+ }
745
+
746
+ [data-ui='dialog']
747
+ > [data-part='viewport']
748
+ > [data-part='content']
749
+ > [data-part='header']
750
+ > :is([data-part='close'], [data-part='fullscreen'])::before {
751
+ content: '';
752
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
753
+ position: absolute;
754
+ }
755
+
756
+ [data-ui='dialog']
757
+ > [data-part='viewport']
758
+ > [data-part='content']
759
+ > [data-part='header']
760
+ > [data-part='fullscreen']:has(+ [data-part='close'])::before {
761
+ inset-inline-end: 0;
762
+ }
763
+
764
+ [data-ui='dialog']
765
+ > [data-part='viewport']
766
+ > [data-part='content']
767
+ > [data-part='header']
768
+ > [data-part='fullscreen']
769
+ + [data-part='close']::before {
770
+ inset-inline-start: 0;
771
+ }
772
+ }
773
+
774
+ /* ── The row that scrolls ─────────────────────────────────────────────── */
775
+
776
+ /**
777
+ * `overscroll-behavior: contain` is what keeps a wheel that reaches the end
778
+ * of this box from scrolling the page behind the layer. The scroll lock in
779
+ * `dismissable-layer` covers the modal case; this covers the non-modal one,
780
+ * where the page behind is not locked at all.
781
+ */
782
+ [data-ui='dialog']
783
+ > [data-part='viewport']
784
+ > [data-part='content']
785
+ > [data-part='body']:not(:first-child) {
786
+ border-block-start: var(--_rule-size) solid
787
+ var(--slotted-dialog-rule-color, var(--slotted-border-subtle, ButtonBorder));
788
+ }
789
+
790
+ [data-ui='dialog'] > [data-part='viewport'] > [data-part='content'] > [data-part='body'] {
791
+ min-block-size: 0;
792
+ overflow-x: hidden;
793
+ overflow-y: auto;
794
+ overflow-inline: hidden;
795
+ overflow-block: auto;
796
+ overscroll-behavior: contain;
797
+ padding-block: var(--_padding-block);
798
+ padding-inline: var(--_padding-inline);
799
+ font-size: var(--_body-font-size);
800
+ line-height: var(--slotted-line-height-md, 1.5);
801
+ color: var(--slotted-dialog-body-color, var(--slotted-text, CanvasText));
802
+ }
803
+
804
+ [data-ui='dialog'][data-scroll='viewport']
805
+ > [data-part='viewport']
806
+ > [data-part='content']
807
+ > [data-part='body'] {
808
+ overflow: visible;
809
+ }
810
+
811
+ /**
812
+ * Content that is not there yet. No surveyed implementation has a state for
813
+ * it, so a consumer renders a spinner conditionally and the panel jumps to
814
+ * size when the data lands. The row keeps a floor here instead, and the
815
+ * header, the close button and Escape all stay operable: a loading dialog a
816
+ * reader cannot leave is the worse failure of the two.
817
+ */
818
+ [data-ui='dialog'][data-loading]
819
+ > [data-part='viewport']
820
+ > [data-part='content']
821
+ > [data-part='body'] {
822
+ min-block-size: var(--slotted-space-12, 4rem);
823
+ }
824
+
825
+ /* ── The actions ──────────────────────────────────────────────────────── */
826
+
827
+ /**
828
+ * `flex-wrap` rather than a grid: two actions that do not fit on one line
829
+ * stack, and the primary one stays last in both readings. The row is the
830
+ * reason `content` is bounded — an unbounded panel pushes it past the fold,
831
+ * which is the whole failure the three-row grid exists to prevent.
832
+ */
833
+ [data-ui='dialog'] > [data-part='viewport'] > [data-part='content'] > [data-part='footer'] {
834
+ display: flex;
835
+ flex-wrap: wrap;
836
+ align-items: center;
837
+ justify-content: flex-end;
838
+ gap: var(--_gap);
839
+ padding-block: var(--_footer-padding-block);
840
+ padding-inline: var(--_padding-inline);
841
+ background: var(--slotted-dialog-footer-surface, var(--slotted-surface-sunken, Canvas));
842
+ }
843
+
844
+ [data-ui='dialog']
845
+ > [data-part='viewport']
846
+ > [data-part='content']
847
+ > [data-part='footer']:not(:first-child) {
848
+ border-block-start: var(--_rule-size) solid
849
+ var(--slotted-dialog-rule-color, var(--slotted-border-subtle, ButtonBorder));
850
+ }
851
+
852
+ /* ── A panel the consumer paints ──────────────────────────────────────── */
853
+
854
+ /**
855
+ * The layer without the picture.
856
+ *
857
+ * `render` substitutes the element the panel is drawn on and not the drawing,
858
+ * so a consumer who wanted the mechanism and their own box — a media viewer,
859
+ * an image at its own aspect ratio, a `card` — had no way to say so. Here the
860
+ * family paints nothing: no surface, no border, no radius, no shadow, no rows
861
+ * and no padding.
862
+ *
863
+ * Everything that is not paint stays: the portal, the scrim, `presence`, the
864
+ * focus containment and restoration, the dismissal policy, the single z-index
865
+ * stack and the motion. It is the one place this library meets a headless one
866
+ * — Radix and Base UI ship no panel at all, so this is what their Content
867
+ * already is — and it is an axis rather than a second family because
868
+ * everything but the paint is the same.
869
+ *
870
+ * The rows go with the paint, so a plain panel does not keep its footer on
871
+ * screen for a long body. That is the cost, and a consumer who wants both
872
+ * writes their own three-row grid, which is the whole of what they are taking
873
+ * over.
874
+ */
875
+ [data-ui='dialog'][data-surface='plain'] > [data-part='viewport'] > [data-part='content'] {
876
+ display: block;
877
+ background: none;
878
+ border: 0;
879
+ border-radius: 0;
880
+ box-shadow: none;
881
+ overflow: visible;
882
+ }
883
+
884
+ [data-ui='dialog'][data-surface='plain']
885
+ > [data-part='viewport']
886
+ > [data-part='content']
887
+ > :is([data-part='header'], [data-part='body'], [data-part='footer']) {
888
+ background: none;
889
+ border: 0;
890
+ padding: 0;
891
+ }
892
+
893
+ /* ── Full screen ──────────────────────────────────────────────────────── */
894
+
895
+ /**
896
+ * The panel spends the whole layer: no margins, no radius, no measure.
897
+ *
898
+ * `align-content: stretch` on the viewport is the half of it that is easy to
899
+ * miss. The first capture had the padding gone and the panel still floating
900
+ * in the middle of the layer with the scrim above and below it, because a
901
+ * percentage block size resolves against a grid area that `safe center` had
902
+ * already sized to the content. The track has to stretch before the panel can
903
+ * fill it.
904
+ *
905
+ * A
906
+ * container query rather than a viewport one, because the family answers its
907
+ * own inline size — a dialog inside a consumer-named portal container is as
908
+ * narrow as that container and not as narrow as the window. The breakpoint is
909
+ * a literal because `@container (max-inline-size: var(--x))` is invalid CSS.
910
+ */
911
+ @container slotted-dialog (max-inline-size: 30rem) {
912
+ [data-ui='dialog'][data-full-screen='narrow'] > [data-part='viewport'] {
913
+ align-content: stretch;
914
+ padding: 0;
915
+ }
916
+
917
+ [data-ui='dialog'][data-full-screen='narrow'] > [data-part='viewport'] > [data-part='content'] {
918
+ max-inline-size: none;
919
+ block-size: 100%;
920
+ border-radius: 0;
921
+ border: 0;
922
+ }
923
+ }
924
+
925
+ /**
926
+ * The axis and the reader's own state produce one picture, so they are one
927
+ * rule. `data-full-screen='always'` is what a consumer chose in advance;
928
+ * `data-fullscreen` is what the reader pressed. Nothing here transitions:
929
+ * R7 admits `transform` and `opacity` alone, and the layer's entry already
930
+ * owns both — a panel that animated its own box would animate `inset`,
931
+ * `inline-size` and `block-size`, which is the rule R7 exists to state.
932
+ */
933
+ [data-ui='dialog'][data-full-screen='always'] > [data-part='viewport'],
934
+ [data-ui='dialog'][data-fullscreen] > [data-part='viewport'] {
935
+ align-content: stretch;
936
+ padding: 0;
937
+ }
938
+
939
+ [data-ui='dialog'][data-full-screen='always'] > [data-part='viewport'] > [data-part='content'],
940
+ [data-ui='dialog'][data-fullscreen] > [data-part='viewport'] > [data-part='content'] {
941
+ max-inline-size: none;
942
+ block-size: 100%;
943
+ border-radius: 0;
944
+ /**
945
+ * No border, not only no radius. A panel that covers the layer hides the
946
+ * scrim entirely, so the edge separates the panel from nothing — and the
947
+ * first full-screen capture had a hairline across the very top of the
948
+ * layer, which on a phone is a stray line under the browser chrome.
949
+ */
950
+ border: 0;
951
+ }
952
+
953
+ /* ── Forced colours ───────────────────────────────────────────────────── */
954
+
955
+ /**
956
+ * The theme remaps every token this family reads, so the only rule here is
957
+ * the one the theme cannot reach: `--slotted-scrim` becomes `Canvas`, an
958
+ * opaque ground, and the panel on it needs an edge the shadow no longer
959
+ * draws. The border above already states one; this thickens it to the weight
960
+ * a system border is read at.
961
+ */
962
+ @media (forced-colors: active) {
963
+ [data-ui='dialog'] > [data-part='viewport'] > [data-part='content'] {
964
+ border-color: CanvasText;
965
+ border-width: var(--slotted-border-width-thick, 2px);
966
+ }
967
+ }
968
+ }