@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,1584 @@
1
+ /**
2
+ * The percentage every part of an indicator reads, registered so it can be
3
+ * transitioned at all: an unregistered custom property has no type and
4
+ * interpolates by discrete jumps.
5
+ *
6
+ * `inherits: true` is the whole mechanism. One transition on the root moves
7
+ * the number, and the four things derived from it — the indicator's length,
8
+ * the trailing track's start, the arc's dash and the buffer's edge — recompute
9
+ * together on every frame. Transitioning each of them separately is what makes
10
+ * a bar whose fill arrives before its gap does, which is the defect this
11
+ * costs one declaration to prevent.
12
+ */
13
+ @property --slotted-progress-indicator-percent {
14
+ syntax: '<number>';
15
+ inherits: true;
16
+ initial-value: 0;
17
+ }
18
+
19
+ @property --slotted-progress-indicator-buffer-percent {
20
+ syntax: '<number>';
21
+ inherits: true;
22
+ initial-value: 0;
23
+ }
24
+
25
+ /**
26
+ * Where the travelling bar of an indeterminate indicator starts and how long
27
+ * it is, as percentages of the track.
28
+ *
29
+ * The keyframes move these two numbers rather than the bar's own box, because
30
+ * two things read them: the plain bar sets its inset and its length from them,
31
+ * and the wavy bar cuts a window out of its path at the same two numbers. One
32
+ * clock, read twice, is what keeps a wavy bar and a plain one travelling the
33
+ * same journey.
34
+ */
35
+ @property --slotted-progress-indicator-travel-start {
36
+ syntax: '<number>';
37
+ inherits: true;
38
+ initial-value: 0;
39
+ }
40
+
41
+ @property --slotted-progress-indicator-travel-size {
42
+ syntax: '<number>';
43
+ inherits: true;
44
+ initial-value: 100;
45
+ }
46
+
47
+ /**
48
+ * Where the wave is in its own cycle, from nought to one.
49
+ *
50
+ * A registered number rather than an animated dash offset, because a keyframe
51
+ * moving `stroke-dashoffset` between two `var()`-bearing values interpolated
52
+ * discretely: the arc held still for half a cycle and then jumped 45 degrees.
53
+ * A registered `<number>` interpolates by its declared syntax, and every
54
+ * angle, length and dash derived from it recomputes each frame.
55
+ *
56
+ * It inherits, because the paths read it from the root.
57
+ */
58
+ @property --slotted-progress-indicator-wave-phase {
59
+ syntax: '<number>';
60
+ inherits: true;
61
+ initial-value: 0;
62
+ }
63
+
64
+ /**
65
+ * How high the crests stand, from nought to one.
66
+ *
67
+ * `WavyProgressIndicatorDefaults.indicatorAmplitude` is M3's own API for this:
68
+ * a function of the progress, whose default is a switch — nought at or below
69
+ * a tenth and at or above ninety-five hundredths — that a 500-millisecond
70
+ * animator then chases. That switch is what made the wave die all at once at
71
+ * ninety-five per cent while the fill still had a visible remainder, and
72
+ * arrive all at once at ten. The function here is a ramp instead: nought to
73
+ * six per cent, rising to one at sixteen, one until eighty-four, falling to
74
+ * nought at ninety-six. The fill's own spring is what moves it, so the crests
75
+ * settle as the bar arrives rather than on a timer of their own.
76
+ *
77
+ * It is registered so the paths read an interpolated number each frame, and so
78
+ * the ramp follows the transitioning percentage rather than the stated value.
79
+ */
80
+ @property --slotted-progress-indicator-wave-amplitude {
81
+ syntax: '<number>';
82
+ inherits: true;
83
+ initial-value: 1;
84
+ }
85
+
86
+ @layer slotted.components {
87
+ /* ── The box ────────────────────────────────────────────────────────────
88
+ *
89
+ * A quantity a consumer states and this family draws. It is never a
90
+ * command: it answers no hover, gives no press feedback, takes no focus and
91
+ * has nothing to disable — a component that moved its own value would be
92
+ * reporting a task it cannot see.
93
+ *
94
+ * Three geometries share this box, and `data-shape` is what tells them
95
+ * apart. It is written by the component rather than chosen by a consumer,
96
+ * for the reason `button` writes `data-shape` for its icon member: one
97
+ * authored stylesheet has to know which of three things it is painting, and
98
+ * a prop would be a fourth way to pick a component already picked.
99
+ */
100
+ [data-ui='progress-indicator'][data-part='root'] {
101
+ /**
102
+ * The ink, and it is the tone's `text` role rather than its `solid` one.
103
+ * Two reasons, and the second is the one that decides it. The indicator is
104
+ * a graphic bearing meaning, so WCAG 2.2 1.4.11 binds it at 3:1; the tone
105
+ * `text` roles are the steps the theme's contrast test holds to the 4.5:1
106
+ * reading ratio against every surface, which clears it with room over.
107
+ * And `--slotted-tone-secondary-solid` is the surface's own colour in
108
+ * `light` and the page's own white in `high-contrast` — a secondary bar
109
+ * painted from it would be an invisible bar in two schemes out of three,
110
+ * which is the deviation `button` recorded and could not fix inside its
111
+ * own family. `status-indicator` reached the same role for the same
112
+ * measurement.
113
+ */
114
+ --_ink: var(--slotted-progress-indicator-indicator-color, var(--slotted-tone-accent-text));
115
+ /**
116
+ * The ground, computed rather than taken from the tone's `surface` role,
117
+ * and the three schemes are why. That role is `#dbeafe` in `light` — a
118
+ * pale blue, exactly right — and then `#181b22` in `dark`, one step off a
119
+ * `#131316` page, and `#ffffff` in `high-contrast`, which *is* the page.
120
+ * A track painted from it disappears in two schemes.
121
+ *
122
+ * Mixing the ink toward the surface moves away from the ground in every
123
+ * scheme instead, because the ink is the far end of each scheme's own
124
+ * range. Twenty-four per cent is measured rather than chosen: it is the
125
+ * lowest ratio at which the track separates from the page in all three
126
+ * schemes at the 2-pixel `sm` thickness, and low enough that a column of
127
+ * bars does not read as a column of filled bars.
128
+ */
129
+ --_track-color: var(
130
+ --slotted-progress-indicator-track-color,
131
+ color-mix(in oklab, var(--_ink) 24%, var(--slotted-surface))
132
+ );
133
+ /**
134
+ * The second quantity, halfway between the two: received but not yet
135
+ * processed. It has to be legible as *neither* — a buffer the colour of
136
+ * the track says nothing has arrived, and a buffer the colour of the
137
+ * indicator says the work is done.
138
+ */
139
+ --_buffer-color: var(--slotted-progress-indicator-buffer-color, transparent);
140
+ --_percent: var(--slotted-progress-indicator-percent, 0);
141
+ --_buffer-percent: var(--slotted-progress-indicator-buffer-percent, 0);
142
+ /** The further of the two edges, which is where the remaining track starts. */
143
+ --_lead: max(var(--_percent), var(--_buffer-percent));
144
+
145
+ /**
146
+ * Type from the control scale, the only one the theme restates per
147
+ * density, so a bar in a dense table shrinks its words and keeps its bar.
148
+ * The gap between the words comes from the space scale: a density step
149
+ * inside a distance this small would be sub-pixel, and density already
150
+ * moves the type.
151
+ */
152
+ --_font-size: var(--slotted-control-font-size-md, 13px);
153
+ --_text-gap: var(--slotted-space-4, 0.5rem);
154
+ --_row-gap: var(--slotted-space-3, 0.375rem);
155
+ --_thickness: var(--slotted-progress-indicator-thickness, var(--slotted-space-2, 0.25rem));
156
+ --_size: var(--slotted-progress-indicator-size, var(--slotted-control-height-md, 34px));
157
+ --_radius: var(--slotted-progress-indicator-radius, var(--slotted-radius-full, 9999px));
158
+ /**
159
+ * M3's indicator–track gap, and the stop indicator's diameter, are both
160
+ * 4dp at the 4dp track. Making them one thickness is the proportional form
161
+ * of that decision rather than a copy of the number: a 4-pixel gap in a
162
+ * 2-pixel bar would be wider than the bar is tall.
163
+ */
164
+ --_gap: var(--slotted-progress-indicator-gap, var(--_thickness));
165
+
166
+ font-family: var(--slotted-control-font-family, sans-serif);
167
+ font-size: var(--_font-size);
168
+ line-height: var(--slotted-line-height-md, 1.5);
169
+ /**
170
+ * The indicator never widens the box that holds it: its own minimum is
171
+ * the longest word of a label it did not choose, and a grid or flex parent
172
+ * sizes its track from that minimum. Zero hands the decision back to the
173
+ * container, and the label breaks instead — the correction
174
+ * `status-indicator` recorded after a container digest pushed a 220-pixel
175
+ * column out to 420.
176
+ */
177
+ min-inline-size: 0;
178
+ /**
179
+ * The one transition, and everything derived from the number moves with
180
+ * it: the fill, the gap, the arc, the buffer's edge and the wave's window.
181
+ *
182
+ * The curve is M3's own. `ProgressIndicatorDefaults.ProgressAnimationSpec`
183
+ * is a critically damped spring at `StiffnessVeryLow` — stiffness 50, no
184
+ * bounce — which settles to a thousandth in 1.3 seconds and has covered
185
+ * nine tenths of the distance after half of one. `linear()` writes that
186
+ * spring out: `1 - (1 + ωt)·e^(-ωt)` sampled at twenty steps, with `ωT`
187
+ * at 9.2. A bar that arrives fast and eases into place over the last
188
+ * second is what makes the wave's flatten, which starts at ninety-five per
189
+ * cent, land while the fill is still settling rather than after it has
190
+ * stopped.
191
+ *
192
+ * Four times `--slotted-duration-long` is the 1.3 seconds of that spring,
193
+ * from the scale rather than a literal, and the theme collapses the scale
194
+ * under `prefers-reduced-motion`, so a reader who asked for stillness gets
195
+ * the number without the travel.
196
+ */
197
+ --_settle: calc(4 * var(--slotted-duration-long));
198
+ --_spring: linear(
199
+ 0,
200
+ 0.078,
201
+ 0.235,
202
+ 0.401,
203
+ 0.549,
204
+ 0.669,
205
+ 0.762,
206
+ 0.831,
207
+ 0.882,
208
+ 0.918,
209
+ 0.944,
210
+ 0.962,
211
+ 0.974,
212
+ 0.982,
213
+ 0.988,
214
+ 0.992,
215
+ 0.995,
216
+ 0.996,
217
+ 0.998,
218
+ 0.998,
219
+ 1
220
+ );
221
+ transition:
222
+ --slotted-progress-indicator-percent var(--_settle) var(--_spring),
223
+ --slotted-progress-indicator-buffer-percent var(--_settle) var(--_spring);
224
+ }
225
+
226
+ /**
227
+ * The withheld indicator. `delay` has not elapsed, so there is nothing to
228
+ * report yet and nothing is drawn — no box, no role, no reserved band. The
229
+ * rule exists because the root's own `display` would otherwise win over the
230
+ * user agent's `[hidden]`, and a family that painted through its own
231
+ * withdrawal would flash exactly what the delay exists to prevent.
232
+ */
233
+ [data-ui='progress-indicator'][hidden] {
234
+ display: none;
235
+ }
236
+
237
+ /* ── Tone ─────────────────────────────────────────────────────────────
238
+ *
239
+ * Five colours the theme owns, and the family adds none. Only the ink is
240
+ * stated: the track and the buffer are derived from it, so a sixth tone
241
+ * added by an application is one declaration rather than three.
242
+ */
243
+
244
+ /**
245
+ * The one tone whose ink is not its own `text` role, and the capture is what
246
+ * decided it. `--slotted-tone-secondary-text` is `#172033` in `light` and
247
+ * `#000000` in `high-contrast`: a background job drawn from it was the
248
+ * darkest thing on the page, which is the opposite of what `secondary`
249
+ * means. `--slotted-border-strong` is the neutral the theme holds to 3:1
250
+ * against every surface it separates — the ratio a meaningful graphic needs
251
+ * and no more — so a secondary indicator reads as grey rather than as ink.
252
+ * It is the same substitution `badge` and `status-indicator` make for their
253
+ * own neutral marks, for the same reason.
254
+ */
255
+ [data-ui='progress-indicator'][data-part='root'][data-tone='secondary'] {
256
+ --_ink: var(--slotted-progress-indicator-indicator-color, var(--slotted-border-strong));
257
+ }
258
+
259
+ [data-ui='progress-indicator'][data-part='root'][data-tone='success'] {
260
+ --_ink: var(--slotted-progress-indicator-indicator-color, var(--slotted-tone-success-text));
261
+ }
262
+
263
+ [data-ui='progress-indicator'][data-part='root'][data-tone='warning'] {
264
+ --_ink: var(--slotted-progress-indicator-indicator-color, var(--slotted-tone-warning-text));
265
+ }
266
+
267
+ [data-ui='progress-indicator'][data-part='root'][data-tone='danger'] {
268
+ --_ink: var(--slotted-progress-indicator-indicator-color, var(--slotted-tone-danger-text));
269
+ }
270
+
271
+ [data-ui='progress-indicator'][data-part='root'][data-buffered] {
272
+ --_buffer-color: var(
273
+ --slotted-progress-indicator-buffer-color,
274
+ color-mix(in oklab, var(--_ink) 52%, var(--slotted-surface))
275
+ );
276
+ }
277
+
278
+ /* ── Size ─────────────────────────────────────────────────────────────
279
+ *
280
+ * One axis, three geometries. The bar reads a thickness from the space
281
+ * scale — 2, 4 and 8 pixels, and M3's own default track is the middle one —
282
+ * while the circle and the loader read the control height scale, so a
283
+ * spinner standing beside a button is the same height as the button at
284
+ * every density. Neither needs a scale of this family's own, and the
285
+ * component layer of the token contract stays as `avatar` left it.
286
+ */
287
+
288
+ [data-ui='progress-indicator'][data-part='root'][data-size='sm'] {
289
+ --_font-size: var(--slotted-control-font-size-sm, 12px);
290
+ --_text-gap: var(--slotted-space-3, 0.375rem);
291
+ --_thickness: var(--slotted-progress-indicator-thickness, var(--slotted-space-1, 0.125rem));
292
+ --_size: var(--slotted-progress-indicator-size, var(--slotted-control-height-sm, 30px));
293
+ }
294
+
295
+ [data-ui='progress-indicator'][data-part='root'][data-size='lg'] {
296
+ --_font-size: var(--slotted-control-font-size-lg, 14px);
297
+ --_row-gap: var(--slotted-space-4, 0.5rem);
298
+ --_thickness: var(--slotted-progress-indicator-thickness, var(--slotted-space-4, 0.5rem));
299
+ --_size: var(--slotted-progress-indicator-size, var(--slotted-control-height-lg, 38px));
300
+ }
301
+
302
+ /* ── The words ────────────────────────────────────────────────────────
303
+ *
304
+ * The label wraps and the value does not. A label is a sentence a consumer
305
+ * wrote in a language this family does not know — “Verificando assinaturas
306
+ * do pacote” is four words that have to break somewhere — and a number is
307
+ * never improved by being broken in half.
308
+ *
309
+ * `overflow-wrap: break-word` rather than `anywhere`, which is the
310
+ * distinction `status-indicator` was caught by: both break the unbreakable
311
+ * token, and only `anywhere` also reports a min-content size of one
312
+ * character, so a grid column sized to its content collapses to that
313
+ * character.
314
+ */
315
+ [data-ui='progress-indicator'] [data-part='label'] {
316
+ color: var(--slotted-text, CanvasText);
317
+ min-inline-size: 0;
318
+ overflow-wrap: break-word;
319
+ }
320
+
321
+ /**
322
+ * Tabular figures, and the reason is the whole point of the part: the value
323
+ * changes while the reader is looking at it, and a proportional `1` is
324
+ * narrower than a `7`, so a counting bar jitters its own label. The colour
325
+ * is the muted step, because the number annotates the bar rather than
326
+ * naming it.
327
+ */
328
+ [data-ui='progress-indicator'] [data-part='value'] {
329
+ color: var(--slotted-text-muted, GrayText);
330
+ font-variant-numeric: tabular-nums;
331
+ white-space: nowrap;
332
+ }
333
+
334
+ /* ── Linear ───────────────────────────────────────────────────────────
335
+ *
336
+ * Two rows of a grid: the label and the value share the first, and the
337
+ * track spans both columns of the second. The empty row costs nothing when
338
+ * neither word is rendered, because a part a consumer did not ask for is
339
+ * absent from the DOM rather than present and empty — so auto-placement
340
+ * puts the track in the first row and the row gap never applies.
341
+ *
342
+ * `justify-self: end` and `grid-column` are logical: the value sits at the
343
+ * inline end, which is the right edge of an English page and the left edge
344
+ * of an Arabic one, with no mirrored rule to write.
345
+ */
346
+ [data-ui='progress-indicator'][data-part='root'][data-shape='linear'] {
347
+ align-items: baseline;
348
+ column-gap: var(--_text-gap);
349
+ display: grid;
350
+ grid-template-columns: 1fr auto;
351
+ row-gap: var(--_row-gap);
352
+ }
353
+
354
+ [data-ui='progress-indicator'][data-shape='linear'] > [data-part='label'] {
355
+ grid-column: 1;
356
+ }
357
+
358
+ [data-ui='progress-indicator'][data-shape='linear'] > [data-part='value'] {
359
+ grid-column: 2;
360
+ justify-self: end;
361
+ }
362
+
363
+ [data-ui='progress-indicator'][data-shape='linear'] > [data-part='track'] {
364
+ grid-column: 1 / -1;
365
+ }
366
+
367
+ /**
368
+ * The track, which paints the buffer and nothing else. The remaining track
369
+ * is a pseudo-element rather than this element's own background, because
370
+ * M3's gap is air between two rounded bars: a background that ran the whole
371
+ * length would fill it, and a gradient stopped at a percentage would need a
372
+ * mirrored copy for every right-to-left document.
373
+ *
374
+ * `overflow: clip` is what the indeterminate indicator travels inside, and
375
+ * it is what rounds the travelling bar's ends against the track's own. It is
376
+ * stated per axis rather than once, because a wave is taller than the band
377
+ * it belongs to: the inline axis still holds the travelling bar, and the
378
+ * block axis has to let a crest out. A single `clip` is what sliced the
379
+ * crests flat in the first capture of this family.
380
+ */
381
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='track'] {
382
+ background-color: var(--_buffer-color);
383
+ block-size: var(--_thickness);
384
+ border-radius: var(--_radius);
385
+ inline-size: 100%;
386
+ isolation: isolate;
387
+ overflow-x: clip;
388
+ overflow-y: visible;
389
+ position: relative;
390
+ }
391
+
392
+ /**
393
+ * The track that is left, from one gap past the further of the two edges to
394
+ * the end. It carries the same radius, so the indicator and the remainder
395
+ * read as two bars with air between them rather than as one bar with a
396
+ * notch — the shape M3 took in 2024 and the one no other surveyed
397
+ * implementation draws.
398
+ */
399
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='track']::before {
400
+ background-color: var(--_track-color);
401
+ border-radius: inherit;
402
+ content: '';
403
+ inset-block: 0;
404
+ inset-inline-end: 0;
405
+ inset-inline-start: calc(1% * var(--_lead) + var(--_gap));
406
+ position: absolute;
407
+ }
408
+
409
+ /**
410
+ * The stop indicator: a dot of one thickness at the end of the track, in the
411
+ * indicator's own ink. It is what tells a reader where the bar is going,
412
+ * and it is the detail that makes a bar at four per cent read as four per
413
+ * cent of something rather than as a stray mark.
414
+ */
415
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='track']::after {
416
+ background-color: var(--_ink);
417
+ border-radius: var(--slotted-radius-full, 9999px);
418
+ content: '';
419
+ inline-size: var(--_thickness);
420
+ inset-block: 0;
421
+ inset-inline-end: 0;
422
+ position: absolute;
423
+ }
424
+
425
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='indicator'] {
426
+ background-color: var(--_ink);
427
+ block-size: 100%;
428
+ border-radius: inherit;
429
+ inline-size: calc(1% * var(--_percent));
430
+ /* Above the remaining track, which is painted by the pseudo-element the
431
+ browser draws before this element's own box. */
432
+ position: relative;
433
+ }
434
+
435
+ /**
436
+ * Complete, and there is nothing left to separate. The remaining track goes
437
+ * and the stop indicator goes with it: a dot in the indicator's colour on
438
+ * top of a full indicator is a dot nobody can see, and leaving the gap
439
+ * would put a notch a whole thickness wide at the end of a finished bar.
440
+ *
441
+ * The switch reads the transitioning percentage rather than `data-complete`.
442
+ * The attribute is written from the stated value the moment it reaches the
443
+ * top of the range, and the fill takes 1.3 seconds to get there: keyed on
444
+ * the attribute, the remaining track vanished while the fill was still half
445
+ * way along it. Keyed on the number, it goes when the fill arrives.
446
+ */
447
+ @container style(--slotted-progress-indicator-percent: 100) {
448
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='track']::before,
449
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='track']::after {
450
+ content: none;
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Indeterminate: the whole track shows, there is no end to mark, and the
456
+ * indicator becomes a short bar travelling the length of it. It is absolutely
457
+ * positioned so the travel is one property — the decision the family this
458
+ * one replaces recorded, and `inset-inline-start` is why a translation runs
459
+ * the way the document reads instead of the way the screen does.
460
+ */
461
+ [data-ui='progress-indicator'][data-shape='linear'][data-indeterminate]
462
+ [data-part='track']::before {
463
+ inset-inline-start: 0;
464
+ }
465
+
466
+ [data-ui='progress-indicator'][data-shape='linear'][data-indeterminate]
467
+ [data-part='track']::after {
468
+ content: none;
469
+ }
470
+
471
+ [data-ui='progress-indicator'][data-shape='linear'][data-indeterminate] [data-part='indicator'] {
472
+ animation: slotted-progress-indicator-travel var(--slotted-duration-loop-slow)
473
+ var(--slotted-easing-standard) infinite;
474
+ inline-size: calc(1% * var(--slotted-progress-indicator-travel-size));
475
+ inset-block: 0;
476
+ inset-inline-start: calc(1% * var(--slotted-progress-indicator-travel-start));
477
+ position: absolute;
478
+ }
479
+
480
+ /**
481
+ * A short bar that grows as it crosses and shrinks as it leaves, which is
482
+ * M3's disjoint indicator drawn with one bar rather than two. The width
483
+ * changes because a bar of constant length reads as a scrollbar being
484
+ * dragged; a bar that swells in the middle of its journey reads as
485
+ * something in progress.
486
+ *
487
+ * The keyframes move the two registered numbers rather than the box, so the
488
+ * wavy bar can cut its window at the same two numbers.
489
+ */
490
+ @keyframes slotted-progress-indicator-travel {
491
+ 0% {
492
+ --slotted-progress-indicator-travel-size: 14;
493
+ --slotted-progress-indicator-travel-start: -14;
494
+ }
495
+
496
+ 45% {
497
+ --slotted-progress-indicator-travel-size: 46;
498
+ }
499
+
500
+ 100% {
501
+ --slotted-progress-indicator-travel-size: 14;
502
+ --slotted-progress-indicator-travel-start: 100;
503
+ }
504
+ }
505
+
506
+ /* ── The page-level bar ───────────────────────────────────────────────
507
+ *
508
+ * The whole of what `loading-bar` was. It is pinned to the top edge of the
509
+ * viewport, it has no corner to round because it has no corner, and its
510
+ * track is transparent: a permanent grey band across the top of a page
511
+ * reads as a rule under the header rather than as a wait — the decision
512
+ * that family recorded.
513
+ *
514
+ * The stacking layer is a token, so an application's own stacking contract
515
+ * still decides. Every router progress bar in the field types a number here.
516
+ */
517
+ [data-ui='progress-indicator'][data-part='root'][data-placement='page'] {
518
+ --_radius: var(--slotted-progress-indicator-radius, var(--slotted-radius-none, 0px));
519
+ --_track-color: var(--slotted-progress-indicator-track-color, transparent);
520
+ --_gap: var(--slotted-progress-indicator-gap, 0);
521
+
522
+ inset-block-start: 0;
523
+ inset-inline: 0;
524
+ position: fixed;
525
+ z-index: var(--slotted-z-index-sticky, 100);
526
+ }
527
+
528
+ [data-ui='progress-indicator'][data-placement='page'] [data-part='track']::after {
529
+ content: none;
530
+ }
531
+
532
+ /* ── The edge bar ─────────────────────────────────────────────────────
533
+ *
534
+ * The page-level bar on the edge of any box instead of the viewport's: the
535
+ * block-start edge under `edge-start`, the block-end edge under `edge-end`,
536
+ * the whole inline size of the nearest positioned box. It is drawn over the
537
+ * line that edge already draws — a seam, a card's border, a header's
538
+ * hairline — so it reserves nothing and nothing moves when it comes or
539
+ * goes. The box is the placer's, and its corners clip the bar: `app-shell`
540
+ * and `page-header` each render one as their `progress` part.
541
+ *
542
+ * No stacking layer of its own. The bar is the last thing its box paints,
543
+ * and the box decides where it stands among its neighbours.
544
+ *
545
+ * `--slotted-progress-indicator-position` is the box's answer when the box
546
+ * is taller than the screen: `sticky` keeps the bar on the visible part of
547
+ * the edge. `app-shell` writes it for a content region that scrolls with
548
+ * the document. It is a composition value, not a design token.
549
+ */
550
+ [data-ui='progress-indicator'][data-part='root']:is(
551
+ [data-placement='edge-start'],
552
+ [data-placement='edge-end']
553
+ ) {
554
+ --_radius: var(--slotted-progress-indicator-radius, var(--slotted-radius-none, 0px));
555
+ --_track-color: var(--slotted-progress-indicator-track-color, transparent);
556
+ --_gap: var(--slotted-progress-indicator-gap, 0);
557
+
558
+ inset-inline: 0;
559
+ position: var(--slotted-progress-indicator-position, absolute);
560
+ }
561
+
562
+ [data-ui='progress-indicator'][data-part='root'][data-placement='edge-start'] {
563
+ inset-block-start: 0;
564
+ }
565
+
566
+ [data-ui='progress-indicator'][data-part='root'][data-placement='edge-end'] {
567
+ inset-block-end: 0;
568
+ }
569
+
570
+ [data-ui='progress-indicator']:is([data-placement='edge-start'], [data-placement='edge-end'])
571
+ [data-part='track']::after {
572
+ content: none;
573
+ }
574
+
575
+ /**
576
+ * A bar standing on an edge grows out of the line it stands on and goes
577
+ * back into it, which is M3's `outward` show and `inward` hide for a linear
578
+ * indicator. It is a clip rather than a scale: the root holds the label,
579
+ * clipped but read, and a box that holds text is never scaled. The clip
580
+ * starts from no thickness rather than from most of one, because the line
581
+ * it grows out of is already drawn, so the bar never comes from nowhere,
582
+ * and a bar that began at nine tenths of two pixels would be a bar that
583
+ * blinked on. The opacity travels with it so the first frames are not a
584
+ * hard sliver of ink.
585
+ *
586
+ * `inset()` takes physical sides and has no logical form; the top and the
587
+ * bottom are the block-start and block-end edges in every horizontal
588
+ * writing mode, which is every mode the library lays out. The page-level
589
+ * bar shares the motion, as the edge bar of the viewport.
590
+ *
591
+ * Enter is the medium step on the entering curve; exit is the short step on
592
+ * the leaving one, so a wait that has ended is gone before the eye returns
593
+ * to it. `display` is transitioned as a discrete property, so `hidden` —
594
+ * the withheld and withdrawn state — takes effect once the bar has gone
595
+ * back into its edge rather than on the frame it is asked to. The theme
596
+ * collapses both steps under `prefers-reduced-motion`.
597
+ */
598
+ [data-ui='progress-indicator'][data-part='root']:is(
599
+ [data-placement='page'],
600
+ [data-placement='edge-start'],
601
+ [data-placement='edge-end']
602
+ ) {
603
+ --_withdrawn: inset(0 0 100% 0);
604
+
605
+ clip-path: inset(0);
606
+ transition:
607
+ --slotted-progress-indicator-percent var(--_settle) var(--_spring),
608
+ --slotted-progress-indicator-buffer-percent var(--_settle) var(--_spring),
609
+ clip-path var(--slotted-duration-medium) var(--slotted-easing-enter),
610
+ opacity var(--slotted-duration-medium) var(--slotted-easing-enter);
611
+
612
+ @starting-style {
613
+ clip-path: var(--_withdrawn);
614
+ opacity: 0;
615
+ }
616
+ }
617
+
618
+ [data-ui='progress-indicator'][data-part='root'][data-placement='edge-end'] {
619
+ --_withdrawn: inset(100% 0 0 0);
620
+ }
621
+
622
+ [data-ui='progress-indicator'][data-part='root']:is(
623
+ [data-placement='page'],
624
+ [data-placement='edge-start'],
625
+ [data-placement='edge-end']
626
+ )[hidden] {
627
+ clip-path: var(--_withdrawn);
628
+ opacity: 0;
629
+ transition:
630
+ clip-path var(--slotted-duration-short) var(--slotted-easing-exit),
631
+ opacity var(--slotted-duration-short) var(--slotted-easing-exit),
632
+ display var(--slotted-duration-short) allow-discrete;
633
+ }
634
+
635
+ /* ── Circular ─────────────────────────────────────────────────────────
636
+ *
637
+ * The ring, the name and the number on one line. The number sits beside the
638
+ * ring rather than inside it, and that is a measurement rather than a
639
+ * preference: the largest ring this family draws is 38 pixels across with a
640
+ * 4.75-pixel stroke, which leaves 28 pixels of hole, and a percentage
641
+ * centred in it would be four-pixel type. Ant Design and MUI both put it
642
+ * inside because both draw rings twice this size.
643
+ */
644
+ [data-ui='progress-indicator'][data-part='root'][data-shape='circular'],
645
+ [data-ui='progress-indicator'][data-part='root'][data-shape='morph'] {
646
+ align-items: center;
647
+ column-gap: var(--_text-gap);
648
+ display: inline-flex;
649
+ }
650
+
651
+ /**
652
+ * The stroke is stated in the viewBox's own units, and six of forty-eight is
653
+ * one eighth of the diameter at every size. It is the ratio `button` derived
654
+ * for its own loading ring — “two pixels is a sixth of the ring at the dense
655
+ * small step and a ninth of it at the large one” — so the ring in a button
656
+ * and this family's circle are the same object, and Button can stop drawing
657
+ * its own.
658
+ */
659
+ [data-ui='progress-indicator'][data-shape='circular'] svg {
660
+ --_stroke: var(--slotted-progress-indicator-stroke, 6);
661
+
662
+ block-size: var(--_size);
663
+ flex: none;
664
+ inline-size: var(--_size);
665
+ /* The arc starts at twelve o'clock. An SVG angle is measured from three,
666
+ and rotating the whole box is what keeps one dash arithmetic for the
667
+ plain ring and the wavy one. */
668
+ rotate: -90deg;
669
+ }
670
+
671
+ [data-ui='progress-indicator'][data-shape='circular'] [data-part='track'],
672
+ [data-ui='progress-indicator'][data-shape='circular'] [data-part='indicator'] {
673
+ fill: none;
674
+ stroke-linecap: round;
675
+ stroke-width: var(--_stroke);
676
+ /* `pathLength="100"` is on the element, so a dash is a percentage and the
677
+ transition on the number moves the arc. The dash itself is not
678
+ transitioned: smoothing a value that is already on the spring would lag
679
+ it by a second smoothing. */
680
+ }
681
+
682
+ [data-ui='progress-indicator'][data-shape='circular'] [data-part='indicator'] {
683
+ stroke: var(--_ink);
684
+ stroke-dasharray: var(--_percent) 100;
685
+ /**
686
+ * A round cap on a dash of zero length is a dot, and a dot at twelve
687
+ * o'clock on an empty ring reads as a rendering fault. There is no `if` in
688
+ * CSS and nothing to compare against, so the opacity is a clamp: nought at
689
+ * nought per cent, one at anything above a thousandth of a per cent.
690
+ */
691
+ stroke-opacity: clamp(0, calc(var(--_percent) * 1000), 1);
692
+ }
693
+
694
+ /**
695
+ * The track is drawn only where the indicator is not, one gap short at each
696
+ * end — M3's indicator–track gap, in a circle. Three and two tenths of the
697
+ * path is four device pixels of a 44-pixel ring, which is the gap M3
698
+ * specifies for the bar.
699
+ */
700
+ [data-ui='progress-indicator'][data-shape='circular'] [data-part='track'] {
701
+ /**
702
+ * Three and two tenths of the path is four device pixels of a 44-pixel
703
+ * ring, and it collapses to nothing at nought per cent by the same clamp
704
+ * the indicator's opacity uses. The first capture is why: an empty ring
705
+ * carried a gap at twelve o'clock, which is a bite out of a circle for an
706
+ * indicator that is not there.
707
+ */
708
+ --_arc-gap: calc(3.2 * clamp(0, calc(var(--_percent) * 1000), 1));
709
+
710
+ stroke: var(--_track-color);
711
+ stroke-dasharray: calc(100 - var(--_percent) - 2 * var(--_arc-gap)) 100;
712
+ stroke-dashoffset: calc(-1 * (var(--_percent) + var(--_arc-gap)));
713
+ }
714
+
715
+ /**
716
+ * Complete, and the ring closes: the gap goes, so a finished circle is a
717
+ * ring rather than a ring with a bite out of it. It reads the transitioning
718
+ * percentage for the reason the bar does: the track has to outlast the arc's
719
+ * travel, not the stated value.
720
+ */
721
+ @container style(--slotted-progress-indicator-percent: 100) {
722
+ [data-ui='progress-indicator'][data-shape='circular'] [data-part='track'] {
723
+ stroke: transparent;
724
+ }
725
+ }
726
+
727
+ /**
728
+ * Indeterminate: no track at all, which is M3's own answer — its circular
729
+ * track colour is transparent — and the reason is sound. An arc with no
730
+ * value has no remainder to show, and a ring behind it would claim there is
731
+ * one.
732
+ *
733
+ * The arc grows and shrinks while the whole ring turns, at two periods that
734
+ * do not divide into each other, so the sweep never repeats the same shape
735
+ * in the same orientation.
736
+ */
737
+ [data-ui='progress-indicator'][data-shape='circular'][data-indeterminate] [data-part='track'] {
738
+ stroke: transparent;
739
+ }
740
+
741
+ [data-ui='progress-indicator'][data-shape='circular'][data-indeterminate] svg {
742
+ animation: slotted-progress-indicator-turn var(--slotted-duration-loop) linear infinite;
743
+ }
744
+
745
+ [data-ui='progress-indicator'][data-shape='circular'][data-indeterminate]
746
+ [data-part='indicator'] {
747
+ animation: slotted-progress-indicator-sweep var(--slotted-duration-loop-slow)
748
+ var(--slotted-easing-standard) infinite;
749
+ stroke-opacity: 1;
750
+ /* The transition belongs to a value that changes; a sweep changes every
751
+ frame, and interpolating an interpolation is what makes it stutter. */
752
+ transition: none;
753
+ }
754
+
755
+ @keyframes slotted-progress-indicator-turn {
756
+ from {
757
+ rotate: -90deg;
758
+ }
759
+
760
+ to {
761
+ rotate: 270deg;
762
+ }
763
+ }
764
+
765
+ @keyframes slotted-progress-indicator-sweep {
766
+ 0% {
767
+ stroke-dasharray: 4 100;
768
+ stroke-dashoffset: 0;
769
+ }
770
+
771
+ 50% {
772
+ stroke-dasharray: 62 100;
773
+ stroke-dashoffset: -18;
774
+ }
775
+
776
+ 100% {
777
+ stroke-dasharray: 4 100;
778
+ stroke-dashoffset: -100;
779
+ }
780
+ }
781
+
782
+ /* ── The wavy indicator ───────────────────────────────────────────────
783
+ *
784
+ * M3 Expressive's wavy active indicator, on the bar and on the ring, and
785
+ * the one capability in this family no headless library and no other
786
+ * design system surveyed ships at all.
787
+ *
788
+ * Both are drawn the way M3 draws them: the indicator is a stroked path,
789
+ * and what is shown of it is a window cut along its length with round caps
790
+ * at both ends. `LinearWavyProgressModifiers` and
791
+ * `CircularWavyProgressModifiers` cut that window with a `PathMeasure`; a
792
+ * stylesheet cuts it with a dash. Nothing is masked and nothing is clipped
793
+ * square, which is why the bar ends in its own crest at every value and the
794
+ * ring's arc keeps its cap on the wave.
795
+ *
796
+ * Three things move, and each is one number:
797
+ *
798
+ * the phase — `--slotted-progress-indicator-wave-phase`, a clock from
799
+ * nought to one that the root carries and the paths read;
800
+ * the amplitude — `--slotted-progress-indicator-wave-amplitude`, a ramp
801
+ * the value sets, that a paused animation scrubs into a
802
+ * position on `d`;
803
+ * the value — `--slotted-progress-indicator-percent`, which the dash
804
+ * reads as it already does for the plain shapes.
805
+ */
806
+
807
+ /**
808
+ * The clock. It runs on the root and the paths inherit it, and its travel
809
+ * is `linear`, which is M3's own answer: `waveSpeed` defaults to one
810
+ * wavelength per second with no easing. A crest that accelerated and slowed
811
+ * would read as a wobble rather than as a wave moving past.
812
+ *
813
+ * The crests travel toward the start of the track — against the fill — on
814
+ * both shapes. Drawn the other way, toward the finish, the wave read on
815
+ * screen as sliding back against the fill; drawn this way it reads as the
816
+ * fill flowing forward. The reading was checked on the bar and on the ring
817
+ * at fast and slow loads, and the geometry below carries the sign once per
818
+ * shape.
819
+ */
820
+ [data-ui='progress-indicator'][data-part='root'][data-wave] {
821
+ --slotted-progress-indicator-wave-amplitude: min(
822
+ clamp(0, calc((var(--_percent) - 6) / 10), 1),
823
+ clamp(0, calc((96 - var(--_percent)) / 12), 1)
824
+ );
825
+
826
+ animation: slotted-progress-indicator-wave var(--slotted-duration-loop) linear infinite;
827
+ }
828
+
829
+ /**
830
+ * An indeterminate wave has no value to ramp against and is wavy
831
+ * throughout, which is what M3 draws: `hasWavyEffect(isDeterminate)` is the
832
+ * flag that separates the two.
833
+ */
834
+ [data-ui='progress-indicator'][data-part='root'][data-wave][data-indeterminate] {
835
+ --slotted-progress-indicator-wave-amplitude: 1;
836
+ }
837
+
838
+ @keyframes slotted-progress-indicator-wave {
839
+ from {
840
+ --slotted-progress-indicator-wave-phase: 0;
841
+ }
842
+
843
+ to {
844
+ --slotted-progress-indicator-wave-phase: 1;
845
+ }
846
+ }
847
+
848
+ /* ── The wavy bar ─────────────────────────────────────────────────────
849
+ *
850
+ * The indicator is an `svg` laid over the whole track, and inside it one
851
+ * path from `@slotted/core/progress`: M3's sine, in a user space where one
852
+ * unit is one thickness. A 40dp wavelength and a 3dp amplitude on a 4dp
853
+ * stroke are ten thicknesses and three quarters of one, and the viewBox is
854
+ * sized so a unit *is* a thickness at every size and density — the box is
855
+ * twelve hundred thicknesses long and two and a half tall, which is the
856
+ * crest plus half the stroke either side of the centre line.
857
+ *
858
+ * The crest needs room the band does not have. `margin-block` reserves it on
859
+ * the track, and the track's block axis does not clip, so the wave is
860
+ * bounded by its own geometry rather than by a box. The track is also the
861
+ * container the path measures itself against: `cqi` is how a dash stated in
862
+ * thicknesses learns how many thicknesses the track is.
863
+ */
864
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave] [data-part='track'] {
865
+ container-type: inline-size;
866
+ margin-block: calc(0.75 * var(--_thickness));
867
+ }
868
+
869
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave] [data-part='indicator'] {
870
+ /** One wavelength, in thicknesses. */
871
+ --_wave-length: 10;
872
+
873
+ background-color: transparent;
874
+ block-size: calc(2.5 * var(--_thickness));
875
+ /* The reach is `PROGRESS_WAVE_BAR_REACH`, and the viewBox in the markup
876
+ states the same number; `progress.test.ts` holds the two together. */
877
+ inline-size: calc(1200 * var(--_thickness));
878
+ inset-block: calc(-0.75 * var(--_thickness)) auto;
879
+ inset-inline-start: 0;
880
+ overflow: visible;
881
+ position: absolute;
882
+ }
883
+
884
+ /**
885
+ * The crest travels with the text. The path is drawn from its own left, so
886
+ * a right-to-left document mirrors the box about its centre — the box is
887
+ * anchored to the inline start, and its centre is the one point that stays
888
+ * inside the track.
889
+ */
890
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave] [data-part='indicator']:dir(rtl) {
891
+ transform: scaleX(-1);
892
+ }
893
+
894
+ /**
895
+ * The window, and the three numbers that place it.
896
+ *
897
+ * The path carries `pathLength` equal to its extent along the inline axis,
898
+ * so a dash of one unit is one thickness of travel whatever the amplitude —
899
+ * the arc length of a sine is longer than its span, and normalising the
900
+ * length to the span is what lets the end sit at the value while `d` is
901
+ * mid-transition between flat and wavy.
902
+ *
903
+ * The phase moves the path back by one wavelength over the cycle, and the
904
+ * dash offset hides the same distance, so the window always begins at the
905
+ * track's start whatever crest happens to be there. The crests therefore
906
+ * pass toward the start, and the cycle closes on itself because the path
907
+ * has moved exactly one period. The half unit either
908
+ * side is the cap: the dash starts half a thickness in and ends half a
909
+ * thickness short, so the round caps land on the track's edges exactly where
910
+ * the plain bar's rounded box does — which is how M3 insets the segment by
911
+ * the stroke's radius before capping it.
912
+ *
913
+ * The dash is at least nought. Below one thickness the window is a dot of
914
+ * one thickness, and at nought it is not painted at all, by the same clamp
915
+ * the ring uses on its own cap.
916
+ */
917
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave] [data-part='indicator'] path {
918
+ /** The track's length, in thicknesses: a ratio of two lengths, read as a number. */
919
+ --_track-units: tan(atan2(100cqi, var(--_thickness)));
920
+ --_window-start: 0;
921
+ --_window-size: var(--_percent);
922
+ --_hidden: calc(var(--slotted-progress-indicator-wave-phase) * var(--_wave-length));
923
+
924
+ fill: none;
925
+ stroke: var(--_ink);
926
+ stroke-dasharray: max(0, calc(var(--_window-size) * var(--_track-units) / 100 - 1)) 100000;
927
+ stroke-dashoffset: calc(
928
+ -1 * (var(--_hidden) + var(--_window-start) * var(--_track-units) / 100 + 0.5)
929
+ );
930
+ stroke-linecap: round;
931
+ stroke-opacity: clamp(0, calc(var(--_window-size) * 1000), 1);
932
+ stroke-width: 1;
933
+ transform: translateX(calc(-1 * var(--_hidden) * 1px));
934
+ }
935
+
936
+ /**
937
+ * The amplitude, scrubbed. The keyframes below run `d` from the wave to the
938
+ * flat fitting; the animation is paused and a negative delay picks the frame
939
+ * that the amplitude names, so the crests stand exactly as high as the value
940
+ * says at every moment of the fill's own transition. An indeterminate bar
941
+ * carries a different path and is left to the markup.
942
+ */
943
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave]:not([data-indeterminate])
944
+ [data-part='indicator']
945
+ path {
946
+ animation: slotted-progress-indicator-flatten-bar var(--_scrub) var(--_taper) both paused;
947
+ animation-delay: calc(
948
+ -1 * var(--_scrub) * (1 - var(--slotted-progress-indicator-wave-amplitude))
949
+ );
950
+ }
951
+
952
+ /**
953
+ * The travelling wave. M3 tightens the wavelength to 20dp while the value
954
+ * is unknown — `IndeterminateActiveWaveWavelength` — so the markup carries
955
+ * the tighter path, and the window is the travelling bar's own two numbers.
956
+ */
957
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave][data-indeterminate]
958
+ [data-part='indicator'] {
959
+ --_wave-length: 5;
960
+
961
+ animation: slotted-progress-indicator-travel var(--slotted-duration-loop-slow)
962
+ var(--slotted-easing-standard) infinite;
963
+ }
964
+
965
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave][data-indeterminate]
966
+ [data-part='indicator']
967
+ path {
968
+ --_window-start: var(--slotted-progress-indicator-travel-start);
969
+ --_window-size: var(--slotted-progress-indicator-travel-size);
970
+ }
971
+
972
+ /* ── The wavy ring ────────────────────────────────────────────────────
973
+ *
974
+ * The track stays a circle and the indicator becomes the wave, which is
975
+ * what `CircularWavyProgressModifiers` draws: its track polygon is
976
+ * `RoundedPolygon.circle` and only the active indicator morphs.
977
+ *
978
+ * The ring is drawn a fifth again as wide as its step, with the stroke it
979
+ * had. That is M3's own measurement — `WaveSize` is 48dp where `Size` is
980
+ * 40dp, both at a 4dp stroke — because a crest needs radial room a plain
981
+ * stroke does not. Five of forty-eight in a box a fifth larger is the same
982
+ * pixel stroke as six of forty-eight in the plain box, so a ring that turns
983
+ * wavy keeps its weight, and the crest may pass the viewBox by the tenth of
984
+ * a unit the stroke needs.
985
+ */
986
+ [data-ui='progress-indicator'][data-shape='circular'][data-wave] svg {
987
+ --_stroke: var(--slotted-progress-indicator-stroke, 5);
988
+
989
+ block-size: calc(1.2 * var(--_size));
990
+ inline-size: calc(1.2 * var(--_size));
991
+ overflow: visible;
992
+ }
993
+
994
+ /**
995
+ * The phase is a rotation, and that is not an approximation. A travelling
996
+ * wave `R + A·sin(Nθ + ωt)` is the standing one rotated by `-ωt/N`, so
997
+ * rotating the path by one crest — 45 degrees at eight — returns the
998
+ * identical shape and the loop has no seam. `CircularWavyProgressModifiers`
999
+ * rotates its own progress path by the same offset angle. The rotation is
1000
+ * anticlockwise, against the arc: the crests travel toward the arc's start,
1001
+ * as the bar's do.
1002
+ *
1003
+ * The dash follows the rotation back, so the arc stays where the value put
1004
+ * it while the crests travel under it. One crest is 12.5 of the hundred
1005
+ * units `pathLength` declares, and a negative offset moves the pattern
1006
+ * away from the path's start — the direction that cancels a rotation
1007
+ * carrying it the other way. The gap is restated as `100 - dash` so the pattern
1008
+ * repeats exactly once per revolution: with the plain ring's gap of 100 the
1009
+ * far end fell into the gap as the pattern shifted, and a full ring opened
1010
+ * a hole at twelve o'clock.
1011
+ */
1012
+ [data-ui='progress-indicator'][data-shape='circular'][data-wave] [data-part='indicator'] {
1013
+ rotate: calc(-45deg * var(--slotted-progress-indicator-wave-phase));
1014
+ stroke-dasharray: var(--_percent) calc(100 - var(--_percent));
1015
+ stroke-dashoffset: calc(-12.5 * var(--slotted-progress-indicator-wave-phase));
1016
+ /* The rotation is about the viewBox's own centre, which is where the ring
1017
+ is; a fill box would turn it about the path's bounding box instead. */
1018
+ transform-box: view-box;
1019
+ transform-origin: 50% 50%;
1020
+ }
1021
+
1022
+ [data-ui='progress-indicator'][data-shape='circular'][data-wave]:not([data-indeterminate])
1023
+ [data-part='indicator'] {
1024
+ animation: slotted-progress-indicator-flatten-ring var(--_scrub) var(--_taper) both paused;
1025
+ animation-delay: calc(
1026
+ -1 * var(--_scrub) * (1 - var(--slotted-progress-indicator-wave-amplitude))
1027
+ );
1028
+ }
1029
+
1030
+ /* ── The amplitude ────────────────────────────────────────────────────
1031
+ *
1032
+ * Two keyframes per shape, from the wave the markup draws to the same
1033
+ * fitting at amplitude nought, and a paused animation the two rules above
1034
+ * scrub with a negative delay. The frame shown is the amplitude, so the
1035
+ * flatten is not an animator of its own with a duration and an easing that
1036
+ * can land early or late: it is the fill's spring, read through the ramp.
1037
+ *
1038
+ * `--_scrub` is the length of the timeline being scrubbed, and any length
1039
+ * would draw the same frames: the delay is the same fraction of it. It is
1040
+ * taken from the scale rather than written as a literal so the stylesheet
1041
+ * states no duration of its own, and the collapsed scale under reduced motion
1042
+ * scrubs a shorter timeline to the same frame. `--_taper` is the shape of
1043
+ * the ramp, not its speed — an S-curve, so the crests leave and return gently
1044
+ * at both ends of the ramp rather than at a constant rate. The two paths of
1045
+ * each pair must share a command list, and `progress.test.ts` holds both
1046
+ * copies to `@slotted/core/progress`.
1047
+ */
1048
+ [data-ui='progress-indicator'][data-part='root'][data-wave] {
1049
+ --_scrub: var(--slotted-duration-long);
1050
+ --_taper: cubic-bezier(0.45, 0, 0.55, 1);
1051
+ }
1052
+
1053
+ @keyframes slotted-progress-indicator-flatten-bar {
1054
+ from {
1055
+ d: path(
1056
+ 'M0-0.75c1.82 0 3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5s3.18 1.5 5 1.5s3.18-1.5 5-1.5'
1057
+ );
1058
+ }
1059
+
1060
+ to {
1061
+ d: path(
1062
+ 'M0 0c1.82 0 3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0s3.18 0 5 0'
1063
+ );
1064
+ }
1065
+ }
1066
+
1067
+ @keyframes slotted-progress-indicator-flatten-ring {
1068
+ from {
1069
+ d: path(
1070
+ 'M45.6 24C45.6 26.83 41.92 28.82 41 31.04C40.08 33.27 41.27 37.27 39.27 39.27C37.27 41.27 33.27 40.08 31.04 41C28.82 41.92 26.83 45.6 24 45.6C21.17 45.6 19.18 41.92 16.96 41C14.73 40.08 10.73 41.27 8.73 39.27C6.73 37.27 7.92 33.27 7 31.04C6.08 28.82 2.4 26.83 2.4 24C2.4 21.17 6.08 19.18 7 16.96C7.92 14.73 6.73 10.73 8.73 8.73C10.73 6.73 14.73 7.92 16.96 7C19.18 6.08 21.17 2.4 24 2.4C26.83 2.4 28.82 6.08 31.04 7C33.27 7.92 37.27 6.73 39.27 8.73C41.27 10.73 40.08 14.73 41 16.96C41.92 19.18 45.6 21.17 45.6 24Z'
1071
+ );
1072
+ }
1073
+
1074
+ to {
1075
+ d: path(
1076
+ 'M44 24C44 26.62 43.48 29.23 42.48 31.65C41.48 34.07 39.99 36.29 38.14 38.14C36.29 39.99 34.07 41.48 31.65 42.48C29.23 43.48 26.62 44 24 44C21.38 44 18.77 43.48 16.35 42.48C13.93 41.48 11.71 39.99 9.86 38.14C8.01 36.29 6.52 34.07 5.52 31.65C4.52 29.23 4 26.62 4 24C4 21.38 4.52 18.77 5.52 16.35C6.52 13.93 8.01 11.71 9.86 9.86C11.71 8.01 13.93 6.52 16.35 5.52C18.77 4.52 21.38 4 24 4C26.62 4 29.23 4.52 31.65 5.52C34.07 6.52 36.29 8.01 38.14 9.86C39.99 11.71 41.48 13.93 42.48 16.35C43.48 18.77 44 21.38 44 24Z'
1077
+ );
1078
+ }
1079
+ }
1080
+
1081
+ /* ── The loader ───────────────────────────────────────────────────────
1082
+ *
1083
+ * M3 Expressive's loading indicator, which Google recommends *instead of* an
1084
+ * indeterminate circular indicator for a wait under five seconds. Nothing
1085
+ * else surveyed ships it.
1086
+ *
1087
+ * A shape morphs through a sequence while it turns. The two are separate
1088
+ * animations at periods that do not divide into each other, so the sequence
1089
+ * never shows the same shape in the same orientation twice — which is what
1090
+ * makes a five-second wait bearable to watch and a spinning ring not.
1091
+ */
1092
+ [data-ui='progress-indicator'][data-shape='morph'] [data-part='track'] {
1093
+ block-size: var(--_size);
1094
+ border-radius: var(--slotted-radius-full, 9999px);
1095
+ display: grid;
1096
+ flex: none;
1097
+ inline-size: var(--_size);
1098
+ place-items: center;
1099
+ }
1100
+
1101
+ /**
1102
+ * The container M3 draws behind the shape, and the ratio is M3's own: a
1103
+ * 38-point indicator inside a 48-point container. The contrast between the
1104
+ * two is the family's derived track tint against the ink, which is the same
1105
+ * pair the bar's own track holds and clears the 3:1 the loading indicator
1106
+ * documentation asks for between a contained shape and its container.
1107
+ */
1108
+ [data-ui='progress-indicator'][data-shape='morph'][data-contained] [data-part='track'] {
1109
+ background-color: var(--_track-color);
1110
+ }
1111
+
1112
+ /**
1113
+ * The rotation, and it is two parcels summed — which is the whole mechanism
1114
+ * and the thing a constant spin gets wrong.
1115
+ *
1116
+ * `LoadingIndicatorAnimatorDelegate.java` decomposes it:
1117
+ * `CONSTANT_ROTATION_PER_SHAPE_DEGREES = 50` advances with a linear clock,
1118
+ * `EXTRA_ROTATION_PER_SHAPE_DEGREES = 90` is driven by the *same*
1119
+ * `SpringAnimation` that drives the morph, and the two are added. Ninety of
1120
+ * every hundred and forty degrees therefore lurch with the shape: the object
1121
+ * throws itself around as it changes form and settles as it arrives. The
1122
+ * fifty is the minority parcel and exists so it never freezes between forms.
1123
+ *
1124
+ * A single constant turn — this family's first answer — is the *old*
1125
+ * circular indicator's behaviour, and it is what the loading indicator was
1126
+ * introduced to replace.
1127
+ *
1128
+ * CSS has no way to add two animations of one property without
1129
+ * `animation-composition`, so the sum is expressed geometrically instead: the
1130
+ * container carries the linear parcel and the shape inside it carries the
1131
+ * sprung one, and nested rotations multiply. The container is a circle with a
1132
+ * flat fill, so turning it is invisible — it is turning its child.
1133
+ *
1134
+ * `--slotted-easing-emphasized` is `cubic-bezier(0.3, 0, 0, 1.3)`, and the
1135
+ * `1.3` is the spring: the shape and its rotation both carry a little past
1136
+ * each target and settle. Both animations take it, at the same period and on
1137
+ * the same keyframe boundaries, because in M3 they are one spring.
1138
+ */
1139
+ [data-ui='progress-indicator'][data-shape='morph'] [data-part='track'] {
1140
+ animation: slotted-progress-indicator-drift calc(2 * var(--slotted-duration-loop-slow)) linear
1141
+ infinite;
1142
+ }
1143
+
1144
+ /**
1145
+ * Two times the slow loop is 3.2 seconds over five shapes — 640 milliseconds
1146
+ * each, which is `DURATION_PER_SHAPE_IN_MS = 650` to within the scale's own
1147
+ * resolution. Multiplying a loop token is deliberate rather than a shortcut
1148
+ * around the motion scale: the three loop durations are the ones the theme
1149
+ * does not collapse, because an endless animation has to stop rather than
1150
+ * hurry, and a multiple of one inherits that where a fourth token would be a
1151
+ * scale step added for a single family.
1152
+ */
1153
+ [data-ui='progress-indicator'][data-shape='morph'] [data-part='indicator'] {
1154
+ animation:
1155
+ slotted-progress-indicator-morph calc(2 * var(--slotted-duration-loop-slow))
1156
+ var(--slotted-easing-emphasized) infinite,
1157
+ slotted-progress-indicator-lurch calc(2 * var(--slotted-duration-loop-slow))
1158
+ var(--slotted-easing-emphasized) infinite;
1159
+ background-color: var(--_ink);
1160
+ block-size: 100%;
1161
+ inline-size: 100%;
1162
+ }
1163
+
1164
+ /**
1165
+ * M3's 38-in-48, rounded to an even number of pixels — and the rounding is
1166
+ * the whole rule rather than a tidy-up.
1167
+ *
1168
+ * A grid centres a box by splitting the remainder in half, so a shape whose
1169
+ * size leaves an odd remainder inside its container sits half a pixel off
1170
+ * centre and the browser rasterises that half-pixel to one side. At `lg` the
1171
+ * ratio happens to land on 30 inside 38 and the shape is centred; at `md` it
1172
+ * lands on 26.86 inside 34, and the capture showed it low and to one side.
1173
+ *
1174
+ * Every control height this family reads is an even number at every density,
1175
+ * so an even shape leaves an even remainder and the two halves are whole
1176
+ * pixels. `round()` is what states that, and it states it once for the three
1177
+ * steps rather than as three numbers.
1178
+ */
1179
+ [data-ui='progress-indicator'][data-shape='morph'][data-contained] [data-part='indicator'] {
1180
+ --_shape: round(nearest, calc(0.79 * var(--_size)), 2px);
1181
+
1182
+ block-size: var(--_shape);
1183
+ inline-size: var(--_shape);
1184
+ }
1185
+
1186
+ /** Fifty degrees a shape, five shapes, one linear turn of the container. */
1187
+ @keyframes slotted-progress-indicator-drift {
1188
+ to {
1189
+ rotate: 250deg;
1190
+ }
1191
+ }
1192
+
1193
+ /**
1194
+ * Ninety degrees a shape, on the morph's own boundaries and the morph's own
1195
+ * curve, so the throw and the change of form are one movement.
1196
+ */
1197
+ @keyframes slotted-progress-indicator-lurch {
1198
+ 0% {
1199
+ rotate: 0deg;
1200
+ }
1201
+
1202
+ 20% {
1203
+ rotate: 90deg;
1204
+ }
1205
+
1206
+ 40% {
1207
+ rotate: 180deg;
1208
+ }
1209
+
1210
+ 60% {
1211
+ rotate: 270deg;
1212
+ }
1213
+
1214
+ 80% {
1215
+ rotate: 360deg;
1216
+ }
1217
+
1218
+ 100% {
1219
+ rotate: 450deg;
1220
+ }
1221
+ }
1222
+
1223
+ @keyframes slotted-progress-indicator-morph {
1224
+ 0% {
1225
+ clip-path: polygon(
1226
+ 50% 0%,
1227
+ 59.8% 1%,
1228
+ 69.1% 3.8%,
1229
+ 77.8% 8.4%,
1230
+ 85.4% 14.6%,
1231
+ 91.6% 22.2%,
1232
+ 96.2% 30.9%,
1233
+ 99% 40.2%,
1234
+ 100% 50%,
1235
+ 99% 59.8%,
1236
+ 96.2% 69.1%,
1237
+ 91.6% 77.8%,
1238
+ 85.4% 85.4%,
1239
+ 77.8% 91.6%,
1240
+ 69.1% 96.2%,
1241
+ 59.8% 99%,
1242
+ 50% 100%,
1243
+ 40.2% 99%,
1244
+ 30.9% 96.2%,
1245
+ 22.2% 91.6%,
1246
+ 14.6% 85.4%,
1247
+ 8.4% 77.8%,
1248
+ 3.8% 69.1%,
1249
+ 1% 59.8%,
1250
+ 0% 50%,
1251
+ 1% 40.2%,
1252
+ 3.8% 30.9%,
1253
+ 8.4% 22.2%,
1254
+ 14.6% 14.6%,
1255
+ 22.2% 8.4%,
1256
+ 30.9% 3.8%,
1257
+ 40.2% 1%
1258
+ );
1259
+ }
1260
+
1261
+ 20% {
1262
+ clip-path: polygon(
1263
+ 50% 0%,
1264
+ 59.3% 3.4%,
1265
+ 65.9% 11.5%,
1266
+ 69.9% 20.3%,
1267
+ 73.6% 26.4%,
1268
+ 79.7% 30.1%,
1269
+ 88.5% 34.1%,
1270
+ 96.6% 40.7%,
1271
+ 100% 50%,
1272
+ 96.6% 59.3%,
1273
+ 88.5% 65.9%,
1274
+ 79.7% 69.9%,
1275
+ 73.6% 73.6%,
1276
+ 69.9% 79.7%,
1277
+ 65.9% 88.5%,
1278
+ 59.3% 96.6%,
1279
+ 50% 100%,
1280
+ 40.7% 96.6%,
1281
+ 34.1% 88.5%,
1282
+ 30.1% 79.7%,
1283
+ 26.4% 73.6%,
1284
+ 20.3% 69.9%,
1285
+ 11.5% 65.9%,
1286
+ 3.4% 59.3%,
1287
+ 0% 50%,
1288
+ 3.4% 40.7%,
1289
+ 11.5% 34.1%,
1290
+ 20.3% 30.1%,
1291
+ 26.4% 26.4%,
1292
+ 30.1% 20.3%,
1293
+ 34.1% 11.5%,
1294
+ 40.7% 3.4%
1295
+ );
1296
+ }
1297
+
1298
+ 40% {
1299
+ clip-path: polygon(
1300
+ 50% 6.9%,
1301
+ 59.5% 2.1%,
1302
+ 68.9% 4.3%,
1303
+ 74.7% 13%,
1304
+ 77% 23%,
1305
+ 80.2% 29.8%,
1306
+ 87.4% 34.5%,
1307
+ 96% 40.8%,
1308
+ 100% 50%,
1309
+ 96% 59.2%,
1310
+ 87.4% 65.5%,
1311
+ 80.2% 70.2%,
1312
+ 77% 77%,
1313
+ 74.7% 87%,
1314
+ 68.9% 95.7%,
1315
+ 59.5% 97.9%,
1316
+ 50% 93.1%,
1317
+ 42.7% 86.7%,
1318
+ 35.9% 83.9%,
1319
+ 26.8% 84.7%,
1320
+ 16.1% 83.9%,
1321
+ 8.5% 77.7%,
1322
+ 7.7% 67.5%,
1323
+ 11.5% 57.7%,
1324
+ 13.8% 50%,
1325
+ 11.5% 42.3%,
1326
+ 7.7% 32.5%,
1327
+ 8.5% 22.3%,
1328
+ 16.1% 16.1%,
1329
+ 26.8% 15.3%,
1330
+ 35.9% 16.1%,
1331
+ 42.7% 13.3%
1332
+ );
1333
+ }
1334
+
1335
+ 60% {
1336
+ clip-path: polygon(
1337
+ 50% 9.7%,
1338
+ 56.8% 15.7%,
1339
+ 62% 21%,
1340
+ 67.1% 24.4%,
1341
+ 73.7% 26.3%,
1342
+ 82% 28.6%,
1343
+ 90.7% 33.2%,
1344
+ 97.4% 40.6%,
1345
+ 100% 50%,
1346
+ 97.4% 59.4%,
1347
+ 90.7% 66.8%,
1348
+ 82% 71.4%,
1349
+ 73.7% 73.7%,
1350
+ 67.1% 75.6%,
1351
+ 62% 79%,
1352
+ 56.8% 84.3%,
1353
+ 50% 90.3%,
1354
+ 41.1% 94.8%,
1355
+ 31.1% 95.5%,
1356
+ 22.3% 91.4%,
1357
+ 16.6% 83.4%,
1358
+ 14.9% 73.5%,
1359
+ 16.2% 64%,
1360
+ 18.3% 56.3%,
1361
+ 19.4% 50%,
1362
+ 18.3% 43.7%,
1363
+ 16.2% 36%,
1364
+ 14.9% 26.5%,
1365
+ 16.6% 16.6%,
1366
+ 22.3% 8.6%,
1367
+ 31.1% 4.5%,
1368
+ 41.1% 5.2%
1369
+ );
1370
+ }
1371
+
1372
+ 80% {
1373
+ clip-path: polygon(
1374
+ 50% 23.1%,
1375
+ 55.4% 22.7%,
1376
+ 61.6% 22%,
1377
+ 68.9% 21.7%,
1378
+ 77.2% 22.8%,
1379
+ 85.7% 26.2%,
1380
+ 93.1% 32.2%,
1381
+ 98.2% 40.4%,
1382
+ 100% 50%,
1383
+ 98.2% 59.6%,
1384
+ 93.1% 67.8%,
1385
+ 85.7% 73.8%,
1386
+ 77.2% 77.2%,
1387
+ 68.9% 78.3%,
1388
+ 61.6% 78%,
1389
+ 55.4% 77.3%,
1390
+ 50% 76.9%,
1391
+ 44.6% 77.3%,
1392
+ 38.4% 78%,
1393
+ 31.1% 78.3%,
1394
+ 22.8% 77.2%,
1395
+ 14.3% 73.8%,
1396
+ 6.9% 67.8%,
1397
+ 1.8% 59.6%,
1398
+ 0% 50%,
1399
+ 1.8% 40.4%,
1400
+ 6.9% 32.2%,
1401
+ 14.3% 26.2%,
1402
+ 22.8% 22.8%,
1403
+ 31.1% 21.7%,
1404
+ 38.4% 22%,
1405
+ 44.6% 22.7%
1406
+ );
1407
+ }
1408
+
1409
+ 100% {
1410
+ clip-path: polygon(
1411
+ 50% 0%,
1412
+ 59.8% 1%,
1413
+ 69.1% 3.8%,
1414
+ 77.8% 8.4%,
1415
+ 85.4% 14.6%,
1416
+ 91.6% 22.2%,
1417
+ 96.2% 30.9%,
1418
+ 99% 40.2%,
1419
+ 100% 50%,
1420
+ 99% 59.8%,
1421
+ 96.2% 69.1%,
1422
+ 91.6% 77.8%,
1423
+ 85.4% 85.4%,
1424
+ 77.8% 91.6%,
1425
+ 69.1% 96.2%,
1426
+ 59.8% 99%,
1427
+ 50% 100%,
1428
+ 40.2% 99%,
1429
+ 30.9% 96.2%,
1430
+ 22.2% 91.6%,
1431
+ 14.6% 85.4%,
1432
+ 8.4% 77.8%,
1433
+ 3.8% 69.1%,
1434
+ 1% 59.8%,
1435
+ 0% 50%,
1436
+ 1% 40.2%,
1437
+ 3.8% 30.9%,
1438
+ 8.4% 22.2%,
1439
+ 14.6% 14.6%,
1440
+ 22.2% 8.4%,
1441
+ 30.9% 3.8%,
1442
+ 40.2% 1%
1443
+ );
1444
+ }
1445
+ }
1446
+
1447
+ /* ── Reduced motion ───────────────────────────────────────────────────
1448
+ *
1449
+ * Stopped, not slowed. `spinner` slowed its ring on the grounds that a
1450
+ * stopped ring says nothing, and the token foundation now refuses that
1451
+ * reading in as many words: an animation that repeats forever needs to stop
1452
+ * rather than to hurry.
1453
+ *
1454
+ * The cost is paid rather than hidden. An indeterminate indicator that does
1455
+ * not move is a static graphic, so its meaning has to come from somewhere
1456
+ * else — the `progressbar` role, the accessible name both bindings require,
1457
+ * and the visible label the reference page recommends for the loader. The
1458
+ * three loop durations are the ones the theme deliberately does not
1459
+ * shorten, which is what makes stopping the only available answer.
1460
+ */
1461
+ @media (prefers-reduced-motion: reduce) {
1462
+ /**
1463
+ * Important, and that is the point of the rule: the preference outranks
1464
+ * every animated rule in the family whatever its specificity. Without it
1465
+ * the travelling bar, the turning ring, the sweep, the drift and the
1466
+ * morph all kept moving, because each is started by a selector that names
1467
+ * its shape and its state and so outweighs one that names the part. A
1468
+ * `path` is left out: the wave's paths carry the paused ramp below, which
1469
+ * is a value and survives.
1470
+ */
1471
+ [data-ui='progress-indicator'][data-part='root'],
1472
+ [data-ui='progress-indicator'] [data-part='indicator']:not(path),
1473
+ [data-ui='progress-indicator'] [data-part='track'],
1474
+ [data-ui='progress-indicator'] svg {
1475
+ animation: none !important;
1476
+ }
1477
+
1478
+ /**
1479
+ * The travelling bar has nowhere to be when it stops. Halfway along the
1480
+ * track at its widest is the one position that reads as a wait rather
1481
+ * than as a value: pinned to either end it would read as nought per cent
1482
+ * or as a hundred, which is a number this indicator does not have.
1483
+ */
1484
+ [data-ui='progress-indicator'][data-shape='linear'][data-indeterminate]
1485
+ [data-part='indicator'] {
1486
+ --slotted-progress-indicator-travel-size: 46;
1487
+ --slotted-progress-indicator-travel-start: 27;
1488
+ }
1489
+
1490
+ /**
1491
+ * The same question for the arc, and the same answer: a third of the ring
1492
+ * is unmistakably a fraction of something rather than a full circle or an
1493
+ * empty one.
1494
+ */
1495
+ [data-ui='progress-indicator'][data-shape='circular'][data-indeterminate]
1496
+ [data-part='indicator'] {
1497
+ stroke-dasharray: 34 100;
1498
+ }
1499
+
1500
+ /**
1501
+ * The wave stands still. The ramp that flattens it at either end of the
1502
+ * range survives, because it is a value rather than motion: a ring at four
1503
+ * per cent is a circle whether or not anything moves.
1504
+ */
1505
+ [data-ui='progress-indicator'][data-part='root'][data-wave] {
1506
+ animation: none;
1507
+ }
1508
+ }
1509
+
1510
+ /* ── Forced colours ───────────────────────────────────────────────────
1511
+ *
1512
+ * The operating system replaces every author colour, so the tone is gone
1513
+ * whatever the family writes and the five tones become one indicator. What
1514
+ * is left to decide is whether the geometry survives, and three things do
1515
+ * not survive on their own: a `color-mix` of two forced colours is still a
1516
+ * mix and paints an author colour the system did not choose; a masked
1517
+ * indicator is masked by artwork the system does not replace; and a
1518
+ * `clip-path` shape has no edge of its own to be seen by.
1519
+ *
1520
+ * The remaining track therefore moves from the pseudo-element to the track
1521
+ * itself, which is a real element the system palette reaches — a decorative
1522
+ * rule may not name a system colour at all, and
1523
+ * `packages/styles/src/fallbacks.test.mjs` is what holds that. The gap goes
1524
+ * with it, and a gap is a refinement rather than a meaning.
1525
+ */
1526
+ @media (forced-colors: active) {
1527
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='track'] {
1528
+ background-color: Canvas;
1529
+ outline: var(--slotted-border-width-thin, 1px) solid ButtonBorder;
1530
+ outline-offset: calc(-1 * var(--slotted-border-width-thin, 1px));
1531
+ }
1532
+
1533
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='track']::before,
1534
+ [data-ui='progress-indicator'][data-shape='linear'] [data-part='track']::after {
1535
+ content: none;
1536
+ }
1537
+
1538
+ /**
1539
+ * An edge bar keeps no track of its own. The box it stands on already
1540
+ * draws its edge in a system colour, and an outline here would be a
1541
+ * second line beside the first.
1542
+ */
1543
+ [data-ui='progress-indicator']:is([data-placement='edge-start'], [data-placement='edge-end'])
1544
+ [data-part='track'] {
1545
+ background-color: transparent;
1546
+ outline: none;
1547
+ }
1548
+
1549
+ [data-ui='progress-indicator'] [data-part='indicator'] {
1550
+ background-color: Highlight;
1551
+ }
1552
+
1553
+ /**
1554
+ * The wavy bar is a stroke, so it keeps its crests under the palette the
1555
+ * way the ring does: the box behind it stays clear and the path takes
1556
+ * the highlight.
1557
+ */
1558
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave] [data-part='indicator'] {
1559
+ background-color: transparent;
1560
+ }
1561
+
1562
+ [data-ui='progress-indicator'][data-shape='linear'][data-wave] [data-part='indicator'] path {
1563
+ stroke: Highlight;
1564
+ }
1565
+
1566
+ [data-ui='progress-indicator'][data-shape='circular'] [data-part='indicator'] {
1567
+ background-color: transparent;
1568
+ stroke: Highlight;
1569
+ }
1570
+
1571
+ [data-ui='progress-indicator'][data-shape='circular'] [data-part='track'] {
1572
+ stroke: ButtonBorder;
1573
+ }
1574
+
1575
+ /**
1576
+ * The morphing shape keeps its silhouette — that is the whole reason the
1577
+ * loader exists — and loses the container behind it, which in a forced
1578
+ * palette would be a `Canvas` disc on a `Canvas` page.
1579
+ */
1580
+ [data-ui='progress-indicator'][data-shape='morph'][data-contained] [data-part='track'] {
1581
+ background-color: Canvas;
1582
+ }
1583
+ }
1584
+ }