@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,1706 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── Files, and what happens to them ───────────────────────────────────
4
+ *
5
+ * One family for what `docs/component-features.md` §17 lists as three: a
6
+ * drop region, a file input, and the manager that shows what is being sent.
7
+ * `variant` decides which of the three shapes is drawn; everything below the
8
+ * region — the list, the rows, their bars and their controls — is the same
9
+ * in all three.
10
+ *
11
+ * **The trigger is the `<label>` and the region is not.** What the reader
12
+ * focuses is an `<input type="file">` clipped to one pixel inside the
13
+ * trigger, so the trigger is what draws the ring, through `:has()`. Putting
14
+ * the label around the trigger rather than around the whole region is what
15
+ * makes the input's accessible name three words instead of every word in the
16
+ * box; the reasoning and its cost are in
17
+ * `docs/history/designs/2026-09-10-upload-design.md` §2.
18
+ *
19
+ * **The family owns its own container.** Four of its layout decisions answer
20
+ * the width of the box rather than the width of the window — a row folds its
21
+ * size under its name, the region's mark and text stop sharing a line, the
22
+ * summary stacks, and a grid of previews changes its track count. A drop
23
+ * region goes in a sidebar, in a dialog and across a page in the same
24
+ * application, and none of those is a viewport.
25
+ */
26
+ /**
27
+ * The private properties, on either box that stands on its own. The viewer is
28
+ * portalled to the end of the document — it must be, because the rule below
29
+ * makes this box a container and a container is a containing block for a
30
+ * fixed descendant — so it is not inside the family it belongs to, inherits
31
+ * none of these, and states the same set for itself. They are declared once,
32
+ * for both, rather than copied into a second block that drifts from the first.
33
+ */
34
+ [data-ui='upload'][data-part='root'],
35
+ [data-ui='upload'][data-part='viewer'] {
36
+ /**
37
+ * Type, glyph, control height and inner gap come from the control scales,
38
+ * the only ones the theme restates per density, so an upload inside a dense
39
+ * form shrinks with the fields around it. The insets come from the space
40
+ * scale, which does not vary by density — the cost `card` and `alert` both
41
+ * accepted, and recorded there for the same reason.
42
+ */
43
+ --_font-size: var(--slotted-control-font-size-md, 13px);
44
+ --_glyph-size: var(--slotted-control-icon-size-md, 16px);
45
+ --_inner-size: var(--slotted-control-inner-size-md, 24px);
46
+ --_control-height: var(--slotted-control-height-md, 34px);
47
+ --_control-inset: var(--slotted-control-padding-inline-md, 13px);
48
+ --_gap: var(--slotted-control-gap-md, 8px);
49
+
50
+ /**
51
+ * The square a row's preview stands in. Two glyphs wide, so it holds a
52
+ * thumbnail at a size worth looking at and still steps with the density
53
+ * that every other measure in the row steps with. A stated pixel size
54
+ * would have been a sixth scale for one box.
55
+ */
56
+ --_preview-size: var(--slotted-upload-preview-size, calc(2 * var(--_glyph-size)));
57
+
58
+ /**
59
+ * How a control inside the family answers a pointer. It is a state layer
60
+ * rather than a step on the tone ramp, for the reason `tag` measured and
61
+ * `alert` restated: the tone's `subtle-hover` is calibrated against the
62
+ * page, and these controls sit on a row that may already be tinted by its
63
+ * own status. `currentcolor` is legible on whatever ground it has by
64
+ * construction — it is the colour the words beside it are read in.
65
+ */
66
+ --_state-hover: color-mix(
67
+ in oklab,
68
+ currentcolor var(--slotted-upload-state-layer-hover, 8%),
69
+ transparent
70
+ );
71
+ --_state-active: color-mix(
72
+ in oklab,
73
+ currentcolor var(--slotted-upload-state-layer-active, 14%),
74
+ transparent
75
+ );
76
+
77
+ /**
78
+ * Each measure is named once and a reach moves only the value it falls
79
+ * back to, so an application that retunes `--slotted-upload-item-gap`
80
+ * retunes every reach rather than discovering that the name it set is the
81
+ * one the default happens to read.
82
+ *
83
+ * Rows stand one step of air apart. At a quarter-rem the two edges of
84
+ * neighbouring rows read as one double rule rather than as two objects —
85
+ * what the `statuses` capture showed down a list of nine — and eight is
86
+ * the gap ReUI's attachment list measures (`c-attachment-5`).
87
+ */
88
+ --_section-gap: var(--slotted-upload-section-gap, var(--slotted-space-5, 0.75rem));
89
+ --_item-gap: var(--slotted-upload-item-gap, var(--slotted-space-4, 0.5rem));
90
+ --_line-gap: var(--slotted-upload-line-gap, var(--slotted-space-1, 0.125rem));
91
+ --_item-inset-block: var(--slotted-upload-item-padding-block, var(--slotted-space-4, 0.5rem));
92
+ --_item-inset-inline: var(
93
+ --slotted-upload-item-padding-inline,
94
+ var(--slotted-space-5, 0.75rem)
95
+ );
96
+ --_radius: var(--slotted-upload-radius, var(--slotted-radius-md, 6px));
97
+ /*
98
+ * The row's corner is the region's and `list-item`'s. A row is a card-sized
99
+ * object holding a four-pixel tile eight pixels in, and a four-pixel outer
100
+ * corner around it was the same curve as the tile's, which reads as a box
101
+ * cut from sheet rather than as a surface.
102
+ */
103
+ --_item-radius: var(--slotted-upload-item-radius, var(--slotted-radius-md, 6px));
104
+ --_border-width: var(--slotted-upload-border-width, var(--slotted-border-width-thin, 1px));
105
+
106
+ /**
107
+ * The region's own frame. The border is dashed and one step thicker than a
108
+ * control's, which is the one convention every surveyed drop region shares:
109
+ * a dashed edge says the box is waiting for something, where a solid edge
110
+ * of the same weight would read as a fieldset.
111
+ */
112
+ --_zone-inset-block: var(--slotted-upload-zone-padding-block, var(--slotted-space-9, 2rem));
113
+ --_zone-inset-inline: var(--slotted-upload-zone-padding-inline, var(--slotted-space-6, 1rem));
114
+ --_zone-border-width: var(
115
+ --slotted-upload-zone-border-width,
116
+ var(--slotted-border-width-medium, 1.5px)
117
+ );
118
+ /*
119
+ * The region at rest. A state of the whole family — invalid, disabled, the
120
+ * field shape — moves these two; a state of the region itself — the drag
121
+ * verdict, a pointer over it — moves the two the region paints with, which
122
+ * it derives from these, so the pointer can step a ground it did not choose.
123
+ */
124
+ --_zone-edge: var(--slotted-upload-zone-border-color, var(--slotted-border-strong));
125
+ --_zone-ground: var(--slotted-upload-zone-background, var(--slotted-surface-sunken));
126
+ --_zone-glyph-size: var(--slotted-upload-zone-icon-size, calc(1.5 * var(--_glyph-size)));
127
+
128
+ /**
129
+ * A row's controls are inner controls (R2): the glyph plus a step of air on
130
+ * each side, floored at WCAG 2.2 SC 2.5.8's twenty-four pixels so the
131
+ * target holds at every density. The glyph itself is floored at fourteen
132
+ * pixels for the reason the field families floor theirs — the control glyph
133
+ * scale falls to twelve at `dense`, and a cross that small beside a file
134
+ * name is a speck.
135
+ */
136
+ --_action-glyph-size: max(var(--slotted-upload-icon-size-min, 14px), var(--_glyph-size));
137
+ --_action-size: var(
138
+ --slotted-upload-action-size,
139
+ max(
140
+ var(--slotted-target-min, 24px),
141
+ min(
142
+ calc(var(--_action-glyph-size) + 2 * var(--slotted-space-2, 0.25rem)),
143
+ var(--_inner-size)
144
+ )
145
+ )
146
+ );
147
+
148
+ /**
149
+ * The status the row is in decides one colour, and every part of the row
150
+ * that says something about the status reads it. Neutral by default, so a
151
+ * row that has not been sent yet says nothing in colour at all.
152
+ */
153
+ --_status-text: var(--slotted-text-muted);
154
+ --_item-surface: var(--slotted-upload-item-background, var(--slotted-surface));
155
+ --_item-border: var(--slotted-upload-item-border-color, var(--slotted-border-subtle));
156
+
157
+ /** What a file's tile is painted when nothing more specific claims it. */
158
+ --_kind-tile: var(--slotted-upload-kind-other);
159
+
160
+ /**
161
+ * The leading, rounded up to a whole pixel. Thirteen at 1.5 is 19.5, and a
162
+ * row built from two lines and whole-pixel insets then stood at 59.5: every
163
+ * edge below the first fractional row landed between two device pixels and
164
+ * blurred at 1×. The rule is `timeline`'s `--_leading`. The computed
165
+ * `line-height` is a length and is inherited as that length, so a part set
166
+ * a step smaller reads `var(--_leading)` again: the custom property is
167
+ * inherited unresolved, and its `1em` resolves against that part's size.
168
+ */
169
+ --_leading: round(up, calc(1em * var(--slotted-line-height-md, 1.5)), 1px);
170
+
171
+ color: var(--slotted-text);
172
+ font-family: var(--slotted-upload-font-family, var(--slotted-control-font-family));
173
+ font-size: var(--_font-size);
174
+ line-height: var(--_leading);
175
+ }
176
+
177
+ /* The family itself: the column the reader sees in the page. */
178
+ [data-ui='upload'][data-part='root'] {
179
+ container-name: slotted-upload;
180
+ container-type: inline-size;
181
+ /**
182
+ * `container-type: inline-size` applies `contain: inline-size`, which makes
183
+ * the box's intrinsic width nought. In normal flow nothing is wrong; in a
184
+ * column that shrink-wraps its children — a flex column aligned to `start`,
185
+ * which is what the workbench stage is — the family would collapse to one
186
+ * character. `alert` found this in a rendered page rather than in a test,
187
+ * and the width is stated here for the same reason.
188
+ */
189
+ inline-size: 100%;
190
+
191
+ display: flex;
192
+ flex-direction: column;
193
+ gap: var(--_section-gap);
194
+ }
195
+
196
+ [data-ui='upload'][data-size='sm'] {
197
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
198
+ --_glyph-size: var(--slotted-control-icon-size-sm, 14px);
199
+ --_inner-size: var(--slotted-control-inner-size-sm, 22px);
200
+ --_control-height: var(--slotted-control-height-sm, 30px);
201
+ --_control-inset: var(--slotted-control-padding-inline-sm, 10px);
202
+ --_gap: var(--slotted-control-gap-sm, 6px);
203
+ }
204
+
205
+ [data-ui='upload'][data-size='lg'] {
206
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
207
+ --_glyph-size: var(--slotted-control-icon-size-lg, 18px);
208
+ --_inner-size: var(--slotted-control-inner-size-lg, 28px);
209
+ --_control-height: var(--slotted-control-height-lg, 38px);
210
+ --_control-inset: var(--slotted-control-padding-inline-lg, 16px);
211
+ --_gap: var(--slotted-control-gap-lg, 8px);
212
+ }
213
+
214
+ /* ── The family's own label and guidance ───────────────────────────────
215
+ *
216
+ * Both are absent when the family sits inside a `field`, which owns the
217
+ * label row, the description and the error slot for every control in this
218
+ * library. They exist for the upload that stands on its own.
219
+ */
220
+ [data-ui='upload'] [data-part='label'] {
221
+ color: var(--slotted-text);
222
+ flex: none;
223
+ font-weight: var(--slotted-upload-label-font-weight, var(--slotted-font-weight-medium, 500));
224
+ }
225
+
226
+ [data-ui='upload'] [data-part='description'] {
227
+ color: var(--slotted-text-muted);
228
+ flex: none;
229
+ margin: 0;
230
+ }
231
+
232
+ /* ── The region ────────────────────────────────────────────────────────
233
+ *
234
+ * A column: the mark, the sentence, and the trigger. The whole box forwards
235
+ * a click to the trigger when `clickToBrowse` is on, so the target is the
236
+ * box the reader is already looking at; the trigger is what the keyboard
237
+ * reaches, and WCAG 2.2 SC 2.5.7 is discharged by its being there rather
238
+ * than by the region pretending to be a control.
239
+ */
240
+ [data-ui='upload'] [data-part='dropzone'] {
241
+ --_zone-border-color: var(--_zone-edge);
242
+ --_zone-surface: var(--_zone-ground);
243
+
244
+ align-items: center;
245
+ background-color: var(--_zone-surface);
246
+ border: var(--_zone-border-width) dashed var(--_zone-border-color);
247
+ border-radius: var(--_radius);
248
+ display: flex;
249
+ flex: none;
250
+ flex-direction: column;
251
+ gap: var(--_gap);
252
+ padding-block: var(--_zone-inset-block);
253
+ padding-inline: var(--_zone-inset-inline);
254
+ text-align: center;
255
+ transition:
256
+ background-color var(--slotted-control-transition-duration)
257
+ var(--slotted-control-transition-easing),
258
+ border-color var(--slotted-control-transition-duration)
259
+ var(--slotted-control-transition-easing);
260
+ }
261
+
262
+ /**
263
+ * The mark: an `Icon` holding the `upload` glyph, which the binding draws in
264
+ * the `dropzone` shape only. The `field` shape is one control's height and
265
+ * has no room for a second mark beside the trigger's.
266
+ */
267
+ [data-ui='upload'] [data-part='dropzone'] > [data-ui='icon'] {
268
+ --slotted-icon-size: var(--_zone-glyph-size);
269
+
270
+ color: var(--slotted-text-muted);
271
+ }
272
+
273
+ /**
274
+ * The pointer is over the region and carrying something it will take. The
275
+ * accent border and its own tint are the pair `docs/token-foundation.md`
276
+ * requires for a state that has to be seen: the border holds the non-text
277
+ * ratio against the page and against the ground it encloses, and the ground
278
+ * alone would not.
279
+ */
280
+ [data-ui='upload'] [data-part='dropzone'][data-drop-target] {
281
+ --_zone-border-color: var(--slotted-tone-accent-border);
282
+ --_zone-surface: var(--slotted-tone-accent-surface);
283
+ }
284
+
285
+ /**
286
+ * The pointer is over the region carrying something it will refuse, and the
287
+ * reader has not let go yet. Only three of the five rules can be answered
288
+ * before the drop — `DataTransfer.items` gives the kind and the type and not
289
+ * the name or the size — so this is painted for a refusal that is already
290
+ * certain and never for one that is merely possible.
291
+ */
292
+ [data-ui='upload'] [data-part='dropzone'][data-drop-invalid] {
293
+ --_zone-border-color: var(--slotted-tone-danger-border);
294
+ --_zone-surface: var(--slotted-tone-danger-surface);
295
+ }
296
+
297
+ /**
298
+ * A region that forwards a click is a target, and a target answers the
299
+ * pointer. The binding writes `data-browsable` only while the forward is
300
+ * live, so a region that does nothing when pressed does not promise
301
+ * anything on hover.
302
+ *
303
+ * The edge takes the muted ink and the ground takes a share of the text
304
+ * colour: four per cent on hover, eight on press. The shares are half the
305
+ * family's control state layer because the layer here covers a card-sized
306
+ * box rather than a glyph-sized one, and eight per cent across the whole
307
+ * region read as a disabled block. Four moves the sunken ground by 0.031 in
308
+ * OKLab L, above the 0.02 the theme holds every surface step to, and eight
309
+ * by 0.062.
310
+ */
311
+ [data-ui='upload'] [data-part='dropzone'][data-browsable] {
312
+ cursor: pointer;
313
+ }
314
+
315
+ [data-ui='upload']:not([data-disabled])
316
+ [data-part='dropzone'][data-browsable]:not([data-drop-target], [data-drop-invalid]):hover {
317
+ --_zone-surface: color-mix(
318
+ in oklab,
319
+ var(--slotted-text) var(--slotted-upload-zone-state-layer-hover, 4%),
320
+ var(--_zone-ground)
321
+ );
322
+ }
323
+
324
+ [data-ui='upload']:not([data-disabled])
325
+ [data-part='dropzone'][data-browsable]:not([data-drop-target], [data-drop-invalid]):active {
326
+ --_zone-surface: color-mix(
327
+ in oklab,
328
+ var(--slotted-text) var(--slotted-upload-zone-state-layer-active, 8%),
329
+ var(--_zone-ground)
330
+ );
331
+ }
332
+
333
+ /* An invalid region keeps its danger edge under the pointer. */
334
+ [data-ui='upload']:not([data-disabled], [data-invalid])
335
+ [data-part='dropzone'][data-browsable]:not([data-drop-target], [data-drop-invalid]):is(
336
+ :hover,
337
+ :active
338
+ ) {
339
+ --_zone-border-color: var(--slotted-text-muted);
340
+ }
341
+
342
+ /*
343
+ * The trigger answers its own pointer. A hover over the trigger is also a
344
+ * hover over the region around it, and the two answering at once drew two
345
+ * targets for one press.
346
+ */
347
+ [data-ui='upload']:not([data-disabled])
348
+ [data-part='dropzone'][data-browsable]:not([data-drop-target], [data-drop-invalid]):has(
349
+ [data-part='trigger']:hover
350
+ ) {
351
+ --_zone-border-color: var(--_zone-edge);
352
+ --_zone-surface: var(--_zone-ground);
353
+ }
354
+
355
+ [data-ui='upload'][data-disabled][data-part='root'] {
356
+ --_zone-edge: var(--slotted-disabled-border);
357
+ --_zone-ground: var(--slotted-disabled-background);
358
+ }
359
+
360
+ [data-ui='upload'][data-disabled] [data-part='dropzone'] {
361
+ color: var(--slotted-disabled-foreground);
362
+ }
363
+
364
+ [data-ui='upload'][data-invalid][data-part='root'] {
365
+ --_zone-edge: var(--slotted-tone-danger-border);
366
+ }
367
+
368
+ /* ── The trigger, and the control inside it ────────────────────────────
369
+ *
370
+ * A `label` drawn as a control. It is not `button`: mounting another family
371
+ * here would give the label a second family's `data-ui` and a second
372
+ * family's contract, and what is wanted is one control-height box that opens
373
+ * the picker natively.
374
+ */
375
+ [data-ui='upload'] [data-part='trigger'] {
376
+ align-items: center;
377
+ background-color: var(--slotted-upload-trigger-background, var(--slotted-surface));
378
+ block-size: var(--_control-height);
379
+ border: var(--slotted-control-border-width, 1px) solid var(--slotted-control-border-color);
380
+ border-radius: var(--slotted-control-radius, 4px);
381
+ box-shadow: var(--slotted-control-shadow);
382
+ color: var(--slotted-text);
383
+ cursor: pointer;
384
+ display: inline-flex;
385
+ flex: none;
386
+ font: inherit;
387
+ font-weight: var(--slotted-control-font-weight, 600);
388
+ gap: var(--_gap);
389
+ justify-content: center;
390
+ padding-inline: var(--_control-inset);
391
+ position: relative;
392
+ transition:
393
+ background-color var(--slotted-control-transition-duration)
394
+ var(--slotted-control-transition-easing),
395
+ border-color var(--slotted-control-transition-duration)
396
+ var(--slotted-control-transition-easing),
397
+ translate var(--slotted-duration-instant) var(--slotted-control-transition-easing);
398
+ white-space: nowrap;
399
+ }
400
+
401
+ /**
402
+ * The binding draws the mark on the trigger only where the region does not:
403
+ * in the `dropzone` shape the region already carries it, and two of the same
404
+ * mark eighty pixels apart is one mark too many.
405
+ */
406
+ [data-ui='upload'] [data-part='trigger'] > [data-ui='icon'] {
407
+ --slotted-icon-size: var(--_glyph-size);
408
+ }
409
+
410
+ [data-ui='upload']:not([data-disabled]) [data-part='trigger']:hover {
411
+ background-color: var(--slotted-tone-secondary-subtle-hover);
412
+ }
413
+
414
+ /*
415
+ * The trigger is drawn as a control and presses like `button` does: the
416
+ * ground takes the next step and the box settles by a hairline, at once.
417
+ */
418
+ [data-ui='upload']:not([data-disabled]) [data-part='trigger']:active {
419
+ background-color: var(--slotted-tone-secondary-subtle-active);
420
+ translate: 0 var(--slotted-border-width-thin, 1px);
421
+ }
422
+
423
+ [data-ui='upload'][data-disabled] [data-part='trigger'] {
424
+ background-color: var(--slotted-disabled-background);
425
+ border-color: var(--slotted-disabled-border);
426
+ box-shadow: none;
427
+ color: var(--slotted-disabled-foreground);
428
+ cursor: default;
429
+ }
430
+
431
+ /**
432
+ * The ring belongs to the trigger and the focus belongs to the input inside
433
+ * it. A ring around a one-pixel input would be a ring around nothing.
434
+ */
435
+ [data-ui='upload'] [data-part='trigger']:has(:focus-visible) {
436
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
437
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
438
+ }
439
+
440
+ [data-ui='upload'][data-invalid] [data-part='trigger'] {
441
+ --slotted-focus-ring-color: var(--slotted-focus-ring-color-invalid);
442
+
443
+ border-color: var(--slotted-tone-danger-border);
444
+ /* Width as well as colour (WCAG 1.4.1): in `high-contrast` the danger
445
+ border is the resting edge's black. The trigger is the control in both
446
+ shapes, so it carries the edge `text-field` draws — inside the border,
447
+ so nothing moves, in place of the resting lift; its focus is an outline
448
+ and leaves the edge alone. The region keeps its dashed line. */
449
+ --_invalid-edge-width: var(--slotted-control-invalid-edge-width, 1px);
450
+
451
+ box-shadow: inset 0 0 0 var(--_invalid-edge-width) var(--slotted-tone-danger-border);
452
+ }
453
+
454
+ /**
455
+ * Clipped, and removed from nothing else. `display: none` and
456
+ * `visibility: hidden` take the control out of the tab order and out of the
457
+ * accessibility tree, which discards the reason the label pairing exists;
458
+ * stretching a transparent input across the region takes the drop away from
459
+ * the region and makes the drag state unobservable.
460
+ */
461
+ [data-ui='upload'] [data-part='input'] {
462
+ block-size: 1px;
463
+ border: 0;
464
+ clip-path: inset(50%);
465
+ inline-size: 1px;
466
+ inset-block-start: 50%;
467
+ inset-inline-start: 50%;
468
+ margin: -1px;
469
+ overflow: hidden;
470
+ padding: 0;
471
+ position: absolute;
472
+ white-space: nowrap;
473
+ }
474
+
475
+ /**
476
+ * The family draws the ring on the trigger, so the one the browser would
477
+ * draw on the element holding focus has to go: `tree` moved its ring and
478
+ * forgot this, and a focused row carried two rings in two colours.
479
+ */
480
+ [data-ui='upload'] [data-part='input']:focus-visible {
481
+ outline: none;
482
+ }
483
+
484
+ /* ── The summary ───────────────────────────────────────────────────────
485
+ *
486
+ * What the whole queue is doing, in one line: how many files, how far along,
487
+ * how much longer, and the controls that act on every row at once. It is
488
+ * drawn only when there is more than one file, because for a single file the
489
+ * row already says all of it.
490
+ */
491
+ [data-ui='upload'] [data-part='summary'] {
492
+ align-items: center;
493
+ color: var(--slotted-text-muted);
494
+ display: flex;
495
+ flex: none;
496
+ gap: var(--_gap);
497
+ justify-content: space-between;
498
+ }
499
+
500
+ [data-ui='upload'] [data-part='clear'] {
501
+ appearance: none;
502
+ flex: none;
503
+ white-space: nowrap;
504
+ background-color: transparent;
505
+ border: 0;
506
+ border-radius: var(--slotted-control-radius, 4px);
507
+ color: inherit;
508
+ cursor: pointer;
509
+ font: inherit;
510
+ padding-block: var(--slotted-space-1, 0.125rem);
511
+ padding-inline: var(--slotted-space-3, 0.375rem);
512
+ text-decoration-line: underline;
513
+ text-underline-offset: 0.2em;
514
+ transition: background-color var(--slotted-control-transition-duration)
515
+ var(--slotted-control-transition-easing);
516
+ }
517
+
518
+ [data-ui='upload'] [data-part='clear']:enabled:hover {
519
+ background-color: var(--_state-hover);
520
+ }
521
+
522
+ [data-ui='upload'] [data-part='clear']:enabled:active {
523
+ background-color: var(--_state-active);
524
+ }
525
+
526
+ [data-ui='upload'] [data-part='clear']:disabled {
527
+ color: var(--slotted-disabled-foreground);
528
+ cursor: default;
529
+ }
530
+
531
+ [data-ui='upload'] [data-part='clear']:focus-visible {
532
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
533
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
534
+ }
535
+
536
+ /* ── The list ──────────────────────────────────────────────────────────
537
+ *
538
+ * A directory selection produces four hundred files, and four hundred rows
539
+ * is not a list a person reads. The block size is bounded in rows and the
540
+ * platform scrolls the rest: no disclosure, no second state model, and a row
541
+ * left half-visible at the fold is what says there is more.
542
+ *
543
+ * `--slotted-upload-visible-rows` is written by the binding from `maxRows`,
544
+ * because it is a fact about this instance rather than a value a theme can
545
+ * hold — the category `--slotted-tree-depth` is in. `maxRows: 0` means no
546
+ * bound, and the binding says so by writing the number of files rather than
547
+ * by a second property: a cap that is the content's own height is a cap
548
+ * nothing reaches, and the sheet then needs one rule instead of two.
549
+ *
550
+ * The row height in the sum is a row at rest. A row that has grown a bar and
551
+ * a message is taller, so the fold falls inside the last row rather than
552
+ * between two — which is what says there is more below without a shadow, a
553
+ * gradient or a count.
554
+ */
555
+ [data-ui='upload'] [data-part='list'] {
556
+ /** One row, from the parts a row is made of. Used twice below. */
557
+ --_row-block-size: calc(
558
+ max(var(--_preview-size), 2lh) + 2 * var(--_item-inset-block) + 2 * var(--_border-width) +
559
+ var(--_item-gap)
560
+ );
561
+
562
+ display: flex;
563
+ flex-direction: column;
564
+ gap: var(--_item-gap);
565
+ list-style: none;
566
+ margin: 0;
567
+ /*
568
+ * The list is the one part of the column that gives. Everything above it
569
+ * holds its size, so an 18rem panel takes the room out of the queue and
570
+ * not out of the region, the summary or the label — and the queue answers
571
+ * by scrolling. A flex item's floor is its content, so the floor is stated
572
+ * here: without it the list is what an outer bound pushes on, and it went
573
+ * to nought inside a short panel while the region kept every pixel.
574
+ */
575
+ flex: 1 1 auto;
576
+ min-block-size: var(--_row-block-size);
577
+ max-block-size: calc(var(--slotted-upload-visible-rows, 6) * var(--_row-block-size));
578
+ overflow-y: auto;
579
+ padding: 0;
580
+ }
581
+
582
+ /* ── One file ──────────────────────────────────────────────────────────
583
+ *
584
+ * A row is one object and it is laid out as a grid, not as a flex line. Four
585
+ * things follow from that, and all four were wrong in the shape before it.
586
+ *
587
+ * **The marks are centred on the block they are about.** The chip and the
588
+ * controls sit in the first track; the sentence a failure carries spans a
589
+ * second track underneath. So a row that has something to explain grows
590
+ * downwards and moves nothing: centring is over the name and its detail,
591
+ * which is what the chip and the controls are about, rather than over a box
592
+ * whose height a two-line message decided.
593
+ *
594
+ * **The size left the name's line.** It stood at the far end of it, so the
595
+ * column of sizes moved with however many controls each row happened to
596
+ * offer, and the list read as ragged. It is secondary information and it now
597
+ * sits under the name with the status, where it lines up with the name above
598
+ * it and needs no reserved width anywhere.
599
+ *
600
+ * **The bar left the flow.** It is the row's own bottom edge, so a row is the
601
+ * same height whether it is waiting, sending or done, and progress is
602
+ * readable straight down a list of nine.
603
+ *
604
+ * **The row has a surface and an edge.** Before, a file was a naked line of
605
+ * text with icons beside it, and only a failed one had a box; a file is a
606
+ * thing the reader acts on, and it reads as one.
607
+ */
608
+ [data-ui='upload'] [data-part='item'] {
609
+ align-items: center;
610
+ /*
611
+ * A row is never squeezed. Flex children shrink by default, so nine rows in
612
+ * the room for three were compressed to slivers — the tile flattened, the
613
+ * names spilled out of the rows that no longer had height for them, and
614
+ * nothing scrolled, because the content had been made to fit. Read at 420
615
+ * pixels with `--slotted-upload-visible-rows: 3` and nine files.
616
+ */
617
+ flex: none;
618
+ background-color: var(--_item-surface);
619
+ border: var(--_border-width) solid var(--_item-border);
620
+ border-radius: var(--_item-radius);
621
+ column-gap: var(--_gap);
622
+ display: grid;
623
+ grid-template-areas:
624
+ 'preview body actions'
625
+ 'message message message';
626
+ grid-template-columns: auto minmax(0, 1fr) auto;
627
+ /*
628
+ * The bar is a child positioned against this box, and the corner radius is
629
+ * what clips it to the row's own shape.
630
+ */
631
+ overflow: hidden;
632
+ padding-block: var(--_item-inset-block);
633
+ padding-inline: var(--_item-inset-inline);
634
+ position: relative;
635
+ row-gap: var(--_line-gap);
636
+ transition:
637
+ background-color var(--slotted-control-transition-duration)
638
+ var(--slotted-control-transition-easing),
639
+ border-color var(--slotted-control-transition-duration)
640
+ var(--slotted-control-transition-easing),
641
+ opacity var(--slotted-duration-medium) var(--slotted-easing-enter),
642
+ translate var(--slotted-duration-medium) var(--slotted-easing-enter);
643
+ }
644
+
645
+ /**
646
+ * A file arrives: it fades in from a step above its place, so a row that
647
+ * lands in a list of nine is seen landing rather than found there. It starts
648
+ * from the style below only on the frame the row is first rendered — a row
649
+ * that re-renders with a new status does not arrive again.
650
+ *
651
+ * A row that leaves does not animate. The binding removes it when the files
652
+ * change, and holding it for an exit would need a second list beside the
653
+ * controlled `files`; the promise is in `docs/process/build-order.md`.
654
+ */
655
+ @starting-style {
656
+ [data-ui='upload'] [data-part='item'] {
657
+ opacity: 0;
658
+ translate: 0 calc(-1 * var(--slotted-space-2, 0.25rem));
659
+ }
660
+ }
661
+
662
+ [data-ui='upload'] [data-part='item'][data-status='success'] {
663
+ --_status-text: var(--slotted-tone-success-text);
664
+ }
665
+
666
+ /**
667
+ * A file the reader has chosen and not yet sent has a dashed edge: the edge
668
+ * the region has, which already means *waiting for something* in this
669
+ * family. It is said in line rather than in colour, which keeps the rule
670
+ * above — a row not yet sent says nothing in colour — and it holds under
671
+ * forced colours, where the line survives and a tint does not. ReUI draws
672
+ * its `idle` attachment the same way (`c-attachment-5`).
673
+ */
674
+ [data-ui='upload'] [data-part='item'][data-status='ready'] {
675
+ border-style: dashed;
676
+ }
677
+
678
+ [data-ui='upload'] [data-part='item'][data-status='paused'],
679
+ [data-ui='upload'] [data-part='item'][data-status='stalled'] {
680
+ --_status-text: var(--slotted-tone-warning-text);
681
+ }
682
+
683
+ /**
684
+ * The three failures, and the two of them that tint their row. A file that
685
+ * was refused before it entered the queue and a file whose transfer failed
686
+ * both need the reader to do something; a file the reader stopped themselves
687
+ * does not, so it stays neutral and only its word changes.
688
+ */
689
+ [data-ui='upload'] [data-part='item'][data-status='rejected'],
690
+ [data-ui='upload'] [data-part='item'][data-status='error'] {
691
+ --_status-text: var(--slotted-tone-danger-text);
692
+ --_item-surface: var(--slotted-tone-danger-surface);
693
+ --_item-border: var(--slotted-tone-danger-border);
694
+ }
695
+
696
+ [data-ui='upload'] [data-part='item'][data-status='canceled'] {
697
+ --_status-text: var(--slotted-text-muted);
698
+ }
699
+
700
+ [data-ui='upload'][data-disabled] [data-part='item'] {
701
+ color: var(--slotted-disabled-foreground);
702
+ }
703
+
704
+ /* ── The chip ──────────────────────────────────────────────────────────
705
+ *
706
+ * A solid tile in the file's own colour with the mark knocked out of it.
707
+ *
708
+ * The shape this replaced was a grey square with a coloured mark inside, and
709
+ * the reviewer's reading of it was the right one: a wash of a tone at twelve
710
+ * per cent over a white page is not a colour, it is a grey box with a colour
711
+ * near it. A tile is a decision; a tint is an apology for not making one.
712
+ *
713
+ * The colours are the ones a reader has already learnt from every product
714
+ * they use — a PDF red, a word-processed document blue, a spreadsheet green,
715
+ * a deck orange, a film indigo — and they are a scale of the theme's rather
716
+ * than five tones stretched over nine kinds, because the tones hold no second
717
+ * blue and no indigo at all. The argument, the values and the measurements
718
+ * are under **File kind** in `docs/token-foundation.md`.
719
+ *
720
+ * One ratio answers both questions the pair raises, because the mark is the
721
+ * tile's own foreground: the tile against the page, and the mark against the
722
+ * tile. `packages/themes/test/default-theme.test.mjs` measures all
723
+ * twenty-seven.
724
+ */
725
+ [data-ui='upload'] [data-part='preview'] {
726
+ align-items: center;
727
+ background-color: var(--_kind-tile);
728
+ block-size: var(--_preview-size);
729
+ border-radius: var(--slotted-upload-preview-radius, var(--slotted-radius-sm, 4px));
730
+ color: var(--slotted-upload-kind-foreground);
731
+ display: inline-flex;
732
+ flex-direction: column;
733
+ grid-area: preview;
734
+ inline-size: var(--_preview-size);
735
+ justify-content: center;
736
+ overflow: hidden;
737
+ }
738
+
739
+ [data-ui='upload'] [data-part='preview'] > img {
740
+ block-size: 100%;
741
+ inline-size: 100%;
742
+ object-fit: cover;
743
+ }
744
+
745
+ /* The kind's glyph, drawn by the binding when there is no picture to show. */
746
+ [data-ui='upload'] [data-part='preview'] > [data-ui='icon'] {
747
+ --slotted-icon-size: var(--_glyph-size);
748
+ }
749
+
750
+ [data-ui='upload'] [data-part='preview'][data-kind='pdf'] {
751
+ --_kind-tile: var(--slotted-upload-kind-pdf);
752
+ }
753
+
754
+ [data-ui='upload'] [data-part='preview'][data-kind='document'] {
755
+ --_kind-tile: var(--slotted-upload-kind-document);
756
+ }
757
+
758
+ [data-ui='upload'] [data-part='preview'][data-kind='spreadsheet'] {
759
+ --_kind-tile: var(--slotted-upload-kind-spreadsheet);
760
+ }
761
+
762
+ [data-ui='upload'] [data-part='preview'][data-kind='presentation'] {
763
+ --_kind-tile: var(--slotted-upload-kind-presentation);
764
+ }
765
+
766
+ [data-ui='upload'] [data-part='preview'][data-kind='image'] {
767
+ --_kind-tile: var(--slotted-upload-kind-image);
768
+ }
769
+
770
+ [data-ui='upload'] [data-part='preview'][data-kind='video'] {
771
+ --_kind-tile: var(--slotted-upload-kind-video);
772
+ }
773
+
774
+ [data-ui='upload'] [data-part='preview'][data-kind='audio'] {
775
+ --_kind-tile: var(--slotted-upload-kind-audio);
776
+ }
777
+
778
+ [data-ui='upload'] [data-part='preview'][data-kind='archive'] {
779
+ --_kind-tile: var(--slotted-upload-kind-archive);
780
+ }
781
+
782
+ /*
783
+ * A tinted row keeps its tiles. The rule here was the other way round for one
784
+ * capture — the status took the tile's colour, on the argument that what went
785
+ * wrong outranks what the file is — and the capture showed what it cost: a
786
+ * refused archive turned red on the one row whose message says archives are
787
+ * not accepted, so the colour stopped answering the question the reader had.
788
+ *
789
+ * The status has four signals already: the row's ground, its edge, its word
790
+ * and its sentence. The tile is about the file. Measured rather than assumed,
791
+ * every kind clears 4.03 : 1 against a failed row's ground in `light`,
792
+ * 6.46 : 1 in `dark` and 8.31 : 1 in `high-contrast`, against a 3 : 1 floor —
793
+ * held by `packages/themes/test/default-theme.test.mjs`.
794
+ */
795
+
796
+ /* ── What the row says ─────────────────────────────────────────────────
797
+ *
798
+ * Two lines: the name, and everything secondary about it on one line under
799
+ * it. The body is the only part of the row with `min-inline-size: 0`, which
800
+ * is what makes a long name truncate instead of pushing the controls off the
801
+ * end.
802
+ */
803
+ [data-ui='upload'] [data-part='item'] > [data-part='body'] {
804
+ display: flex;
805
+ flex-direction: column;
806
+ gap: var(--_line-gap);
807
+ grid-area: body;
808
+ min-inline-size: 0;
809
+ }
810
+
811
+ /**
812
+ * The name and its extension are one run, and the run is laid out in the
813
+ * name's own direction rather than the document's. The binding writes
814
+ * `dir="auto"` here, so `claim-form-2026-0914.pdf` reads in that order inside
815
+ * a right-to-left page: without the wrapper the two are separate flex items,
816
+ * the container places them right to left, and the reader is shown
817
+ * `pdf.claim-form-2026-0914`. Found in the harness capture at `dir="rtl"`,
818
+ * where nothing failed and the name was backwards.
819
+ */
820
+ [data-ui='upload'] [data-part='filename'] {
821
+ display: flex;
822
+ /*
823
+ * The name is the row's title and the detail line under it is at the same
824
+ * weight as body text, so the two lines differed by one pixel of size and
825
+ * nothing else. Medium is the weight `label` already uses for the family's
826
+ * own heading.
827
+ */
828
+ font-weight: var(--slotted-upload-name-font-weight, var(--slotted-font-weight-medium, 500));
829
+ min-inline-size: 0;
830
+ unicode-bidi: isolate;
831
+ }
832
+
833
+ /**
834
+ * The name opens the file. A reader who has just attached a scan clicks the
835
+ * scan, which is what every product they already use does, and it is why the
836
+ * eye that used to sit in the controls is gone: a third affordance for an
837
+ * action the name already implies, in a row that has enough in it.
838
+ *
839
+ * It is a real `button` when the file can be previewed and a plain `span`
840
+ * when it cannot, so the affordance is present exactly when the action is.
841
+ * Nothing about it looks like a control until the pointer is on it — a list
842
+ * of nine underlined names is a list nobody can read.
843
+ */
844
+ [data-ui='upload'] button[data-part='filename'] {
845
+ appearance: none;
846
+ background: none;
847
+ border: 0;
848
+ color: inherit;
849
+ cursor: pointer;
850
+ /* Everything but the weight, which the name states for itself above. */
851
+ font-family: inherit;
852
+ font-size: inherit;
853
+ line-height: inherit;
854
+ padding: 0;
855
+ text-align: start;
856
+ transition: translate var(--slotted-duration-instant) var(--slotted-control-transition-easing);
857
+ }
858
+
859
+ [data-ui='upload'] button[data-part='filename']:enabled:hover {
860
+ text-decoration-line: underline;
861
+ text-underline-offset: 0.2em;
862
+ }
863
+
864
+ /*
865
+ * A name has no ground to step, and its hover already drew the underline, so
866
+ * a press answers in place: the name settles by a hairline — the answer
867
+ * `timeline` gives its group heading.
868
+ */
869
+ [data-ui='upload'] button[data-part='filename']:enabled:active {
870
+ text-decoration-line: underline;
871
+ text-underline-offset: 0.2em;
872
+ translate: 0 var(--slotted-border-width-thin, 1px);
873
+ }
874
+
875
+ [data-ui='upload'] button[data-part='filename']:focus-visible {
876
+ border-radius: var(--slotted-control-radius, 4px);
877
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
878
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
879
+ }
880
+
881
+ /* In a wall of pictures the picture is the thing the reader is pointing at. */
882
+ [data-ui='upload'][data-layout='grid'] button[data-part='preview'] {
883
+ appearance: none;
884
+ border: 0;
885
+ cursor: pointer;
886
+ padding: 0;
887
+ }
888
+
889
+ /**
890
+ * The name shrinks to an ellipsis and the extension does not, so
891
+ * `quarterly-report-consolidated-final-v7.pdf` becomes
892
+ * `quarterly-report-conso….pdf` rather than losing the part that says what
893
+ * the file is. CSS cannot truncate in the middle; the split is made at the
894
+ * last dot in the binding, and this is the half of the answer the sheet owes.
895
+ */
896
+ [data-ui='upload'] [data-part='name'] {
897
+ flex: 0 1 auto;
898
+ min-inline-size: 0;
899
+ overflow: hidden;
900
+ text-overflow: ellipsis;
901
+ white-space: nowrap;
902
+ }
903
+
904
+ [data-ui='upload'] [data-part='extension'] {
905
+ flex: none;
906
+ }
907
+
908
+ /** The second line: the size, then the word for what is happening to it. */
909
+ [data-ui='upload'] [data-part='detail'] {
910
+ align-items: baseline;
911
+ color: var(--slotted-text-muted);
912
+ display: flex;
913
+ flex-wrap: wrap;
914
+ font-size: var(--slotted-font-size-xs, 0.75rem);
915
+ line-height: var(--_leading);
916
+ min-inline-size: 0;
917
+ }
918
+
919
+ [data-ui='upload'] [data-part='meta'] {
920
+ font-variant-numeric: tabular-nums;
921
+ white-space: nowrap;
922
+ }
923
+
924
+ /**
925
+ * The word, in the colour the status set. A status said in colour alone fails
926
+ * WCAG 2.2 SC 1.4.1, which is why it is a word first and a colour second.
927
+ */
928
+ [data-ui='upload'] [data-part='status'] {
929
+ color: var(--_status-text);
930
+ font-weight: var(--slotted-upload-status-font-weight, var(--slotted-font-weight-medium, 500));
931
+ min-inline-size: 0;
932
+ }
933
+
934
+ /**
935
+ * The separator between the two, and it is a character because it is one: a
936
+ * middot between two pieces of text is punctuation, not a mark the family
937
+ * draws. `breadcrumb` writes its own the same way.
938
+ *
939
+ * It hangs off the size rather than off the word, so that a detail line which
940
+ * wraps breaks *after* the dot and not before it. Written the other way round
941
+ * it left an orphaned middot at the start of the second line, which is what
942
+ * the card at 720 pixels showed.
943
+ */
944
+ [data-ui='upload'] [data-part='meta']:has(+ [data-part='status'])::after {
945
+ color: var(--slotted-text-muted);
946
+ content: ' ·';
947
+ margin-inline-end: 0.4em;
948
+ }
949
+
950
+ [data-ui='upload'] [data-part='message'] {
951
+ color: var(--_status-text);
952
+ font-size: var(--slotted-font-size-xs, 0.75rem);
953
+ line-height: var(--_leading);
954
+ grid-area: message;
955
+ margin: 0;
956
+ }
957
+
958
+ /**
959
+ * The bar is the row's own bottom edge. It is `progress-indicator`, drawn
960
+ * where the family put it: the family says how much room a bar gets and
961
+ * nothing about how it is drawn, because a second bar here would be a second
962
+ * answer to one question, drifting from the first the moment either is
963
+ * retuned.
964
+ */
965
+ [data-ui='upload'] [data-part='progress'] {
966
+ display: block;
967
+ inset-block-end: 0;
968
+ inset-inline: 0;
969
+ position: absolute;
970
+ }
971
+
972
+ /* ── A row's controls ──────────────────────────────────────────────────
973
+ *
974
+ * Inner controls (R2): the glyph plus a step of air, no frame and no ground
975
+ * at rest, one ground step on hover and one more on press, the theme's ring
976
+ * on focus, and a target that reaches the WCAG minimum without the glyph
977
+ * growing to fill it.
978
+ */
979
+ /**
980
+ * Two slots, whatever this row happens to offer. A row with one control and a
981
+ * row with two would otherwise end their bodies fifty pixels apart, and the
982
+ * column of sizes down the list read as ragged rather than as a column —
983
+ * which is what the first capture at 420 pixels showed.
984
+ */
985
+ /**
986
+ * Packed to the end of the row and reserving nothing. The reservation this
987
+ * replaced existed to keep a column of sizes straight while the size lived at
988
+ * the far end of the name's line; the size is on its own line now, so there
989
+ * is no column to keep and a row that offers one control no longer carries
990
+ * two empty slots to prove it.
991
+ */
992
+ [data-ui='upload'] [data-part='actions'] {
993
+ display: inline-flex;
994
+ gap: var(--slotted-space-1, 0.125rem);
995
+ grid-area: actions;
996
+ justify-content: flex-end;
997
+ }
998
+
999
+ [data-ui='upload'] [data-part='viewerExternal'],
1000
+ [data-ui='upload'] [data-part='viewerClose'],
1001
+ [data-ui='upload'] [data-part='start'],
1002
+ [data-ui='upload'] [data-part='pause'],
1003
+ [data-ui='upload'] [data-part='retry'],
1004
+ [data-ui='upload'] [data-part='cancel'],
1005
+ [data-ui='upload'] [data-part='remove'] {
1006
+ align-items: center;
1007
+ appearance: none;
1008
+ background-color: transparent;
1009
+ block-size: var(--_action-size);
1010
+ border: 0;
1011
+ border-radius: var(--slotted-upload-action-radius, var(--slotted-control-radius, 4px));
1012
+ color: inherit;
1013
+ cursor: pointer;
1014
+ display: inline-flex;
1015
+ flex: none;
1016
+ font: inherit;
1017
+ inline-size: var(--_action-size);
1018
+ justify-content: center;
1019
+ padding: 0;
1020
+ transition:
1021
+ background-color var(--slotted-control-transition-duration)
1022
+ var(--slotted-control-transition-easing),
1023
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
1024
+ }
1025
+
1026
+ /**
1027
+ * Each control holds the `Icon` its binding chose for it: external, x, play,
1028
+ * pause, rotate-ccw, x and trash. A cross means *stop this* and a bin means
1029
+ * *take it away*, because the marks appear together on one row.
1030
+ */
1031
+ [data-ui='upload'] [data-part='viewerExternal'] > [data-ui='icon'],
1032
+ [data-ui='upload'] [data-part='viewerClose'] > [data-ui='icon'],
1033
+ [data-ui='upload'] [data-part='start'] > [data-ui='icon'],
1034
+ [data-ui='upload'] [data-part='pause'] > [data-ui='icon'],
1035
+ [data-ui='upload'] [data-part='retry'] > [data-ui='icon'],
1036
+ [data-ui='upload'] [data-part='cancel'] > [data-ui='icon'],
1037
+ [data-ui='upload'] [data-part='remove'] > [data-ui='icon'] {
1038
+ --slotted-icon-size: var(--_action-glyph-size);
1039
+ }
1040
+
1041
+ /**
1042
+ * On a finger the target is the comfortable size in both axes and the box
1043
+ * is the one a mouse sees: a transparent `::after` centred on each control
1044
+ * reaches 44 pixels, and the ground and the ring stay square. The controls
1045
+ * of one row stand two pixels apart, so none reaches across the edge it
1046
+ * shares with the one beside it; between two of them each keeps its own
1047
+ * width and is 44 tall.
1048
+ */
1049
+ @media (pointer: coarse) {
1050
+ [data-ui='upload']
1051
+ :is(
1052
+ [data-part='viewerExternal'],
1053
+ [data-part='viewerClose'],
1054
+ [data-part='start'],
1055
+ [data-part='pause'],
1056
+ [data-part='retry'],
1057
+ [data-part='cancel'],
1058
+ [data-part='remove']
1059
+ ) {
1060
+ position: relative;
1061
+ }
1062
+
1063
+ [data-ui='upload']
1064
+ :is(
1065
+ [data-part='viewerExternal'],
1066
+ [data-part='viewerClose'],
1067
+ [data-part='start'],
1068
+ [data-part='pause'],
1069
+ [data-part='retry'],
1070
+ [data-part='cancel'],
1071
+ [data-part='remove']
1072
+ )::after {
1073
+ content: '';
1074
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
1075
+ position: absolute;
1076
+ }
1077
+
1078
+ [data-ui='upload']
1079
+ :is(
1080
+ [data-part='viewerExternal'],
1081
+ [data-part='start'],
1082
+ [data-part='pause'],
1083
+ [data-part='retry'],
1084
+ [data-part='cancel']
1085
+ ):has(
1086
+ + :is(
1087
+ [data-part='viewerClose'],
1088
+ [data-part='pause'],
1089
+ [data-part='retry'],
1090
+ [data-part='cancel'],
1091
+ [data-part='remove']
1092
+ ):not([hidden])
1093
+ )::after {
1094
+ inset-inline-end: 0;
1095
+ }
1096
+
1097
+ [data-ui='upload']
1098
+ :is(
1099
+ [data-part='viewerExternal'],
1100
+ [data-part='start'],
1101
+ [data-part='pause'],
1102
+ [data-part='retry'],
1103
+ [data-part='cancel']
1104
+ ):not([hidden])
1105
+ + :is(
1106
+ [data-part='viewerClose'],
1107
+ [data-part='pause'],
1108
+ [data-part='retry'],
1109
+ [data-part='cancel'],
1110
+ [data-part='remove']
1111
+ )::after {
1112
+ inset-inline-start: 0;
1113
+ }
1114
+ }
1115
+
1116
+ /**
1117
+ * `:enabled` is the guard `packages/styles/src/states.test.mjs` requires,
1118
+ * and the reason it exists: a control the browser has already refused must
1119
+ * not answer a pointer.
1120
+ */
1121
+ [data-ui='upload'] [data-part='viewerExternal']:enabled:hover,
1122
+ [data-ui='upload'] [data-part='viewerClose']:enabled:hover,
1123
+ [data-ui='upload'] [data-part='start']:enabled:hover,
1124
+ [data-ui='upload'] [data-part='pause']:enabled:hover,
1125
+ [data-ui='upload'] [data-part='retry']:enabled:hover,
1126
+ [data-ui='upload'] [data-part='cancel']:enabled:hover,
1127
+ [data-ui='upload'] [data-part='remove']:enabled:hover {
1128
+ background-color: var(--_state-hover);
1129
+ }
1130
+
1131
+ [data-ui='upload'] [data-part='viewerExternal']:enabled:active,
1132
+ [data-ui='upload'] [data-part='viewerClose']:enabled:active,
1133
+ [data-ui='upload'] [data-part='start']:enabled:active,
1134
+ [data-ui='upload'] [data-part='pause']:enabled:active,
1135
+ [data-ui='upload'] [data-part='retry']:enabled:active,
1136
+ [data-ui='upload'] [data-part='cancel']:enabled:active,
1137
+ [data-ui='upload'] [data-part='remove']:enabled:active {
1138
+ background-color: var(--_state-active);
1139
+ }
1140
+
1141
+ [data-ui='upload'] [data-part='viewerExternal']:disabled,
1142
+ [data-ui='upload'] [data-part='viewerClose']:disabled,
1143
+ [data-ui='upload'] [data-part='start']:disabled,
1144
+ [data-ui='upload'] [data-part='pause']:disabled,
1145
+ [data-ui='upload'] [data-part='retry']:disabled,
1146
+ [data-ui='upload'] [data-part='cancel']:disabled,
1147
+ [data-ui='upload'] [data-part='remove']:disabled {
1148
+ color: var(--slotted-disabled-foreground);
1149
+ cursor: default;
1150
+ }
1151
+
1152
+ [data-ui='upload'] [data-part='viewerExternal']:focus-visible,
1153
+ [data-ui='upload'] [data-part='viewerClose']:focus-visible,
1154
+ [data-ui='upload'] [data-part='start']:focus-visible,
1155
+ [data-ui='upload'] [data-part='pause']:focus-visible,
1156
+ [data-ui='upload'] [data-part='retry']:focus-visible,
1157
+ [data-ui='upload'] [data-part='cancel']:focus-visible,
1158
+ [data-ui='upload'] [data-part='remove']:focus-visible {
1159
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
1160
+ outline-offset: calc(-1 * var(--slotted-focus-ring-width, 2px));
1161
+ }
1162
+
1163
+ /* ── The viewer ────────────────────────────────────────────────────────
1164
+ *
1165
+ * A file, full screen, on a surface of its own. `dialog` owns the layer, the
1166
+ * scrim, the focus containment, the scroll lock and Escape; what this family
1167
+ * owns is the stage, the header and the two controls that move through the
1168
+ * set.
1169
+ *
1170
+ * A preview that left the page was the first answer and it was the wrong one:
1171
+ * it loses the form the reader was in the middle of filling in. The new tab
1172
+ * survives as the escape hatch inside the viewer, where a PDF is better read
1173
+ * in the browser's own full window.
1174
+ */
1175
+ [data-ui='upload'][data-part='viewer'] {
1176
+ background-color: var(--slotted-surface);
1177
+ block-size: 100%;
1178
+ display: grid;
1179
+ /*
1180
+ * The dialog's panel is a three-row grid — header, body, footer — and this
1181
+ * family brings a header of its own. Written into the first row the viewer
1182
+ * is as tall as its own content, with the rest of a full-screen layer left
1183
+ * blank under it; spanning the three makes the layer the viewer's surface.
1184
+ * The declaration is ignored wherever the parent is not a grid, which is
1185
+ * what lets the same box be photographed on its own in the harness.
1186
+ */
1187
+ grid-row: 1 / -1;
1188
+ grid-template-rows: auto minmax(0, 1fr);
1189
+ inline-size: 100%;
1190
+ }
1191
+
1192
+ [data-ui='upload'] [data-part='viewerHeader'] {
1193
+ align-items: center;
1194
+ border-block-end: var(--_border-width) solid var(--slotted-border-subtle);
1195
+ column-gap: var(--_gap);
1196
+ display: grid;
1197
+ grid-template-columns: minmax(0, 1fr) auto;
1198
+ padding-block: var(--slotted-space-4, 0.5rem);
1199
+ padding-inline: var(--slotted-space-6, 1rem);
1200
+ }
1201
+
1202
+ [data-ui='upload'] [data-part='viewerTitle'] {
1203
+ font-size: var(--slotted-font-size-md, 0.875rem);
1204
+ line-height: var(--_leading);
1205
+ font-weight: var(--slotted-font-weight-semibold, 600);
1206
+ margin: 0;
1207
+ overflow: hidden;
1208
+ text-overflow: ellipsis;
1209
+ white-space: nowrap;
1210
+ }
1211
+
1212
+ [data-ui='upload'] [data-part='viewerMeta'] {
1213
+ color: var(--slotted-text-muted);
1214
+ font-size: var(--slotted-font-size-xs, 0.75rem);
1215
+ line-height: var(--_leading);
1216
+ }
1217
+
1218
+ [data-ui='upload'] [data-part='viewerActions'] {
1219
+ align-items: center;
1220
+ display: inline-flex;
1221
+ gap: var(--slotted-space-1, 0.125rem);
1222
+ }
1223
+
1224
+ /**
1225
+ * The stage. A sunken ground rather than the page's own, because what stands
1226
+ * on it is the consumer's content and a photograph with a white border on a
1227
+ * white page has no edge at all.
1228
+ */
1229
+ [data-ui='upload'] [data-part='viewerStage'] {
1230
+ background-color: var(--slotted-surface-sunken);
1231
+ display: grid;
1232
+ min-block-size: 0;
1233
+ /*
1234
+ * The stage clips. Read at 720 pixels without it, an SVG with a `viewBox`
1235
+ * and no intrinsic size filled the width, ran its height past the surface
1236
+ * and covered the header of the next one: a percentage maximum against an
1237
+ * auto height is not a maximum.
1238
+ */
1239
+ overflow: hidden;
1240
+ padding: var(--slotted-space-6, 1rem);
1241
+ place-items: center;
1242
+ position: relative;
1243
+ }
1244
+
1245
+ /*
1246
+ * The picture takes the whole stage and is contained inside it. It is
1247
+ * stretched into its grid area rather than given `block-size: 100%`, because
1248
+ * a percentage needs an area that is definite to resolve against and falls
1249
+ * back to the picture's own height where it is not: a portrait scan four
1250
+ * thousand pixels tall then grew downwards and out of the layer, which is what
1251
+ * a reader saw as an image running off the screen. `contain` is what keeps a
1252
+ * portrait scan and a wide dashcam frame both whole in the same box.
1253
+ */
1254
+ [data-ui='upload'] [data-part='viewerStage'] > img,
1255
+ [data-ui='upload'] [data-part='viewerStage'] > iframe {
1256
+ align-self: stretch;
1257
+ block-size: auto;
1258
+ inline-size: auto;
1259
+ justify-self: stretch;
1260
+ min-block-size: 0;
1261
+ min-inline-size: 0;
1262
+ }
1263
+
1264
+ [data-ui='upload'] [data-part='viewerStage'] > img {
1265
+ object-fit: contain;
1266
+ }
1267
+
1268
+ /*
1269
+ * A PDF frame carries no `sandbox` and a text frame does, and the asymmetry is
1270
+ * measured: Chrome's PDF viewer is blocked in a sandboxed frame either way,
1271
+ * and an HTML document served as `application/pdf` in an unsandboxed one runs
1272
+ * no script. Both bindings state it; it is written here because a reader of
1273
+ * this file will ask why one frame differs from the other.
1274
+ */
1275
+ [data-ui='upload'] [data-part='viewerStage'] > iframe {
1276
+ background-color: var(--slotted-surface);
1277
+ border: 0;
1278
+ }
1279
+
1280
+ /* Sized by the film rather than by the stage, and never past it. */
1281
+ [data-ui='upload'] [data-part='viewerStage'] > video,
1282
+ [data-ui='upload'] [data-part='viewerStage'] > audio {
1283
+ max-block-size: 100%;
1284
+ max-inline-size: 100%;
1285
+ min-block-size: 0;
1286
+ min-inline-size: 0;
1287
+ }
1288
+
1289
+ /**
1290
+ * What a kind the viewer cannot render gets: its own tile at the size of the
1291
+ * stage, its name, and one press to the file. `.docx` and `.xlsx` are here
1292
+ * because a browser cannot draw them and drawing them needs a server or a
1293
+ * megabyte of engine — which is the honest answer rather than an empty box.
1294
+ */
1295
+ [data-ui='upload'] [data-part='viewerStage'] > [data-part='preview'] {
1296
+ block-size: var(--slotted-upload-viewer-tile-size, 6rem);
1297
+ inline-size: var(--slotted-upload-viewer-tile-size, 6rem);
1298
+ }
1299
+
1300
+ [data-ui='upload'] [data-part='viewerStage'] > [data-part='preview'] > [data-ui='icon'] {
1301
+ --slotted-icon-size: calc(3 * var(--_glyph-size));
1302
+ }
1303
+
1304
+ /*
1305
+ * The one sentence that says what is different about this file. It is a label
1306
+ * the defaults provider holds, so an application translates it once; the name
1307
+ * and the way out are in the header, where they are for every other file.
1308
+ */
1309
+ [data-ui='upload'] [data-part='viewerStage'] > p {
1310
+ color: var(--slotted-text-muted);
1311
+ margin: 0;
1312
+ max-inline-size: 28ch;
1313
+ text-align: center;
1314
+ }
1315
+
1316
+ /*
1317
+ * The unsupported stage is a column rather than a grid. Read at 720 pixels as
1318
+ * a grid, the tile and its sentence landed in two cells that centred against
1319
+ * different axes and sat diagonally apart; a column of two things centred is
1320
+ * one rule with one meaning.
1321
+ */
1322
+ [data-ui='upload'] [data-part='viewerStage']:has(> [data-part='preview']) {
1323
+ align-items: center;
1324
+ display: flex;
1325
+ flex-direction: column;
1326
+ gap: var(--slotted-space-5, 0.75rem);
1327
+ justify-content: center;
1328
+ }
1329
+
1330
+ /**
1331
+ * The two that move through the set. They sit over the stage rather than in
1332
+ * the header, where the reader's pointer already is, and they are held off
1333
+ * the edge so a photograph that fills the stage is not covered at its corner.
1334
+ */
1335
+ [data-ui='upload'] [data-part='viewerPrevious'],
1336
+ [data-ui='upload'] [data-part='viewerNext'] {
1337
+ align-items: center;
1338
+ appearance: none;
1339
+ background-color: color-mix(
1340
+ in oklab,
1341
+ var(--slotted-surface) var(--slotted-upload-viewer-control-opacity, 92%),
1342
+ transparent
1343
+ );
1344
+ block-size: var(--slotted-upload-viewer-control-size, 2.5rem);
1345
+ border: 0;
1346
+ border-radius: var(--slotted-radius-full, 9999px);
1347
+ box-shadow: var(--slotted-shadow-sm);
1348
+ color: var(--slotted-text);
1349
+ cursor: pointer;
1350
+ display: inline-flex;
1351
+ inline-size: var(--slotted-upload-viewer-control-size, 2.5rem);
1352
+ justify-content: center;
1353
+ padding: 0;
1354
+ position: absolute;
1355
+ transition:
1356
+ background-color var(--slotted-control-transition-duration)
1357
+ var(--slotted-control-transition-easing),
1358
+ translate var(--slotted-duration-instant) var(--slotted-control-transition-easing);
1359
+ }
1360
+
1361
+ [data-ui='upload'] [data-part='viewerPrevious'] {
1362
+ inset-inline-start: var(--slotted-space-5, 0.75rem);
1363
+ }
1364
+
1365
+ [data-ui='upload'] [data-part='viewerNext'] {
1366
+ inset-inline-end: var(--slotted-space-5, 0.75rem);
1367
+ }
1368
+
1369
+ /**
1370
+ * One mark for both, turned for the one that goes back — which is the rule
1371
+ * every chevron in this library follows, and the reason the scale holds one
1372
+ * chevron rather than a pair. The binding marks both icons `directional`, so
1373
+ * they mirror where reading runs right to left.
1374
+ */
1375
+ [data-ui='upload'] [data-part='viewerPrevious'] > [data-ui='icon'],
1376
+ [data-ui='upload'] [data-part='viewerNext'] > [data-ui='icon'] {
1377
+ --slotted-icon-size: var(--_glyph-size);
1378
+ }
1379
+
1380
+ [data-ui='upload'] [data-part='viewerPrevious'] > [data-ui='icon'] {
1381
+ rotate: 180deg;
1382
+ }
1383
+
1384
+ /*
1385
+ * The plate is already nearly opaque, so going to fully opaque on hover moved
1386
+ * the ground by 0.002 in OKLab L — no answer at all. The family's state layer
1387
+ * is mixed into the plate instead, which keeps it opaque over the photograph
1388
+ * and steps it by the amount every other control in the family steps.
1389
+ */
1390
+ [data-ui='upload'] [data-part='viewerPrevious']:enabled:hover,
1391
+ [data-ui='upload'] [data-part='viewerNext']:enabled:hover {
1392
+ background-color: color-mix(
1393
+ in oklab,
1394
+ currentcolor var(--slotted-upload-state-layer-hover, 8%),
1395
+ var(--slotted-surface)
1396
+ );
1397
+ }
1398
+
1399
+ [data-ui='upload'] [data-part='viewerPrevious']:enabled:active,
1400
+ [data-ui='upload'] [data-part='viewerNext']:enabled:active {
1401
+ background-color: color-mix(
1402
+ in oklab,
1403
+ currentcolor var(--slotted-upload-state-layer-active, 14%),
1404
+ var(--slotted-surface)
1405
+ );
1406
+ translate: 0 var(--slotted-border-width-thin, 1px);
1407
+ }
1408
+
1409
+ [data-ui='upload'] [data-part='viewerPrevious']:disabled,
1410
+ [data-ui='upload'] [data-part='viewerNext']:disabled {
1411
+ color: var(--slotted-disabled-foreground);
1412
+ cursor: default;
1413
+ }
1414
+
1415
+ [data-ui='upload'] [data-part='viewerPrevious']:focus-visible,
1416
+ [data-ui='upload'] [data-part='viewerNext']:focus-visible {
1417
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
1418
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
1419
+ }
1420
+
1421
+ /* ── Inside a field ────────────────────────────────────────────────────
1422
+ *
1423
+ * A field owns the label row, the description, the necessity mark and the
1424
+ * error slot for every control in this library, so an upload under one draws
1425
+ * none of its own: two labels on one control and two messages under it is
1426
+ * what a form gets otherwise, and the second of each is the one no other
1427
+ * control in that form has.
1428
+ *
1429
+ * The rule is written here as well as in both bindings, because the harness
1430
+ * is a third place the DOM is written and a scene that puts an upload in a
1431
+ * field has to look like the form does.
1432
+ */
1433
+ [data-ui='field'] [data-ui='upload'] > [data-part='label'],
1434
+ [data-ui='field'] [data-ui='upload'] > [data-part='description'],
1435
+ [data-ui='field'] [data-ui='upload'] > [data-part='message'] {
1436
+ display: none;
1437
+ }
1438
+
1439
+ /**
1440
+ * A form is a column of rows the same height, and an upload in it must not be
1441
+ * the one that pushes the rhythm out. Inside a field the sections close up by
1442
+ * one step: the region, the summary and the list are one block rather than
1443
+ * three things with a paragraph's air between them.
1444
+ */
1445
+ [data-ui='field'] [data-ui='upload'][data-part='root'] {
1446
+ --_section-gap: var(--slotted-upload-field-section-gap, var(--slotted-space-4, 0.5rem));
1447
+ }
1448
+
1449
+ /* ── The bare trigger ──────────────────────────────────────────────────
1450
+ *
1451
+ * No region at all. The trigger still takes a drop and a paste, because it
1452
+ * is still a target; what it does not do is claim a box the size of a card
1453
+ * for an application that wanted a button.
1454
+ */
1455
+ [data-ui='upload'][data-variant='button'] [data-part='trigger'] {
1456
+ align-self: start;
1457
+ }
1458
+
1459
+ /* ── The field shape ───────────────────────────────────────────────────
1460
+ *
1461
+ * One control-height row: the trigger at the inline start and what has been
1462
+ * chosen beside it, inside the frame a text field would have. This is the
1463
+ * shape `file-input` was, and the reason it is a variant rather than a
1464
+ * family is that everything under the frame is the same list.
1465
+ */
1466
+ /*
1467
+ * The frame a text field has, at rest. It moves the region's resting pair
1468
+ * rather than painting over it, so the family's states — invalid, disabled,
1469
+ * the drag verdict, a pointer — reach this shape as they reach the other:
1470
+ * written as a border colour on the region, it held one colour through all
1471
+ * of them. `:where` keeps it below the invalid and disabled pair.
1472
+ */
1473
+ [data-ui='upload'][data-variant='field']:where([data-part='root']) {
1474
+ --_zone-edge: var(--slotted-control-border-color);
1475
+ --_zone-ground: var(--slotted-control-surface, Field);
1476
+ }
1477
+
1478
+ [data-ui='upload'][data-variant='field'] [data-part='dropzone'] {
1479
+ align-items: center;
1480
+ border-style: solid;
1481
+ border-width: var(--slotted-control-border-width, 1px);
1482
+ flex-direction: row;
1483
+ gap: var(--_gap);
1484
+ padding-block: var(--slotted-space-2, 0.25rem);
1485
+ padding-inline: var(--slotted-space-2, 0.25rem);
1486
+ text-align: start;
1487
+ }
1488
+
1489
+ /* ── A wall of pictures ────────────────────────────────────────────────
1490
+ *
1491
+ * The same rows, arranged. An image set is read by looking rather than by
1492
+ * reading, so the preview takes the card and the name sits under it; the
1493
+ * controls move to the corner of the picture, where they are over the one
1494
+ * thing the reader is pointing at.
1495
+ */
1496
+ [data-ui='upload'][data-layout='grid'] [data-part='list'] {
1497
+ display: grid;
1498
+ gap: var(--_gap);
1499
+ grid-template-columns: repeat(auto-fill, minmax(var(--slotted-upload-card-measure, 9rem), 1fr));
1500
+ }
1501
+
1502
+ [data-ui='upload'][data-layout='grid'] [data-part='item'] {
1503
+ /*
1504
+ * Cards in one row share its height, and the spare height of the shorter
1505
+ * ones goes under their content rather than between the picture and the
1506
+ * name: without this, a card whose neighbour carried a two-line message
1507
+ * set its name lower than the name beside it.
1508
+ */
1509
+ align-content: start;
1510
+ align-items: stretch;
1511
+ /* So the picture can be sized from the card's own width; see `preview`. */
1512
+ container-type: inline-size;
1513
+ grid-template-areas:
1514
+ 'preview'
1515
+ 'body'
1516
+ 'message';
1517
+ grid-template-columns: minmax(0, 1fr);
1518
+ }
1519
+
1520
+ /*
1521
+ * Four by three, rounded down to a whole pixel. The card's width is a share
1522
+ * of the row and is fractional, and `aspect-ratio` carried the fraction into
1523
+ * the height: 117.66 at 720 pixels, with every edge under it blurred. The
1524
+ * card is a container, so `cqi` is the width the picture fills.
1525
+ */
1526
+ [data-ui='upload'][data-layout='grid'] [data-part='preview'] {
1527
+ block-size: round(down, calc(100cqi * 3 / 4), 1px);
1528
+ inline-size: 100%;
1529
+ }
1530
+
1531
+ /**
1532
+ * Over the picture, the controls need a ground of their own: the consumer's
1533
+ * photograph is any colour, and a dark mark on a mid-blue sky was the reading
1534
+ * the first capture at 720 pixels gave. The ground is on the group rather than
1535
+ * on each control, so the five keep the frameless rest state R2 requires of
1536
+ * them and the reader still gets one legible plate.
1537
+ */
1538
+ /**
1539
+ * Over the picture, and inside it. Three things the first capture at 720
1540
+ * pixels got wrong and this rule fixes: the plate stood at the item's own
1541
+ * edge rather than inside the photograph, so it read as stuck to the corner;
1542
+ * it was translucent enough for a bright sky to come through the marks; and
1543
+ * it reserved three slots it did not need, which pushed it off the picture.
1544
+ *
1545
+ * The ground is on the group rather than on each control, so the five keep
1546
+ * the frameless rest state R2 requires of them and the reader still gets one
1547
+ * legible plate.
1548
+ */
1549
+ [data-ui='upload'][data-layout='grid'] [data-part='actions'] {
1550
+ background-color: color-mix(
1551
+ in oklab,
1552
+ var(--slotted-surface) var(--slotted-upload-card-actions-opacity, 96%),
1553
+ transparent
1554
+ );
1555
+ border-radius: var(--_item-radius);
1556
+ box-shadow: var(--slotted-shadow-xs);
1557
+ inset-block-start: calc(var(--_item-inset-block) + var(--slotted-space-3, 0.375rem));
1558
+ inset-inline-end: calc(var(--_item-inset-inline) + var(--slotted-space-3, 0.375rem));
1559
+ margin-block-start: 0;
1560
+ min-inline-size: 0;
1561
+ padding: var(--slotted-space-1, 0.125rem);
1562
+ position: absolute;
1563
+ }
1564
+
1565
+ /**
1566
+ * A card has room for the extension in words, and a reader scanning a wall of
1567
+ * thumbnails for the one PDF among nine photographs finds it faster by the
1568
+ * three letters than by the silhouette. It is drawn from the attribute the
1569
+ * binding already writes, so the label costs no element.
1570
+ */
1571
+ [data-ui='upload'][data-layout='grid'] [data-part='preview']:not(:has(img))::after {
1572
+ content: attr(data-extension);
1573
+ font-size: var(--slotted-font-size-sm, 0.8125rem);
1574
+ font-weight: var(--slotted-font-weight-semibold, 600);
1575
+ letter-spacing: 0.06em;
1576
+ margin-block-start: var(--slotted-space-3, 0.375rem);
1577
+ text-transform: uppercase;
1578
+ }
1579
+
1580
+ /* A card's tile carries a mark the size of the card, not the size of a row. */
1581
+ [data-ui='upload'][data-layout='grid'] [data-part='preview'] > [data-ui='icon'] {
1582
+ --slotted-icon-size: calc(2 * var(--_glyph-size));
1583
+ }
1584
+
1585
+ /* ── Composition that answers the box, not the window ──────────────────
1586
+ *
1587
+ * Below 26rem the row has no room for a name, a size and four controls on
1588
+ * one line, so the size leaves the name's line and joins the status under
1589
+ * it, and the region's mark stops asking for a column of its own.
1590
+ */
1591
+ /**
1592
+ * The summary is a sentence and a control, and the sentence is the longer of
1593
+ * the two by a factor of ten. It stacks a step earlier than everything else
1594
+ * below, because at 420 pixels the row still fitted and the control's two
1595
+ * words broke across two lines with the underline broken between them.
1596
+ */
1597
+ @container slotted-upload (max-width: 30rem) {
1598
+ [data-ui='upload'] [data-part='summary'] {
1599
+ align-items: start;
1600
+ flex-direction: column;
1601
+ }
1602
+ }
1603
+
1604
+ @container slotted-upload (max-width: 26rem) {
1605
+ /*
1606
+ * The chip keeps the start of the row and the controls drop under the text,
1607
+ * because at this width three of them beside a name leave the name seven
1608
+ * characters. The chip spans both rows, so the block it is centred on is
1609
+ * still the block it is about.
1610
+ */
1611
+ [data-ui='upload'] [data-part='item'] {
1612
+ grid-template-areas:
1613
+ 'preview body'
1614
+ 'preview actions'
1615
+ 'message message';
1616
+ grid-template-columns: auto minmax(0, 1fr);
1617
+ }
1618
+
1619
+ [data-ui='upload'] [data-part='preview'] {
1620
+ align-self: start;
1621
+ }
1622
+
1623
+ [data-ui='upload'] [data-part='actions'] {
1624
+ justify-content: flex-start;
1625
+ margin-inline-start: calc(-1 * var(--slotted-space-2, 0.25rem));
1626
+ }
1627
+
1628
+ [data-ui='upload'] [data-part='dropzone'] {
1629
+ padding-block: var(--slotted-space-6, 1rem);
1630
+ }
1631
+ }
1632
+
1633
+ /* ── Forced colours ────────────────────────────────────────────────────
1634
+ *
1635
+ * The theme remaps every semantic token to a system colour, so a family that
1636
+ * reads tokens needs no rule of its own for colour. What it does need is the
1637
+ * region's dashed edge, whose colour comes from a token that becomes
1638
+ * `CanvasText` — correct — and the row tint, which the system flattens to
1639
+ * `Canvas` and which then leaves the failed row saying nothing. The border
1640
+ * carries it there instead.
1641
+ */
1642
+ @media (forced-colors: active) {
1643
+ [data-ui='upload'] [data-part='item'][data-status='rejected'],
1644
+ [data-ui='upload'] [data-part='item'][data-status='error'] {
1645
+ --_item-border: CanvasText;
1646
+ }
1647
+
1648
+ /*
1649
+ * The theme maps every kind to `ButtonFace` and the foreground to
1650
+ * `ButtonText`, so the tile is still a tile and the mark is still knocked
1651
+ * out of it — in the system's two colours rather than in nine. What kind a
1652
+ * file is, is then said by its shape, which is what survives this mode.
1653
+ */
1654
+ [data-ui='upload'] [data-part='preview'] {
1655
+ border: var(--_border-width) solid ButtonBorder;
1656
+ }
1657
+
1658
+ [data-ui='upload'] [data-part='dropzone'][data-drop-target],
1659
+ [data-ui='upload'] [data-part='dropzone'][data-drop-invalid] {
1660
+ --_zone-border-color: Highlight;
1661
+ }
1662
+
1663
+ /*
1664
+ * The state layer is a mix of `currentcolor` into nothing, and the system
1665
+ * paints every ground `Canvas`, so a pointer over a control changed nothing
1666
+ * the reader could see. The system's own pair answers instead — the one
1667
+ * `alert` gives its dismiss.
1668
+ */
1669
+ [data-ui='upload']
1670
+ :is(
1671
+ [data-part='clear'],
1672
+ [data-part='viewerExternal'],
1673
+ [data-part='viewerClose'],
1674
+ [data-part='viewerPrevious'],
1675
+ [data-part='viewerNext'],
1676
+ [data-part='start'],
1677
+ [data-part='pause'],
1678
+ [data-part='retry'],
1679
+ [data-part='cancel'],
1680
+ [data-part='remove']
1681
+ ):enabled:is(:hover, :active) {
1682
+ background-color: Highlight;
1683
+ color: HighlightText;
1684
+ }
1685
+ }
1686
+
1687
+ /*
1688
+ * Forced colours remove every box shadow, and the edge with them. The width
1689
+ * comes back as an outline inside the border, and where focus draws the ring
1690
+ * on the same box an invalid ring is double, which a valid one is not.
1691
+ */
1692
+ @media (forced-colors: active) {
1693
+ [data-ui='upload'][data-invalid] [data-part='trigger'] {
1694
+ outline: var(--_invalid-edge-width) solid CanvasText;
1695
+ outline-offset: calc(
1696
+ -1 * (var(--slotted-control-border-width, 1px) + var(--_invalid-edge-width))
1697
+ );
1698
+ }
1699
+
1700
+ [data-ui='upload'][data-invalid] [data-part='trigger']:has(:focus-visible) {
1701
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
1702
+ var(--slotted-focus-ring-color, Highlight);
1703
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
1704
+ }
1705
+ }
1706
+ }