@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,1453 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * ── One message of a conversation ──────────────────────────────────────
4
+ *
5
+ * Two members under one identifier: the row a sender speaks in
6
+ * (`data-part="root"`, an `article`) and the surface the words are painted
7
+ * on (`data-part="bubble"`). A bubble nests inside a row, so its root is not
8
+ * written `root` — every selector for the row would match it
9
+ * (`specs/components/message/contract.json` › `rootPartReason`).
10
+ *
11
+ * The side is asked once, on the row. The bubble, its reactions and the
12
+ * header follow `data-align`; the surveyed shadcn examples pass `align` to
13
+ * both `Message` and `Bubble`, and two answers to one question can disagree
14
+ * (`docs/history/designs/2026-09-30-message-design.md` §2.2).
15
+ */
16
+ [data-ui='message'][data-part='root'] {
17
+ /**
18
+ * The avatar column is the small avatar step, which the theme restates per
19
+ * density; ReUI's 32 has no step there (dossier §3), and a row that follows
20
+ * the density of the page around it is worth four pixels.
21
+ */
22
+ --_avatar-size: var(--slotted-message-avatar-size, var(--slotted-avatar-size-sm, 28px));
23
+ --_column-gap: var(--slotted-message-column-gap, var(--slotted-space-4, 0.5rem));
24
+ --_stack-gap: var(--slotted-message-stack-gap, var(--slotted-space-2, 0.25rem));
25
+ --_meta-size: var(--slotted-font-size-xs, 0.75rem);
26
+ --_meta-leading: round(up, calc(var(--_meta-size) * var(--slotted-line-height-xs, 1.4)), 1px);
27
+ /**
28
+ * The inset of the words inside a bubble, which the header and the footer
29
+ * repeat so a name stands over the first letter it names. Eight and twelve
30
+ * at comfortable, ReUI's measure (`c-bubble-2`), falling with the control
31
+ * gap the theme restates per density.
32
+ */
33
+ --_pad-block: var(--slotted-message-bubble-padding-block, var(--slotted-control-gap-md, 8px));
34
+ --_pad-inline: var(
35
+ --slotted-message-bubble-padding-inline,
36
+ calc(var(--slotted-control-gap-md, 8px) + var(--slotted-space-2, 0.25rem))
37
+ );
38
+ /**
39
+ * The inner controls of a row — the version pager — at the R2 size: the
40
+ * glyph plus a step of air on each side, never under the WCAG 2.2 target.
41
+ */
42
+ --_glyph-size: var(--slotted-control-icon-size-sm, 14px);
43
+ --_inner-size: max(
44
+ var(--slotted-target-min, 24px),
45
+ calc(var(--_glyph-size) + 2 * var(--slotted-space-2, 0.25rem))
46
+ );
47
+
48
+ display: grid;
49
+ grid-template-areas: 'body';
50
+ grid-template-columns: minmax(0, 1fr);
51
+ column-gap: var(--_column-gap);
52
+ position: relative;
53
+ min-inline-size: 0;
54
+ color: var(--slotted-text);
55
+ font-family: var(--slotted-font-family-sans, sans-serif);
56
+ }
57
+
58
+ /**
59
+ * A row with an avatar, and every row of a run that holds one anywhere,
60
+ * gives the avatar a column. The earlier rows of a run carry no avatar and
61
+ * still stand on the spine the last one sits on — shadcn asks for an empty
62
+ * `MessageAvatar` in each of them (`c-message-6`); the column is read here
63
+ * from the DOM instead.
64
+ */
65
+ [data-ui='message'][data-part='root']:has(> [data-part='avatar']),
66
+ [data-ui='message'][data-part='group']:has([data-part='avatar'])
67
+ > [data-ui='message'][data-part='root'] {
68
+ grid-template-areas: 'avatar body';
69
+ grid-template-columns: var(--_avatar-size) minmax(0, 1fr);
70
+ }
71
+
72
+ [data-ui='message'][data-part='root'][data-align='end']:has(> [data-part='avatar']),
73
+ [data-ui='message'][data-part='group']:has([data-part='avatar'])
74
+ > [data-ui='message'][data-part='root'][data-align='end'] {
75
+ grid-template-areas: 'body avatar';
76
+ grid-template-columns: minmax(0, 1fr) var(--_avatar-size);
77
+ }
78
+
79
+ /**
80
+ * The avatar sits at the foot of the row, on the line of the last bubble
81
+ * rather than of the footer under it: in a run it marks where the run ends,
82
+ * which is where the reader's eye arrives.
83
+ */
84
+ [data-ui='message'][data-part='root'] > [data-part='avatar'] {
85
+ grid-area: avatar;
86
+ display: flex;
87
+ align-self: end;
88
+ justify-content: center;
89
+ inline-size: var(--_avatar-size);
90
+ min-block-size: var(--_avatar-size);
91
+ }
92
+
93
+ [data-ui='message'][data-part='root']:has(> [data-part='body'] > [data-part='footer'])
94
+ > [data-part='avatar'] {
95
+ margin-block-end: calc(var(--_meta-leading) + var(--_stack-gap));
96
+ }
97
+
98
+ [data-ui='message'][data-part='root'] > [data-part='body'] {
99
+ grid-area: body;
100
+ display: flex;
101
+ flex-direction: column;
102
+ align-items: flex-start;
103
+ gap: var(--_stack-gap);
104
+ min-inline-size: 0;
105
+ }
106
+
107
+ [data-ui='message'][data-part='root'][data-align='end'] > [data-part='body'] {
108
+ align-items: flex-end;
109
+ }
110
+
111
+ /**
112
+ * ── A run from one sender ──────────────────────────────────────────────
113
+ *
114
+ * Two pixels between the rows of a run, where ReUI puts eight (`c-bubble-3`).
115
+ * At eight the run read as four messages; at two, with the corners on the
116
+ * sender's side drawn in, it reads as one speaker holding the floor. The
117
+ * gap between runs belongs to whatever stacks them — the scroller gives it.
118
+ */
119
+ [data-ui='message'][data-part='group'] {
120
+ display: flex;
121
+ flex-direction: column;
122
+ gap: var(--slotted-message-run-gap, var(--slotted-space-1, 0.125rem));
123
+ min-inline-size: 0;
124
+ }
125
+
126
+ /* ── Header and footer ──────────────────────────────────────────────── */
127
+
128
+ [data-ui='message'][data-part='root'] :is([data-part='header'], [data-part='footer']) {
129
+ display: flex;
130
+ flex-wrap: wrap;
131
+ align-items: center;
132
+ column-gap: var(--slotted-space-3, 0.375rem);
133
+ max-inline-size: 100%;
134
+ min-inline-size: 0;
135
+ padding-inline: var(--_pad-inline);
136
+ color: var(--slotted-text-muted);
137
+ font-size: var(--_meta-size);
138
+ font-weight: var(--slotted-font-weight-medium, 500);
139
+ line-height: var(--_meta-leading);
140
+ }
141
+
142
+ [data-ui='message'][data-part='root'][data-align='end']
143
+ :is([data-part='header'], [data-part='footer']) {
144
+ justify-content: flex-end;
145
+ }
146
+
147
+ /**
148
+ * The name and the hour are two facts, and the dot between them is drawn by
149
+ * the stylesheet rather than written into the text: a character would be
150
+ * read aloud, copied with the name and wrapped onto a line of its own.
151
+ * ReUI asks the consumer to write the dot (`c-message-3`).
152
+ */
153
+ [data-ui='message'][data-part='root'] [data-part='header'] > * + [data-part='time']::before {
154
+ content: '';
155
+ display: inline-block;
156
+ inline-size: var(--slotted-space-2, 0.25rem);
157
+ block-size: var(--slotted-space-2, 0.25rem);
158
+ margin-inline-end: var(--slotted-space-3, 0.375rem);
159
+ border-radius: var(--slotted-radius-full, 9999px);
160
+ background-color: currentColor;
161
+ opacity: 0.5;
162
+ vertical-align: middle;
163
+ }
164
+
165
+ [data-ui='message'][data-part='root'] [data-part='author'] {
166
+ min-inline-size: 0;
167
+ overflow: hidden;
168
+ text-overflow: ellipsis;
169
+ white-space: nowrap;
170
+ }
171
+
172
+ [data-ui='message'][data-part='root'] [data-part='time'] {
173
+ font-variant-numeric: tabular-nums;
174
+ white-space: nowrap;
175
+ }
176
+
177
+ /* ── Receipt ──────────────────────────────────────────────────────────
178
+ *
179
+ * Where a message is in its delivery is said inside the bubble, in `meta`
180
+ * (below), as a mark beside the hour — the convention WhatsApp and Telegram
181
+ * taught. Outside a bubble — an unframed reply, a thread — the receipt is a
182
+ * word in the footer, set in the meta type, with no mark: a glyph beside a
183
+ * word of twelve pixels is two voices saying one thing, and the two never
184
+ * shared a centre line.
185
+ */
186
+ [data-ui='message'][data-part='root'] [data-part='footer'] > [data-part='receipt'] {
187
+ white-space: nowrap;
188
+ }
189
+
190
+ [data-ui='message'][data-part='root']
191
+ [data-part='footer']
192
+ > [data-part='receipt']
193
+ > [data-ui='icon'] {
194
+ display: none;
195
+ }
196
+
197
+ /**
198
+ * A message that did not go says so in a word under its surface, in the
199
+ * danger ink, lined up with the surface's own edge on the sender's side —
200
+ * not with the words inside it — so the note reads as the bubble's caption.
201
+ * The control that sends it again stands beside the surface (`retry`).
202
+ */
203
+ [data-ui='message'][data-part='root'][data-invalid] > [data-part='body'] > [data-part='footer'] {
204
+ padding-inline: var(--slotted-space-2, 0.25rem);
205
+ color: var(--slotted-tone-danger-text);
206
+ }
207
+
208
+ /* ── Actions ────────────────────────────────────────────────────────────
209
+ *
210
+ * The actions keep their room at rest, so a revealed toolbar moves nothing.
211
+ * `hover` reveals them on the pointer, on focus anywhere in the row, and
212
+ * always on a screen that cannot hover: a reveal by hover alone would hide
213
+ * them from the keyboard and from a finger.
214
+ */
215
+ [data-ui='message'][data-part='root'] [data-part='actions'] {
216
+ display: inline-flex;
217
+ align-items: center;
218
+ gap: var(--slotted-space-1, 0.125rem);
219
+ transition: opacity var(--slotted-duration-short) var(--slotted-easing-exit, ease-in);
220
+ }
221
+
222
+ @media (hover: hover) {
223
+ [data-ui='message'][data-part='root'][data-actions='hover'] [data-part='actions'] {
224
+ opacity: 0;
225
+ }
226
+
227
+ [data-ui='message'][data-part='root'][data-actions='hover']:is(
228
+ :hover:not([data-disabled]),
229
+ :focus-within
230
+ )
231
+ [data-part='actions'] {
232
+ opacity: 1;
233
+ transition-timing-function: var(--slotted-easing-enter, ease-out);
234
+ }
235
+ }
236
+
237
+ /* ── Activity ───────────────────────────────────────────────────────────
238
+ *
239
+ * What a reply is doing before it has words, as a short log of steps on a
240
+ * raised card: the steps already done carry a tick and step back into the
241
+ * muted ink, and the step in progress carries the shared ring (`spinner`)
242
+ * and a band of light that travels across its words. A single step is the
243
+ * same card with one line. ReUI's `c-message-7` draws one muted line that
244
+ * pulses; the log says what has been done, not only that something is.
245
+ *
246
+ * The band is the one place the family paints text with a gradient, which
247
+ * the polish lens refuses as decoration: here it is the wait itself, it
248
+ * moves only while the step is current, and under reduced motion the words
249
+ * stand in the plain ink.
250
+ */
251
+ [data-ui='message'][data-part='root'] [data-part='activity'] {
252
+ display: inline-flex;
253
+ flex-direction: column;
254
+ gap: var(--slotted-space-3, 0.375rem);
255
+ box-sizing: border-box;
256
+ max-inline-size: 100%;
257
+ padding-block: var(--_pad-block);
258
+ padding-inline: var(--_pad-inline) calc(var(--_pad-inline) + var(--slotted-space-2, 0.25rem));
259
+ border: var(--slotted-border-width-thin, 1px) solid var(--slotted-border-subtle);
260
+ border-radius: calc(var(--_pad-block) * 2);
261
+ background-color: var(--slotted-surface-raised);
262
+ box-shadow: var(--slotted-shadow-xs);
263
+ color: var(--slotted-text-muted);
264
+ font-size: var(--slotted-control-font-size-md, 13px);
265
+ line-height: round(up, calc(1em * var(--slotted-line-height-md, 1.5)), 1px);
266
+ }
267
+
268
+ [data-ui='message'][data-part='root'] [data-part='activity-step'] {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: var(--slotted-space-4, 0.5rem);
272
+ min-inline-size: 0;
273
+ transition:
274
+ opacity var(--slotted-duration-medium) var(--slotted-easing-enter, ease-out),
275
+ translate var(--slotted-duration-medium) var(--slotted-easing-enter, ease-out),
276
+ color var(--slotted-duration-medium) var(--slotted-easing-standard, ease);
277
+
278
+ @starting-style {
279
+ opacity: 0;
280
+ translate: 0 var(--slotted-space-2, 0.25rem);
281
+ }
282
+ }
283
+
284
+ /** The mark column: a tick, or the ring, in one box so the words line up. */
285
+ [data-ui='message'][data-part='root'] [data-part='activity-step'] > :first-child {
286
+ --slotted-icon-size: var(--_glyph-size);
287
+ --slotted-spinner-size: var(--_glyph-size);
288
+
289
+ display: inline-grid;
290
+ flex: none;
291
+ place-items: center;
292
+ inline-size: var(--_glyph-size);
293
+ block-size: var(--_glyph-size);
294
+ }
295
+
296
+ [data-ui='message'][data-part='root'] [data-part='activity-step'][data-complete] > :first-child {
297
+ color: var(--slotted-tone-success-text);
298
+ }
299
+
300
+ [data-ui='message'][data-part='root'] [data-part='activity-step'][data-current] {
301
+ color: var(--slotted-text);
302
+ }
303
+
304
+ [data-ui='message'][data-part='root'] [data-part='activity-step'][data-current] > :first-child {
305
+ color: var(--slotted-tone-accent-text);
306
+ }
307
+
308
+ [data-ui='message'][data-part='root'] [data-part='activity-step'][data-current] > :last-child {
309
+ --_base: color-mix(in oklab, var(--slotted-text) 72%, var(--slotted-text-muted));
310
+
311
+ background-image: linear-gradient(
312
+ 90deg,
313
+ var(--_base) 0%,
314
+ var(--_base) 38%,
315
+ var(--slotted-text-muted) 50%,
316
+ var(--_base) 62%,
317
+ var(--_base) 100%
318
+ );
319
+ background-size: 220% 100%;
320
+ background-clip: text;
321
+ color: transparent;
322
+ animation: slotted-message-sweep var(--slotted-duration-loop-slow) linear infinite;
323
+ }
324
+
325
+ @keyframes slotted-message-sweep {
326
+ from {
327
+ background-position: 100% 0;
328
+ }
329
+
330
+ to {
331
+ background-position: 0% 0;
332
+ }
333
+ }
334
+
335
+ /* ── Versions ───────────────────────────────────────────────────────────
336
+ *
337
+ * `2 de 3` between two inner controls. The chevron is one glyph turned for
338
+ * the previous control, and a right-to-left document turns both.
339
+ */
340
+ [data-ui='message'][data-part='root'] [data-part='versions'] {
341
+ display: inline-flex;
342
+ align-items: center;
343
+ color: var(--slotted-text-muted);
344
+ }
345
+
346
+ [data-ui='message'][data-part='root'] [data-part='version-label'] {
347
+ padding-inline: var(--slotted-space-2, 0.25rem);
348
+ font-variant-numeric: tabular-nums;
349
+ white-space: nowrap;
350
+ }
351
+
352
+ [data-ui='message'][data-part='root']
353
+ :is([data-part='version-previous'], [data-part='version-next']) {
354
+ display: inline-grid;
355
+ place-items: center;
356
+ inline-size: var(--_inner-size);
357
+ block-size: var(--_inner-size);
358
+ margin: 0;
359
+ padding: 0;
360
+ border: 0;
361
+ border-radius: var(--slotted-control-radius, 4px);
362
+ background-color: transparent;
363
+ color: inherit;
364
+ font: inherit;
365
+ transition:
366
+ background-color var(--slotted-duration-short) var(--slotted-easing-standard, ease),
367
+ color var(--slotted-duration-short) var(--slotted-easing-standard, ease);
368
+ }
369
+
370
+ [data-ui='message'][data-part='root']
371
+ :is([data-part='version-previous'], [data-part='version-next'])
372
+ > [data-ui='icon'] {
373
+ --slotted-icon-size: var(--_glyph-size);
374
+ }
375
+
376
+ [data-ui='message'][data-part='root'] [data-part='version-previous'] > [data-ui='icon'] {
377
+ scale: -1 1;
378
+ }
379
+
380
+ [data-ui='message'][data-part='root']:dir(rtl)
381
+ :is([data-part='version-previous'], [data-part='version-next'])
382
+ > [data-ui='icon'] {
383
+ scale: -1 1;
384
+ }
385
+
386
+ [data-ui='message'][data-part='root']:dir(rtl) [data-part='version-previous'] > [data-ui='icon'] {
387
+ scale: 1 1;
388
+ }
389
+
390
+ [data-ui='message'][data-part='root']
391
+ :is([data-part='version-previous'], [data-part='version-next']):hover:not(:disabled) {
392
+ background-color: var(--slotted-tone-secondary-subtle-hover);
393
+ color: var(--slotted-text);
394
+ }
395
+
396
+ [data-ui='message'][data-part='root']
397
+ :is([data-part='version-previous'], [data-part='version-next']):active:not(:disabled) {
398
+ background-color: var(--slotted-tone-secondary-subtle-active);
399
+ color: var(--slotted-text);
400
+ }
401
+
402
+ [data-ui='message'][data-part='root']
403
+ :is([data-part='version-previous'], [data-part='version-next']):disabled {
404
+ color: var(--slotted-disabled-foreground);
405
+ }
406
+
407
+ [data-ui='message'][data-part='root']
408
+ :is([data-part='version-previous'], [data-part='version-next']):focus-visible {
409
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color);
410
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
411
+ }
412
+
413
+ /* ── A message that did not go ─────────────────────────────────────────
414
+ *
415
+ * The words stay as they were written, on their own surface. A red rim round
416
+ * a blue bubble said "error" twice and read as a second, clashing bubble,
417
+ * and a faded fill dropped white words on the accent under 4.5:1 (4.2 at
418
+ * 85%). What says it is a round control beside the surface, in the danger
419
+ * tone, which is also the way to send it again (`bubble` › `retry`,
420
+ * iMessage's placement), and the receipt under it, in words.
421
+ */
422
+
423
+ /* ── Layout: thread ─────────────────────────────────────────────────────
424
+ *
425
+ * The comment thread (Slack, ReUI `sheet-5`): every row at the start, the
426
+ * avatar at the top beside the name, the words unframed under it, and a
427
+ * band under the pointer that says which message the actions belong to.
428
+ */
429
+ [data-ui='message'][data-part='root'][data-layout='thread'] {
430
+ --_avatar-size: var(--slotted-message-thread-avatar-size, var(--slotted-avatar-size-md, 36px));
431
+ --_pad-inline: 0px;
432
+
433
+ padding-block: var(--slotted-space-3, 0.375rem);
434
+ padding-inline: var(--slotted-space-4, 0.5rem);
435
+ border-radius: var(--slotted-radius-md, 6px);
436
+ transition: background-color var(--slotted-duration-short) var(--slotted-easing-standard, ease);
437
+ }
438
+
439
+ [data-ui='message'][data-part='root'][data-layout='thread'] > [data-part='avatar'] {
440
+ align-self: start;
441
+ margin-block-end: 0;
442
+ }
443
+
444
+ [data-ui='message'][data-part='root'][data-layout='thread'] [data-part='author'] {
445
+ color: var(--slotted-text);
446
+ font-size: var(--slotted-control-font-size-md, 13px);
447
+ font-weight: var(--slotted-font-weight-semibold, 600);
448
+ }
449
+
450
+ [data-ui='message'][data-part='root'][data-layout='thread']:hover:not([data-disabled]) {
451
+ background-color: var(--slotted-tone-secondary-subtle-hover);
452
+ }
453
+
454
+ /**
455
+ * In a thread the actions stand in the flow under the words, on the words'
456
+ * column. Where the pointer can hover and the row reveals them, they float
457
+ * instead at the row's top end over the band, the way every comment surface
458
+ * places them — a toolbar that appears only under the pointer takes no room.
459
+ * A plate that is always painted, on a touch screen or with `always`, floated
460
+ * over the words and covered them at 360 pixels.
461
+ */
462
+ [data-ui='message'][data-part='root'][data-layout='thread'] > [data-part='actions'] {
463
+ grid-column: body;
464
+ justify-self: start;
465
+ }
466
+
467
+ @media (hover: hover) {
468
+ [data-ui='message'][data-part='root'][data-layout='thread'][data-actions='hover']
469
+ > [data-part='actions'] {
470
+ position: absolute;
471
+ inset-block-start: calc(var(--slotted-space-2, 0.25rem) * -1);
472
+ inset-inline-end: var(--slotted-space-4, 0.5rem);
473
+ padding: var(--slotted-space-1, 0.125rem);
474
+ border: var(--slotted-border-width-thin, 1px) solid var(--slotted-border-subtle);
475
+ border-radius: var(--slotted-radius-md, 6px);
476
+ background-color: var(--slotted-surface-raised);
477
+ box-shadow: var(--slotted-shadow-sm);
478
+ }
479
+ }
480
+
481
+ /* ══ The bubble ════════════════════════════════════════════════════════ */
482
+
483
+ [data-ui='message'][data-part='bubble'] {
484
+ /**
485
+ * The five tones, read into the roles a painting needs. `soft` writes in
486
+ * the page's ink on the accent and secondary grounds — a paragraph set in
487
+ * blue is a link — and in the tone's own ink on the three status tones,
488
+ * where the colour of the words is the point.
489
+ */
490
+ --_solid: var(--slotted-tone-secondary-solid);
491
+ --_on-solid: var(--slotted-tone-secondary-on-solid);
492
+ --_surface: var(--slotted-tone-secondary-surface);
493
+ --_border: var(--slotted-border-subtle);
494
+ --_soft-ink: var(--slotted-text);
495
+
496
+ /**
497
+ * Twice the block inset: sixteen at comfortable, where ReUI draws fourteen
498
+ * (no step), falling to twelve and eight with the density. A one-line
499
+ * bubble is 39, 34 and 28 pixels tall at the three densities, so the
500
+ * corner never meets its twin and a bubble stays a bubble rather than a
501
+ * pill; a stated sixteen made the dense bubble a capsule.
502
+ */
503
+ --_radius: var(--slotted-message-bubble-radius, calc(var(--_pad-block) * 2));
504
+ --_radius-joined: var(--slotted-message-bubble-radius-joined, var(--slotted-radius-sm, 4px));
505
+ /* The row's inset, restated so a bubble outside a row keeps the density. */
506
+ --_pad-block: var(--slotted-message-bubble-padding-block, var(--slotted-control-gap-md, 8px));
507
+ --_pad-inline: var(
508
+ --slotted-message-bubble-padding-inline,
509
+ calc(var(--slotted-control-gap-md, 8px) + var(--slotted-space-2, 0.25rem))
510
+ );
511
+ --_top-side: var(--_radius);
512
+ --_bottom-side: var(--_radius);
513
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
514
+ /**
515
+ * The line rounded up to a whole pixel: fourteen at 1.5 is 21, and a
516
+ * one-line bubble stands at 21 + 2 × 8 + 2 × 1 = 39. ReUI's 1.625 put it at
517
+ * 40.75, with every edge below the first line between two device pixels
518
+ * (dossier §3).
519
+ */
520
+ --_leading: round(up, calc(var(--_font-size) * var(--slotted-line-height-md, 1.5)), 1px);
521
+ --_reactions-size: max(
522
+ var(--slotted-target-min, 24px),
523
+ var(--slotted-control-inner-size-md, 24px)
524
+ );
525
+
526
+ display: flex;
527
+ flex-direction: column;
528
+ align-items: flex-start;
529
+ gap: var(--slotted-space-1, 0.125rem);
530
+ position: relative;
531
+ inline-size: fit-content;
532
+ min-inline-size: 0;
533
+ /**
534
+ * A bubble never reaches the far side of its row, so the side it hangs
535
+ * from stays readable: ReUI's 80%. It is a share of the row, not a measure
536
+ * of the text — the measure belongs to `container`.
537
+ */
538
+ max-inline-size: var(--slotted-message-bubble-share, 80%);
539
+ }
540
+
541
+ [data-ui='message'][data-part='bubble'][data-tone='accent'] {
542
+ --_solid: var(--slotted-tone-accent-solid);
543
+ --_on-solid: var(--slotted-tone-accent-on-solid);
544
+ --_surface: var(--slotted-tone-accent-surface);
545
+ --_border: var(--slotted-tone-accent-border);
546
+ }
547
+
548
+ [data-ui='message'][data-part='bubble'][data-tone='success'] {
549
+ --_solid: var(--slotted-tone-success-solid);
550
+ --_on-solid: var(--slotted-tone-success-on-solid);
551
+ --_surface: var(--slotted-tone-success-surface);
552
+ --_border: var(--slotted-tone-success-border);
553
+ --_soft-ink: var(--slotted-tone-success-text);
554
+ }
555
+
556
+ [data-ui='message'][data-part='bubble'][data-tone='warning'] {
557
+ --_solid: var(--slotted-tone-warning-solid);
558
+ --_on-solid: var(--slotted-tone-warning-on-solid);
559
+ --_surface: var(--slotted-tone-warning-surface);
560
+ --_border: var(--slotted-tone-warning-border);
561
+ --_soft-ink: var(--slotted-tone-warning-text);
562
+ }
563
+
564
+ [data-ui='message'][data-part='bubble'][data-tone='danger'] {
565
+ --_solid: var(--slotted-tone-danger-solid);
566
+ --_on-solid: var(--slotted-tone-danger-on-solid);
567
+ --_surface: var(--slotted-tone-danger-surface);
568
+ --_border: var(--slotted-tone-danger-border);
569
+ --_soft-ink: var(--slotted-tone-danger-text);
570
+ }
571
+
572
+ /**
573
+ * ── Variant: four paintings ────────────────────────────────────────────
574
+ *
575
+ * `solid` is the tone's fill with its own ink and no rim. `soft` is the
576
+ * tone's tinted ground; its rim is that ground mixed half-way to the subtle
577
+ * rule, which is next to invisible in `light` and `dark` and a grey line in
578
+ * `high-contrast`, where every tone surface is the page's white and a bubble
579
+ * without an edge is a paragraph. `outline` is the tone's edge around the
580
+ * page's ground, `plain` no box at all — the unframed assistant answer that
581
+ * takes the whole row.
582
+ */
583
+ [data-ui='message'][data-part='bubble'] > [data-part='content'] {
584
+ --_ground: var(--_surface);
585
+ --_ink: var(--_soft-ink);
586
+ --_edge: color-mix(in oklab, var(--_surface), var(--slotted-border-subtle));
587
+
588
+ display: block;
589
+ box-sizing: border-box;
590
+ min-inline-size: 0;
591
+ max-inline-size: 100%;
592
+ margin: 0;
593
+ padding-block: var(--_pad-block, 8px);
594
+ padding-inline: var(--_pad-inline, 12px);
595
+ border: var(--slotted-border-width-thin, 1px) solid var(--_edge);
596
+ border-start-start-radius: var(--_top-side);
597
+ border-start-end-radius: var(--_radius);
598
+ border-end-start-radius: var(--_bottom-side);
599
+ border-end-end-radius: var(--_radius);
600
+ background-color: var(--_ground);
601
+ color: var(--_ink);
602
+ font-family: inherit;
603
+ font-size: var(--_font-size);
604
+ font-weight: var(--slotted-font-weight-regular, 400);
605
+ line-height: var(--_leading);
606
+ text-align: start;
607
+ text-decoration: none;
608
+ /**
609
+ * A URL or a hash of three hundred characters breaks where it must rather
610
+ * than pushing the bubble past its share, and each paragraph takes the
611
+ * direction of its own first strong letter: Arabic in an English thread
612
+ * reads from its own side.
613
+ */
614
+ overflow-wrap: anywhere;
615
+ unicode-bidi: plaintext;
616
+ }
617
+
618
+ [data-ui='message'][data-part='bubble'][data-variant='solid'] > [data-part='content'] {
619
+ --_ground: var(--_solid);
620
+ --_ink: var(--_on-solid);
621
+ --_edge: var(--_solid);
622
+ }
623
+
624
+ [data-ui='message'][data-part='bubble'][data-variant='outline'] > [data-part='content'] {
625
+ --_ground: var(--slotted-surface);
626
+ --_ink: var(--slotted-text);
627
+ --_edge: var(--_border);
628
+ }
629
+
630
+ [data-ui='message'][data-part='bubble'][data-variant='plain'] {
631
+ --_pad-inline: 0px;
632
+
633
+ max-inline-size: 100%;
634
+ }
635
+
636
+ [data-ui='message'][data-part='bubble'][data-variant='plain'] > [data-part='content'] {
637
+ --_ground: transparent;
638
+ --_ink: var(--slotted-text);
639
+ --_edge: transparent;
640
+
641
+ padding: 0;
642
+ border-width: 0;
643
+ border-radius: 0;
644
+ }
645
+
646
+ /**
647
+ * The bubble sits on the sender's side, and so does everything it carries:
648
+ * the corners that join a run are the ones that face the avatar.
649
+ */
650
+ [data-ui='message'][data-part='root'][data-align='end'] [data-part='bubble'] {
651
+ align-items: flex-end;
652
+ }
653
+
654
+ [data-ui='message'][data-part='root'][data-align='end']
655
+ [data-part='bubble']
656
+ > [data-part='content'] {
657
+ border-start-start-radius: var(--_radius);
658
+ border-start-end-radius: var(--_top-side);
659
+ border-end-start-radius: var(--_radius);
660
+ border-end-end-radius: var(--_bottom-side);
661
+ }
662
+
663
+ /**
664
+ * ── The corners of a run ───────────────────────────────────────────────
665
+ *
666
+ * Bubbles that follow one another from one sender draw in the corners that
667
+ * face each other on the sender's side, inside one row and across the rows
668
+ * of a group. A run reads as one speaker by its shape, with no position
669
+ * prop on each message (Chatscope asks for `first`, `normal`, `last`).
670
+ */
671
+ [data-ui='message'][data-part='bubble']:has(~ [data-part='bubble']),
672
+ [data-ui='message'][data-part='group']
673
+ > [data-ui='message'][data-part='root']:not(:last-child)
674
+ > [data-part='body']
675
+ > [data-part='bubble']:not(:has(~ [data-part='bubble'])) {
676
+ --_bottom-side: var(--_radius-joined);
677
+ }
678
+
679
+ [data-ui='message'][data-part='bubble'] ~ [data-part='bubble'],
680
+ [data-ui='message'][data-part='group']
681
+ > [data-ui='message'][data-part='root']:not(:first-child)
682
+ > [data-part='body']
683
+ > [data-part='bubble']:not([data-part='bubble'] ~ [data-part='bubble']) {
684
+ --_top-side: var(--_radius-joined);
685
+ }
686
+
687
+ /**
688
+ * ── A bubble that is a control ─────────────────────────────────────────
689
+ *
690
+ * A quick reply is the content itself as a `button`, a way out of the
691
+ * thread the content as an `a`. The pointer's answer is the ink laid over
692
+ * the ground — six and twelve per cent — which answers every painting and
693
+ * every tone with one rule: 0.03 OKLab lightness over the accent fill in
694
+ * `light`, 0.044 under the secondary ground, over the 0.02 step
695
+ * `theme-conformance` asks of a hover, and the press twice that.
696
+ */
697
+ [data-ui='message'][data-part='bubble'] > [data-part='content']:is(button, a) {
698
+ --_ground-hover: color-mix(
699
+ in oklab,
700
+ var(--_ink) var(--slotted-message-state-layer-hover, 6%),
701
+ var(--_ground)
702
+ );
703
+ --_ground-active: color-mix(
704
+ in oklab,
705
+ var(--_ink) var(--slotted-message-state-layer-active, 12%),
706
+ var(--_ground)
707
+ );
708
+
709
+ appearance: none;
710
+ cursor: pointer;
711
+ transition:
712
+ background-color var(--slotted-duration-short) var(--slotted-easing-standard, ease),
713
+ border-color var(--slotted-duration-short) var(--slotted-easing-standard, ease);
714
+ }
715
+
716
+ [data-ui='message'][data-part='bubble'][data-variant='plain']
717
+ > [data-part='content']:is(button, a) {
718
+ padding-block: var(--slotted-space-1, 0.125rem);
719
+ padding-inline: var(--slotted-space-2, 0.25rem);
720
+ margin-inline: calc(var(--slotted-space-2, 0.25rem) * -1);
721
+ border-radius: var(--slotted-radius-sm, 4px);
722
+ }
723
+
724
+ [data-ui='message'][data-part='bubble']
725
+ > [data-part='content']:is(button, a):hover:not(:disabled):not([data-disabled]) {
726
+ background-color: var(--_ground-hover);
727
+ }
728
+
729
+ [data-ui='message'][data-part='bubble']
730
+ > [data-part='content']:is(button, a):active:not(:disabled):not([data-disabled]) {
731
+ background-color: var(--_ground-active);
732
+ }
733
+
734
+ [data-ui='message'][data-part='bubble'] > [data-part='content']:is(button, a):focus-visible {
735
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color);
736
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
737
+ }
738
+
739
+ /**
740
+ * ── Meta: the hour and the ticks inside the bubble ────────────────────
741
+ *
742
+ * The hour and the delivery mark ride at the end of the last line, inside
743
+ * the surface, in the bubble's own ink a step back: a float at the end of
744
+ * the words, so it shares the last line when there is room and takes a line
745
+ * of its own, still at the end, when there is not. The bubble never grows a
746
+ * caption under itself for a fact this small.
747
+ *
748
+ * The box is the height of the words' line and its contents stand on that
749
+ * line's centre, a pixel down, where the twelve-pixel digits share the
750
+ * fourteen-pixel words' baseline to within a pixel and the mark sits on the
751
+ * same axis as the digits. WhatsApp hangs its meta below the baseline; set
752
+ * beside a line of text, the hanging meta read as misaligned. Read is the one state that changes the ink: full strength on a
753
+ * solid fill, the accent on a tinted one; the other marks differ by shape.
754
+ */
755
+ [data-ui='message'][data-part='bubble'] [data-part='meta'] {
756
+ /*
757
+ * Ninety per cent of the bubble's ink: the lowest share at which the hour
758
+ * keeps 4.5:1 on every fill — 4.50 on the accent in `dark`, 5.74 in
759
+ * `light`, 8.39 in `high-contrast` against its 7:1 — measured in sRGB.
760
+ * Two thirds, the first choice, left it at 3.24 on the dark accent.
761
+ */
762
+ --_meta-ink: color-mix(in srgb, currentColor 90%, transparent);
763
+ --_read-ink: var(--slotted-tone-accent-text);
764
+
765
+ display: inline-flex;
766
+ float: inline-end;
767
+ align-items: center;
768
+ gap: var(--slotted-space-1, 0.125rem);
769
+ /* The line rounded down to an even length, so a fourteen-pixel mark and a fourteen-pixel line of digits centre on whole pixels. */
770
+ block-size: round(down, var(--_leading), 2px);
771
+ margin-inline-start: var(--slotted-space-4, 0.5rem);
772
+ color: var(--_meta-ink);
773
+ font-size: var(--slotted-font-size-xs, 0.75rem);
774
+ font-weight: var(--slotted-font-weight-regular, 400);
775
+ line-height: var(--slotted-control-icon-size-sm, 14px);
776
+ font-variant-numeric: tabular-nums;
777
+ white-space: nowrap;
778
+ translate: 0 1px;
779
+ }
780
+
781
+ [data-ui='message'][data-part='bubble'][data-variant='solid'] [data-part='meta'] {
782
+ --_read-ink: var(--_on-solid);
783
+ }
784
+
785
+ [data-ui='message'][data-part='bubble'] [data-part='meta'] [data-part='receipt'] {
786
+ display: inline-grid;
787
+ place-items: center;
788
+ }
789
+
790
+ /**
791
+ * The mark is the small control glyph, the height of the digits' line, so
792
+ * the two share one box and one centre; two ticks at twelve pixels were two
793
+ * scratches.
794
+ */
795
+ [data-ui='message'][data-part='bubble']
796
+ [data-part='meta']
797
+ [data-part='receipt']
798
+ > [data-ui='icon'] {
799
+ --slotted-icon-size: var(--slotted-control-icon-size-sm, 14px);
800
+ }
801
+
802
+ [data-ui='message'][data-part='bubble']
803
+ [data-part='meta']
804
+ [data-part='receipt']
805
+ > span:not([data-ui]) {
806
+ position: absolute;
807
+ inline-size: 1px;
808
+ block-size: 1px;
809
+ overflow: hidden;
810
+ clip-path: inset(50%);
811
+ white-space: nowrap;
812
+ }
813
+
814
+ [data-ui='message'][data-part='bubble']
815
+ [data-part='meta']
816
+ [data-part='receipt'][data-status='read'] {
817
+ color: var(--_read-ink);
818
+ }
819
+
820
+ /** A clock that breathes while the message is on its way. */
821
+ [data-ui='message'][data-part='bubble']
822
+ [data-part='meta']
823
+ [data-part='receipt'][data-status='sending']
824
+ > [data-ui='icon'] {
825
+ animation: slotted-message-pending var(--slotted-duration-loop-slow)
826
+ var(--slotted-easing-standard, ease) infinite alternate;
827
+ }
828
+
829
+ @keyframes slotted-message-pending {
830
+ from {
831
+ opacity: 1;
832
+ }
833
+
834
+ to {
835
+ opacity: 0.4;
836
+ }
837
+ }
838
+
839
+ /** The meta of an unframed reply has no surface to ride in; it is the footer's. */
840
+ [data-ui='message'][data-part='bubble'][data-variant='plain'] [data-part='meta'] {
841
+ display: none;
842
+ }
843
+
844
+ /* ── Clamp ──────────────────────────────────────────────────────────────
845
+ *
846
+ * A long bubble shows its first lines and the rest stays in the DOM — for
847
+ * copying, for finding in the page and for a screen reader — behind a line
848
+ * clamp. `more` shows it whole; the binding draws `more` only while the
849
+ * text overflows.
850
+ *
851
+ * The reveal snaps. A line clamp is not an interpolable length, and the
852
+ * alternative — a measured height transitioned through script — makes the
853
+ * text jump twice when a reply streaming above it reflows the column. The
854
+ * chevron turning is the motion that answers the press.
855
+ */
856
+ [data-ui='message'][data-part='bubble'][data-clamped]:not([data-revealed])
857
+ > [data-part='content'] {
858
+ display: -webkit-box;
859
+ -webkit-box-orient: vertical;
860
+ -webkit-line-clamp: var(--slotted-message-max-lines, 6);
861
+ /**
862
+ * The clamp stops painting after the last line it keeps, but a box clips
863
+ * its overflow at the padding edge, and the inset under that line painted
864
+ * the top of the next one. Clipping at the content edge keeps the inset
865
+ * clean; the ellipsis is still the clamp's.
866
+ */
867
+ overflow: clip;
868
+ overflow-clip-margin: content-box;
869
+ }
870
+
871
+ [data-ui='message'][data-part='bubble'] > [data-part='more'] {
872
+ display: inline-flex;
873
+ align-items: center;
874
+ gap: var(--slotted-space-1, 0.125rem);
875
+ min-block-size: var(--slotted-target-min, 24px);
876
+ margin: 0;
877
+ padding-block: 0;
878
+ padding-inline: var(--_pad-inline, 12px);
879
+ border: 0;
880
+ border-radius: var(--slotted-control-radius, 4px);
881
+ background-color: transparent;
882
+ color: var(--slotted-text-muted);
883
+ font: inherit;
884
+ font-size: var(--slotted-font-size-xs, 0.75rem);
885
+ font-weight: var(--slotted-font-weight-medium, 500);
886
+ transition: color var(--slotted-duration-short) var(--slotted-easing-standard, ease);
887
+ }
888
+
889
+ [data-ui='message'][data-part='bubble'] > [data-part='more'] > [data-ui='icon'] {
890
+ --slotted-icon-size: var(--slotted-control-icon-size-sm, 14px);
891
+
892
+ transition: rotate var(--slotted-duration-medium) var(--slotted-easing-standard, ease);
893
+ }
894
+
895
+ [data-ui='message'][data-part='bubble'][data-revealed] > [data-part='more'] > [data-ui='icon'] {
896
+ rotate: 180deg;
897
+ }
898
+
899
+ [data-ui='message'][data-part='bubble'] > [data-part='more']:hover:not(:disabled) {
900
+ color: var(--slotted-text);
901
+ }
902
+
903
+ [data-ui='message'][data-part='bubble'] > [data-part='more']:active:not(:disabled) {
904
+ color: var(--slotted-tone-accent-text);
905
+ }
906
+
907
+ [data-ui='message'][data-part='bubble'] > [data-part='more']:focus-visible {
908
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color);
909
+ outline-offset: calc(var(--slotted-focus-ring-offset, 2px) * -1);
910
+ }
911
+
912
+ /* ── Quote ──────────────────────────────────────────────────────────────
913
+ *
914
+ * The message a reply answers, at the head of the surface: a block tinted
915
+ * with the surface's own ink, the author over two lines of the words. No
916
+ * rail on its start edge — a tinted block reads as quoted in every painting,
917
+ * the accent fill included, where a rail in the ink would be the only
918
+ * saturated stroke in the thread.
919
+ */
920
+ [data-ui='message'][data-part='bubble'] [data-part='quote'] {
921
+ display: block;
922
+ box-sizing: border-box;
923
+ inline-size: 100%;
924
+ margin: 0;
925
+ margin-block-end: var(--slotted-space-3, 0.375rem);
926
+ padding-block: var(--slotted-space-2, 0.25rem);
927
+ padding-inline: var(--slotted-space-4, 0.5rem);
928
+ border: 0;
929
+ border-radius: calc(var(--_radius) - var(--_pad-block, 8px));
930
+ background-color: color-mix(in oklab, currentColor 8%, transparent);
931
+ color: inherit;
932
+ font: inherit;
933
+ font-size: var(--slotted-control-font-size-md, 13px);
934
+ line-height: round(up, calc(1em * var(--slotted-line-height-md, 1.5)), 1px);
935
+ text-align: start;
936
+ unicode-bidi: plaintext;
937
+ overflow: hidden;
938
+ display: -webkit-box;
939
+ -webkit-box-orient: vertical;
940
+ -webkit-line-clamp: 3;
941
+ }
942
+
943
+ [data-ui='message'][data-part='bubble'] [data-part='quote-author'] {
944
+ display: block;
945
+ font-weight: var(--slotted-font-weight-semibold, 600);
946
+ }
947
+
948
+ [data-ui='message'][data-part='bubble'] button[data-part='quote'] {
949
+ cursor: pointer;
950
+ transition: background-color var(--slotted-duration-short) var(--slotted-easing-standard, ease);
951
+ }
952
+
953
+ [data-ui='message'][data-part='bubble'] button[data-part='quote']:hover:not(:disabled) {
954
+ background-color: color-mix(in oklab, currentColor 13%, transparent);
955
+ }
956
+
957
+ [data-ui='message'][data-part='bubble'] button[data-part='quote']:active:not(:disabled) {
958
+ background-color: color-mix(in oklab, currentColor 18%, transparent);
959
+ }
960
+
961
+ [data-ui='message'][data-part='bubble'] button[data-part='quote']:focus-visible {
962
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color);
963
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
964
+ }
965
+
966
+ /* ── Streaming text, caret and typing ──────────────────────────────────
967
+ *
968
+ * A streamed reply enters word by word. `stream` holds the settled words as
969
+ * plain text and each word still entering as a `stream-chunk`, whose delay
970
+ * the binding writes from `@slotted/core/text-stream`: a burst of forty
971
+ * words enters as a wave of at most 320 ms instead of landing as a block.
972
+ * A word comes out of a blur of a sixth of its own size, at nothing, and
973
+ * the ink and the edge arrive together: a word that is opaque while still
974
+ * blurred reads as a smudge, and five of them in a row as a smeared line.
975
+ *
976
+ * The caret is a dot after the last word, centred on the middle of the
977
+ * x-height, breathing between full and 72 % while the reply is open. A bar
978
+ * blinking in two steps read as a text field waiting for input; the reply is
979
+ * not waiting for the reader, it is still being written. While words are
980
+ * entering, the wave is the leading edge and the dot steps aside: a word
981
+ * still invisible already holds its place in the line, and a dot after it
982
+ * would stand apart from the last word painted. It returns once the wave has
983
+ * landed and the transport has been quiet for a long transition.
984
+ *
985
+ * Under reduced motion the binding settles every word on arrival, the caret
986
+ * stands lit and the typing dots stand at three strengths, so the state is
987
+ * still drawn.
988
+ */
989
+ [data-ui='message'][data-part='bubble'] [data-part='stream'] {
990
+ white-space: pre-wrap;
991
+ }
992
+
993
+ [data-ui='message'][data-part='bubble'] [data-part='stream-chunk'] {
994
+ animation: slotted-message-word var(--slotted-duration-long) var(--slotted-easing-standard) both;
995
+ }
996
+
997
+ @keyframes slotted-message-word {
998
+ from {
999
+ opacity: 0;
1000
+ filter: blur(0.16em);
1001
+ }
1002
+
1003
+ to {
1004
+ opacity: 1;
1005
+ filter: blur(0);
1006
+ }
1007
+ }
1008
+
1009
+ [data-ui='message'][data-part='bubble'] [data-part='caret'] {
1010
+ --_dot: round(nearest, 0.6em, 2px);
1011
+
1012
+ display: inline-block;
1013
+ inline-size: var(--_dot);
1014
+ block-size: var(--_dot);
1015
+ /* A streamed word carries its own space; the margin only parts the dot from a word cut mid-way. */
1016
+ margin-inline-start: var(--slotted-space-1, 0.125rem);
1017
+ border-radius: var(--slotted-radius-full, 9999px);
1018
+ background-color: currentColor;
1019
+ vertical-align: middle;
1020
+ transition: filter var(--slotted-duration-medium) var(--slotted-easing-enter)
1021
+ var(--slotted-duration-long);
1022
+ animation:
1023
+ slotted-message-dot-in var(--slotted-duration-medium) var(--slotted-easing-enter),
1024
+ slotted-message-breathe var(--slotted-duration-loop) var(--slotted-easing-standard)
1025
+ var(--slotted-duration-medium) infinite alternate;
1026
+ }
1027
+
1028
+ /* `filter`, not `opacity`: the breath animates opacity, and an animation outranks the rule. */
1029
+ [data-ui='message'][data-part='bubble']
1030
+ [data-part='stream']:has(> [data-part='stream-chunk'])
1031
+ + [data-part='caret'] {
1032
+ filter: opacity(0);
1033
+ transition: filter var(--slotted-duration-short) var(--slotted-easing-exit);
1034
+ }
1035
+
1036
+ @keyframes slotted-message-dot-in {
1037
+ from {
1038
+ opacity: 0;
1039
+ scale: 0;
1040
+ }
1041
+ }
1042
+
1043
+ /* 0.72 of 8 px is 5.76: the breath is seen, and the dot never shrinks to a full stop's 2 px. */
1044
+ @keyframes slotted-message-breathe {
1045
+ from {
1046
+ opacity: 1;
1047
+ scale: 1;
1048
+ }
1049
+
1050
+ to {
1051
+ opacity: 0.5;
1052
+ scale: 0.72;
1053
+ }
1054
+ }
1055
+
1056
+ /**
1057
+ * Three dots a third of the line tall — seven pixels at comfortable — that
1058
+ * rise and brighten in a wave, each a sixth of the loop behind the one
1059
+ * before, in the bubble of the person writing. The wave travels half a dot:
1060
+ * enough to read as a voice, not so much that the bubble seems to bounce.
1061
+ */
1062
+ [data-ui='message'][data-part='bubble'] [data-part='typing'] {
1063
+ --_dot: round(calc(var(--_leading) / 3), 1px);
1064
+
1065
+ display: inline-flex;
1066
+ align-items: center;
1067
+ gap: calc(var(--_dot) * 0.6);
1068
+ block-size: var(--_leading);
1069
+ padding-inline: var(--slotted-space-1, 0.125rem);
1070
+ vertical-align: top;
1071
+ }
1072
+
1073
+ /**
1074
+ * The bubble of someone writing trails two small circles from its foot on
1075
+ * the sender's side, the way a thought is drawn, and arrives from them: it
1076
+ * rises and fades in on the enter curve, and its trail follows one step
1077
+ * behind. The circles are the surface's own fill and edge, so they answer
1078
+ * every painting and every scheme with the bubble.
1079
+ */
1080
+ [data-ui='message'][data-part='bubble'] > [data-part='content']:has(> [data-part='typing']) {
1081
+ position: relative;
1082
+ padding-inline: calc(var(--_pad-inline) + var(--slotted-space-1, 0.125rem));
1083
+ transition:
1084
+ opacity var(--slotted-duration-medium) var(--slotted-easing-enter, ease-out),
1085
+ translate var(--slotted-duration-medium) var(--slotted-easing-enter, ease-out);
1086
+
1087
+ @starting-style {
1088
+ opacity: 0;
1089
+ translate: 0 var(--slotted-space-3, 0.375rem);
1090
+ }
1091
+ }
1092
+
1093
+ [data-ui='message'][data-part='bubble']
1094
+ > [data-part='content']:has(> [data-part='typing'])::before,
1095
+ [data-ui='message'][data-part='bubble']
1096
+ > [data-part='content']:has(> [data-part='typing'])::after {
1097
+ content: '';
1098
+ position: absolute;
1099
+ box-sizing: border-box;
1100
+ border: inherit;
1101
+ border-radius: var(--slotted-radius-full, 9999px);
1102
+ background-color: inherit;
1103
+ animation: slotted-message-trail var(--slotted-duration-loop-slow)
1104
+ var(--slotted-easing-standard, ease) infinite alternate;
1105
+ }
1106
+
1107
+ [data-ui='message'][data-part='bubble']
1108
+ > [data-part='content']:has(> [data-part='typing'])::before {
1109
+ inset-block-end: calc(var(--slotted-space-1, 0.125rem) * -1);
1110
+ inset-inline-start: calc(var(--slotted-space-1, 0.125rem) * -1);
1111
+ inline-size: var(--slotted-space-4, 0.5rem);
1112
+ block-size: var(--slotted-space-4, 0.5rem);
1113
+ }
1114
+
1115
+ [data-ui='message'][data-part='bubble']
1116
+ > [data-part='content']:has(> [data-part='typing'])::after {
1117
+ inset-block-end: calc(var(--slotted-space-3, 0.375rem) * -1);
1118
+ inset-inline-start: calc(var(--slotted-space-3, 0.375rem) * -1);
1119
+ inline-size: var(--slotted-space-2, 0.25rem);
1120
+ block-size: var(--slotted-space-2, 0.25rem);
1121
+ animation-delay: calc(var(--slotted-duration-loop-slow) / -2);
1122
+ }
1123
+
1124
+ /** The line under a writing bubble clears the trail. */
1125
+ [data-ui='message'][data-part='root']
1126
+ > [data-part='body']:has(> [data-part='bubble'] [data-part='typing'])
1127
+ > [data-part='footer'] {
1128
+ margin-block-start: var(--slotted-space-2, 0.25rem);
1129
+ }
1130
+
1131
+ @keyframes slotted-message-trail {
1132
+ from {
1133
+ opacity: 1;
1134
+ }
1135
+
1136
+ to {
1137
+ opacity: 0.55;
1138
+ }
1139
+ }
1140
+
1141
+ [data-ui='message'][data-part='bubble'] [data-part='typing'] > span {
1142
+ inline-size: var(--_dot);
1143
+ block-size: var(--_dot);
1144
+ border-radius: var(--slotted-radius-full, 9999px);
1145
+ background-color: currentColor;
1146
+ opacity: 0.3;
1147
+ animation: slotted-message-typing var(--slotted-duration-loop)
1148
+ var(--slotted-easing-standard, ease) infinite;
1149
+ }
1150
+
1151
+ [data-ui='message'][data-part='bubble'] [data-part='typing'] > span:nth-child(2) {
1152
+ animation-delay: calc(var(--slotted-duration-loop) / 6);
1153
+ }
1154
+
1155
+ [data-ui='message'][data-part='bubble'] [data-part='typing'] > span:nth-child(3) {
1156
+ animation-delay: calc(var(--slotted-duration-loop) / 3);
1157
+ }
1158
+
1159
+ @keyframes slotted-message-typing {
1160
+ 0%,
1161
+ 55%,
1162
+ 100% {
1163
+ opacity: 0.3;
1164
+ translate: 0 0;
1165
+ }
1166
+
1167
+ 25% {
1168
+ opacity: 0.85;
1169
+ translate: 0 calc(var(--_dot) * -0.5);
1170
+ }
1171
+ }
1172
+
1173
+ /**
1174
+ * ── Retry ──────────────────────────────────────────────────────────────
1175
+ *
1176
+ * A message that did not go carries a round control beside its surface, on
1177
+ * the side away from the sender, in the danger tone, centred on the last
1178
+ * line. It is the way to send it again, and its arrow turns a little under
1179
+ * the pointer toward the way it will go.
1180
+ */
1181
+ [data-ui='message'][data-part='bubble'] > [data-part='retry'] {
1182
+ --_size: max(var(--slotted-target-min, 24px), var(--slotted-control-inner-size-md, 24px));
1183
+ --_ground: var(--slotted-tone-danger-solid);
1184
+
1185
+ display: inline-grid;
1186
+ place-items: center;
1187
+ position: absolute;
1188
+ inset-block-end: calc(
1189
+ (
1190
+ var(--_leading) + 2 * var(--_pad-block) + 2 * var(--slotted-border-width-thin, 1px) -
1191
+ var(--_size)
1192
+ ) /
1193
+ 2
1194
+ );
1195
+ inset-inline-start: calc(100% + var(--slotted-space-4, 0.5rem));
1196
+ box-sizing: border-box;
1197
+ inline-size: var(--_size);
1198
+ block-size: var(--_size);
1199
+ margin: 0;
1200
+ padding: 0;
1201
+ border: 0;
1202
+ border-radius: var(--slotted-radius-full, 9999px);
1203
+ background-color: var(--_ground);
1204
+ color: var(--slotted-tone-danger-on-solid);
1205
+ cursor: pointer;
1206
+ transition: background-color var(--slotted-duration-short) var(--slotted-easing-standard, ease);
1207
+ }
1208
+
1209
+ [data-ui='message'][data-part='root'][data-align='end']
1210
+ [data-part='bubble']
1211
+ > [data-part='retry'] {
1212
+ inset-inline: auto calc(100% + var(--slotted-space-4, 0.5rem));
1213
+ }
1214
+
1215
+ [data-ui='message'][data-part='bubble'] > [data-part='retry'] > [data-ui='icon'] {
1216
+ --slotted-icon-size: var(--slotted-control-icon-size-sm, 14px);
1217
+
1218
+ transition: rotate var(--slotted-duration-medium) var(--slotted-easing-emphasized, ease);
1219
+ }
1220
+
1221
+ /**
1222
+ * The tone's own hover step sits 0.01 OKLab lightness from its fill, under
1223
+ * the 0.02 `theme-conformance` asks; the control's ink laid over the fill at
1224
+ * fourteen per cent answers visibly in every scheme.
1225
+ */
1226
+ [data-ui='message'][data-part='bubble']
1227
+ > [data-part='retry']:hover:not(:disabled):not([data-disabled]) {
1228
+ background-color: color-mix(
1229
+ in oklab,
1230
+ var(--slotted-tone-danger-on-solid) var(--slotted-message-retry-state-layer, 14%),
1231
+ var(--_ground)
1232
+ );
1233
+ }
1234
+
1235
+ [data-ui='message'][data-part='bubble']
1236
+ > [data-part='retry']:hover:not(:disabled):not([data-disabled])
1237
+ > [data-ui='icon'] {
1238
+ rotate: -45deg;
1239
+ }
1240
+
1241
+ [data-ui='message'][data-part='bubble']
1242
+ > [data-part='retry']:active:not(:disabled):not([data-disabled]) {
1243
+ background-color: var(--slotted-tone-danger-solid-active);
1244
+ }
1245
+
1246
+ [data-ui='message'][data-part='bubble'] > [data-part='retry']:focus-visible {
1247
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color);
1248
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
1249
+ }
1250
+
1251
+ @media (prefers-reduced-motion: reduce) {
1252
+ [data-ui='message'][data-part='bubble'] [data-part='caret'],
1253
+ [data-ui='message'][data-part='bubble'] [data-part='stream-chunk'],
1254
+ [data-ui='message'][data-part='bubble'] [data-part='typing'] > span,
1255
+ [data-ui='message'][data-part='bubble']
1256
+ > [data-part='content']:has(> [data-part='typing'])::before,
1257
+ [data-ui='message'][data-part='bubble']
1258
+ > [data-part='content']:has(> [data-part='typing'])::after,
1259
+ [data-ui='message'][data-part='bubble']
1260
+ [data-part='meta']
1261
+ [data-part='receipt'][data-status='sending']
1262
+ > [data-ui='icon'] {
1263
+ animation: none;
1264
+ }
1265
+
1266
+ [data-ui='message'][data-part='root'] [data-part='activity-step'][data-current] > :last-child {
1267
+ animation: none;
1268
+ background-image: none;
1269
+ color: inherit;
1270
+ }
1271
+
1272
+ [data-ui='message'][data-part='bubble'] [data-part='typing'] > span:nth-child(2) {
1273
+ opacity: 0.55;
1274
+ }
1275
+
1276
+ [data-ui='message'][data-part='bubble'] [data-part='typing'] > span:nth-child(3) {
1277
+ opacity: 0.8;
1278
+ }
1279
+ }
1280
+
1281
+ /* ── Reactions ──────────────────────────────────────────────────────────
1282
+ *
1283
+ * A raised pill straddling the lower edge of the surface on the side away
1284
+ * from the sender. It rises into the surface by half the surface's own
1285
+ * inset, clear of the descenders of the last line; the bubble keeps the rest as room
1286
+ * below itself, so the pill never lands on the next message. ReUI hangs
1287
+ * the row three quarters out and its example needs a 32-pixel gap to clear
1288
+ * it (`c-bubble-4`). The pill draws its own edge and lift rather than a
1289
+ * ring in the colour of a card it assumes it is on.
1290
+ */
1291
+ [data-ui='message'][data-part='bubble']:has(> [data-part='reactions']) {
1292
+ padding-block-end: calc(var(--_reactions-size) - var(--_pad-block, 8px) / 2);
1293
+ }
1294
+
1295
+ [data-ui='message'][data-part='bubble'] > [data-part='reactions'] {
1296
+ display: inline-flex;
1297
+ align-items: center;
1298
+ gap: var(--slotted-space-1, 0.125rem);
1299
+ position: absolute;
1300
+ inset-block-end: 0;
1301
+ inset-inline-end: var(--_pad-inline, 12px);
1302
+ padding: var(--slotted-space-1, 0.125rem);
1303
+ border: var(--slotted-border-width-thin, 1px) solid var(--slotted-border-subtle);
1304
+ border-radius: var(--slotted-radius-full, 9999px);
1305
+ background-color: var(--slotted-surface-raised);
1306
+ box-shadow: var(--slotted-shadow-xs);
1307
+ }
1308
+
1309
+ [data-ui='message'][data-part='root'][data-align='end']
1310
+ [data-part='bubble']
1311
+ > [data-part='reactions'] {
1312
+ inset-inline: var(--_pad-inline, 12px) auto;
1313
+ }
1314
+
1315
+ /**
1316
+ * In a thread there is no surface to straddle: the reactions are a row in
1317
+ * the flow under the words, the way Slack and GitHub set them.
1318
+ */
1319
+ [data-ui='message'][data-part='bubble'][data-variant='plain']:has(> [data-part='reactions']) {
1320
+ padding-block-end: 0;
1321
+ }
1322
+
1323
+ [data-ui='message'][data-part='bubble'][data-variant='plain'] > [data-part='reactions'] {
1324
+ position: static;
1325
+ padding: 0;
1326
+ border: 0;
1327
+ background-color: transparent;
1328
+ box-shadow: none;
1329
+ translate: none;
1330
+ }
1331
+
1332
+ [data-ui='message'][data-part='bubble'] [data-part='reaction'] {
1333
+ display: inline-flex;
1334
+ align-items: center;
1335
+ gap: var(--slotted-space-1, 0.125rem);
1336
+ box-sizing: border-box;
1337
+ min-inline-size: var(--_reactions-size);
1338
+ block-size: var(--_reactions-size);
1339
+ margin: 0;
1340
+ padding-block: 0;
1341
+ padding-inline: var(--slotted-space-3, 0.375rem);
1342
+ --_reaction-ground: transparent;
1343
+
1344
+ border: var(--slotted-border-width-thin, 1px) solid transparent;
1345
+ border-radius: var(--slotted-radius-full, 9999px);
1346
+ background-color: var(--_reaction-ground);
1347
+ color: var(--slotted-text);
1348
+ font: inherit;
1349
+ font-size: var(--slotted-font-size-xs, 0.75rem);
1350
+ line-height: 1;
1351
+ transition:
1352
+ background-color var(--slotted-duration-short) var(--slotted-easing-standard, ease),
1353
+ border-color var(--slotted-duration-short) var(--slotted-easing-standard, ease),
1354
+ color var(--slotted-duration-short) var(--slotted-easing-standard, ease);
1355
+ }
1356
+
1357
+ [data-ui='message'][data-part='bubble'][data-variant='plain'] [data-part='reaction'] {
1358
+ --_reaction-ground: var(--slotted-surface);
1359
+
1360
+ border-color: var(--slotted-border-subtle);
1361
+ }
1362
+
1363
+ [data-ui='message'][data-part='bubble'] [data-part='reaction-count'] {
1364
+ color: var(--slotted-text-muted);
1365
+ font-weight: var(--slotted-font-weight-medium, 500);
1366
+ font-variant-numeric: tabular-nums;
1367
+ }
1368
+
1369
+ /**
1370
+ * The pointer answers on whatever ground the reaction stands on — none, the
1371
+ * page, or the accent's tint when it is the reader's own — with the text's
1372
+ * ink laid over it at six and twelve per cent, so a pressed reaction answers
1373
+ * a hover and a press as clearly as a bare one and the two never match.
1374
+ */
1375
+ [data-ui='message'][data-part='bubble']
1376
+ [data-part='reaction']:hover:not(:disabled):not([data-disabled]) {
1377
+ background-color: color-mix(
1378
+ in oklab,
1379
+ var(--slotted-text) var(--slotted-message-state-layer-hover, 6%),
1380
+ var(--_reaction-ground)
1381
+ );
1382
+ }
1383
+
1384
+ [data-ui='message'][data-part='bubble']
1385
+ [data-part='reaction']:active:not(:disabled):not([data-disabled]) {
1386
+ background-color: color-mix(
1387
+ in oklab,
1388
+ var(--slotted-text) var(--slotted-message-state-layer-active, 12%),
1389
+ var(--_reaction-ground)
1390
+ );
1391
+ }
1392
+
1393
+ /**
1394
+ * The reader's own reaction: the accent's tinted ground inside the accent's
1395
+ * edge, and the count in the accent's ink. The pair carries the ratio the
1396
+ * ground alone cannot (`docs/token-foundation.md` › Semantic colour).
1397
+ */
1398
+ [data-ui='message'][data-part='bubble'] [data-part='reaction'][data-pressed] {
1399
+ --_reaction-ground: var(--slotted-tone-accent-surface);
1400
+
1401
+ border-color: var(--slotted-tone-accent-border);
1402
+ }
1403
+
1404
+ [data-ui='message'][data-part='bubble']
1405
+ [data-part='reaction'][data-pressed]
1406
+ > [data-part='reaction-count'] {
1407
+ color: var(--slotted-tone-accent-text);
1408
+ }
1409
+
1410
+ [data-ui='message'][data-part='bubble'] [data-part='reaction']:is(:disabled, [data-disabled]) {
1411
+ color: var(--slotted-disabled-foreground);
1412
+ cursor: default;
1413
+ }
1414
+
1415
+ [data-ui='message'][data-part='bubble'] [data-part='reaction']:focus-visible {
1416
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color);
1417
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
1418
+ }
1419
+
1420
+ /* ── Forced colours ─────────────────────────────────────────────────────
1421
+ *
1422
+ * The system owns the palette: every bubble is a framed box of text, the
1423
+ * reader's own reaction is `Highlight`, and the tints of the quote and the
1424
+ * pointer go, because `color-mix` over system colours is not a colour the
1425
+ * reader chose.
1426
+ */
1427
+ @media (forced-colors: active) {
1428
+ [data-ui='message'][data-part='bubble'] > [data-part='content'] {
1429
+ border-color: CanvasText;
1430
+ }
1431
+
1432
+ /* A background is the system's to repaint; the dot is ink, so it keeps the text's colour. */
1433
+ [data-ui='message'][data-part='bubble'] [data-part='caret'] {
1434
+ forced-color-adjust: none;
1435
+ background-color: CanvasText;
1436
+ }
1437
+
1438
+ [data-ui='message'][data-part='bubble'][data-variant='plain'] > [data-part='content'] {
1439
+ border-color: transparent;
1440
+ }
1441
+
1442
+ [data-ui='message'][data-part='bubble'] [data-part='reaction'][data-pressed] {
1443
+ border-color: Highlight;
1444
+ forced-color-adjust: none;
1445
+ background-color: Highlight;
1446
+ color: HighlightText;
1447
+ }
1448
+
1449
+ [data-ui='message'][data-part='bubble'] [data-part='quote'] {
1450
+ border: var(--slotted-border-width-thin, 1px) solid CanvasText;
1451
+ }
1452
+ }
1453
+ }