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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/package.json +276 -3
  4. package/src/accordion/accordion.css +96 -0
  5. package/src/accordion/accordion.tokens.json +14 -0
  6. package/src/action-bar/action-bar.css +455 -0
  7. package/src/action-bar/action-bar.tokens.json +50 -0
  8. package/src/action-sheet/action-sheet.css +462 -0
  9. package/src/action-sheet/action-sheet.tokens.json +59 -0
  10. package/src/alert/alert.css +764 -0
  11. package/src/alert/alert.tokens.json +77 -0
  12. package/src/app-shell/app-shell.css +1899 -0
  13. package/src/app-shell/app-shell.tokens.json +88 -0
  14. package/src/aspect-ratio/aspect-ratio.css +131 -0
  15. package/src/aspect-ratio/aspect-ratio.tokens.json +1 -0
  16. package/src/attachment/attachment.css +906 -0
  17. package/src/attachment/attachment.tokens.json +85 -0
  18. package/src/avatar/avatar.css +598 -0
  19. package/src/avatar/avatar.tokens.json +64 -0
  20. package/src/avatar-group/avatar-group.css +268 -0
  21. package/src/avatar-group/avatar-group.tokens.json +16 -0
  22. package/src/badge/badge.css +462 -0
  23. package/src/badge/badge.tokens.json +58 -0
  24. package/src/breadcrumb/breadcrumb.css +441 -0
  25. package/src/breadcrumb/breadcrumb.tokens.json +50 -0
  26. package/src/button/button.css +754 -0
  27. package/src/button/button.tokens.json +72 -0
  28. package/src/button-group/button-group.css +455 -0
  29. package/src/button-group/button-group.tokens.json +11 -0
  30. package/src/calendar/calendar.css +846 -0
  31. package/src/calendar/calendar.tokens.json +67 -0
  32. package/src/card/card.css +602 -0
  33. package/src/card/card.tokens.json +68 -0
  34. package/src/carousel/carousel.css +860 -0
  35. package/src/carousel/carousel.tokens.json +62 -0
  36. package/src/cascader/cascader.css +1320 -0
  37. package/src/cascader/cascader.tokens.json +103 -0
  38. package/src/chart/chart.css +1473 -0
  39. package/src/chart/chart.tokens.json +107 -0
  40. package/src/checkbox/checkbox.css +1011 -0
  41. package/src/checkbox/checkbox.tokens.json +102 -0
  42. package/src/checkbox-group/checkbox-group.css +70 -0
  43. package/src/checkbox-group/checkbox-group.tokens.json +6 -0
  44. package/src/code-block/code-block.css +555 -0
  45. package/src/code-block/code-block.tokens.json +69 -0
  46. package/src/collapsible/collapsible.css +546 -0
  47. package/src/collapsible/collapsible.tokens.json +55 -0
  48. package/src/color-picker/color-picker.css +607 -0
  49. package/src/color-picker/color-picker.tokens.json +58 -0
  50. package/src/column-manager/column-manager.css +452 -0
  51. package/src/column-manager/column-manager.tokens.json +43 -0
  52. package/src/combobox/combobox.css +1048 -0
  53. package/src/combobox/combobox.tokens.json +112 -0
  54. package/src/command-palette/command-palette.css +1000 -0
  55. package/src/command-palette/command-palette.tokens.json +75 -0
  56. package/src/container/container.css +198 -0
  57. package/src/container/container.tokens.json +18 -0
  58. package/src/context-menu/context-menu.css +104 -0
  59. package/src/context-menu/context-menu.tokens.json +11 -0
  60. package/src/currency-field/currency-field.css +787 -0
  61. package/src/currency-field/currency-field.tokens.json +80 -0
  62. package/src/dashboard-grid/dashboard-grid.css +748 -0
  63. package/src/dashboard-grid/dashboard-grid.tokens.json +67 -0
  64. package/src/data-grid/data-grid.css +2484 -0
  65. package/src/data-grid/data-grid.tokens.json +100 -0
  66. package/src/data-list/data-list.css +455 -0
  67. package/src/data-list/data-list.tokens.json +35 -0
  68. package/src/date-field/date-field.css +560 -0
  69. package/src/date-field/date-field.tokens.json +80 -0
  70. package/src/date-picker/date-picker.css +420 -0
  71. package/src/date-picker/date-picker.tokens.json +49 -0
  72. package/src/date-range-picker/date-range-picker.css +983 -0
  73. package/src/date-range-picker/date-range-picker.tokens.json +109 -0
  74. package/src/datetime-field/datetime-field.css +850 -0
  75. package/src/datetime-field/datetime-field.tokens.json +91 -0
  76. package/src/datetime-picker/datetime-picker.css +843 -0
  77. package/src/datetime-picker/datetime-picker.tokens.json +78 -0
  78. package/src/datetime-range-picker/datetime-range-picker.css +1375 -0
  79. package/src/datetime-range-picker/datetime-range-picker.tokens.json +135 -0
  80. package/src/description-list/description-list.css +675 -0
  81. package/src/description-list/description-list.tokens.json +66 -0
  82. package/src/detail-panel/detail-panel.css +329 -0
  83. package/src/detail-panel/detail-panel.tokens.json +33 -0
  84. package/src/dialog/dialog.css +968 -0
  85. package/src/dialog/dialog.tokens.json +66 -0
  86. package/src/divider/divider.css +131 -0
  87. package/src/divider/divider.tokens.json +18 -0
  88. package/src/drawer/drawer.css +1303 -0
  89. package/src/drawer/drawer.tokens.json +75 -0
  90. package/src/entity-picker/entity-picker.css +1367 -0
  91. package/src/entity-picker/entity-picker.tokens.json +118 -0
  92. package/src/event-calendar/event-calendar.css +2148 -0
  93. package/src/event-calendar/event-calendar.tokens.json +87 -0
  94. package/src/field/field.css +707 -0
  95. package/src/field/field.tokens.json +58 -0
  96. package/src/field-group/field-group.css +226 -0
  97. package/src/field-group/field-group.tokens.json +33 -0
  98. package/src/filter-bar/filter-bar.css +1306 -0
  99. package/src/filter-bar/filter-bar.tokens.json +78 -0
  100. package/src/form/form.css +218 -0
  101. package/src/form/form.tokens.json +32 -0
  102. package/src/fullscreen/fullscreen.css +376 -0
  103. package/src/fullscreen/fullscreen.tokens.json +50 -0
  104. package/src/grid/grid.css +173 -0
  105. package/src/grid/grid.tokens.json +20 -0
  106. package/src/highlight/highlight.css +135 -0
  107. package/src/highlight/highlight.tokens.json +16 -0
  108. package/src/hover-card/hover-card.css +93 -0
  109. package/src/hover-card/hover-card.tokens.json +17 -0
  110. package/src/icon/icon.css +136 -0
  111. package/src/icon/icon.tokens.json +14 -0
  112. package/src/image/image.css +449 -0
  113. package/src/image/image.tokens.json +27 -0
  114. package/src/import-export-center/import-export-center.css +1175 -0
  115. package/src/import-export-center/import-export-center.tokens.json +84 -0
  116. package/src/inline/inline.css +149 -0
  117. package/src/inline/inline.tokens.json +14 -0
  118. package/src/inline-message/inline-message.css +396 -0
  119. package/src/inline-message/inline-message.tokens.json +19 -0
  120. package/src/kbd/kbd.css +490 -0
  121. package/src/kbd/kbd.tokens.json +40 -0
  122. package/src/link/link.css +237 -0
  123. package/src/link/link.tokens.json +25 -0
  124. package/src/list/list.css +240 -0
  125. package/src/list/list.tokens.json +42 -0
  126. package/src/list-item/list-item.css +937 -0
  127. package/src/list-item/list-item.tokens.json +114 -0
  128. package/src/listbox/listbox.css +674 -0
  129. package/src/listbox/listbox.tokens.json +72 -0
  130. package/src/loading-overlay/loading-overlay.css +730 -0
  131. package/src/loading-overlay/loading-overlay.tokens.json +48 -0
  132. package/src/markdown/markdown.css +168 -0
  133. package/src/markdown/markdown.tokens.json +18 -0
  134. package/src/menu/menu.css +817 -0
  135. package/src/menu/menu.tokens.json +78 -0
  136. package/src/menubar/menubar.css +170 -0
  137. package/src/menubar/menubar.tokens.json +32 -0
  138. package/src/message/message.css +1453 -0
  139. package/src/message/message.tokens.json +85 -0
  140. package/src/message-scroller/message-scroller.css +371 -0
  141. package/src/message-scroller/message-scroller.tokens.json +47 -0
  142. package/src/multi-select/multi-select.css +1427 -0
  143. package/src/multi-select/multi-select.tokens.json +114 -0
  144. package/src/navigation-menu/navigation-menu.css +1029 -0
  145. package/src/navigation-menu/navigation-menu.tokens.json +70 -0
  146. package/src/notification-center/notification-center.css +856 -0
  147. package/src/notification-center/notification-center.tokens.json +84 -0
  148. package/src/number-field/number-field.css +794 -0
  149. package/src/number-field/number-field.tokens.json +80 -0
  150. package/src/otp-input/otp-input.css +525 -0
  151. package/src/otp-input/otp-input.tokens.json +69 -0
  152. package/src/page/page.css +179 -0
  153. package/src/page/page.tokens.json +20 -0
  154. package/src/page-header/page-header.css +1181 -0
  155. package/src/page-header/page-header.tokens.json +71 -0
  156. package/src/pagination/pagination.css +500 -0
  157. package/src/pagination/pagination.tokens.json +56 -0
  158. package/src/popover/popover.css +526 -0
  159. package/src/popover/popover.tokens.json +60 -0
  160. package/src/progress-indicator/progress-indicator.css +1584 -0
  161. package/src/progress-indicator/progress-indicator.tokens.json +50 -0
  162. package/src/prompt-composer/prompt-composer.css +680 -0
  163. package/src/prompt-composer/prompt-composer.tokens.json +99 -0
  164. package/src/prose/prose.css +733 -0
  165. package/src/prose/prose.tokens.json +79 -0
  166. package/src/radio-group/radio-group.css +778 -0
  167. package/src/radio-group/radio-group.tokens.json +103 -0
  168. package/src/rating/rating.css +1071 -0
  169. package/src/rating/rating.tokens.json +88 -0
  170. package/src/result-state/result-state.css +980 -0
  171. package/src/result-state/result-state.tokens.json +63 -0
  172. package/src/runtime-properties.mjs +321 -0
  173. package/src/saved-views/saved-views.css +394 -0
  174. package/src/saved-views/saved-views.tokens.json +41 -0
  175. package/src/scroll-area/scroll-area.css +481 -0
  176. package/src/scroll-area/scroll-area.tokens.json +15 -0
  177. package/src/section/section.css +177 -0
  178. package/src/section/section.tokens.json +39 -0
  179. package/src/segmented-control/segmented-control.css +564 -0
  180. package/src/segmented-control/segmented-control.tokens.json +67 -0
  181. package/src/select/select.css +1593 -0
  182. package/src/select/select.tokens.json +110 -0
  183. package/src/side-navigation/side-navigation.css +1096 -0
  184. package/src/side-navigation/side-navigation.tokens.json +87 -0
  185. package/src/skeleton/skeleton.css +431 -0
  186. package/src/skeleton/skeleton.tokens.json +29 -0
  187. package/src/slider/slider.css +792 -0
  188. package/src/slider/slider.tokens.json +75 -0
  189. package/src/sparkline/sparkline.css +282 -0
  190. package/src/sparkline/sparkline.tokens.json +45 -0
  191. package/src/spinner/spinner.css +224 -0
  192. package/src/spinner/spinner.tokens.json +8 -0
  193. package/src/split-button/split-button.css +253 -0
  194. package/src/split-button/split-button.tokens.json +12 -0
  195. package/src/splitter/splitter.css +336 -0
  196. package/src/splitter/splitter.tokens.json +24 -0
  197. package/src/stack/stack.css +128 -0
  198. package/src/stack/stack.tokens.json +14 -0
  199. package/src/stat-card/stat-card.css +641 -0
  200. package/src/stat-card/stat-card.tokens.json +95 -0
  201. package/src/status-indicator/status-indicator.css +481 -0
  202. package/src/status-indicator/status-indicator.tokens.json +38 -0
  203. package/src/stepper/stepper.css +629 -0
  204. package/src/stepper/stepper.tokens.json +72 -0
  205. package/src/switch/switch.css +703 -0
  206. package/src/switch/switch.tokens.json +75 -0
  207. package/src/table/table.css +1482 -0
  208. package/src/table/table.tokens.json +69 -0
  209. package/src/table-of-contents/table-of-contents.css +533 -0
  210. package/src/table-of-contents/table-of-contents.tokens.json +53 -0
  211. package/src/tabs/tabs.css +998 -0
  212. package/src/tabs/tabs.tokens.json +82 -0
  213. package/src/tag/tag.css +768 -0
  214. package/src/tag/tag.tokens.json +85 -0
  215. package/src/tags-input/tags-input.css +757 -0
  216. package/src/tags-input/tags-input.tokens.json +84 -0
  217. package/src/text-field/text-field.css +900 -0
  218. package/src/text-field/text-field.tokens.json +87 -0
  219. package/src/textarea/textarea.css +365 -0
  220. package/src/textarea/textarea.tokens.json +59 -0
  221. package/src/time-field/time-field.css +719 -0
  222. package/src/time-field/time-field.tokens.json +86 -0
  223. package/src/time-picker/time-picker.css +665 -0
  224. package/src/time-picker/time-picker.tokens.json +66 -0
  225. package/src/time-range-picker/time-range-picker.css +1287 -0
  226. package/src/time-range-picker/time-range-picker.tokens.json +134 -0
  227. package/src/timeline/timeline.css +1596 -0
  228. package/src/timeline/timeline.tokens.json +96 -0
  229. package/src/toast/toast.css +943 -0
  230. package/src/toast/toast.tokens.json +90 -0
  231. package/src/toggle/toggle.css +290 -0
  232. package/src/toggle/toggle.tokens.json +54 -0
  233. package/src/toggle-group/toggle-group.css +463 -0
  234. package/src/toggle-group/toggle-group.tokens.json +54 -0
  235. package/src/toolbar/toolbar.css +434 -0
  236. package/src/toolbar/toolbar.tokens.json +49 -0
  237. package/src/tooltip/tooltip.css +368 -0
  238. package/src/tooltip/tooltip.tokens.json +39 -0
  239. package/src/top-navigation/top-navigation.css +1287 -0
  240. package/src/top-navigation/top-navigation.tokens.json +91 -0
  241. package/src/tour/tour.css +680 -0
  242. package/src/tour/tour.tokens.json +67 -0
  243. package/src/transfer/transfer.css +571 -0
  244. package/src/transfer/transfer.tokens.json +49 -0
  245. package/src/tree/tree.css +1000 -0
  246. package/src/tree/tree.tokens.json +80 -0
  247. package/src/typography/typography.css +211 -0
  248. package/src/typography/typography.tokens.json +31 -0
  249. package/src/upload/upload.css +1706 -0
  250. package/src/upload/upload.tokens.json +123 -0
  251. package/src/visually-hidden/visually-hidden.css +52 -0
  252. package/src/visually-hidden/visually-hidden.tokens.json +1 -0
  253. package/src/wizard/wizard.css +740 -0
  254. package/src/wizard/wizard.tokens.json +63 -0
@@ -0,0 +1,906 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── A file that already belongs to something ──────────────────────────
4
+ *
5
+ * An attachment is a file shown inside a message, a record, a timeline event
6
+ * or a composer's tray. It says what the file is — the kind's tile, the
7
+ * name, a line of detail — gives the reader the file through its trigger, and
8
+ * carries the actions the application offers on it. `upload` acquires and
9
+ * transfers files; this family does neither. The two share the file
10
+ * vocabulary of `@slotted/core/upload` and the kind scale
11
+ * `--slotted-upload-kind-*`, and nothing else
12
+ * (`docs/history/designs/2026-09-30-attachment-design.md` §1).
13
+ *
14
+ * **The whole attachment is the target, and the actions stay beside it.**
15
+ * The trigger is a real `a` or `button` holding the file name, and a
16
+ * transparent `::after` on it covers the root. `actions` is a sibling of the
17
+ * body and stands above that layer, because interactive content may not nest
18
+ * inside `a` or `button` — the arrangement `list-item` states for `content`
19
+ * and `actions`.
20
+ */
21
+ /**
22
+ * The private properties, on both boxes that stand on their own. The set
23
+ * resolves the same size step its attachments do, because a grid of cards
24
+ * sizes its tracks from the card.
25
+ */
26
+ [data-ui='attachment'][data-part='root'],
27
+ [data-ui='attachment'][data-part='group'] {
28
+ /**
29
+ * Type and glyph come from the control scales, the ones the theme restates
30
+ * per density, so an attachment inside a dense form shrinks with the
31
+ * fields around it. The insets come from the space scale, which does not
32
+ * vary by density — the cost `upload` and `card` both accepted.
33
+ */
34
+ --_font-size: var(--slotted-control-font-size-md, 13px);
35
+ --_glyph-size: var(--slotted-control-icon-size-md, 16px);
36
+ --_gap: var(--slotted-control-gap-md, 8px);
37
+ --_inset: var(--slotted-attachment-padding-md, var(--slotted-space-4, 0.5rem));
38
+
39
+ /**
40
+ * The two lines, each rounded up to a whole pixel. Thirteen at 1.5 is 19.5,
41
+ * and an attachment built from it stood at 59.5 with every edge below the
42
+ * first line between two device pixels. The rule is `upload`'s and
43
+ * `timeline`'s `--_leading`, stated here from a length rather than from
44
+ * `1em` so the tile below can be sized from it outside the text.
45
+ */
46
+ --_line-height: var(--slotted-line-height-md, 1.5);
47
+ --_title-leading: round(up, calc(var(--_font-size) * var(--_line-height)), 1px);
48
+ --_detail-size: var(--slotted-font-size-xs, 0.75rem);
49
+ --_detail-leading: round(up, calc(var(--_detail-size) * var(--_line-height)), 1px);
50
+ --_line-gap: var(--slotted-attachment-line-gap, var(--slotted-space-1, 0.125rem));
51
+
52
+ /**
53
+ * The tile is the height of the two lines beside it — 20 + 2 + 18 = 40 at
54
+ * `md`, the 40 ReUI measures (`c-attachment-5`) — so its edges line up with
55
+ * the cap of the name and the foot of the detail instead of floating
56
+ * between them. It steps with the type, which steps with the density.
57
+ */
58
+ --_preview-size: var(
59
+ --slotted-attachment-preview-size,
60
+ calc(var(--_title-leading) + var(--_line-gap) + var(--_detail-leading))
61
+ );
62
+ --_preview-radius: var(--slotted-attachment-preview-radius, var(--slotted-radius-sm, 4px));
63
+
64
+ /**
65
+ * A card is three tiles wide: 120 at `md`, ReUI's vertical card, with the
66
+ * picture 102 square inside it. Derived rather than stated, so a card at
67
+ * `sm` or at `dense` is still three of its own tiles and still a whole
68
+ * pixel.
69
+ */
70
+ --_card-size: var(--slotted-attachment-card-size, calc(3 * var(--_preview-size)));
71
+ --_border-width: var(--slotted-attachment-border-width, var(--slotted-border-width-thin, 1px));
72
+
73
+ /**
74
+ * The actions are inner controls (R2): the glyph plus a step of air on each
75
+ * side, floored at WCAG 2.2 SC 2.5.8's twenty-four pixels so the target
76
+ * holds at every density, and the glyph floored at fourteen for the reason
77
+ * `upload` floors its own — the control glyph scale falls to twelve at
78
+ * `dense`, and a cross that small beside a name is a speck.
79
+ */
80
+ --_action-glyph-size: max(var(--slotted-attachment-icon-size-min, 14px), var(--_glyph-size));
81
+ --_action-size: var(
82
+ --slotted-attachment-action-size,
83
+ max(
84
+ var(--slotted-target-min, 24px),
85
+ calc(var(--_action-glyph-size) + 2 * var(--slotted-space-2, 0.25rem))
86
+ )
87
+ );
88
+ }
89
+
90
+ [data-ui='attachment'][data-size='sm']:is([data-part='root'], [data-part='group']) {
91
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
92
+ --_glyph-size: var(--slotted-control-icon-size-sm, 14px);
93
+ --_gap: var(--slotted-control-gap-sm, 6px);
94
+ --_inset: var(--slotted-attachment-padding-sm, var(--slotted-space-3, 0.375rem));
95
+ }
96
+
97
+ [data-ui='attachment'][data-size='lg']:is([data-part='root'], [data-part='group']) {
98
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
99
+ --_glyph-size: var(--slotted-control-icon-size-lg, 18px);
100
+ --_gap: var(--slotted-control-gap-lg, 8px);
101
+ --_inset: var(--slotted-attachment-padding-lg, var(--slotted-space-5, 0.75rem));
102
+ }
103
+
104
+ /* ── One attachment ────────────────────────────────────────────────────
105
+ *
106
+ * Laid out as a grid rather than a flex line, for the reason `upload` gives
107
+ * its row: the tile and the actions are centred on the name and its detail,
108
+ * and a sentence under them — the message — grows the attachment downwards
109
+ * without moving either. The message starts under the name, not under the
110
+ * tile, so its first letter lines up with the name's.
111
+ */
112
+ [data-ui='attachment'][data-part='root'] {
113
+ /**
114
+ * What the attachment is painted with at rest. A state of the file —
115
+ * invalid — moves these two; a pointer on the trigger moves `--_fill`,
116
+ * which mixes the ink into whichever ground the state chose.
117
+ */
118
+ --_surface: var(--slotted-attachment-background, var(--slotted-surface));
119
+ --_edge: var(--slotted-attachment-border-color, var(--slotted-border-subtle));
120
+ --_ink: var(--slotted-text);
121
+ --_message-color: var(--slotted-text-muted);
122
+ /**
123
+ * The pointer's answer is a state layer of the ink over the ground rather
124
+ * than a step on the tone ramp: `--slotted-tone-danger-subtle-hover` is
125
+ * lighter than `--slotted-tone-danger-surface`, so a failed attachment
126
+ * would have brightened under the pointer. Four per cent of the ink is
127
+ * 0.034 of OKLab lightness on `light`'s white and 0.031 on `dark`'s
128
+ * surface, over the 0.02 step `theme-conformance` asks of a hover; eight
129
+ * is twice that for the press, so the two never read as one.
130
+ */
131
+ --_fill-hover: color-mix(
132
+ in oklab,
133
+ var(--_ink) var(--slotted-attachment-state-layer-hover, 4%),
134
+ var(--_surface)
135
+ );
136
+ --_fill-active: color-mix(
137
+ in oklab,
138
+ var(--_ink) var(--slotted-attachment-state-layer-active, 8%),
139
+ var(--_surface)
140
+ );
141
+ --_fill: var(--_surface);
142
+ /*
143
+ * One step larger than the tile's four, so the two corners read as a
144
+ * surface holding a tile rather than as one curve cut twice. ReUI's 14
145
+ * has no step on the radius scale.
146
+ */
147
+ --_radius: var(--slotted-attachment-radius, var(--slotted-radius-lg, 8px));
148
+
149
+ align-items: center;
150
+ background-color: var(--_fill);
151
+ border: var(--_border-width) solid var(--_edge);
152
+ border-radius: var(--_radius);
153
+ box-sizing: border-box;
154
+ color: var(--_ink);
155
+ column-gap: var(--_gap);
156
+ display: grid;
157
+ font-family: var(--slotted-attachment-font-family, var(--slotted-control-font-family));
158
+ font-size: var(--_font-size);
159
+ grid-template-areas:
160
+ 'preview body actions'
161
+ '. message message';
162
+ grid-template-columns: auto minmax(0, 1fr) auto;
163
+ line-height: var(--_title-leading);
164
+ list-style: none;
165
+ margin: 0;
166
+ min-inline-size: 0;
167
+ /*
168
+ * The bar is a child placed against this box, and the clip is what bends
169
+ * it to the corner. `clip` rather than `hidden`, because this box never
170
+ * scrolls and `hidden` would make it a scroll container.
171
+ */
172
+ overflow: clip;
173
+ padding: var(--_inset);
174
+ position: relative;
175
+ text-align: start;
176
+ transition:
177
+ background-color var(--slotted-control-transition-duration)
178
+ var(--slotted-control-transition-easing),
179
+ border-color var(--slotted-control-transition-duration)
180
+ var(--slotted-control-transition-easing),
181
+ opacity var(--slotted-duration-medium) var(--slotted-easing-enter),
182
+ translate var(--slotted-duration-medium) var(--slotted-easing-enter);
183
+ }
184
+
185
+ /*
186
+ * An absent part leaves no column behind. A column of nought still carries
187
+ * the gap beside it, and an attachment without actions ended its name eight
188
+ * pixels short of the edge it could have used.
189
+ */
190
+ [data-ui='attachment'][data-part='root']:not(:has(> [data-part='actions'])) {
191
+ grid-template-areas:
192
+ 'preview body'
193
+ '. message';
194
+ grid-template-columns: auto minmax(0, 1fr);
195
+ }
196
+
197
+ [data-ui='attachment'][data-part='root']:not(:has(> [data-part='preview'])) {
198
+ grid-template-areas:
199
+ 'body actions'
200
+ 'message message';
201
+ grid-template-columns: minmax(0, 1fr) auto;
202
+ }
203
+
204
+ [data-ui='attachment'][data-part='root']:not(:has(> [data-part='preview'])):not(
205
+ :has(> [data-part='actions'])
206
+ ) {
207
+ grid-template-areas:
208
+ 'body'
209
+ 'message';
210
+ grid-template-columns: minmax(0, 1fr);
211
+ }
212
+
213
+ /**
214
+ * A file arrives: it fades in from a step above its place, so an attachment
215
+ * that lands in a tray is seen landing rather than found there. The start
216
+ * style applies only on the frame the attachment is first rendered.
217
+ *
218
+ * An attachment that leaves does not animate: the binding removes it when
219
+ * the consumer's list changes, and holding it for an exit would need a second
220
+ * list beside the consumer's — the debt `upload` records for its rows.
221
+ */
222
+ @starting-style {
223
+ [data-ui='attachment'][data-part='root'] {
224
+ opacity: 0;
225
+ translate: 0 calc(-1 * var(--slotted-space-2, 0.25rem));
226
+ }
227
+ }
228
+
229
+ /* ── The tile ──────────────────────────────────────────────────────────
230
+ *
231
+ * A solid tile in the file's own colour with the mark knocked out of it, or
232
+ * the picture when the page has one. The colour argument and its
233
+ * measurements are under **File kind** in `docs/token-foundation.md`; the
234
+ * scale is `upload`'s and is read here under its own name.
235
+ */
236
+ [data-ui='attachment'] [data-part='preview'] {
237
+ --_kind-tile: var(--slotted-upload-kind-other);
238
+
239
+ align-items: center;
240
+ background-color: var(--_kind-tile);
241
+ block-size: var(--_preview-size);
242
+ border-radius: var(--_preview-radius);
243
+ box-sizing: border-box;
244
+ color: var(--slotted-upload-kind-foreground);
245
+ display: inline-flex;
246
+ flex-direction: column;
247
+ grid-area: preview;
248
+ inline-size: var(--_preview-size);
249
+ justify-content: center;
250
+ overflow: hidden;
251
+ }
252
+
253
+ [data-ui='attachment'] [data-part='preview'] > img {
254
+ block-size: 100%;
255
+ display: block;
256
+ inline-size: 100%;
257
+ object-fit: cover;
258
+ }
259
+
260
+ /*
261
+ * Half the tile, rounded to a whole pixel: 20 in a 40 tile. The row glyph
262
+ * (16) in a tile two lines tall read as a mark lost in its box.
263
+ */
264
+ [data-ui='attachment'] [data-part='preview'] > [data-ui='icon'] {
265
+ --slotted-icon-size: round(calc(var(--_preview-size) / 2), 1px);
266
+ }
267
+
268
+ [data-ui='attachment'] [data-part='preview'][data-kind='pdf'] {
269
+ --_kind-tile: var(--slotted-upload-kind-pdf);
270
+ }
271
+
272
+ [data-ui='attachment'] [data-part='preview'][data-kind='document'] {
273
+ --_kind-tile: var(--slotted-upload-kind-document);
274
+ }
275
+
276
+ [data-ui='attachment'] [data-part='preview'][data-kind='spreadsheet'] {
277
+ --_kind-tile: var(--slotted-upload-kind-spreadsheet);
278
+ }
279
+
280
+ [data-ui='attachment'] [data-part='preview'][data-kind='presentation'] {
281
+ --_kind-tile: var(--slotted-upload-kind-presentation);
282
+ }
283
+
284
+ [data-ui='attachment'] [data-part='preview'][data-kind='image'] {
285
+ --_kind-tile: var(--slotted-upload-kind-image);
286
+ }
287
+
288
+ [data-ui='attachment'] [data-part='preview'][data-kind='video'] {
289
+ --_kind-tile: var(--slotted-upload-kind-video);
290
+ }
291
+
292
+ [data-ui='attachment'] [data-part='preview'][data-kind='audio'] {
293
+ --_kind-tile: var(--slotted-upload-kind-audio);
294
+ }
295
+
296
+ [data-ui='attachment'] [data-part='preview'][data-kind='archive'] {
297
+ --_kind-tile: var(--slotted-upload-kind-archive);
298
+ }
299
+
300
+ /* ── What the attachment says ──────────────────────────────────────────
301
+ *
302
+ * The name, and one line of detail under it. The body is the one box with
303
+ * `min-inline-size: 0`, which is what makes a long name truncate instead of
304
+ * pushing the actions off the end.
305
+ */
306
+ [data-ui='attachment'] [data-part='body'] {
307
+ display: flex;
308
+ flex-direction: column;
309
+ gap: var(--_line-gap);
310
+ grid-area: body;
311
+ min-inline-size: 0;
312
+ }
313
+
314
+ /**
315
+ * The file's target. A real `a` when the file has an address — so `href`,
316
+ * `download` and the modified click a browser gives a link stay the
317
+ * platform's — and a real `button` when the page opens it itself. Nothing
318
+ * about it looks like a control at rest; the attachment is the control, and
319
+ * it answers as one.
320
+ */
321
+ [data-ui='attachment'] [data-part='trigger'] {
322
+ appearance: none;
323
+ background: none;
324
+ border: 0;
325
+ color: inherit;
326
+ cursor: pointer;
327
+ display: flex;
328
+ font: inherit;
329
+ line-height: inherit;
330
+ margin: 0;
331
+ min-inline-size: 0;
332
+ padding: 0;
333
+ text-align: start;
334
+ text-decoration: none;
335
+ transition: translate var(--slotted-duration-instant) var(--slotted-control-transition-easing);
336
+ }
337
+
338
+ /**
339
+ * The stretched target. Transparent, over the whole attachment, and below
340
+ * `actions`, which is positioned after it in the same stacking context.
341
+ */
342
+ [data-ui='attachment'] [data-part='trigger']::after {
343
+ content: '';
344
+ inset: 0;
345
+ position: absolute;
346
+ }
347
+
348
+ /*
349
+ * The name answers as well as the ground, because the ground's step is
350
+ * 0.03 of lightness and a reader looking at the words should not have to
351
+ * find it at the edge. The underline says link; the press settles the name
352
+ * by a hairline — the answer `upload` gives its file name — so press and
353
+ * hover differ on the trigger itself and not only on the attachment.
354
+ */
355
+ [data-ui='attachment'] [data-part='trigger']:not(:disabled):not([aria-disabled='true']):hover {
356
+ text-decoration-line: underline;
357
+ text-underline-offset: 0.2em;
358
+ }
359
+
360
+ [data-ui='attachment'] [data-part='trigger']:not(:disabled):not([aria-disabled='true']):active {
361
+ text-decoration-line: underline;
362
+ text-underline-offset: 0.2em;
363
+ translate: 0 var(--slotted-border-width-thin, 1px);
364
+ }
365
+
366
+ [data-ui='attachment'] [data-part='trigger']:is(:disabled, [aria-disabled='true']) {
367
+ cursor: default;
368
+ }
369
+
370
+ /**
371
+ * The ring is drawn around the attachment, because the attachment is the
372
+ * target the pointer sees; a ring around the name alone would say that only
373
+ * the words can be pressed. The browser's own ring on the trigger is
374
+ * suppressed so the two are never drawn at once.
375
+ */
376
+ [data-ui='attachment'] [data-part='trigger']:focus-visible {
377
+ outline: none;
378
+ }
379
+
380
+ [data-ui='attachment'][data-part='root']:has(
381
+ > [data-part='body'] > [data-part='trigger']:focus-visible
382
+ ) {
383
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
384
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
385
+ }
386
+
387
+ /*
388
+ * The pointer's answer, on the whole attachment. Held back while the file
389
+ * cannot be opened: a disabled or loading attachment refuses its trigger,
390
+ * and a fill would promise the press it refuses.
391
+ */
392
+ [data-ui='attachment'][data-part='root']:not([data-disabled]):not([data-loading]):has(
393
+ > [data-part='body'] > [data-part='trigger']:hover
394
+ ) {
395
+ --_fill: var(--_fill-hover);
396
+ }
397
+
398
+ [data-ui='attachment'][data-part='root']:not([data-disabled]):not([data-loading]):has(
399
+ > [data-part='body'] > [data-part='trigger']:active
400
+ ) {
401
+ --_fill: var(--_fill-active);
402
+ }
403
+
404
+ /**
405
+ * The name and its extension are one run, laid out in the name's own
406
+ * direction. The binding writes `dir="auto"`, so `claim-form.pdf` reads in
407
+ * that order inside a right-to-left page instead of `pdf.claim-form` — the
408
+ * defect `upload` found in its capture at `dir="rtl"`.
409
+ */
410
+ [data-ui='attachment'] [data-part='filename'] {
411
+ display: flex;
412
+ font-weight: var(--slotted-attachment-name-font-weight, var(--slotted-font-weight-medium, 500));
413
+ line-height: var(--_title-leading);
414
+ min-inline-size: 0;
415
+ unicode-bidi: isolate;
416
+ }
417
+
418
+ /*
419
+ * A name that is not a trigger is its own item in the body's column, and
420
+ * stretched to the column it laid `policy.pdf` out from the left edge of a
421
+ * right-to-left page while the detail under it stood at the right: `dir="auto"`
422
+ * gives the run its own direction, so it must also be only as wide as the run.
423
+ * Found in the capture at `dir="rtl"`.
424
+ */
425
+ [data-ui='attachment'] [data-part='body'] > [data-part='filename'],
426
+ [data-ui='attachment'] [data-part='body'] > [data-part='trigger'] {
427
+ align-self: start;
428
+ max-inline-size: 100%;
429
+ }
430
+
431
+ /**
432
+ * The stem shrinks to an ellipsis and the extension does not, so a long
433
+ * name keeps the part that says what the file is. CSS cannot truncate in the
434
+ * middle; `splitName` makes the split in the binding, and the whole name
435
+ * stays in the document and in the trigger's `title`.
436
+ */
437
+ [data-ui='attachment'] [data-part='name'] {
438
+ flex: 0 1 auto;
439
+ min-inline-size: 0;
440
+ overflow: hidden;
441
+ text-overflow: ellipsis;
442
+ white-space: nowrap;
443
+ }
444
+
445
+ [data-ui='attachment'] [data-part='extension'] {
446
+ flex: none;
447
+ white-space: nowrap;
448
+ }
449
+
450
+ /** The second line: a size, a date, a sender — whatever the page says. */
451
+ [data-ui='attachment'] [data-part='detail'] {
452
+ color: var(--slotted-text-muted);
453
+ font-size: var(--_detail-size);
454
+ font-variant-numeric: tabular-nums;
455
+ line-height: var(--_detail-leading);
456
+ margin: 0;
457
+ min-inline-size: 0;
458
+ overflow: hidden;
459
+ text-overflow: ellipsis;
460
+ white-space: nowrap;
461
+ }
462
+
463
+ /**
464
+ * Why the file failed, or anything else the page must say about it at more
465
+ * length than a line of detail. It wraps, because a reason cut off by an
466
+ * ellipsis is a reason nobody reads.
467
+ */
468
+ [data-ui='attachment'] [data-part='message'] {
469
+ color: var(--_message-color);
470
+ font-size: var(--_detail-size);
471
+ grid-area: message;
472
+ line-height: var(--_detail-leading);
473
+ margin: 0;
474
+ margin-block-start: var(--_line-gap);
475
+ min-inline-size: 0;
476
+ overflow-wrap: anywhere;
477
+ }
478
+
479
+ /**
480
+ * The transfer. It is `progress-indicator`'s bar, drawn where this family
481
+ * places it: a second bar would be a second answer to one question. Out of
482
+ * the flow, so the attachment is the same height whether it is sending or
483
+ * sent.
484
+ *
485
+ * It stands in the text's column and inside the bottom inset, not on the
486
+ * attachment's edge. Laid along the edge from corner to corner — `upload`'s
487
+ * placement — it ran under the tile, and on a failed attachment the danger
488
+ * bar sat against the danger edge and its inset second line: three red
489
+ * lines at the bottom, read as the edge doubled wherever the bar reached.
490
+ * Centred in the inset, the bar clears the edge by the same distance it
491
+ * clears the detail line above it.
492
+ */
493
+ [data-ui='attachment'] [data-part='progress'] {
494
+ display: block;
495
+ inset-block-end: round(
496
+ down,
497
+ /* the `sm` bar is one step of the space scale thick (`progress-indicator`) */
498
+ calc((var(--_inset) - var(--slotted-space-1, 0.125rem)) / 2),
499
+ 1px
500
+ );
501
+ inset-inline-end: var(--_inset);
502
+ inset-inline-start: calc(var(--_inset) + var(--_preview-size) + var(--_gap));
503
+ position: absolute;
504
+ transition: opacity var(--slotted-duration-medium) var(--slotted-easing-enter);
505
+ }
506
+
507
+ /* Without a tile, or on a card, the text's column starts at the inset. */
508
+ [data-ui='attachment'][data-part='root']:not(:has(> [data-part='preview']))
509
+ > [data-part='progress'],
510
+ [data-ui='attachment'][data-orientation='vertical'] > [data-part='progress'] {
511
+ inset-inline-start: var(--_inset);
512
+ }
513
+
514
+ @starting-style {
515
+ [data-ui='attachment'] [data-part='progress'] {
516
+ opacity: 0;
517
+ }
518
+ }
519
+
520
+ /* ── The actions ───────────────────────────────────────────────────────
521
+ *
522
+ * Above the stretched target, so a press on a control is the control's.
523
+ */
524
+ [data-ui='attachment'] [data-part='actions'] {
525
+ align-items: center;
526
+ display: inline-flex;
527
+ gap: var(--slotted-space-1, 0.125rem);
528
+ grid-area: actions;
529
+ justify-content: flex-end;
530
+ position: relative;
531
+ z-index: 1;
532
+ }
533
+
534
+ /**
535
+ * Inner controls (R2): no frame and no ground at rest, one ground step on
536
+ * hover and one more on press, the theme's ring on focus. The ground is a
537
+ * state layer of `currentcolor`, for the reason `upload` gives its row
538
+ * controls: it is legible on whatever ground the attachment has, including
539
+ * a failed one's tint.
540
+ */
541
+ [data-ui='attachment'] [data-part='action'],
542
+ [data-ui='attachment'] [data-part='remove'] {
543
+ --_state-hover: color-mix(
544
+ in oklab,
545
+ currentcolor var(--slotted-attachment-action-state-layer-hover, 8%),
546
+ transparent
547
+ );
548
+ --_state-active: color-mix(
549
+ in oklab,
550
+ currentcolor var(--slotted-attachment-action-state-layer-active, 14%),
551
+ transparent
552
+ );
553
+
554
+ align-items: center;
555
+ appearance: none;
556
+ background-color: transparent;
557
+ block-size: var(--_action-size);
558
+ border: 0;
559
+ border-radius: var(--slotted-attachment-action-radius, var(--slotted-control-radius, 4px));
560
+ box-sizing: border-box;
561
+ color: inherit;
562
+ cursor: pointer;
563
+ display: inline-flex;
564
+ flex: none;
565
+ font: inherit;
566
+ inline-size: var(--_action-size);
567
+ justify-content: center;
568
+ margin: 0;
569
+ padding: 0;
570
+ text-decoration: none;
571
+ transition:
572
+ background-color var(--slotted-control-transition-duration)
573
+ var(--slotted-control-transition-easing),
574
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
575
+ }
576
+
577
+ [data-ui='attachment'] [data-part='action'] > [data-ui='icon'],
578
+ [data-ui='attachment'] [data-part='remove'] > [data-ui='icon'] {
579
+ --slotted-icon-size: var(--_action-glyph-size);
580
+ }
581
+
582
+ /**
583
+ * `:not(:disabled)` guards a `button`; `aria-disabled` guards an `a`, which
584
+ * has no native disabled state. A control that cannot act does not answer a
585
+ * pointer.
586
+ */
587
+ [data-ui='attachment'] [data-part='action']:not(:disabled):not([aria-disabled='true']):hover,
588
+ [data-ui='attachment'] [data-part='remove']:not(:disabled):not([aria-disabled='true']):hover {
589
+ background-color: var(--_state-hover);
590
+ }
591
+
592
+ [data-ui='attachment'] [data-part='action']:not(:disabled):not([aria-disabled='true']):active,
593
+ [data-ui='attachment'] [data-part='remove']:not(:disabled):not([aria-disabled='true']):active {
594
+ background-color: var(--_state-active);
595
+ }
596
+
597
+ [data-ui='attachment'] [data-part='action']:is(:disabled, [aria-disabled='true']),
598
+ [data-ui='attachment'] [data-part='remove']:is(:disabled, [aria-disabled='true']) {
599
+ color: var(--slotted-disabled-foreground);
600
+ cursor: default;
601
+ }
602
+
603
+ /*
604
+ * Inside the control, one ring width in: the controls of one attachment
605
+ * stand two pixels apart, and an outward ring would be drawn over the
606
+ * neighbour's box.
607
+ */
608
+ [data-ui='attachment'] [data-part='action']:focus-visible,
609
+ [data-ui='attachment'] [data-part='remove']:focus-visible {
610
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
611
+ outline-offset: calc(-1 * var(--slotted-focus-ring-width, 2px));
612
+ }
613
+
614
+ /**
615
+ * On a finger the target is the comfortable size and the box is the one a
616
+ * mouse sees: a transparent `::after` centred on each control reaches 44
617
+ * pixels in both axes, and the ground and the ring stay square. The rule is
618
+ * `upload`'s.
619
+ */
620
+ @media (pointer: coarse) {
621
+ [data-ui='attachment'] :is([data-part='action'], [data-part='remove']) {
622
+ position: relative;
623
+ }
624
+
625
+ [data-ui='attachment'] :is([data-part='action'], [data-part='remove'])::after {
626
+ content: '';
627
+ inset: min(0px, calc((100% - var(--slotted-target-comfortable, 44px)) / 2));
628
+ position: absolute;
629
+ }
630
+ }
631
+
632
+ /* ── States of the file ────────────────────────────────────────────────
633
+ *
634
+ * A file that failed: its ground, its edge and its sentence turn, and the
635
+ * tile keeps the kind's colour. `upload` measured why — a refused archive
636
+ * that turned red stopped saying it was an archive, on the one row whose
637
+ * message was about archives — and every kind clears 4.03 : 1 against the
638
+ * failed ground in `light` (`default-theme.test.mjs`).
639
+ */
640
+ [data-ui='attachment'][data-part='root'][data-invalid] {
641
+ --_surface: var(--slotted-tone-danger-surface);
642
+ --_edge: var(--slotted-tone-danger-border);
643
+ --_message-color: var(--slotted-tone-danger-text);
644
+
645
+ /*
646
+ * The edge doubles inward. Every tone surface is the page's own white in
647
+ * `high-contrast`, so the failed attachment's tint vanished there and the
648
+ * sentence's colour was the one signal left — colour alone, which WCAG 2.2
649
+ * SC 1.4.1 refuses. A second pixel of edge says it in line, in every
650
+ * scheme, and moves nothing: an inset shadow takes no room.
651
+ */
652
+ --_invalid-edge-width: var(--slotted-control-invalid-edge-width, var(--_border-width));
653
+
654
+ box-shadow: inset 0 0 0 var(--_invalid-edge-width) var(--_edge);
655
+ }
656
+
657
+ /**
658
+ * An unavailable file. The words take the disabled ink the foundation holds
659
+ * at its contrast, and the tile gives up the kind's colour, because a file
660
+ * that cannot be opened should not still be the brightest thing in the row.
661
+ */
662
+ [data-ui='attachment'][data-part='root'][data-disabled] {
663
+ --_ink: var(--slotted-disabled-foreground);
664
+ }
665
+
666
+ [data-ui='attachment'][data-part='root'][data-disabled] [data-part='detail'] {
667
+ color: var(--slotted-disabled-foreground);
668
+ }
669
+
670
+ [data-ui='attachment'][data-part='root'][data-disabled] [data-part='preview'] {
671
+ --_kind-tile: var(--slotted-disabled-background);
672
+
673
+ color: var(--slotted-disabled-foreground);
674
+ }
675
+
676
+ /*
677
+ * A picture has no ground to give up, so it gives up its colour: the
678
+ * whole-strength grey is the one value that says unavailable without
679
+ * dimming the thumbnail below legibility.
680
+ */
681
+ [data-ui='attachment'][data-part='root'][data-disabled] [data-part='preview'] > img {
682
+ filter: grayscale(1);
683
+ }
684
+
685
+ /* ── The card ──────────────────────────────────────────────────────────
686
+ *
687
+ * The same attachment stood on end: the picture takes the card and the name
688
+ * sits under it. The actions move onto the picture's corner, over the thing
689
+ * the reader is pointing at, on a plate of their own — the consumer's
690
+ * photograph is any colour, and a dark mark on a mid-blue sky was what
691
+ * `upload`'s first grid capture showed.
692
+ */
693
+ [data-ui='attachment'][data-part='root'][data-orientation='vertical'] {
694
+ align-items: stretch;
695
+ grid-template-areas:
696
+ 'preview'
697
+ 'body'
698
+ 'message';
699
+ grid-template-columns: minmax(0, 1fr);
700
+ inline-size: var(--_card-size);
701
+ }
702
+
703
+ /*
704
+ * The picture fills the card's content box. Its block size is stated from
705
+ * the card rather than from `aspect-ratio`, which carried a fractional
706
+ * width into a fractional height in `upload`'s grid.
707
+ */
708
+ [data-ui='attachment'][data-orientation='vertical'] > [data-part='preview'] {
709
+ --_preview-radius: var(--slotted-attachment-card-preview-radius, var(--slotted-radius-md, 6px));
710
+
711
+ block-size: calc(var(--_card-size) - 2 * var(--_inset) - 2 * var(--_border-width));
712
+ inline-size: 100%;
713
+ }
714
+
715
+ [data-ui='attachment'][data-orientation='vertical'] > [data-part='preview'] > [data-ui='icon'] {
716
+ --slotted-icon-size: calc(2 * var(--_glyph-size));
717
+ }
718
+
719
+ /**
720
+ * A card has room for the extension in words, and a reader scanning a wall
721
+ * of thumbnails for the one PDF finds it faster by the three letters than by
722
+ * the silhouette. It is text, drawn from the attribute the binding writes.
723
+ */
724
+ [data-ui='attachment'][data-orientation='vertical']
725
+ > [data-part='preview']:not(:has(img))::after {
726
+ content: attr(data-extension);
727
+ font-size: var(--_detail-size);
728
+ font-weight: var(--slotted-font-weight-semibold, 600);
729
+ /* Three capitals set tight read as one blot at twelve pixels; the
730
+ tracking is the one `upload`'s card label carries. */
731
+ letter-spacing: 0.06em;
732
+ line-height: var(--_detail-leading);
733
+ margin-block-start: var(--slotted-space-2, 0.25rem);
734
+ }
735
+
736
+ [data-ui='attachment'][data-orientation='vertical'] > [data-part='body'] {
737
+ margin-block-start: var(--_gap);
738
+ }
739
+
740
+ [data-ui='attachment'][data-orientation='vertical'] > [data-part='actions'] {
741
+ background-color: color-mix(
742
+ in oklab,
743
+ var(--slotted-surface) var(--slotted-attachment-card-actions-opacity, 96%),
744
+ transparent
745
+ );
746
+ border-radius: var(--slotted-attachment-action-radius, var(--slotted-control-radius, 4px));
747
+ box-shadow: var(--slotted-shadow-xs);
748
+ color: var(--slotted-text);
749
+ grid-area: auto;
750
+ inset-block-start: calc(var(--_inset) + var(--slotted-space-2, 0.25rem));
751
+ inset-inline-end: calc(var(--_inset) + var(--slotted-space-2, 0.25rem));
752
+ padding: var(--slotted-space-1, 0.125rem);
753
+ position: absolute;
754
+ }
755
+
756
+ /*
757
+ * On a card three glyphs wide the plate stands over the kind's mark: at 120
758
+ * pixels the mark is centred at 51 and the plate reaches 36 from the top
759
+ * across the inline end, so a PDF card showed half its page under the
760
+ * controls. Where there is a pointer that can hover, the plate appears with
761
+ * the pointer or with focus anywhere in the card — the keyboard reaches the
762
+ * controls by tabbing into them, which is what shows them. On a finger it
763
+ * stays, because there is no hover to summon it with, and on a disabled
764
+ * card it stays too: a card that answers no pointer cannot summon it either.
765
+ */
766
+ @media (hover: hover) and (pointer: fine) {
767
+ [data-ui='attachment'][data-orientation='vertical']:not([data-disabled])
768
+ > [data-part='actions'] {
769
+ opacity: 0;
770
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit);
771
+ }
772
+
773
+ [data-ui='attachment'][data-orientation='vertical']:not([data-disabled]):is(
774
+ :hover,
775
+ :focus-within
776
+ )
777
+ > [data-part='actions'] {
778
+ opacity: 1;
779
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-enter);
780
+ }
781
+ }
782
+
783
+ /* ── The set ───────────────────────────────────────────────────────────
784
+ *
785
+ * A native list. It takes focus only when its last attachment is removed
786
+ * with the keyboard, which is what `tabindex="-1"` is for.
787
+ */
788
+ [data-ui='attachment'][data-part='group'] {
789
+ box-sizing: border-box;
790
+ display: flex;
791
+ flex-direction: column;
792
+ gap: var(--slotted-attachment-group-gap, var(--slotted-space-4, 0.5rem));
793
+ list-style: none;
794
+ margin: 0;
795
+ min-inline-size: 0;
796
+ padding: 0;
797
+ }
798
+
799
+ [data-ui='attachment'][data-part='group']:focus-visible {
800
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
801
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
802
+ }
803
+
804
+ /*
805
+ * An empty set takes no room: a message whose last file was removed does
806
+ * not keep a gap where the files were.
807
+ */
808
+ [data-ui='attachment'][data-part='group']:empty {
809
+ display: none;
810
+ }
811
+
812
+ /** Rows that share the width: two or three to a line, never narrower than a name. */
813
+ [data-ui='attachment'][data-part='group'][data-layout='grid'] {
814
+ display: grid;
815
+ grid-template-columns: repeat(
816
+ auto-fill,
817
+ minmax(min(100%, var(--slotted-attachment-grid-min, 16rem)), 1fr)
818
+ );
819
+ }
820
+
821
+ /* Cards keep their own width, so every card is a whole pixel wide. */
822
+ [data-ui='attachment'][data-part='group'][data-layout='grid'][data-orientation='vertical'] {
823
+ grid-template-columns: repeat(auto-fill, var(--_card-size));
824
+ }
825
+
826
+ /**
827
+ * One line that scrolls — a composer's tray. The strip is padded by the
828
+ * ring's reach and pulled back by the same amount, so a focused attachment
829
+ * at either end keeps its whole ring inside the scroll box instead of
830
+ * losing two pixels of it to the clip.
831
+ */
832
+ [data-ui='attachment'][data-part='group'][data-layout='strip'] {
833
+ --_ring-reach: calc(
834
+ var(--slotted-focus-ring-width, 2px) + var(--slotted-focus-ring-offset, 2px)
835
+ );
836
+
837
+ flex-direction: row;
838
+ margin: calc(-1 * var(--_ring-reach));
839
+ overflow-x: auto;
840
+ overscroll-behavior-inline: contain;
841
+ padding: var(--_ring-reach);
842
+ scroll-padding-inline: var(--_ring-reach);
843
+ scroll-snap-type: inline proximity;
844
+ scrollbar-color: var(--slotted-border-strong) transparent;
845
+ scrollbar-width: thin;
846
+ }
847
+
848
+ [data-ui='attachment'][data-part='group'][data-layout='strip'] > [data-part='root'] {
849
+ flex: none;
850
+ scroll-snap-align: start;
851
+ }
852
+
853
+ [data-ui='attachment'][data-part='group'][data-layout='strip']
854
+ > [data-part='root'][data-orientation='horizontal'] {
855
+ inline-size: var(--slotted-attachment-strip-item-size, 15rem);
856
+ }
857
+
858
+ /* ── Forced colours ────────────────────────────────────────────────────
859
+ *
860
+ * The theme remaps every token this sheet reads. What it cannot reach is a
861
+ * state said by a mix into a ground the system flattens to `Canvas`: the
862
+ * pointer on the attachment and on its controls changed nothing a reader
863
+ * could see. The system's own pair answers instead, as it does for
864
+ * `upload`'s controls, and the failed attachment keeps its edge.
865
+ */
866
+ @media (forced-colors: active) {
867
+ [data-ui='attachment'][data-part='root'][data-invalid] {
868
+ --_edge: CanvasText;
869
+ }
870
+
871
+ /* The doubled edge is a shadow, which forced colours remove: it comes back
872
+ as an outline inside the border, and a focused failed attachment draws
873
+ its ring double. */
874
+ [data-ui='attachment'][data-part='root'][data-invalid] {
875
+ outline: var(--_invalid-edge-width) solid CanvasText;
876
+ outline-offset: calc(-1 * (var(--_border-width) + var(--_invalid-edge-width)));
877
+ }
878
+
879
+ [data-ui='attachment'][data-part='root'][data-invalid]:has(
880
+ > [data-part='body'] > [data-part='trigger']:focus-visible
881
+ ) {
882
+ outline: calc(var(--slotted-focus-ring-width, 2px) + var(--_invalid-edge-width)) double
883
+ var(--slotted-focus-ring-color, Highlight);
884
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
885
+ }
886
+
887
+ [data-ui='attachment'][data-part='root']:not([data-disabled]):not([data-loading]):has(
888
+ > [data-part='body'] > [data-part='trigger']:is(:hover, :active)
889
+ ) {
890
+ border-color: Highlight;
891
+ }
892
+
893
+ [data-ui='attachment'] [data-part='preview'] {
894
+ border: var(--_border-width) solid ButtonBorder;
895
+ }
896
+
897
+ [data-ui='attachment']
898
+ :is([data-part='action'], [data-part='remove']):not(:disabled):not([aria-disabled='true']):is(
899
+ :hover,
900
+ :active
901
+ ) {
902
+ background-color: Highlight;
903
+ color: HighlightText;
904
+ }
905
+ }
906
+ }