@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,1596 @@
1
+ @layer slotted.components {
2
+ /**
3
+ * An ordered list of events on one line, each with a marker, a machine
4
+ * timestamp and a body — and, in feed mode, the APG Feed pattern drawn the
5
+ * same way.
6
+ *
7
+ * The whole family is drawn from the control glyph size. An empty marker is
8
+ * a dot as large as the glyph a badge holds, so a dot, a spinner and the
9
+ * glyph inside a badge are one size; a marker holding a glyph is a badge the
10
+ * control icon wide plus one step of air on each side, and a marker holding
11
+ * an avatar is the smallest avatar. The rail is a
12
+ * column as wide as the widest marker the timeline holds. The connector is a
13
+ * hairline that runs from the centre of one marker to the centre of the
14
+ * next. Everything else — how far the words stand off the rail, the rhythm
15
+ * between events — is the control gap plus a step, so density moves the
16
+ * whole timeline together.
17
+ *
18
+ * Every width a centre depends on — the dot, its core, the badge, the rail —
19
+ * is the line's own width plus an even number of pixels, rounded up to it.
20
+ * The line then stands a whole number of pixels inside each of them. An
21
+ * eight-pixel dot on a one-pixel line put the line three and a half pixels
22
+ * in, the browser rounded the half to a whole CSS pixel, and the line was
23
+ * drawn one device pixel off the dot's centre at 100%, 150% and 200%.
24
+ */
25
+ [data-ui='timeline'] {
26
+ --_icon: var(--slotted-control-icon-size-md, 16px);
27
+ --_font-size: var(--slotted-timeline-font-size, var(--slotted-control-font-size-md, 13px));
28
+ /**
29
+ * The actor, the description, the group heading and the two pills: the
30
+ * title's size, told apart from it by weight and ink. A fixed
31
+ * `--slotted-font-size-sm` stood a description of 13 under a title of 12
32
+ * at `sm` and in the tighter densities, and its 19.5-pixel line put every
33
+ * event after the first at half a pixel.
34
+ */
35
+ --_meta-size: var(--slotted-timeline-meta-font-size, var(--_font-size));
36
+ /* The control gap and one step more: a mark eight pixels from a word reads as touching it. */
37
+ --_gap: var(
38
+ --slotted-timeline-gap,
39
+ calc(var(--slotted-control-gap-md, 8px) + var(--slotted-space-2, 0.25rem))
40
+ );
41
+ /* The glyph a badge holds, and the size of a dot and of a spinner: the glyph less a step of air a side. */
42
+ --_glyph: calc(var(--_icon) - 2 * var(--slotted-space-1, 0.125rem));
43
+ --_dot: calc(var(--_rule) + round(up, var(--_glyph) - var(--_rule), 2px));
44
+ /* The solid centre of a dot, a proportion of it: the literal the contract records. */
45
+ --_core: calc(
46
+ var(--_rule) +
47
+ round(nearest, var(--_dot) * var(--slotted-timeline-core-ratio, 0.4) - var(--_rule), 2px)
48
+ );
49
+ /* The badge around a glyph: the control icon and one step of air on each side, 21 at `md`. */
50
+ --_badge: calc(
51
+ var(--_rule) +
52
+ round(up, var(--_icon) + 2 * var(--slotted-space-1, 0.125rem) - var(--_rule), 2px)
53
+ );
54
+ /**
55
+ * A face in the marker: the smallest avatar, rounded to the line's parity.
56
+ * The badge's 21 put the initials at 8.4 pixels, under
57
+ * `--slotted-avatar-size-xs`, the smallest step `avatar` draws; this is 25
58
+ * at comfortable density and 21 at the two tighter ones, where that step
59
+ * is 20.
60
+ */
61
+ --_avatar: calc(
62
+ var(--_rule) + round(up, var(--slotted-avatar-size-xs, 24px) - var(--_rule), 2px)
63
+ );
64
+ --_rail: var(--_dot);
65
+ --_rule: var(--slotted-timeline-connector-thickness, var(--slotted-border-width-thin, 1px));
66
+ /* The ring of a hollow dot, and of the halo around the current one. */
67
+ --_edge: var(--slotted-border-width-medium, 1.5px);
68
+ /* How far the current ring stands outside its marker. */
69
+ --_reach: calc(var(--_edge) + var(--slotted-border-width-thick, 2px));
70
+ /* The gap and one step more, so the rhythm between events tightens with density. */
71
+ --_item-gap: var(
72
+ --slotted-timeline-item-gap,
73
+ calc(var(--_gap) + var(--slotted-space-2, 0.25rem))
74
+ );
75
+ /* The air the line leaves around each marker, drawn as a halo of the ground. */
76
+ --_line-gap: var(--slotted-timeline-line-gap, var(--slotted-space-1, 0.125rem));
77
+ --_opposite: var(--slotted-timeline-opposite-size, 8rem);
78
+ --_line-height: var(--slotted-timeline-line-height, var(--slotted-line-height-md, 1.5));
79
+ /**
80
+ * The ratio set in whole pixels: 20 at `md` rather than 19.5. Every event
81
+ * is a stack of these lines, and a one-line event of 19.5 put the next
82
+ * event, its marker and its ring at half a pixel.
83
+ */
84
+ --_leading: round(up, calc(var(--_font-size) * var(--_line-height)), 1px);
85
+ --_muted: var(--slotted-timeline-color-muted, var(--slotted-text-muted, GrayText));
86
+ --_strong: var(--slotted-timeline-color, var(--slotted-text, CanvasText));
87
+ /**
88
+ * The track is decoration and may be faint; the marker's ring is the
89
+ * frame of the mark itself and holds the 3:1 a non-text element needs —
90
+ * the split `stepper` records for the same two parts.
91
+ */
92
+ --_line: var(--slotted-timeline-connector-color, var(--slotted-border-subtle, GrayText));
93
+ --_ring: var(
94
+ --slotted-timeline-marker-border,
95
+ var(--slotted-control-border-color, ButtonBorder)
96
+ );
97
+ --_surface: var(--slotted-timeline-marker-surface, var(--slotted-surface, Canvas));
98
+
99
+ color: var(--_strong);
100
+ container-name: slotted-timeline;
101
+ container-type: inline-size;
102
+ display: block;
103
+ font-family: var(--slotted-timeline-font-family, var(--slotted-font-family-sans, inherit));
104
+ font-size: var(--_font-size);
105
+ line-height: var(--_leading);
106
+ }
107
+
108
+ [data-ui='timeline'][data-size='sm'] {
109
+ --_icon: var(--slotted-control-icon-size-sm, 14px);
110
+ --_font-size: var(--slotted-timeline-font-size, var(--slotted-control-font-size-sm, 12px));
111
+ --_gap: var(
112
+ --slotted-timeline-gap,
113
+ calc(var(--slotted-control-gap-sm, 6px) + var(--slotted-space-2, 0.25rem))
114
+ );
115
+ }
116
+
117
+ [data-ui='timeline'][data-size='lg'] {
118
+ --_icon: var(--slotted-control-icon-size-lg, 18px);
119
+ --_font-size: var(--slotted-timeline-font-size, var(--slotted-control-font-size-lg, 14px));
120
+ --_gap: var(
121
+ --slotted-timeline-gap,
122
+ calc(var(--slotted-control-gap-lg, 8px) + var(--slotted-space-2, 0.25rem))
123
+ );
124
+ }
125
+
126
+ /**
127
+ * One badge anywhere widens the rail everywhere, groups included, so the
128
+ * dots of the plain events stay on the same centre as the badge and every
129
+ * title starts at the same indent. A spinner and the check of a finished
130
+ * dot are a dot's size and widen nothing.
131
+ */
132
+ [data-ui='timeline']:has([data-part='marker'] > :not([data-ui='spinner'], [data-check])) {
133
+ --_rail: var(--_badge);
134
+ }
135
+
136
+ [data-ui='timeline']:has([data-part='marker'] > [data-ui='avatar']) {
137
+ --_rail: max(var(--_badge), var(--_avatar));
138
+ }
139
+
140
+ /* ── Tone ───────────────────────────────────────────────────────────────
141
+ *
142
+ * Five tones, each read on the item so one event can be a failure in a list
143
+ * of successes. The root states the tone every item starts from, and an
144
+ * item that names its own wins by being nearer.
145
+ */
146
+
147
+ [data-ui='timeline'],
148
+ [data-ui='timeline'] [data-tone='secondary'] {
149
+ --_tone-solid: var(--slotted-tone-secondary-solid, ButtonFace);
150
+ --_tone-on-solid: var(--slotted-tone-secondary-on-solid, ButtonText);
151
+ --_tone-surface: var(--slotted-tone-secondary-surface, Canvas);
152
+ --_tone-border: var(--slotted-tone-secondary-border, ButtonBorder);
153
+ --_tone-text: var(--slotted-tone-secondary-text, CanvasText);
154
+ /**
155
+ * The ink of a dot. A tone's border step, which holds 3:1 on the page;
156
+ * for `secondary` the theme's strong border, because the secondary fill
157
+ * is a ground and a grey dot in it vanishes on white.
158
+ */
159
+ --_tone-mark: var(--slotted-border-strong, GrayText);
160
+ }
161
+
162
+ [data-ui='timeline'][data-tone='accent'],
163
+ [data-ui='timeline'] [data-tone='accent'] {
164
+ --_tone-solid: var(--slotted-tone-accent-solid, Highlight);
165
+ --_tone-on-solid: var(--slotted-tone-accent-on-solid, HighlightText);
166
+ --_tone-surface: var(--slotted-tone-accent-surface, Canvas);
167
+ --_tone-border: var(--slotted-tone-accent-border, Highlight);
168
+ --_tone-mark: var(--slotted-tone-accent-border, Highlight);
169
+ --_tone-text: var(--slotted-tone-accent-text, Highlight);
170
+ }
171
+
172
+ [data-ui='timeline'][data-tone='success'],
173
+ [data-ui='timeline'] [data-tone='success'] {
174
+ --_tone-solid: var(--slotted-tone-success-solid, ButtonFace);
175
+ --_tone-on-solid: var(--slotted-tone-success-on-solid, ButtonText);
176
+ --_tone-surface: var(--slotted-tone-success-surface, Canvas);
177
+ --_tone-border: var(--slotted-tone-success-border, ButtonBorder);
178
+ --_tone-mark: var(--slotted-tone-success-border, ButtonBorder);
179
+ --_tone-text: var(--slotted-tone-success-text, CanvasText);
180
+ }
181
+
182
+ [data-ui='timeline'][data-tone='warning'],
183
+ [data-ui='timeline'] [data-tone='warning'] {
184
+ --_tone-solid: var(--slotted-tone-warning-solid, ButtonFace);
185
+ --_tone-on-solid: var(--slotted-tone-warning-on-solid, ButtonText);
186
+ --_tone-surface: var(--slotted-tone-warning-surface, Canvas);
187
+ --_tone-border: var(--slotted-tone-warning-border, ButtonBorder);
188
+ --_tone-mark: var(--slotted-tone-warning-border, ButtonBorder);
189
+ --_tone-text: var(--slotted-tone-warning-text, CanvasText);
190
+ }
191
+
192
+ [data-ui='timeline'][data-tone='danger'],
193
+ [data-ui='timeline'] [data-tone='danger'] {
194
+ --_tone-solid: var(--slotted-tone-danger-solid, Mark);
195
+ --_tone-on-solid: var(--slotted-tone-danger-on-solid, MarkText);
196
+ --_tone-surface: var(--slotted-tone-danger-surface, Canvas);
197
+ --_tone-border: var(--slotted-tone-danger-border, Mark);
198
+ --_tone-mark: var(--slotted-tone-danger-border, Mark);
199
+ --_tone-text: var(--slotted-tone-danger-text, MarkText);
200
+ }
201
+
202
+ /* ── The list ───────────────────────────────────────────────────────────
203
+ *
204
+ * An `ol`, or a `div[role="feed"]`; both are painted the same, because what
205
+ * changes between them is what a reader hears, not what a timeline looks
206
+ * like. A group nests a second list of the same kind.
207
+ */
208
+
209
+ [data-ui='timeline'] :is([data-part='list'], [data-part='group-list']) {
210
+ display: flex;
211
+ flex-direction: column;
212
+ list-style: none;
213
+ margin: 0;
214
+ padding: 0;
215
+ }
216
+
217
+ /**
218
+ * The opposite column exists when any item in the list projects one. It is
219
+ * read from the list rather than declared, so a consumer who writes the part
220
+ * on one event gets the column on every event and the titles keep lining up.
221
+ */
222
+ [data-ui='timeline'] [data-part='list']:has([data-part='opposite']) {
223
+ --_opposite-column: var(--_opposite);
224
+ }
225
+
226
+ /* ── The item ───────────────────────────────────────────────────────────
227
+ *
228
+ * Three columns: the opposite side, the rail, and the body. The rail is
229
+ * stretched to the row so the line can run its whole height; the body and
230
+ * the opposite side share a baseline so a timestamp set smaller than the
231
+ * title still sits on the title's first line.
232
+ */
233
+
234
+ [data-ui='timeline'] [data-part='item'] {
235
+ align-items: stretch;
236
+ column-gap: var(--_gap);
237
+ display: grid;
238
+ grid-template-columns:
239
+ [opposite] var(--_opposite-column, 0)
240
+ [rail] var(--_rail)
241
+ [content] minmax(0, 1fr);
242
+ position: relative;
243
+ }
244
+
245
+ [data-ui='timeline']
246
+ :where([data-part='list']:not(:has([data-part='opposite'])))
247
+ [data-part='item'] {
248
+ grid-template-columns: [rail] var(--_rail) [content] minmax(0, 1fr);
249
+ }
250
+
251
+ /**
252
+ * The space between two events is the item's margin, so a focus ring drawn
253
+ * around an article wraps the event and not the air under it; the connector
254
+ * runs on through the margin to the next marker.
255
+ */
256
+ [data-ui='timeline'] [data-part='item'] {
257
+ margin-block-end: var(--_item-gap);
258
+ }
259
+
260
+ [data-ui='timeline'] [data-part='item']:last-child {
261
+ margin-block-end: 0;
262
+ }
263
+
264
+ [data-ui='timeline'] [data-part='content'] {
265
+ align-self: baseline;
266
+ grid-column: content;
267
+ /**
268
+ * The row is named on all three cells. Auto-placement puts a cell whose
269
+ * column lies before the cursor on the next row, so with the body before
270
+ * the rail in the columns and after it in the DOM the body dropped a row.
271
+ */
272
+ grid-row: 1;
273
+ min-inline-size: 0;
274
+ }
275
+
276
+ [data-ui='timeline'] [data-part='opposite'] {
277
+ align-self: baseline;
278
+ color: var(--_muted);
279
+ grid-column: opposite;
280
+ grid-row: 1;
281
+ min-inline-size: 0;
282
+ overflow-wrap: anywhere;
283
+ text-align: end;
284
+ }
285
+
286
+ /* ── The rail ───────────────────────────────────────────────────────────
287
+ *
288
+ * The column the marker and the line share, the full height of the row.
289
+ * Its type is the title's, so `1lh` here is the title's first line, and the
290
+ * middle of that line is where every marker centres and where every
291
+ * connector starts and ends — whatever size the marker is.
292
+ */
293
+
294
+ [data-ui='timeline'] [data-part='rail'] {
295
+ align-items: center;
296
+ display: flex;
297
+ flex-direction: column;
298
+ grid-column: rail;
299
+ grid-row: 1;
300
+ line-height: var(--_leading);
301
+ position: relative;
302
+ }
303
+
304
+ /**
305
+ * A dot when the marker is empty or holds a spinner, a badge when it holds
306
+ * a glyph or an avatar. The marker is pushed from the top of the row by half
307
+ * the difference between one line and itself, the rule `toast` and `alert`
308
+ * draw their marks with, so a dot and a badge in the same list centre on the
309
+ * same line. It stands above the connector and wears a halo of the ground,
310
+ * which is the air the line leaves around it; a halo rather than a shortened
311
+ * line, because the line then runs centre to centre and never has to know
312
+ * how big the next marker is.
313
+ *
314
+ * A dot is drawn in three layers of one box: the box's ground, its border,
315
+ * and a core laid over it at a whole-pixel inset. There is no smaller box
316
+ * centred by arithmetic, so the core lands where the ring does.
317
+ */
318
+ [data-ui='timeline'] [data-part='marker'] {
319
+ --_marker: var(--_dot);
320
+ /**
321
+ * Rounded to a whole pixel: a line of 19.5 put a 13-pixel dot at a
322
+ * quarter pixel and antialiased its edge on both sides.
323
+ */
324
+ --_lift: round(nearest, calc((1lh - var(--_marker)) / 2), 1px);
325
+ /* The tone's ink faded into the ground: the soft disc a dot sits in. */
326
+ --_tint: color-mix(in srgb, var(--_tone-mark) 16%, var(--_surface));
327
+
328
+ align-items: center;
329
+ background-color: var(--_tint);
330
+ block-size: var(--_marker);
331
+ border: 0 solid transparent;
332
+ border-radius: var(--slotted-radius-full, 9999px);
333
+ box-shadow: 0 0 0 var(--_line-gap) var(--_surface);
334
+ box-sizing: border-box;
335
+ color: var(--_muted);
336
+ display: inline-flex;
337
+ flex: none;
338
+ inline-size: var(--_marker);
339
+ justify-content: center;
340
+ margin-block-start: var(--_lift);
341
+ /* The halo and the current ring are outside the box; nothing inside may push it wider. */
342
+ overflow: hidden;
343
+ position: relative;
344
+ transition:
345
+ background-color var(--slotted-control-transition-duration)
346
+ var(--slotted-control-transition-easing),
347
+ border-color var(--slotted-control-transition-duration)
348
+ var(--slotted-control-transition-easing),
349
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
350
+ z-index: 1;
351
+ }
352
+
353
+ /**
354
+ * The core, on one box that fills the marker: inset by a whole number of
355
+ * pixels, since the dot and the core differ by an even number. A finished
356
+ * dot holds its check instead, so it is not empty and draws no core.
357
+ */
358
+ [data-ui='timeline'] [data-part='marker']:empty::after {
359
+ background-color: var(--_tone-mark);
360
+ border-radius: inherit;
361
+ content: '';
362
+ inset: calc((var(--_marker) - var(--_core)) / 2);
363
+ position: absolute;
364
+ }
365
+
366
+ /**
367
+ * A badge. The glyph is the control icon less one step of air on each side,
368
+ * made the badge's parity: 13 in a badge of 21 at `md`, four whole pixels
369
+ * off each edge. The badge is odd so the line runs through its middle; a
370
+ * glyph of 12 stood four and a half pixels in, the browser painted it on
371
+ * the next whole pixel, and its ink sat half a CSS pixel right of and below
372
+ * the disc's centre at every zoom — two device pixels at 400%, measured on
373
+ * the painted capture. The avatar fills its own marker without a ring.
374
+ *
375
+ * The check of a finished dot is the one icon the family puts in a marker
376
+ * itself, marked `data-check` by the binding so it is never read as a badge:
377
+ * the marker stays a dot, and every rule a dot takes names it beside
378
+ * `:empty`.
379
+ */
380
+ [data-ui='timeline']
381
+ [data-part='marker']:not(:empty, :has(> [data-ui='spinner'], > [data-check])) {
382
+ --_marker: var(--_badge);
383
+
384
+ background-color: var(--_surface);
385
+ border-color: var(--_ring);
386
+ border-width: var(--slotted-border-width-thin, 1px);
387
+ }
388
+
389
+ [data-ui='timeline'] [data-part='marker'] > [data-ui='icon'] {
390
+ --slotted-icon-size: calc(
391
+ var(--_badge) - 2 * round(down, (var(--_badge) - var(--_glyph)) / 2, 1px)
392
+ );
393
+
394
+ color: inherit;
395
+ }
396
+
397
+ /* A spinner is a dot: the same box, and the same size as the glyph in a badge. */
398
+ [data-ui='timeline'] [data-part='marker'] > [data-ui='spinner'] {
399
+ --slotted-spinner-size: var(--_dot);
400
+
401
+ color: inherit;
402
+ }
403
+
404
+ [data-ui='timeline'] [data-part='marker'] > [data-ui='avatar'] {
405
+ --slotted-avatar-size: var(--_avatar);
406
+ }
407
+
408
+ [data-ui='timeline'] [data-part='marker']:has(> [data-ui='avatar']) {
409
+ --_marker: var(--_avatar);
410
+
411
+ background-color: transparent;
412
+ border-width: 0;
413
+ }
414
+
415
+ /* ── Variants ───────────────────────────────────────────────────────────
416
+ *
417
+ * Four emphases, read on the item, each drawn twice: once as a dot, once as
418
+ * a badge. `subtle` is the default and the quiet one — a solid core in a
419
+ * soft disc of its own tone, or a badge on the tone's ground with its edge,
420
+ * the pair rule `docs/token-foundation.md` states under semantic colour.
421
+ * `outline` is the hollow ring, `solid` fills, and `plain` drops every frame:
422
+ * the core alone, threaded on the line, or the bare glyph over it.
423
+ */
424
+
425
+ [data-ui='timeline']
426
+ [data-variant='outline']
427
+ [data-part='marker']:is(:empty, :has(> [data-check])) {
428
+ background-color: var(--_surface);
429
+ border-color: var(--_tone-mark);
430
+ border-width: var(--_edge);
431
+ }
432
+
433
+ [data-ui='timeline'] [data-variant='outline'] [data-part='marker']:empty::after,
434
+ [data-ui='timeline'] [data-variant='solid'] [data-part='marker']:empty::after {
435
+ content: none;
436
+ }
437
+
438
+ [data-ui='timeline']
439
+ [data-variant='solid']
440
+ [data-part='marker']:is(:empty, :has(> [data-check])) {
441
+ background-color: var(--_tone-mark);
442
+ /* A border as wide as the radius, so forced colours, which drop the ground, keep the disc. */
443
+ border-color: var(--_tone-mark);
444
+ border-width: calc(var(--_marker) / 2);
445
+ }
446
+
447
+ /* The core alone, on the line: the ground and the halo give way so the line runs into it. */
448
+ [data-ui='timeline']
449
+ [data-variant='plain']
450
+ [data-part='marker']:is(:empty, :has(> [data-check])) {
451
+ background-color: transparent;
452
+ box-shadow: none;
453
+ }
454
+
455
+ /* The glyph of a hairline or frameless badge is drawn in the dot's own ink. */
456
+ [data-ui='timeline']
457
+ [data-variant='outline']
458
+ [data-part='marker']:not(:empty, :has(> [data-check])) {
459
+ color: var(--_tone-mark);
460
+ }
461
+
462
+ [data-ui='timeline']
463
+ [data-variant='outline'][data-tone]:not([data-tone='secondary'])
464
+ [data-part='marker']:not(:empty, :has(> [data-ui='spinner'], > [data-check])) {
465
+ border-color: var(--_tone-border);
466
+ }
467
+
468
+ [data-ui='timeline']
469
+ [data-variant='solid']
470
+ [data-part='marker']:not(:empty, :has(> [data-ui='spinner'], > [data-check])) {
471
+ background-color: var(--_tone-solid);
472
+ border-color: var(--_tone-solid);
473
+ color: var(--_tone-on-solid);
474
+ }
475
+
476
+ [data-ui='timeline']
477
+ [data-variant='subtle']
478
+ [data-part='marker']:not(:empty, :has(> [data-ui='spinner'], > [data-check])) {
479
+ background-color: var(--_tone-surface);
480
+ border-color: var(--_tone-border);
481
+ color: var(--_tone-text);
482
+ }
483
+
484
+ [data-ui='timeline']
485
+ [data-variant='plain']
486
+ [data-part='marker']:not(:empty, :has(> [data-check])) {
487
+ background-color: var(--_surface);
488
+ border-color: transparent;
489
+ color: var(--_tone-mark);
490
+ }
491
+
492
+ /* ── Statuses ───────────────────────────────────────────────────────────
493
+ *
494
+ * Each one is a shape as well as a colour, because `high-contrast` paints
495
+ * every tone in one ink and forced colours paint every ring in one. Pending
496
+ * is the variant's own mark; complete is a filled disc with the check
497
+ * knocked out of it; current is a filled disc with a ring around it, a
498
+ * target; loading is the shared spinner. The ring is a shadow and not an
499
+ * outline, because an outline in this library is the focus ring and a ring
500
+ * in the tone's ink around a marker would read as focus; forced colours drop
501
+ * the shadow, and the block at the end draws it there as an outline in the
502
+ * system's ink, where no colour can be mistaken.
503
+ */
504
+
505
+ /**
506
+ * A finished dot fills with its ground, not with a border as wide as its
507
+ * radius: that border would leave the check no box to stand in. Forced
508
+ * colours keep the ground through the block at the end.
509
+ */
510
+ [data-ui='timeline']
511
+ [data-complete]:not([data-current])
512
+ [data-part='marker']:has(> [data-check]) {
513
+ background-color: var(--_tone-mark);
514
+ border-width: 0;
515
+ }
516
+
517
+ [data-ui='timeline'] [data-current] [data-part='marker']:empty {
518
+ background-color: var(--_tone-mark);
519
+ border-color: var(--_tone-mark);
520
+ border-width: calc(var(--_marker) / 2);
521
+ }
522
+
523
+ [data-ui='timeline'] [data-current] [data-part='marker']:empty::after {
524
+ content: none;
525
+ }
526
+
527
+ /**
528
+ * The check is knocked out of the disc in the ground's own colour, so it
529
+ * reads in every tone and scheme without a token of its own; it is the
530
+ * dot less a step a side, the same air a badge gives its glyph.
531
+ */
532
+ [data-ui='timeline'] [data-part='marker'] > [data-ui='icon'][data-check] {
533
+ --slotted-icon-size: calc(var(--_marker) - 2 * var(--slotted-space-1, 0.125rem));
534
+
535
+ color: var(--_surface);
536
+ }
537
+
538
+ [data-ui='timeline'] [data-current] [data-part='marker'] {
539
+ box-shadow:
540
+ 0 0 0 var(--slotted-border-width-thick, 2px) var(--_surface),
541
+ 0 0 0 var(--_reach) var(--_tone-mark),
542
+ 0 0 0 calc(var(--_reach) + var(--_line-gap)) var(--_surface);
543
+ }
544
+
545
+ [data-ui='timeline']
546
+ [data-complete]:not([data-current])
547
+ [data-part='marker']:not(:empty, :has(> [data-ui='spinner'], > [data-check])),
548
+ [data-ui='timeline']
549
+ [data-current]
550
+ [data-part='marker']:not(:empty, :has(> [data-ui='spinner'], > [data-check])) {
551
+ background-color: var(--_tone-solid);
552
+ border-color: var(--_tone-solid);
553
+ color: var(--_tone-on-solid);
554
+ }
555
+
556
+ /**
557
+ * An event still arriving. The ring is `spinner`'s — one arc, drawn once,
558
+ * for every family that waits — standing bare on the ground, the size of a
559
+ * dot, in the tone's ink.
560
+ */
561
+ [data-ui='timeline'] [data-loading][data-variant] [data-part='marker'] {
562
+ background-color: var(--_surface);
563
+ border-color: transparent;
564
+ box-shadow: 0 0 0 var(--_line-gap) var(--_surface);
565
+ color: var(--_tone-mark);
566
+ }
567
+
568
+ [data-ui='timeline'] [data-loading] > [data-part='content'] {
569
+ color: var(--_muted);
570
+ }
571
+
572
+ /* ── The connector ──────────────────────────────────────────────────────
573
+ *
574
+ * A hairline from the middle of this row's first line to the middle of the
575
+ * next row's, which are the two markers' centres; the markers stand above
576
+ * it and their halo opens the gap. It is a border rather than a painted
577
+ * background, so `dashed` is a word rather than a gradient and the line
578
+ * keeps its colour under forced colours. The last item of a list draws
579
+ * none, because there is nothing after it.
580
+ */
581
+
582
+ [data-ui='timeline'] [data-part='connector'] {
583
+ /* Both edges named, so the dashed style below reaches only the one drawn. */
584
+ border-block-start: 0 solid var(--_line);
585
+ border-inline-start: var(--_rule) solid var(--_line);
586
+ display: block;
587
+ inline-size: 0;
588
+ inset-block-end: calc(-0.5lh - var(--_item-gap));
589
+ inset-block-start: 0.5lh;
590
+ inset-inline-start: calc(50% - var(--_rule) / 2);
591
+ position: absolute;
592
+ transition: border-color var(--slotted-control-transition-duration)
593
+ var(--slotted-control-transition-easing);
594
+ }
595
+
596
+ [data-ui='timeline'] [data-connector='dashed'] [data-part='connector'] {
597
+ border-block-start-style: dashed;
598
+ border-inline-start-style: dashed;
599
+ }
600
+
601
+ [data-ui='timeline'] [data-connector='none'] [data-part='connector'],
602
+ [data-ui='timeline'] [data-part='item']:last-child [data-part='connector'] {
603
+ display: none;
604
+ }
605
+
606
+ /**
607
+ * The line after a finished event is painted in its tone, so a tracking
608
+ * timeline reads as a filled track up to where the reader is.
609
+ */
610
+ [data-ui='timeline'] [data-complete] [data-part='connector'] {
611
+ border-color: var(--_tone-mark);
612
+ }
613
+
614
+ /* ── The body ───────────────────────────────────────────────────────────
615
+ *
616
+ * Title and timestamp on the first line, the actor under them, then the
617
+ * description, whatever the consumer wrote, and the actions. The timestamp
618
+ * keeps to the end of the line while there is room and drops under the
619
+ * title when there is not.
620
+ *
621
+ * Two columns and no named areas: the title takes the first cell and the
622
+ * time the second, and everything else spans both and follows the DOM. A
623
+ * body laid out by named areas put a consumer's own child in the first
624
+ * empty cell — the actor's row — above the description it was written
625
+ * under.
626
+ */
627
+
628
+ [data-ui='timeline'] [data-part='content'] {
629
+ /**
630
+ * No column gap: the timestamp carries the distance itself, so a body
631
+ * with no timestamp has no empty column and no gap beside it.
632
+ */
633
+ column-gap: 0;
634
+ display: grid;
635
+ grid-template-columns: minmax(0, 1fr) auto;
636
+ /* No row gap either: an empty row would still cost one, so each line
637
+ below the first carries its own distance. */
638
+ row-gap: 0;
639
+ }
640
+
641
+ [data-ui='timeline'] [data-part='content'] > :not([data-part='title'], [data-part='time']) {
642
+ grid-column: 1 / -1;
643
+ }
644
+
645
+ [data-ui='timeline'] [data-part='content'] > :is([data-part='actor'], [data-part='description']) {
646
+ margin-block-start: var(--slotted-timeline-content-gap, var(--slotted-space-2, 0.25rem));
647
+ }
648
+
649
+ [data-ui='timeline'] [data-part='title'] {
650
+ color: var(--_strong);
651
+ font-weight: var(--slotted-timeline-title-weight, var(--slotted-font-weight-medium, 500));
652
+ grid-column: 1;
653
+ min-inline-size: 0;
654
+ overflow-wrap: anywhere;
655
+ }
656
+
657
+ [data-ui='timeline'] [data-part='content'] > [data-part='time'] {
658
+ grid-column: 2;
659
+ grid-row: 1;
660
+ margin-inline-start: var(--_gap);
661
+ }
662
+
663
+ [data-ui='timeline'] [data-part='time'] {
664
+ color: var(--_muted);
665
+ font-size: var(--slotted-timeline-time-font-size, var(--slotted-font-size-xs, 0.75rem));
666
+ font-variant-numeric: tabular-nums;
667
+ white-space: nowrap;
668
+ }
669
+
670
+ [data-ui='timeline'] [data-part='actor'] {
671
+ align-items: center;
672
+ color: var(--_muted);
673
+ column-gap: var(--slotted-space-3, 0.375rem);
674
+ display: flex;
675
+ font-size: var(--_meta-size);
676
+ min-inline-size: 0;
677
+ }
678
+
679
+ [data-ui='timeline'] [data-part='description'] {
680
+ color: var(--_muted);
681
+ font-size: var(--_meta-size);
682
+ min-inline-size: 0;
683
+ overflow-wrap: anywhere;
684
+ }
685
+
686
+ [data-ui='timeline'] [data-part='actions'] {
687
+ column-gap: var(--_gap);
688
+ display: flex;
689
+ flex-wrap: wrap;
690
+ margin-block-start: var(--slotted-space-4, 0.5rem);
691
+ row-gap: var(--slotted-space-2, 0.25rem);
692
+ }
693
+
694
+ /* ── The article ────────────────────────────────────────────────────────
695
+ *
696
+ * In feed mode every item is a tab stop, so it draws the theme's ring when
697
+ * the keyboard lands on it. The ring stands off the item by the offset the
698
+ * theme states, around the whole row rather than around the words.
699
+ */
700
+
701
+ [data-ui='timeline'] [data-part='item'][tabindex]:focus-visible {
702
+ border-radius: var(--slotted-timeline-radius, var(--slotted-control-radius, 4px));
703
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
704
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
705
+ }
706
+
707
+ /* ── Groups ─────────────────────────────────────────────────────────────
708
+ *
709
+ * A period with a heading and its own list. The heading spans the whole
710
+ * width and the line breaks at it: a group is a break in time, and the
711
+ * track that ran into it would say the opposite.
712
+ */
713
+
714
+ [data-ui='timeline'] [data-part='group'] {
715
+ display: block;
716
+ }
717
+
718
+ [data-ui='timeline'] [data-part='group'] + [data-part='group'] {
719
+ margin-block-start: var(--_item-gap);
720
+ }
721
+
722
+ /**
723
+ * A flex box, so a trigger inside it is the whole line: an inline-flex
724
+ * trigger on the heading's baseline added the font's descent under its
725
+ * 24 pixels, and the heading measured 26.5.
726
+ */
727
+ [data-ui='timeline'] [data-part='group-label'] {
728
+ align-items: center;
729
+ color: var(--_muted);
730
+ display: flex;
731
+ font-size: var(--_meta-size);
732
+ font-weight: var(--slotted-timeline-group-weight, var(--slotted-font-weight-semibold, 600));
733
+ line-height: var(--_leading);
734
+ margin: 0 0 var(--slotted-timeline-group-gap, var(--slotted-space-4, 0.5rem));
735
+ }
736
+
737
+ [data-ui='timeline'] [data-part='group-trigger'] {
738
+ align-items: center;
739
+ appearance: none;
740
+ background: none;
741
+ border: 0;
742
+ border-radius: var(--slotted-timeline-radius, var(--slotted-control-radius, 4px));
743
+ color: inherit;
744
+ column-gap: var(--slotted-space-2, 0.25rem);
745
+ cursor: pointer;
746
+ display: inline-flex;
747
+ font: inherit;
748
+ /* The row's own height, so the target is never under the minimum. */
749
+ min-block-size: var(--slotted-target-min, 24px);
750
+ padding: 0;
751
+ text-align: start;
752
+ transition:
753
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing),
754
+ translate var(--slotted-duration-instant) var(--slotted-control-transition-easing);
755
+ }
756
+
757
+ [data-ui='timeline'] [data-part='group-trigger']:enabled:hover {
758
+ color: var(--_strong);
759
+ }
760
+
761
+ /**
762
+ * The heading has no ground to step, and its hover already takes the
763
+ * strongest ink, so a press answers in place: the label settles by a
764
+ * hairline, the press `button` draws on a solid fill.
765
+ */
766
+ [data-ui='timeline'] [data-part='group-trigger']:enabled:active {
767
+ color: var(--_strong);
768
+ translate: 0 var(--slotted-border-width-thin, 1px);
769
+ }
770
+
771
+ [data-ui='timeline'] [data-part='group-trigger']:focus-visible {
772
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
773
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
774
+ }
775
+
776
+ /**
777
+ * The binding's `chevron-down` glyph, turned rather than swapped: a chevron
778
+ * that rotates between down and up is the same mark in every direction. The
779
+ * part sizes the icon box and the box turns.
780
+ */
781
+ [data-ui='timeline'] [data-part='group-indicator'] {
782
+ --slotted-icon-size: var(--_icon);
783
+
784
+ display: inline-flex;
785
+ flex: none;
786
+ }
787
+
788
+ /**
789
+ * A fold is one movement: the chevron, the list's height, its ink and the
790
+ * heading's distance to it share a duration and a curve, the disclosure
791
+ * motion `collapsible` draws. Measured frame by frame before this rule, the
792
+ * list faded while the page below stood still and then jumped 56 pixels in
793
+ * one frame, the exiting curve held the list at 0.9 for the first fifty-five milliseconds
794
+ * after the press, and the chevron finished seventy milliseconds after the list had gone.
795
+ */
796
+ [data-ui='timeline'] [data-part='group'] {
797
+ --_fold: var(--slotted-duration-medium)
798
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
799
+ }
800
+
801
+ [data-ui='timeline'] [data-part='group-indicator'] > [data-ui='icon'] {
802
+ rotate: -90deg;
803
+ transition: rotate var(--_fold);
804
+ }
805
+
806
+ [data-ui='timeline']
807
+ [data-open]
808
+ > [data-part='group-label']
809
+ [data-part='group-indicator']
810
+ > [data-ui='icon'] {
811
+ rotate: 0deg;
812
+ }
813
+
814
+ /**
815
+ * A folded list stays laid out at no height rather than leaving the page:
816
+ * the page below then slides with it instead of jumping, and a group open
817
+ * at the first render has no earlier style to grow from, so nothing moves
818
+ * while a page loads. `hidden` still says what it did — the binding writes
819
+ * it, and `visibility` takes the folded events out of the accessibility
820
+ * tree and the tab order, at the end of the fold and at the start of an
821
+ * opening. The clip margin keeps the current marker's ring and a focus
822
+ * ring at the list's edge whole.
823
+ */
824
+ [data-ui='timeline'] [data-part='group-list'] {
825
+ interpolate-size: allow-keywords;
826
+ opacity: 1;
827
+ overflow: clip;
828
+ overflow-clip-margin: max(
829
+ round(up, var(--_reach), 1px),
830
+ calc(var(--slotted-focus-ring-width, 2px) + var(--slotted-focus-ring-offset, 2px))
831
+ );
832
+ transition:
833
+ block-size var(--_fold),
834
+ opacity var(--_fold),
835
+ visibility var(--_fold);
836
+ }
837
+
838
+ [data-ui='timeline'] [data-part='group-list'][hidden] {
839
+ block-size: 0;
840
+ display: flex;
841
+ opacity: 0;
842
+ visibility: hidden;
843
+ }
844
+
845
+ /* A folded group ends at its heading; the distance to the next is the group's own. */
846
+ [data-ui='timeline'] [data-part='group-label'] {
847
+ transition: margin-block-end var(--_fold);
848
+ }
849
+
850
+ [data-ui='timeline'] [data-part='group-label']:has(+ [data-part='group-list'][hidden]) {
851
+ margin-block-end: 0;
852
+ }
853
+
854
+ /* ── Fresh, more and empty ──────────────────────────────────────────────
855
+ *
856
+ * Three things that are not events. `fresh` is the pill that holds new
857
+ * events back until the reader asks for them; `more` is the end of what has
858
+ * been loaded; `empty` is the list with nothing in it yet.
859
+ */
860
+
861
+ [data-ui='timeline'] [data-part='fresh'],
862
+ [data-ui='timeline'] button[data-part='more'] {
863
+ align-items: center;
864
+ appearance: none;
865
+ background-color: var(--_surface);
866
+ border: var(--slotted-border-width-thin, 1px) solid var(--_ring);
867
+ border-radius: var(--slotted-radius-full, 9999px);
868
+ color: var(--_strong);
869
+ column-gap: var(--_gap);
870
+ cursor: pointer;
871
+ /* Block-level, so no line box of the list's leading stands around it. */
872
+ display: flex;
873
+ font: inherit;
874
+ font-size: var(--_meta-size);
875
+ font-weight: var(--slotted-timeline-title-weight, var(--slotted-font-weight-medium, 500));
876
+ inline-size: fit-content;
877
+ justify-content: center;
878
+ line-height: var(--slotted-control-line-height, 1);
879
+ min-block-size: var(--slotted-control-height-sm, 30px);
880
+ padding-inline: var(--slotted-control-padding-inline-md, 13px);
881
+ transition:
882
+ background-color var(--slotted-control-transition-duration)
883
+ var(--slotted-control-transition-easing),
884
+ border-color var(--slotted-control-transition-duration)
885
+ var(--slotted-control-transition-easing),
886
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing);
887
+ }
888
+
889
+ [data-ui='timeline'] [data-part='fresh'] {
890
+ --_tone-solid: var(--slotted-tone-accent-solid, Highlight);
891
+ --_tone-surface: var(--slotted-tone-accent-surface, Canvas);
892
+ --_tone-border: var(--slotted-tone-accent-border, Highlight);
893
+ --_tone-text: var(--slotted-tone-accent-text, Highlight);
894
+ /**
895
+ * The pill arrives and leaves as the group lists fold: its height, its
896
+ * border, the distance under it and its ink move together, so the events
897
+ * below slide by the pill's height rather than jumping by it in a frame.
898
+ */
899
+ --_fold: var(--slotted-duration-medium)
900
+ var(--slotted-control-transition-easing, var(--slotted-easing-standard));
901
+
902
+ background-color: var(--_tone-surface);
903
+ border-color: var(--_tone-border);
904
+ color: var(--_tone-text);
905
+ interpolate-size: allow-keywords;
906
+ margin-block-end: var(--_item-gap);
907
+ opacity: 1;
908
+ overflow: clip;
909
+ transition:
910
+ background-color var(--slotted-control-transition-duration)
911
+ var(--slotted-control-transition-easing),
912
+ border-color var(--slotted-control-transition-duration)
913
+ var(--slotted-control-transition-easing),
914
+ color var(--slotted-control-transition-duration) var(--slotted-control-transition-easing),
915
+ block-size var(--_fold),
916
+ min-block-size var(--_fold),
917
+ border-block-width var(--_fold),
918
+ margin-block-end var(--_fold),
919
+ opacity var(--_fold),
920
+ display var(--_fold) allow-discrete;
921
+ }
922
+
923
+ /**
924
+ * The bindings keep the pill mounted once it has appeared and write
925
+ * `hidden` at a count of zero; `allow-discrete` holds it on the page until
926
+ * it has folded away. Its first appearance grows from nothing, because the
927
+ * pill arriving is itself the news. Scale is not used: the pill holds text
928
+ * (`text-scale.test.mjs`).
929
+ */
930
+ @starting-style {
931
+ [data-ui='timeline'] [data-part='fresh']:not([hidden]) {
932
+ block-size: 0;
933
+ border-block-width: 0;
934
+ margin-block-end: 0;
935
+ min-block-size: 0;
936
+ opacity: 0;
937
+ }
938
+ }
939
+
940
+ [data-ui='timeline'] [data-part='fresh'][hidden] {
941
+ block-size: 0;
942
+ border-block-width: 0;
943
+ display: none;
944
+ margin-block-end: 0;
945
+ min-block-size: 0;
946
+ opacity: 0;
947
+ pointer-events: none;
948
+ }
949
+
950
+ /**
951
+ * The pill answers with a state layer: its own text colour, at a low
952
+ * strength, mixed into the tinted ground, the step `tag` and `avatar` take
953
+ * for the same reason. The tinted ramp ends at the surface the pill rests
954
+ * on — `subtle-hover` lightened it toward the page in the light scheme, and
955
+ * matched the page in the dark one, and `subtle-active` was the rest colour.
956
+ */
957
+ [data-ui='timeline'] [data-part='fresh']:enabled:hover {
958
+ background-color: color-mix(
959
+ in oklab,
960
+ var(--_tone-surface),
961
+ currentColor var(--slotted-timeline-state-layer-hover, 8%)
962
+ );
963
+ }
964
+
965
+ [data-ui='timeline'] [data-part='fresh']:enabled:active {
966
+ background-color: color-mix(
967
+ in oklab,
968
+ var(--_tone-surface),
969
+ currentColor var(--slotted-timeline-state-layer-active, 14%)
970
+ );
971
+ }
972
+
973
+ [data-ui='timeline'] button[data-part='more']:enabled:hover {
974
+ background-color: var(--slotted-tone-secondary-subtle-hover, Canvas);
975
+ }
976
+
977
+ [data-ui='timeline'] button[data-part='more']:enabled:active {
978
+ background-color: var(--slotted-tone-secondary-subtle-active, Canvas);
979
+ }
980
+
981
+ /* A press lands at once, as a `button` does; the release eases back. */
982
+ [data-ui='timeline'] :is([data-part='fresh'], button[data-part='more']):enabled:active {
983
+ transition-duration: var(--slotted-duration-instant);
984
+ }
985
+
986
+ [data-ui='timeline'] [data-part='fresh']:focus-visible,
987
+ [data-ui='timeline'] button[data-part='more']:focus-visible {
988
+ outline: var(--slotted-focus-ring-width, 2px) solid var(--slotted-focus-ring-color, Highlight);
989
+ outline-offset: var(--slotted-focus-ring-offset, 2px);
990
+ }
991
+
992
+ /**
993
+ * The end of the loaded list, in the body's own column so it reads as the
994
+ * next event's place rather than as a footer.
995
+ */
996
+ [data-ui='timeline'] [data-part='more'] {
997
+ margin-block-start: var(--_item-gap);
998
+ margin-inline-start: calc(var(--_opposite-column, 0px) + var(--_rail) + var(--_gap));
999
+ }
1000
+
1001
+ [data-ui='timeline'] [data-part='list']:has([data-part='opposite']) ~ [data-part='more'] {
1002
+ margin-inline-start: calc(var(--_opposite) + var(--_rail) + 2 * var(--_gap));
1003
+ }
1004
+
1005
+ [data-ui='timeline'] div[data-part='more'] {
1006
+ align-items: center;
1007
+ color: var(--_muted);
1008
+ column-gap: var(--_gap);
1009
+ display: flex;
1010
+ font-size: var(--_meta-size);
1011
+ min-block-size: var(--slotted-control-height-sm, 30px);
1012
+ }
1013
+
1014
+ [data-ui='timeline'] [data-part='more'] > [data-ui='spinner'] {
1015
+ --slotted-spinner-size: var(--_icon);
1016
+ }
1017
+
1018
+ [data-ui='timeline'] [data-part='empty'] {
1019
+ color: var(--_muted);
1020
+ padding-block: var(--_item-gap);
1021
+ text-align: center;
1022
+ }
1023
+
1024
+ /* ── Placement ──────────────────────────────────────────────────────────
1025
+ *
1026
+ * `start` mirrors the row: the body before the rail, the opposite side
1027
+ * after it. `alternate` gives every row both sides and the items take turns,
1028
+ * so the rail runs down the middle. Both are logical: in a right-to-left
1029
+ * document `start` is the same timeline mirrored.
1030
+ */
1031
+
1032
+ [data-ui='timeline'][data-placement='start'] [data-part='item'] {
1033
+ grid-template-columns:
1034
+ [content] minmax(0, 1fr)
1035
+ [rail] var(--_rail)
1036
+ [opposite] var(--_opposite-column, 0);
1037
+ }
1038
+
1039
+ [data-ui='timeline'][data-placement='start']
1040
+ :where([data-part='list']:not(:has([data-part='opposite'])))
1041
+ [data-part='item'] {
1042
+ grid-template-columns: [content] minmax(0, 1fr) [rail] var(--_rail);
1043
+ }
1044
+
1045
+ [data-ui='timeline'][data-placement='start'] [data-part='content'] {
1046
+ text-align: end;
1047
+ }
1048
+
1049
+ [data-ui='timeline'][data-placement='start'] [data-part='content'] {
1050
+ grid-template-columns: auto minmax(0, 1fr);
1051
+ }
1052
+
1053
+ [data-ui='timeline'][data-placement='start'] [data-part='content'] > [data-part='title'] {
1054
+ grid-column: 2;
1055
+ }
1056
+
1057
+ [data-ui='timeline'][data-placement='start'] [data-part='content'] > [data-part='time'] {
1058
+ grid-column: 1;
1059
+ margin-inline: 0 var(--_gap);
1060
+ }
1061
+
1062
+ [data-ui='timeline'][data-placement='start'] :is([data-part='actor'], [data-part='actions']) {
1063
+ justify-content: flex-end;
1064
+ }
1065
+
1066
+ [data-ui='timeline'][data-placement='start'] [data-part='opposite'] {
1067
+ text-align: start;
1068
+ }
1069
+
1070
+ [data-ui='timeline'][data-placement='start'] [data-part='more'] {
1071
+ margin-inline-start: 0;
1072
+ }
1073
+
1074
+ /**
1075
+ * The first side is a whole number of pixels and the second takes the
1076
+ * rest: two equal fractions of an odd remainder put the rail, and every
1077
+ * marker on it, at half a pixel.
1078
+ */
1079
+ [data-ui='timeline'][data-placement='alternate'] [data-part='item'] {
1080
+ grid-template-columns:
1081
+ [before] round(down, calc((100% - var(--_rail)) / 2 - var(--_gap)), 1px)
1082
+ [rail] var(--_rail)
1083
+ [after] minmax(0, 1fr);
1084
+ }
1085
+
1086
+ [data-ui='timeline'][data-placement='alternate'] [data-part='content'] {
1087
+ grid-column: after;
1088
+ }
1089
+
1090
+ [data-ui='timeline'][data-placement='alternate'] [data-part='opposite'] {
1091
+ grid-column: before;
1092
+ }
1093
+
1094
+ /**
1095
+ * Even rows swap sides. Parity is read from the list rather than written by
1096
+ * a binding, and an item that names its own side wins.
1097
+ */
1098
+ [data-ui='timeline'][data-placement='alternate']
1099
+ [data-part='item']:nth-child(even)
1100
+ [data-part='content'],
1101
+ [data-ui='timeline'][data-placement='alternate'] [data-side='start'] [data-part='content'] {
1102
+ grid-column: before;
1103
+ text-align: end;
1104
+ }
1105
+
1106
+ [data-ui='timeline'][data-placement='alternate']
1107
+ [data-part='item']:nth-child(even)
1108
+ [data-part='opposite'],
1109
+ [data-ui='timeline'][data-placement='alternate'] [data-side='start'] [data-part='opposite'] {
1110
+ grid-column: after;
1111
+ text-align: start;
1112
+ }
1113
+
1114
+ [data-ui='timeline'][data-placement='alternate'] [data-side='end'] [data-part='content'] {
1115
+ grid-column: after;
1116
+ text-align: start;
1117
+ }
1118
+
1119
+ [data-ui='timeline'][data-placement='alternate'] [data-side='end'] [data-part='opposite'] {
1120
+ grid-column: before;
1121
+ text-align: end;
1122
+ }
1123
+
1124
+ [data-ui='timeline'][data-placement='alternate']
1125
+ [data-part='item']:nth-child(even)
1126
+ [data-part='content'],
1127
+ [data-ui='timeline'][data-placement='alternate'] [data-side='start'] [data-part='content'] {
1128
+ grid-template-columns: auto minmax(0, 1fr);
1129
+ }
1130
+
1131
+ [data-ui='timeline'][data-placement='alternate']
1132
+ [data-part='item']:nth-child(even)
1133
+ [data-part='content']
1134
+ > [data-part='title'],
1135
+ [data-ui='timeline'][data-placement='alternate']
1136
+ [data-side='start']
1137
+ [data-part='content']
1138
+ > [data-part='title'] {
1139
+ grid-column: 2;
1140
+ }
1141
+
1142
+ [data-ui='timeline'][data-placement='alternate']
1143
+ [data-part='item']:nth-child(even)
1144
+ [data-part='content']
1145
+ > [data-part='time'],
1146
+ [data-ui='timeline'][data-placement='alternate']
1147
+ [data-side='start']
1148
+ [data-part='content']
1149
+ > [data-part='time'] {
1150
+ grid-column: 1;
1151
+ margin-inline: 0 var(--_gap);
1152
+ }
1153
+
1154
+ [data-ui='timeline'][data-placement='alternate']
1155
+ [data-part='item']:nth-child(even)
1156
+ :is([data-part='actor'], [data-part='actions']),
1157
+ [data-ui='timeline'][data-placement='alternate']
1158
+ [data-side='start']
1159
+ :is([data-part='actor'], [data-part='actions']) {
1160
+ justify-content: flex-end;
1161
+ }
1162
+
1163
+ [data-ui='timeline'][data-placement='alternate'] [data-part='more'] {
1164
+ margin-inline-start: calc(50% + var(--_rail) / 2 + var(--_gap));
1165
+ }
1166
+
1167
+ /* ── Horizontal ─────────────────────────────────────────────────────────
1168
+ *
1169
+ * The same grid turned on its side: the rail above the body, the line
1170
+ * running along the marker's centre to the next item, and the list rolling
1171
+ * in the inline axis when there are more events than width. Every item is
1172
+ * the same width, so the markers stay a track apart however long the
1173
+ * titles are.
1174
+ */
1175
+
1176
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='list'] {
1177
+ display: grid;
1178
+ grid-auto-columns: minmax(
1179
+ var(--slotted-timeline-track-min, var(--slotted-space-12, 4rem)),
1180
+ 1fr
1181
+ );
1182
+ grid-auto-flow: column;
1183
+ overflow-x: auto;
1184
+ overscroll-behavior-x: contain;
1185
+ /**
1186
+ * A scroll container clips what stands outside it, and the ring of the
1187
+ * current marker stands outside the rail by its offset and its width: the
1188
+ * list keeps that much room above and before the first marker, rounded
1189
+ * up to a whole pixel: the ring's 3.5 put every marker and every scene
1190
+ * below the list at half a pixel.
1191
+ */
1192
+ padding-block: round(up, var(--_reach), 1px) var(--slotted-space-2, 0.25rem);
1193
+ padding-inline: round(up, var(--_reach), 1px);
1194
+ }
1195
+
1196
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='item'] {
1197
+ /**
1198
+ * The rows keep their own height. Every item in the row is stretched to
1199
+ * the tallest, and `auto` rows would share that extra space out — which
1200
+ * put the marker of a one-line event seven pixels under its neighbours'.
1201
+ */
1202
+ align-content: start;
1203
+ align-items: start;
1204
+ column-gap: 0;
1205
+ grid-template-columns: minmax(0, 1fr);
1206
+ grid-template-rows: [before] auto [rail] auto [after] auto;
1207
+ margin-block-end: 0;
1208
+ padding-inline-end: var(--_item-gap);
1209
+ /* The distance is on the cells, so an empty row above the rail costs nothing. */
1210
+ row-gap: 0;
1211
+ }
1212
+
1213
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='item']:last-child {
1214
+ padding-inline-end: 0;
1215
+ }
1216
+
1217
+ /**
1218
+ * A square as tall and as wide as the rail at the start of the item, with
1219
+ * the marker centred in it, so every centre sits half a rail in and the
1220
+ * line runs from one to the next along the same row.
1221
+ */
1222
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='rail'] {
1223
+ align-items: start;
1224
+ align-self: start;
1225
+ block-size: var(--_rail);
1226
+ grid-column: 1;
1227
+ grid-row: rail;
1228
+ justify-content: center;
1229
+ }
1230
+
1231
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='marker'] {
1232
+ --_lift: 0px;
1233
+
1234
+ margin-inline-start: calc((var(--_rail) - var(--_marker)) / 2);
1235
+ }
1236
+
1237
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='connector'] {
1238
+ /* Widths only: the colour and the dash are the rules above, for both edges. */
1239
+ border-block-start-width: var(--_rule);
1240
+ border-inline-start-width: 0;
1241
+ block-size: 0;
1242
+ inline-size: auto;
1243
+ inset-block-end: auto;
1244
+ inset-block-start: calc(50% - var(--_rule) / 2);
1245
+ inset-inline-end: calc(-1 * var(--_item-gap) - var(--_rail) / 2);
1246
+ inset-inline-start: calc(var(--_rail) / 2);
1247
+ }
1248
+
1249
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='content'] {
1250
+ align-self: start;
1251
+ grid-column: 1;
1252
+ grid-row: after;
1253
+ margin-block: var(--_gap) 0;
1254
+ text-align: start;
1255
+ }
1256
+
1257
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='opposite'] {
1258
+ align-self: start;
1259
+ grid-column: 1;
1260
+ grid-row: before;
1261
+ margin-block: 0 var(--_gap);
1262
+ text-align: start;
1263
+ }
1264
+
1265
+ [data-ui='timeline'][data-orientation='horizontal'][data-placement='start'] [data-part='content'],
1266
+ [data-ui='timeline'][data-orientation='horizontal'][data-placement='alternate']
1267
+ [data-part='item']:nth-child(even)
1268
+ [data-part='content'] {
1269
+ grid-column: 1;
1270
+ grid-row: before;
1271
+ margin-block: 0 var(--_gap);
1272
+ text-align: start;
1273
+ }
1274
+
1275
+ [data-ui='timeline'][data-orientation='horizontal'][data-placement='start']
1276
+ [data-part='opposite'],
1277
+ [data-ui='timeline'][data-orientation='horizontal'][data-placement='alternate']
1278
+ [data-part='item']:nth-child(even)
1279
+ [data-part='opposite'] {
1280
+ grid-column: 1;
1281
+ grid-row: after;
1282
+ margin-block: var(--_gap) 0;
1283
+ }
1284
+
1285
+ /**
1286
+ * One column, as a flex column rather than as a one-column grid: the areas
1287
+ * the placements swap are then irrelevant, and the DOM order — title, time,
1288
+ * actor, description, actions — is the order a narrow body reads in.
1289
+ */
1290
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='content'] {
1291
+ display: flex;
1292
+ flex-direction: column;
1293
+ }
1294
+
1295
+ /* On a line of its own the timestamp may wrap: nowrap is for the title's line. */
1296
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='content'] > [data-part='time'] {
1297
+ margin-inline: 0;
1298
+ white-space: normal;
1299
+ }
1300
+
1301
+ [data-ui='timeline'][data-orientation='horizontal']
1302
+ :is([data-part='actor'], [data-part='actions']) {
1303
+ justify-content: flex-start;
1304
+ }
1305
+
1306
+ [data-ui='timeline'][data-orientation='horizontal'] [data-part='more'] {
1307
+ margin-block-start: var(--_gap);
1308
+ margin-inline-start: 0;
1309
+ }
1310
+
1311
+ /* ── Narrow width ───────────────────────────────────────────────────────
1312
+ *
1313
+ * Two things give way when the column is narrow. The timestamp leaves the
1314
+ * title's line and takes its own, so a title of nine words keeps the whole
1315
+ * measure; and below the breakpoint a horizontal timeline becomes a column
1316
+ * and an alternating one keeps to one side — the composition change section
1317
+ * 2.7 of `docs/component-features.md` asks for rather than a smaller version
1318
+ * of the same row. A timeline the consumer set to `reflow={false}` is left
1319
+ * alone.
1320
+ *
1321
+ * The breakpoints are literals because `@container (min-width: var(--x))` is
1322
+ * invalid CSS, which rule 4 of `docs/token-foundation.md` states.
1323
+ */
1324
+
1325
+ @container slotted-timeline (max-width: 20rem) {
1326
+ [data-ui='timeline'][data-orientation='vertical'] [data-part='content'] {
1327
+ display: flex;
1328
+ flex-direction: column;
1329
+ }
1330
+
1331
+ [data-ui='timeline'][data-orientation='vertical'] [data-part='content'] > [data-part='time'] {
1332
+ margin-inline: 0;
1333
+ white-space: normal;
1334
+ }
1335
+
1336
+ /**
1337
+ * The opposite column gives way: eight rem of a twenty-rem column is the
1338
+ * body's measure. The opposite side takes the line above the body, and
1339
+ * the rail spans both.
1340
+ */
1341
+ [data-ui='timeline'][data-orientation='vertical']:not([data-reflow='false'])
1342
+ [data-part='list']
1343
+ [data-part='item']:nth-child(n) {
1344
+ grid-template-columns: [rail] var(--_rail) [content] minmax(0, 1fr);
1345
+ }
1346
+
1347
+ [data-ui='timeline'][data-orientation='vertical']:not([data-reflow='false'])
1348
+ [data-part='item']:nth-child(n)
1349
+ > [data-part='opposite'] {
1350
+ grid-column: content;
1351
+ grid-row: 1;
1352
+ text-align: start;
1353
+ }
1354
+
1355
+ [data-ui='timeline'][data-orientation='vertical']:not([data-reflow='false'])
1356
+ [data-part='item']:nth-child(n):has(> [data-part='opposite'])
1357
+ > [data-part='content'] {
1358
+ grid-row: 2;
1359
+ }
1360
+
1361
+ [data-ui='timeline'][data-orientation='vertical']:not([data-reflow='false'])
1362
+ [data-part='item']:nth-child(n):has(> [data-part='opposite'])
1363
+ > [data-part='rail'] {
1364
+ grid-row: 1 / 3;
1365
+ }
1366
+
1367
+ [data-ui='timeline'][data-orientation='vertical']:not([data-reflow='false'])
1368
+ [data-part='item']:nth-child(n)
1369
+ > [data-part='content'] {
1370
+ grid-column: content;
1371
+ text-align: start;
1372
+ }
1373
+
1374
+ [data-ui='timeline'][data-orientation='vertical']:not([data-reflow='false'])
1375
+ [data-part='item']:nth-child(n)
1376
+ :is([data-part='actor'], [data-part='actions']) {
1377
+ justify-content: flex-start;
1378
+ }
1379
+
1380
+ [data-ui='timeline'][data-orientation='vertical']:not([data-reflow='false'])
1381
+ [data-part='more'] {
1382
+ margin-inline-start: calc(var(--_rail) + var(--_gap));
1383
+ }
1384
+ }
1385
+
1386
+ @container slotted-timeline (max-width: 30rem) {
1387
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1388
+ [data-part='list'] {
1389
+ display: flex;
1390
+ flex-direction: column;
1391
+ overflow-x: visible;
1392
+ padding: 0;
1393
+ }
1394
+
1395
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1396
+ [data-part='item'] {
1397
+ align-items: stretch;
1398
+ column-gap: var(--_gap);
1399
+ grid-template-columns: [rail] var(--_rail) [content] minmax(0, 1fr);
1400
+ grid-template-rows: none;
1401
+ margin-block-end: var(--_item-gap);
1402
+ padding-inline-end: 0;
1403
+ row-gap: 0;
1404
+ }
1405
+
1406
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1407
+ [data-part='item']:last-child {
1408
+ margin-block-end: 0;
1409
+ }
1410
+
1411
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1412
+ [data-part='rail'] {
1413
+ align-items: center;
1414
+ align-self: stretch;
1415
+ block-size: auto;
1416
+ grid-column: rail;
1417
+ /* Two rows when the opposite side takes the first; one row otherwise. */
1418
+ grid-row: 1 / 3;
1419
+ justify-content: flex-start;
1420
+ }
1421
+
1422
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1423
+ [data-part='marker'] {
1424
+ --_lift: round(nearest, calc((1lh - var(--_marker)) / 2), 1px);
1425
+
1426
+ margin-inline-start: 0;
1427
+ }
1428
+
1429
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1430
+ [data-part='connector'] {
1431
+ border-block-start-width: 0;
1432
+ border-inline-start-width: var(--_rule);
1433
+ block-size: auto;
1434
+ inline-size: 0;
1435
+ inset-block-end: calc(-0.5lh - var(--_item-gap));
1436
+ inset-block-start: 0.5lh;
1437
+ inset-inline-end: auto;
1438
+ inset-inline-start: calc(50% - var(--_rule) / 2);
1439
+ }
1440
+
1441
+ /**
1442
+ * `:nth-child(n)` weighs the same as the parity the alternate rules name,
1443
+ * so the column wins over them here; a container query adds no weight of
1444
+ * its own.
1445
+ */
1446
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1447
+ [data-part='item']:nth-child(n)
1448
+ > [data-part='opposite'] {
1449
+ align-self: baseline;
1450
+ grid-column: content;
1451
+ grid-row: 1;
1452
+ margin-block: 0;
1453
+ text-align: start;
1454
+ }
1455
+
1456
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1457
+ [data-part='item']:nth-child(n)
1458
+ > [data-part='content'] {
1459
+ align-self: baseline;
1460
+ grid-column: content;
1461
+ grid-row: auto;
1462
+ margin-block: 0;
1463
+ text-align: start;
1464
+ }
1465
+
1466
+ [data-ui='timeline'][data-orientation='horizontal']:not([data-reflow='false'])
1467
+ [data-part='more'] {
1468
+ margin-block-start: var(--_item-gap);
1469
+ margin-inline-start: calc(var(--_rail) + var(--_gap));
1470
+ }
1471
+
1472
+ /**
1473
+ * Alternate keeps one side: two columns of a hundred and eighty pixels each
1474
+ * are two columns of three words. The opposite side takes the line above
1475
+ * the body, in reading order.
1476
+ */
1477
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1478
+ [data-reflow='false']
1479
+ )
1480
+ [data-part='item'] {
1481
+ grid-template-columns: [rail] var(--_rail) [content] minmax(0, 1fr);
1482
+ }
1483
+
1484
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1485
+ [data-reflow='false']
1486
+ )
1487
+ [data-part='item']
1488
+ :is([data-part='content'], [data-part='opposite']) {
1489
+ grid-column: content;
1490
+ text-align: start;
1491
+ }
1492
+
1493
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1494
+ [data-reflow='false']
1495
+ )
1496
+ [data-part='item']
1497
+ [data-part='opposite'] {
1498
+ grid-row: 1;
1499
+ }
1500
+
1501
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1502
+ [data-reflow='false']
1503
+ )
1504
+ [data-part='item']:has(> [data-part='opposite'])
1505
+ > [data-part='content'] {
1506
+ grid-row: 2;
1507
+ }
1508
+
1509
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1510
+ [data-reflow='false']
1511
+ )
1512
+ [data-part='item']:has(> [data-part='opposite'])
1513
+ > [data-part='rail'] {
1514
+ grid-row: 1 / 3;
1515
+ }
1516
+
1517
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1518
+ [data-reflow='false']
1519
+ )
1520
+ [data-part='item']:nth-child(n)
1521
+ [data-part='content'] {
1522
+ grid-template-columns: minmax(0, 1fr) auto;
1523
+ }
1524
+
1525
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1526
+ [data-reflow='false']
1527
+ )
1528
+ [data-part='item']:nth-child(n)
1529
+ [data-part='content']
1530
+ > [data-part='title'] {
1531
+ grid-column: 1;
1532
+ }
1533
+
1534
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1535
+ [data-reflow='false']
1536
+ )
1537
+ [data-part='item']:nth-child(n)
1538
+ [data-part='content']
1539
+ > [data-part='time'] {
1540
+ grid-column: 2;
1541
+ margin-inline: var(--_gap) 0;
1542
+ }
1543
+
1544
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1545
+ [data-reflow='false']
1546
+ )
1547
+ [data-part='item']
1548
+ :is([data-part='actor'], [data-part='actions']) {
1549
+ justify-content: flex-start;
1550
+ }
1551
+
1552
+ [data-ui='timeline'][data-orientation='vertical'][data-placement='alternate']:not(
1553
+ [data-reflow='false']
1554
+ )
1555
+ [data-part='more'] {
1556
+ margin-inline-start: calc(var(--_rail) + var(--_gap));
1557
+ }
1558
+ }
1559
+
1560
+ /* ── Forced colours ─────────────────────────────────────────────────────
1561
+ *
1562
+ * The system replaces every painted surface with the page's own background
1563
+ * and every border with one ink, and drops every shadow. The shapes survive
1564
+ * that — core, hollow, filled, filled and ringed — because the filled disc
1565
+ * is drawn in borders, the core keeps the system's text ink through
1566
+ * `forced-color-adjust`, and the current ring is redrawn here as an outline.
1567
+ * A finished marker keeps its fill, so it reads apart from a pending one in
1568
+ * the system's highlight. The chevron and the check are `Glyph` elements
1569
+ * drawn in `currentColor`, which the system has already forced.
1570
+ */
1571
+
1572
+ @media (forced-colors: active) {
1573
+ /* The core in the marker's own ink, which the system has already forced. */
1574
+ [data-ui='timeline'] [data-part='marker']:empty::after {
1575
+ background-color: currentColor;
1576
+ forced-color-adjust: none;
1577
+ }
1578
+
1579
+ [data-ui='timeline'] [data-complete]:not([data-current]) [data-part='marker'] {
1580
+ background-color: Highlight;
1581
+ border-color: Highlight;
1582
+ color: HighlightText;
1583
+ forced-color-adjust: none;
1584
+ }
1585
+
1586
+ /* The check in the finished disc's forced ink, not the author's ground. */
1587
+ [data-ui='timeline'] [data-part='marker'] > [data-ui='icon'][data-check] {
1588
+ color: inherit;
1589
+ }
1590
+
1591
+ [data-ui='timeline'] [data-current]:not([data-loading]) [data-part='marker'] {
1592
+ outline: var(--_edge) solid CanvasText;
1593
+ outline-offset: var(--slotted-border-width-thick, 2px);
1594
+ }
1595
+ }
1596
+ }