PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | assets/css/dock.css +809 -245 0.9.51.1.10 View file →
@@ -1,10 +1,10 @@
1 1 /**
2 - * Desktop Mode — Dock.
2 + * OpenStation — Dock.
3 3 *
4 4 * A rail that hosts admin menu tiles plus shell-level system tiles.
5 5 * Placement (left / right / bottom) is reflected on each dock element
6 - * itself as `data-desktop-mode-dock-placement`; this stylesheet keys
6 + * itself as `data-os-dock-placement`; this stylesheet keys
7 7 * off that attribute for layout, tooltip anchor, and indicator
8 8 * position. Two instances can coexist (Classic layout: a left side
9 9 * bar with core menus + a bottom dock with plugin menus); each
10 10 * carries its own placement attribute, so the two rails are styled
@@ -16,13 +16,23 @@
16 16 /* ------------------------------------------------------------------
17 17 * Shared dock chrome — applies to every placement.
18 18 * ------------------------------------------------------------------ */
19 19
20 -.desktop-mode-dock {
20 +.os-dock {
21 21 display: flex;
22 22 align-items: center;
23 - z-index: var( --desktop-mode-z-dock );
24 - background: var( --desktop-mode-dock-bg );
23 + z-index: var( --os-z-dock );
24 + background-color: var( --os-dock-bg );
25 + /*
26 + * Desktop-theme dock texture (DOCK slot). Layered OVER the dock
27 + * background colour, so a theme can ship a semi-transparent
28 + * texture and still get the translucent wash underneath. `none`
29 + * when unset — no theme, no cost.
30 + */
31 + background-image: var( --os-dock-bg-image, none );
32 + background-repeat: var( --os-dock-bg-image-repeat, repeat );
33 + background-size: var( --os-dock-bg-image-size, auto );
34 + background-position: var( --os-dock-bg-image-position, center );
25 35 backdrop-filter: blur( 20px );
26 36 -webkit-backdrop-filter: blur( 20px );
27 37 flex-shrink: 0;
28 38 /*
@@ -35,9 +45,9 @@
35 45 overflow: visible;
36 46 }
37 47
38 48 /* A single dock tile — icon button (+ optional instance rail below). */
39 -.desktop-mode-dock__item {
49 +.os-dock__item {
40 50 position: relative;
41 51 display: flex;
42 52 flex-direction: column;
43 53 align-items: center;
@@ -45,9 +55,9 @@
45 55 flex-shrink: 0;
46 56 }
47 57
48 58 /* Primary icon button — opens or focuses the page. */
49 -.desktop-mode-dock__item-primary {
59 +.os-dock__item-primary {
50 60 position: relative;
51 61 display: flex;
52 62 align-items: center;
53 63 justify-content: center;
@@ -56,40 +66,99 @@
56 66 border: none;
57 67 border-radius: 10px;
58 68 cursor: pointer;
59 69 padding: 0;
60 - background: transparent;
61 - color: rgba( 255, 255, 255, 0.7 );
70 + background-color: transparent;
71 + /*
72 + * DOCK_ITEM texture slot — the face of a single dock tile, so a
73 + * theme can give every launcher a physical key/plate instead of
74 + * relying on the dock strip's texture showing through. Painted
75 + * under the icon; the hover wash below is a background-COLOR so
76 + * it composes with the texture rather than erasing it.
77 + */
78 + background-image: var( --os-dock-item-image, none );
79 + background-repeat: var( --os-dock-item-image-repeat, no-repeat );
80 + background-size: var( --os-dock-item-image-size, auto );
81 + background-position: var( --os-dock-item-image-position, center );
82 + /*
83 + * Dock glyph colour. The literal is what this rule always said;
84 + * the token is the name a theme aims at when its dock strip is
85 + * not dark. See the "Dock glyphs" block in variables.css.
86 + */
87 + color: var( --os-dock-icon-color, rgba( 255, 255, 255, 0.7 ) );
62 88 transition: background-color 0.15s ease, transform 0.15s ease, color 0.15s ease;
63 89 }
64 90
65 -.desktop-mode-dock__item-primary:hover {
66 - background: rgba( 255, 255, 255, 0.15 );
67 - color: #fff;
91 +/*
92 + * The tile whose window is in front sits on a lit plate. Declared
93 + * before the hover rule at the same specificity so the pointer still
94 + * wins while it is over the tile. Falls back to nothing, which is
95 + * what every placement drew before the token existed.
96 + */
97 +.os-dock__item--focused .os-dock__item-primary {
98 + background-color: var( --os-dock-item-bg-focused, transparent );
99 + color: var( --os-dock-icon-color-hover, var( --os-ui-fg-on-accent, #fff ) );
100 +}
101 +
102 +.os-dock__item-primary:hover {
103 + background-color: var(
104 + --os-dock-item-bg-hover,
105 + rgba( 255, 255, 255, 0.15 )
106 + );
107 + color: var(
108 + --os-dock-icon-color-hover,
109 + var( --os-ui-fg-on-accent, #fff )
110 + );
68 111 transform: scale( 1.1 );
69 112 }
70 113
71 -.desktop-mode-dock__item-primary:active {
114 +.os-dock__item-primary:active {
72 115 transform: scale( 0.95 );
73 116 }
74 117
75 -.desktop-mode-dock__item-primary:focus-visible {
76 - outline: 2px solid rgba( 255, 255, 255, 0.7 );
118 +.os-dock__item-primary:focus-visible {
119 + outline: 2px solid var(
120 + --os-dock-item-outline,
121 + rgba( 255, 255, 255, 0.7 )
122 + );
77 123 outline-offset: 2px;
78 124 }
79 125
126 +/* Stacked card hint on hover — a second card peeks from behind the
127 + icon when the tile has multiple open windows. Only visible on hover so
128 + it never conflicts with focused tiles. */
129 +.os-dock__item--stacked:hover .os-dock__item-primary::before,
130 +.os-dock__item--stacked[data-peek-active] .os-dock__item-primary::before {
131 + content: "";
132 + position: absolute;
133 + top: -4px;
134 + inset-inline-start: -4px;
135 + width: 100%;
136 + height: 100%;
137 + border-radius: inherit;
138 + background: rgba( 255, 255, 255, 0.06 );
139 + border: 1px solid rgba( 255, 255, 255, 0.10 );
140 + z-index: -1;
141 + pointer-events: none;
142 +}
143 +
80 144 /* Dashicon inside dock item. */
81 -.desktop-mode-dock__item .dashicons {
82 - font-size: var( --desktop-mode-dock-icon-size, 20px );
83 - width: var( --desktop-mode-dock-icon-size, 20px );
84 - height: var( --desktop-mode-dock-icon-size, 20px );
145 +.os-dock__item .dashicons {
146 + font-size: var( --os-dock-icon-size, 20px );
147 + width: var( --os-dock-icon-size, 20px );
148 + height: var( --os-dock-icon-size, 20px );
85 149 line-height: 1;
86 150 }
87 151
88 -/* SVG icon (for custom post types with data:image/svg+xml icons). */
89 -.desktop-mode-dock__item-svg {
90 - width: var( --desktop-mode-dock-icon-size, 20px );
91 - height: var( --desktop-mode-dock-icon-size, 20px );
152 +/* SVG icon (for custom post types with data:image/svg+xml icons).
153 + *
154 + * The FALLBACK path now: `_makeSvgIcon()` paints these as a mask
155 + * filled with `currentColor` (see `__item-mask` below) and only lands
156 + * here when the mask refuses the URL — one carrying quotes, spaces or
157 + * parens. The filter is what that fallback still needs. */
158 +.os-dock__item-svg {
159 + width: var( --os-dock-icon-size, 20px );
160 + height: var( --os-dock-icon-size, 20px );
92 161 display: block;
93 162 fill: currentColor;
94 163 opacity: 0.7;
95 164 /* Some plugin SVGs ship hardcoded `fill="..."` attributes inside
@@ -94,23 +163,58 @@
94 163 opacity: 0.7;
95 164 /* Some plugin SVGs ship hardcoded `fill="..."` attributes inside
96 165 the markup, which override `fill: currentColor` and make the
97 166 icon keep its brand color. Force the monochrome white palette
98 - to match dashicons. */
167 + to match dashicons. Flattens to WHITE specifically, which is why
168 + the mask path is preferred: a filter has no colour to name, so a
169 + theme cannot reach it. */
99 170 filter: brightness(0) invert(1);
100 171 transition: opacity 0.15s ease;
101 172 }
102 173
103 -.desktop-mode-dock__item-primary:hover .desktop-mode-dock__item-svg {
174 +.os-dock__item-primary:hover .os-dock__item-svg {
104 175 opacity: 1;
105 176 }
106 177
107 178 /*
179 + * MASKED icon — the glyph is painted as a CSS mask filled with a
180 + * colour, so only the source artwork's alpha is used.
181 + *
182 + * Two callers, one shape:
183 + *
184 + * - A desktop theme's tinted iconset, filled with the colour the
185 + * theme named for that slot.
186 + * - Every other plugin / CPT image icon, filled with
187 + * `currentColor` — which is `--os-dock-icon-color`, the
188 + * same token the dashicons follow.
189 + *
190 + * Deliberately NOT `__item-svg`: that class force-whitens its content
191 + * with `filter: brightness(0) invert(1)`. Both mechanisms discard the
192 + * artwork's colours and keep its alpha, but only one of them has a
193 + * colour a theme can name.
194 + *
195 + * Full opacity, unlike `__item-svg`'s 0.7: the fill carries its own
196 + * alpha. Unthemed that is `rgba( 255, 255, 255, 0.7 )` at rest and
197 + * `#fff` on hover — exactly what the filter plus the opacity pair
198 + * used to compute — and a theme that names a colour has already
199 + * decided how prominent its glyphs should be, so dimming it again
200 + * would silently mix it with the dock.
201 + *
202 + * @since 0.9.8
203 + */
204 +.os-dock__item-mask {
205 + width: var( --os-dock-icon-size, 20px );
206 + height: var( --os-dock-icon-size, 20px );
207 + display: block;
208 + flex-shrink: 0;
209 +}
210 +
211 +/*
108 212 * Letter-badge icon — rendered for items registered without a
109 213 * dashicon / SVG / image URL. Miniature "app placeholder": rounded
110 214 * square tinted with a title-derived hue, first letter centered.
111 215 */
112 -.desktop-mode-dock__item-letter {
216 +.os-dock__item-letter {
113 217 display: flex;
114 218 align-items: center;
115 219 justify-content: center;
116 220 width: 28px;
@@ -115,12 +219,15 @@
115 219 justify-content: center;
116 220 width: 28px;
117 221 height: 28px;
118 222 border-radius: 8px;
119 - color: #fff;
223 + color: var( --os-ui-fg-on-accent, #fff );
120 224 font-size: 15px;
121 225 font-weight: 700;
122 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
226 + font-family: var(
227 + --os-font,
228 + -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
229 + );
123 230 line-height: 1;
124 231 letter-spacing: 0;
125 232 text-shadow: 0 1px 1px rgba( 0, 0, 0, 0.22 );
126 233 box-shadow:
@@ -127,17 +234,17 @@
127 234 inset 0 1px 0 rgba( 255, 255, 255, 0.2 ),
128 235 inset 0 -1px 0 rgba( 0, 0, 0, 0.15 );
129 236 }
130 237
131 -.desktop-mode-dock__item img.desktop-mode-dock__item-img {
132 - width: var( --desktop-mode-dock-icon-size, 20px );
133 - height: var( --desktop-mode-dock-icon-size, 20px );
238 +.os-dock__item img.os-dock__item-img {
239 + width: var( --os-dock-icon-size, 20px );
240 + height: var( --os-dock-icon-size, 20px );
134 241 display: block;
135 242 opacity: 0.7;
136 243 transition: opacity 0.15s ease;
137 244 }
138 245
139 -.desktop-mode-dock__item-primary:hover img.desktop-mode-dock__item-img {
246 +.os-dock__item-primary:hover img.os-dock__item-img {
140 247 opacity: 1;
141 248 }
142 249
143 250 /* "Open another" floating chip — shown on multi-capable tiles that
@@ -142,9 +249,9 @@
142 249
143 250 /* "Open another" floating chip — shown on multi-capable tiles that
144 251 * already have ≥1 open instance. Positioned inside each orientation
145 252 * block so the chip hugs the outward edge of the tile. */
146 -.desktop-mode-dock__item-new {
253 +.os-dock__item-new {
147 254 position: absolute;
148 255 display: flex;
149 256 align-items: center;
150 257 justify-content: center;
@@ -150,27 +257,27 @@
150 257 justify-content: center;
151 258 width: 20px;
152 259 height: 20px;
153 260 padding: 0;
154 - border: 2px solid var( --desktop-mode-dock-bg, rgba( 0, 0, 0, 0.4 ) );
261 + border: 2px solid var( --os-dock-bg, rgba( 0, 0, 0, 0.4 ) );
155 262 border-radius: 50%;
156 263 background: var( --wp-admin-theme-color, #2271b1 );
157 - color: #fff;
264 + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) );
158 265 cursor: pointer;
159 266 transition: background-color 0.15s ease, transform 0.15s ease;
160 267 z-index: 1;
161 268 }
162 269
163 -.desktop-mode-dock__item-new[hidden] {
270 +.os-dock__item-new[hidden] {
164 271 display: none;
165 272 }
166 273
167 -.desktop-mode-dock__item-new:hover {
168 - background: #fff;
274 +.os-dock__item-new:hover {
275 + background: var( --os-ui-surface, #fff );
169 276 color: var( --wp-admin-theme-color, #2271b1 );
170 277 }
171 278
172 -.desktop-mode-dock__item-new:focus-visible {
279 +.os-dock__item-new:focus-visible {
173 280 outline: 2px solid rgba( 255, 255, 255, 0.85 );
174 281 outline-offset: 2px;
175 282 }
176 283
@@ -176,36 +283,75 @@
176 283
177 284 /* Notification badge — iOS/macOS-style pill straddling the tile
178 285 * corner. Ring matches the dock bg so it reads as "punched out."
179 286 * Badge positioning is placement-aware via the blocks below. */
180 -.desktop-mode-dock__badge {
287 +.os-dock__badge {
288 + /*
289 + * Badge geometry DERIVES from the dock's icon size instead of
290 + * being pinned to 16px.
291 + *
292 + * The dock is user-sizable (18 / 20 / 26px icons via OS Settings →
293 + * dock size). A fixed badge tracked none of that: at "Large" it
294 + * read as a speck stuck to an oversized tile. The ratios below are
295 + * chosen so the DEFAULT dock (20px icons) resolves to exactly the
296 + * old numbers — 16px box, 10px text, ~5px padding — so nothing
297 + * moves for anyone who hasn't changed the setting.
298 + *
299 + * `--os-dock-badge-size` overrides the whole derivation
300 + * for themes that want a specific badge; the font size and padding
301 + * then follow it, so one token is usually enough.
302 + */
303 + --os-dock-badge-computed-size: var(
304 + --os-dock-badge-size,
305 + calc( var( --os-dock-icon-size, 20px ) * 0.8 )
306 + );
181 307 position: absolute;
182 - min-width: 16px;
183 - height: 16px;
184 - padding: 0 5px;
308 + min-width: var( --os-dock-badge-computed-size );
309 + height: var( --os-dock-badge-computed-size );
310 + padding: 0 var(
311 + --os-dock-badge-padding,
312 + calc( var( --os-dock-badge-computed-size ) * 0.3 )
313 + );
185 314 box-sizing: border-box;
186 315 border-radius: 999px;
187 - background: linear-gradient( 180deg, #ff5a5a 0%, #d63638 100% );
188 - color: #fff;
316 + background: var(
317 + --os-dock-badge-bg,
318 + linear-gradient( 180deg, #ff5a5a 0%, var( --os-ui-danger, #d63638 ) 100% )
319 + );
320 + color: var( --os-dock-badge-fg, var( --os-ui-fg-on-accent, #fff ) );
189 321 display: inline-flex;
190 322 align-items: center;
191 323 justify-content: center;
192 - font-size: 10px;
193 - font-weight: 700;
324 + /*
325 + * 0.625 of the badge box — 10px on the default 16px badge, the
326 + * value this rule carried before the geometry was derived. It is
327 + * the right weight for a menu tile's alert count: these badges sit
328 + * on the tile CORNER where the pill itself carries the signal, so
329 + * the digit only has to confirm it.
330 + *
331 + * The Recycle Bin's badge is a different problem and has its own
332 + * ratio below — it sits INSIDE the bin glyph, where the pill reads
333 + * as part of the artwork and the number has to carry itself.
334 + */
335 + font-size: var(
336 + --os-dock-badge-font-size,
337 + calc( var( --os-dock-badge-computed-size ) * 0.625 )
338 + );
339 + font-weight: var( --os-dock-badge-font-weight, 700 );
194 340 line-height: 1;
195 341 letter-spacing: 0.01em;
196 342 text-align: center;
197 343 font-variant-numeric: tabular-nums;
198 344 box-shadow:
199 - 0 0 0 1.5px var( --desktop-mode-dock-bg, rgba( 0, 0, 0, 0.4 ) ),
345 + 0 0 0 1.5px var( --os-dock-bg, rgba( 0, 0, 0, 0.4 ) ),
200 346 0 1px 3px rgba( 0, 0, 0, 0.35 );
201 347 pointer-events: none;
202 348 transition: transform 0.15s ease;
203 - top: -3px;
204 - inset-inline-end: -3px;
349 + top: var( --os-dock-badge-offset, -3px );
350 + inset-inline-end: var( --os-dock-badge-offset, -3px );
205 351 }
206 352
207 -.desktop-mode-dock__item:hover .desktop-mode-dock__badge {
353 +.os-dock__item:hover .os-dock__badge {
208 354 transform: scale( 1.05 );
209 355 top: -4px;
210 356 }
211 357
@@ -213,18 +359,51 @@
213 359 * Recycle Bin count — unlike unread/update badges, the trash count
214 360 * is ambient state. Keep it inside the bin glyph as a quiet neutral
215 361 * marker instead of a red alert on the tile corner.
216 362 */
217 -.desktop-mode-dock__item[ data-system-id="desktop-mode-recycle-bin" ] .desktop-mode-dock__badge {
218 - top: 20px;
219 - inset-inline-end: 7px;
220 - min-width: 13px;
221 - height: 13px;
222 - padding: 0 3px;
363 +.os-dock__item[ data-system-id="desktop-mode-recycle-bin" ] .os-dock__badge {
364 + /*
365 + * Sized off the dock icon like every other badge, so it tracks the
366 + * dock-size setting instead of sitting at a fixed 13px on an icon
367 + * that may be 18, 20 or 26px.
368 + *
369 + * Deliberately quieter than the corner alert badge above — this is
370 + * ambient state, not a notification — but it still has to be a
371 + * READABLE number. It was 13px with an 8px digit, which is below
372 + * the point where a count reads as a count.
373 + *
374 + * Override chain: bin-specific token → the generic dock-badge
375 + * token → the derivation, so a theme that says "dock badges are
376 + * 20px" reaches the bin without naming it.
377 + */
378 + --os-dock-recycle-badge-computed-size: var(
379 + --os-dock-recycle-badge-size,
380 + var(
381 + --os-dock-badge-size,
382 + calc( var( --os-dock-icon-size, 20px ) * 0.75 )
383 + )
384 + );
385 + top: calc( var( --os-dock-icon-size, 20px ) * 1 );
386 + inset-inline-end: calc( var( --os-dock-icon-size, 20px ) * 0.35 );
387 + min-width: var( --os-dock-recycle-badge-computed-size );
388 + height: var( --os-dock-recycle-badge-computed-size );
389 + padding: 0 calc( var( --os-dock-recycle-badge-computed-size ) * 0.25 );
223 390 border-radius: 999px;
224 - background: rgba( 29, 35, 39, 0.72 );
225 - color: rgba( 255, 255, 255, 0.92 );
226 - font-size: 8px;
391 + background: var(
392 + --os-dock-recycle-badge-bg,
393 + rgba( 29, 35, 39, 0.72 )
394 + );
395 + color: var(
396 + --os-dock-recycle-badge-fg,
397 + rgba( 255, 255, 255, 0.92 )
398 + );
399 + font-size: var(
400 + --os-dock-recycle-badge-font-size,
401 + var(
402 + --os-dock-badge-font-size,
403 + calc( var( --os-dock-recycle-badge-computed-size ) * 0.8 )
404 + )
405 + );
227 406 font-weight: 700;
228 407 letter-spacing: 0;
229 408 box-shadow:
230 409 inset 0 0 0 1px rgba( 255, 255, 255, 0.26 ),
@@ -230,16 +409,25 @@
230 409 inset 0 0 0 1px rgba( 255, 255, 255, 0.26 ),
231 410 0 1px 2px rgba( 0, 0, 0, 0.24 );
232 411 }
233 412
234 -.desktop-mode-dock__item[ data-system-id="desktop-mode-recycle-bin" ]:hover .desktop-mode-dock__badge {
235 - top: 20px;
413 +.os-dock__item[ data-system-id="desktop-mode-recycle-bin" ]:hover .os-dock__badge {
414 + /* Re-assert the derived offset, not a literal: the base rule above
415 + * is overridden by the generic `__item:hover .__badge` rule, and
416 + * hardcoding 20px here would strand the badge the moment the dock
417 + * icon size changed. */
418 + top: calc( var( --os-dock-icon-size, 20px ) * 1 );
236 419 transform: scale( 1.03 );
237 420 }
238 421
239 -/* System tiles — same footprint as menu tiles, no badges, no rails. */
240 -.desktop-mode-dock__item--system .desktop-mode-dock__item-primary {
241 - color: rgba( 255, 255, 255, 0.8 );
422 +/* System tiles — same footprint as menu tiles, no badges, no rails.
423 + *
424 + * Reads the SAME glyph token as a menu tile, with its own literal as
425 + * the fallback: unthemed, system tiles keep sitting a notch brighter
426 + * than the rest (0.8 vs 0.7); themed, one colour covers every glyph
427 + * in the dock rather than leaving these four stranded white. */
428 +.os-dock__item--system .os-dock__item-primary {
429 + color: var( --os-dock-icon-color, rgba( 255, 255, 255, 0.8 ) );
242 430 }
243 431
244 432 /* -----------------------------------------------------------------------------
245 433 * Attention animations — driven by `Dock.setAttention()` /
@@ -251,9 +439,9 @@
251 439 * shows a static accent ring for the same duration so the affordance
252 440 * still works.
253 441 * ------------------------------------------------------------------------- */
254 442
255 -@keyframes wpd-dock-attention-pulse {
443 +@keyframes os-dock-attention-pulse {
256 444 0%, 100% {
257 445 transform: scale( 1 );
258 446 box-shadow: 0 0 0 0 rgba( 255, 90, 90, 0.55 );
259 447 }
@@ -262,9 +450,9 @@
262 450 box-shadow: 0 0 0 8px rgba( 255, 90, 90, 0 );
263 451 }
264 452 }
265 453
266 -@keyframes wpd-dock-attention-shake {
454 +@keyframes os-dock-attention-shake {
267 455 0%, 100% {
268 456 transform: translateX( 0 );
269 457 }
270 458 20%, 60% {
@@ -274,9 +462,9 @@
274 462 transform: translateX( 3px );
275 463 }
276 464 }
277 465
278 -@keyframes wpd-dock-attention-bounce {
466 +@keyframes os-dock-attention-bounce {
279 467 0%, 100% {
280 468 transform: translateY( 0 );
281 469 }
282 470 30% {
@@ -286,36 +474,36 @@
286 474 transform: translateY( -2px );
287 475 }
288 476 }
289 477
290 -.desktop-mode-dock__item--attention-pulse .desktop-mode-dock__item-primary {
291 - animation: wpd-dock-attention-pulse 1.4s ease-in-out infinite;
478 +.os-dock__item--attention-pulse .os-dock__item-primary {
479 + animation: os-dock-attention-pulse 1.4s ease-in-out infinite;
292 480 }
293 -.desktop-mode-dock__item--attention-shake .desktop-mode-dock__item-primary {
294 - animation: wpd-dock-attention-shake 0.8s ease-in-out infinite;
481 +.os-dock__item--attention-shake .os-dock__item-primary {
482 + animation: os-dock-attention-shake 0.8s ease-in-out infinite;
295 483 }
296 -.desktop-mode-dock__item--attention-bounce .desktop-mode-dock__item-primary {
297 - animation: wpd-dock-attention-bounce 1.2s ease-in-out infinite;
484 +.os-dock__item--attention-bounce .os-dock__item-primary {
485 + animation: os-dock-attention-bounce 1.2s ease-in-out infinite;
298 486 }
299 487
300 488 /* Intensity modulates animation duration — strong = faster, more
301 489 urgent; subtle = slower, calmer. */
302 -.desktop-mode-dock__item--intensity-strong.desktop-mode-dock__item--attention-pulse .desktop-mode-dock__item-primary {
490 +.os-dock__item--intensity-strong.os-dock__item--attention-pulse .os-dock__item-primary {
303 491 animation-duration: 1s;
304 492 }
305 -.desktop-mode-dock__item--intensity-strong.desktop-mode-dock__item--attention-shake .desktop-mode-dock__item-primary {
493 +.os-dock__item--intensity-strong.os-dock__item--attention-shake .os-dock__item-primary {
306 494 animation-duration: 0.6s;
307 495 }
308 -.desktop-mode-dock__item--intensity-strong.desktop-mode-dock__item--attention-bounce .desktop-mode-dock__item-primary {
496 +.os-dock__item--intensity-strong.os-dock__item--attention-bounce .os-dock__item-primary {
309 497 animation-duration: 0.9s;
310 498 }
311 -.desktop-mode-dock__item--intensity-subtle.desktop-mode-dock__item--attention-pulse .desktop-mode-dock__item-primary {
499 +.os-dock__item--intensity-subtle.os-dock__item--attention-pulse .os-dock__item-primary {
312 500 animation-duration: 1.8s;
313 501 }
314 -.desktop-mode-dock__item--intensity-subtle.desktop-mode-dock__item--attention-shake .desktop-mode-dock__item-primary {
502 +.os-dock__item--intensity-subtle.os-dock__item--attention-shake .os-dock__item-primary {
315 503 animation-duration: 1s;
316 504 }
317 -.desktop-mode-dock__item--intensity-subtle.desktop-mode-dock__item--attention-bounce .desktop-mode-dock__item-primary {
505 +.os-dock__item--intensity-subtle.os-dock__item--attention-bounce .os-dock__item-primary {
318 506 animation-duration: 1.6s;
319 507 }
320 508
321 509 /* Reduced-motion: kill the animation, replace with a static accent ring
@@ -320,11 +508,11 @@
320 508
321 509 /* Reduced-motion: kill the animation, replace with a static accent ring
322 510 for the same visual prominence without the movement. */
323 511 @media ( prefers-reduced-motion: reduce ) {
324 - .desktop-mode-dock__item--attention-pulse .desktop-mode-dock__item-primary,
325 - .desktop-mode-dock__item--attention-shake .desktop-mode-dock__item-primary,
326 - .desktop-mode-dock__item--attention-bounce .desktop-mode-dock__item-primary {
512 + .os-dock__item--attention-pulse .os-dock__item-primary,
513 + .os-dock__item--attention-shake .os-dock__item-primary,
514 + .os-dock__item--attention-bounce .os-dock__item-primary {
327 515 animation: none;
328 516 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
329 517 outline-offset: 2px;
330 518 border-radius: 8px;
@@ -331,13 +519,19 @@
331 519 }
332 520 }
333 521
334 522 /* Shared tooltip — placement-specific anchor modifiers applied by JS. */
335 -.desktop-mode-dock__tooltip {
523 +.os-dock__tooltip {
336 524 position: fixed;
337 525 padding: 6px 12px;
338 - background: rgba( 0, 0, 0, 0.85 );
339 - color: #fff;
526 + background: var(
527 + --os-tooltip-bg,
528 + var( --os-ui-scrim, rgba( 0, 0, 0, 0.85 ) )
529 + );
530 + color: var(
531 + --os-tooltip-fg,
532 + var( --os-ui-fg-on-accent, #fff )
533 + );
340 534 font-size: 12px;
341 535 font-weight: 400;
342 536 line-height: 1.4;
343 537 white-space: nowrap;
@@ -342,14 +536,14 @@
342 536 line-height: 1.4;
343 537 white-space: nowrap;
344 538 border-radius: 6px;
345 539 pointer-events: none;
346 - z-index: calc( var( --desktop-mode-z-dock ) + 1 );
540 + z-index: calc( var( --os-z-dock ) + 1 );
347 541 opacity: 0;
348 542 transition: opacity 0.15s ease, transform 0.15s ease;
349 543 }
350 544
351 -.desktop-mode-dock__tooltip--visible {
545 +.os-dock__tooltip--visible {
352 546 opacity: 1;
353 547 }
354 548
355 549 /* ------------------------------------------------------------------
@@ -356,11 +550,11 @@
356 550 * Vertical placement (left + right) — dock runs the full shell body
357 551 * height along one edge, with items stacked vertically.
358 552 * ------------------------------------------------------------------ */
359 553
360 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ],
361 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ] {
362 - width: var( --desktop-mode-dock-width );
554 +.os-dock[ data-os-dock-placement="left" ],
555 +.os-dock[ data-os-dock-placement="right" ] {
556 + width: var( --os-dock-width );
363 557 flex-direction: column;
364 558 padding: 16px 0 12px;
365 559 gap: 6px;
366 560 /*
@@ -392,10 +586,10 @@
392 586 * Firefox via `scrollbar-width: none`, WebKit via the pseudo-element
393 587 * rule below — so the rail keeps its clean macOS look. Mouse wheel,
394 588 * touchpad, touch, and keyboard focus-into-view all still work.
395 589 */
396 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ] .desktop-mode-dock__scroll,
397 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ] .desktop-mode-dock__scroll {
590 +.os-dock[ data-os-dock-placement="left" ] .os-dock__scroll,
591 +.os-dock[ data-os-dock-placement="right" ] .os-dock__scroll {
398 592 display: flex;
399 593 flex-direction: column;
400 594 align-items: center;
401 595 gap: 6px;
@@ -406,10 +600,10 @@
406 600 overflow-x: clip;
407 601 scrollbar-width: none;
408 602 }
409 603
410 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ] .desktop-mode-dock__scroll::-webkit-scrollbar,
411 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ] .desktop-mode-dock__scroll::-webkit-scrollbar {
604 +.os-dock[ data-os-dock-placement="left" ] .os-dock__scroll::-webkit-scrollbar,
605 +.os-dock[ data-os-dock-placement="right" ] .os-dock__scroll::-webkit-scrollbar {
412 606 display: none;
413 607 }
414 608
415 609 /*
@@ -416,10 +610,10 @@
416 610 * Pinned area below `__scroll` — system tiles stay reachable here
417 611 * regardless of how the menu list scrolls. `flex-shrink: 0` so it's
418 612 * never compressed; sized to its content's natural height.
419 613 */
420 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ] .desktop-mode-dock__pinned,
421 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ] .desktop-mode-dock__pinned {
614 +.os-dock[ data-os-dock-placement="left" ] .os-dock__pinned,
615 +.os-dock[ data-os-dock-placement="right" ] .os-dock__pinned {
422 616 display: flex;
423 617 flex-direction: column;
424 618 align-items: center;
425 619 gap: 6px;
@@ -427,22 +621,22 @@
427 621 width: 100%;
428 622 }
429 623
430 624 /* Left: dock is the first flex item in __body (row layout). */
431 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ] {
432 - border-inline-end: 1px solid var( --desktop-mode-dock-border );
625 +.os-dock[ data-os-dock-placement="left" ] {
626 + border-inline-end: 1px solid var( --os-dock-border );
433 627 order: -1;
434 628 }
435 629
436 630 /* Right: push the dock to the end of __body and flip the border edge. */
437 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ] {
438 - border-inline-start: 1px solid var( --desktop-mode-dock-border );
631 +.os-dock[ data-os-dock-placement="right" ] {
632 + border-inline-start: 1px solid var( --os-dock-border );
439 633 order: 1;
440 634 }
441 635
442 636 /* Active-indicator dot — left dock: inside-edge vertical pill. */
443 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
444 - .desktop-mode-dock__item--active::before {
637 +.os-dock[ data-os-dock-placement="left" ]
638 + .os-dock__item--active::before {
445 639 content: "";
446 640 position: absolute;
447 641 inset-inline-start: -6px;
448 642 top: 50%;
@@ -449,13 +643,13 @@
449 643 transform: translateY( -50% );
450 644 width: 4px;
451 645 height: 4px;
452 646 border-radius: 50%;
453 - background: #fff;
647 + background: var( --os-dock-item-outline, #fff );
454 648 }
455 649
456 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
457 - .desktop-mode-dock__item--focused::before {
650 +.os-dock[ data-os-dock-placement="left" ]
651 + .os-dock__item--focused::before {
458 652 width: 4px;
459 653 height: 16px;
460 654 border-radius: 2px;
461 655 }
@@ -469,20 +663,20 @@
469 663 *
470 664 * Sized slightly larger than the solid dot so the ring is legible at
471 665 * dock size — a 4×4 outline reads as a dirty pixel.
472 666 */
473 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
474 - .desktop-mode-dock__item--all-minimized::before {
667 +.os-dock[ data-os-dock-placement="left" ]
668 + .os-dock__item--all-minimized::before {
475 669 width: 6px;
476 670 height: 6px;
477 671 border-radius: 50%;
478 672 background: transparent;
479 - border: 1px solid rgba( 255, 255, 255, 0.85 );
673 + border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
480 674 }
481 675
482 676 /* Right dock: indicator on the opposite edge (inside-facing). */
483 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
484 - .desktop-mode-dock__item--active::before {
677 +.os-dock[ data-os-dock-placement="right" ]
678 + .os-dock__item--active::before {
485 679 content: "";
486 680 position: absolute;
487 681 inset-inline-end: -6px;
488 682 top: 50%;
@@ -489,78 +683,95 @@
489 683 transform: translateY( -50% );
490 684 width: 4px;
491 685 height: 4px;
492 686 border-radius: 50%;
493 - background: #fff;
687 + background: var( --os-dock-item-outline, #fff );
494 688 }
495 689
496 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
497 - .desktop-mode-dock__item--focused::before {
690 +.os-dock[ data-os-dock-placement="right" ]
691 + .os-dock__item--focused::before {
498 692 width: 4px;
499 693 height: 16px;
500 694 border-radius: 2px;
501 695 }
502 696
503 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
504 - .desktop-mode-dock__item--all-minimized::before {
697 +.os-dock[ data-os-dock-placement="right" ]
698 + .os-dock__item--all-minimized::before {
505 699 width: 6px;
506 700 height: 6px;
507 701 border-radius: 50%;
508 702 background: transparent;
509 - border: 1px solid rgba( 255, 255, 255, 0.85 );
703 + border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
510 704 }
511 705
512 706 /* "Open another" chip — left: right of tile; right: left of tile. */
513 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
514 - .desktop-mode-dock__item-new {
707 +.os-dock[ data-os-dock-placement="left" ]
708 + .os-dock__item-new {
515 709 top: 50%;
516 710 inset-inline-end: -13px;
517 711 transform: translateY( -50% );
518 712 }
519 713
520 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
521 - .desktop-mode-dock__item-new:hover {
714 +.os-dock[ data-os-dock-placement="left" ]
715 + .os-dock__item-new:hover {
522 716 transform: translateY( -50% ) scale( 1.1 );
523 717 }
524 718
525 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
526 - .desktop-mode-dock__item-new {
719 +.os-dock[ data-os-dock-placement="right" ]
720 + .os-dock__item-new {
527 721 top: 50%;
528 722 inset-inline-start: -13px;
529 723 transform: translateY( -50% );
530 724 }
531 725
532 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
533 - .desktop-mode-dock__item-new:hover {
726 +.os-dock[ data-os-dock-placement="right" ]
727 + .os-dock__item-new:hover {
534 728 transform: translateY( -50% ) scale( 1.1 );
535 729 }
536 730
537 -/* Separator — horizontal hairline between menu tiles and system
538 - * tiles. With the `__scroll` / `__pinned` split the system separator
539 - * lives at the top of `__pinned`; flex-flow handles the "system
540 - * tiles at the bottom" placement, no margin-top: auto needed. The
541 - * `--group` variant is used inline between core and plugin menu
542 - * tiles in `__scroll`; it keeps the hairline styling and sits
543 - * exactly where it's inserted. */
544 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
545 - .desktop-mode-dock__separator,
546 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
547 - .desktop-mode-dock__separator {
731 +/* Separator — the line between two groups of tiles. With the
732 + * `__scroll` / `__pinned` split the system separator lives at the top
733 + * of `__pinned`; flex-flow handles the "system tiles at the bottom"
734 + * placement, no margin-top: auto needed. The `--group` variant is used
735 + * inline between core and plugin menu tiles in `__scroll` and sits
736 + * exactly where it's inserted.
737 + *
738 + * ONE treatment for every division in the rail. The selector carries
739 + * only the base class on purpose: `--group` elements carry it too, so
740 + * both boundaries resolve here and cannot drift apart. Two weights in
741 + * one short rail read as two unrelated ideas rather than one system,
742 + * which is why the earlier loud-plus-hairline pairing is gone.
743 + *
744 + * A gradient, not a flat fill: the line is brightest where the eye
745 + * lands and gone by the time it reaches the rail's padding, so it
746 + * never terminates in a visible stub against the glass. The soft glow
747 + * around it is what keeps a 2px line from disappearing into the dock
748 + * tint; nothing is drawn on the line itself. */
749 +.os-dock[ data-os-dock-placement="left" ]
750 + .os-dock__separator,
751 +.os-dock[ data-os-dock-placement="right" ]
752 + .os-dock__separator {
753 + position: relative;
548 754 width: 60%;
549 - height: 1px;
550 - margin: 8px auto 4px;
551 - background: var( --desktop-mode-dock-border );
755 + height: 2px;
756 + margin: 12px auto;
757 + border-radius: 2px;
758 + background: linear-gradient(
759 + to right,
760 + transparent 0%,
761 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 28%,
762 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 72%,
763 + transparent 100%
764 + );
765 + box-shadow: 0 0 10px
766 + color-mix(
767 + in srgb,
768 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 45%,
769 + transparent
770 + );
552 771 flex-shrink: 0;
553 772 }
554 773
555 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
556 - .desktop-mode-dock__separator--group,
557 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
558 - .desktop-mode-dock__separator--group {
559 - margin: 10px auto;
560 - background: rgba( 255, 255, 255, 0.22 );
561 -}
562 -
563 774 /*
564 775 * Hide the system separator when nothing precedes it — on a clean
565 776 * install (or a low-cap user with zero menu items) the separator
566 777 * would render as a stray hairline with nothing to divide.
@@ -570,18 +781,19 @@
570 781 * when it's the first such child.
571 782 * 2. Vertical docks — separator lives inside `__pinned`; hide when
572 783 * `__scroll` (its sibling above) is empty of menu tiles.
573 784 */
574 -.desktop-mode-dock > .desktop-mode-dock__separator:first-child {
785 +.os-dock > .os-dock__separator:first-child {
575 786 display: none;
576 787 }
577 788
578 -.desktop-mode-dock__scroll:not(:has( .desktop-mode-dock__item ))
579 - + .desktop-mode-dock__pinned
580 - .desktop-mode-dock__separator {
789 +.os-dock__scroll:not(:has( .os-dock__item ))
790 + + .os-dock__pinned
791 + .os-dock__separator {
581 792 display: none;
582 793 }
583 794
795 +
584 796 /*
585 797 * Drop an empty `__scroll` from the flex flow entirely. When only
586 798 * system tiles are present (e.g. just the Recycle Bin), the empty
587 799 * wrapper would otherwise still occupy the rail: its `flex-grow`
@@ -591,20 +803,20 @@
591 803 * it lets the dock center the lone system tile. As soon as a menu
592 804 * tile is added the `:has()` no longer matches and normal flow
593 805 * resumes.
594 806 *
595 - * The `[data-desktop-mode-dock-placement]` qualifier is load-bearing:
807 + * The `[data-os-dock-placement]` qualifier is load-bearing:
596 808 * the per-placement rules that set `display: flex` on `__scroll` are
597 809 * (0,3,0); this selector must out-specify them, so it carries the
598 810 * placement attribute to reach (0,4,0).
599 811 */
600 -.desktop-mode-dock[ data-desktop-mode-dock-placement ]
601 - .desktop-mode-dock__scroll:not(:has( .desktop-mode-dock__item )) {
812 +.os-dock[ data-os-dock-placement ]
813 + .os-dock__scroll:not(:has( .os-dock__item )) {
602 814 display: none;
603 815 }
604 816
605 817 /* Tooltip — anchor modifiers are applied directly to the tooltip
606 - * element (NOT via a descendant selector on `.desktop-mode-dock`)
818 + * element (NOT via a descendant selector on `.os-dock`)
607 819 * because the tooltip lives in `document.body`. JS writes the
608 820 * absolute `top` / `left` coords; CSS handles the slide-in
609 821 * animation direction per modifier.
610 822 *
@@ -614,17 +826,17 @@
614 826 */
615 827
616 828 /* Left dock — tooltip slides in from the left by 4px. JS sets
617 829 * `left` to `tile.right + 8px`; the transform animates the entry. */
618 -.desktop-mode-dock__tooltip--after {
830 +.os-dock__tooltip--after {
619 831 transform: translateX( -4px );
620 832 }
621 833
622 -.desktop-mode-dock__tooltip--after.desktop-mode-dock__tooltip--visible {
834 +.os-dock__tooltip--after.os-dock__tooltip--visible {
623 835 transform: translateX( 0 );
624 836 }
625 837
626 -.desktop-mode-dock__tooltip--before {
838 +.os-dock__tooltip--before {
627 839 /* Right-placed dock — tooltip sits to the left of the tile. JS
628 840 * writes the left coord to the tile's left edge; this translate
629 841 * pulls the tooltip further left by its own width. */
630 842 transform: translate( calc( -100% - 8px ), 0 );
@@ -629,9 +841,9 @@
629 841 * pulls the tooltip further left by its own width. */
630 842 transform: translate( calc( -100% - 8px ), 0 );
631 843 }
632 844
633 -.desktop-mode-dock__tooltip--before.desktop-mode-dock__tooltip--visible {
845 +.os-dock__tooltip--before.os-dock__tooltip--visible {
634 846 transform: translate( calc( -100% - 8px ), 0 );
635 847 opacity: 1;
636 848 }
637 849
@@ -640,9 +852,9 @@
640 852 * viewport floor. Absolutely positioned so it floats as pure chrome
641 853 * rather than stealing vertical space from the shell body.
642 854 * ------------------------------------------------------------------ */
643 855
644 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ] {
856 +.os-dock[ data-os-dock-placement="bottom" ] {
645 857 position: absolute;
646 858 inset-inline-start: 0;
647 859 inset-inline-end: 0;
648 860 bottom: 12px;
@@ -661,37 +873,76 @@
661 873 border-radius: 18px;
662 874
663 875 /* Warmer, more saturated glass tint than vertical placements so
664 876 * the floating pill reads as a distinct piece of chrome rather
665 - * than a section of the sidebar. */
666 - background: rgba( 22, 22, 26, 0.42 );
877 + * than a section of the sidebar.
878 + *
879 + * background-COLOR, not the shorthand: the base `.os-dock`
880 + * rule declares the DOCK texture slot's background-image, and a
881 + * shorthand here would reset it — a themed dock texture would
882 + * simply never appear in the bottom placement.
883 + *
884 + * The tint is its own token rather than `--os-dock-bg`
885 + * precisely because the two are meant to differ; a theme that
886 + * wants them identical points this at the same value. */
887 + background-color: var(
888 + --os-dock-floating-bg,
889 + rgba( 22, 22, 26, 0.42 )
890 + );
667 891 backdrop-filter: blur( 28px ) saturate( 170% );
668 892 -webkit-backdrop-filter: blur( 28px ) saturate( 170% );
669 893
670 - border: 1px solid rgba( 255, 255, 255, 0.12 );
671 - border-block-start: 1px solid rgba( 255, 255, 255, 0.18 );
894 + /*
895 + * Hairline + inset highlight. These used to be hardcoded, which
896 + * meant a theme could restyle every dock placement EXCEPT this
897 + * one and had no way to remove the outline it saw floating over
898 + * an empty desktop.
899 + *
900 + * The chain matters: the floating-specific token wins, then the
901 + * general `--os-dock-border` (so a theme that tokenized
902 + * its side docks gets the pill for free, which is what anyone
903 + * setting that token expects), then the literal. Set either to
904 + * `transparent` for a borderless pill.
905 + */
906 + border: 1px solid var(
907 + --os-dock-floating-border,
908 + var( --os-dock-border, rgba( 255, 255, 255, 0.12 ) )
909 + );
910 + border-block-start: 1px solid var(
911 + --os-dock-floating-border-top,
912 + var(
913 + --os-dock-floating-border,
914 + var( --os-dock-border, rgba( 255, 255, 255, 0.18 ) )
915 + )
916 + );
672 917 box-shadow:
673 - inset 0 1px 0 rgba( 255, 255, 255, 0.08 ),
674 - 0 10px 32px rgba( 0, 0, 0, 0.45 );
918 + inset 0 1px 0 var(
919 + --os-dock-floating-highlight,
920 + rgba( 255, 255, 255, 0.08 )
921 + ),
922 + 0 10px 32px var(
923 + --os-dock-floating-shadow,
924 + rgba( 0, 0, 0, 0.45 )
925 + );
675 926 }
676 927
677 928 /* Active-indicator dot — below the tile, macOS-style. */
678 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
679 - .desktop-mode-dock__item--active::before {
929 +.os-dock[ data-os-dock-placement="bottom" ]
930 + .os-dock__item--active::before {
680 931 content: "";
681 932 position: absolute;
682 933 inset-inline-start: 50%;
683 934 top: auto;
684 - bottom: -4px;
935 + bottom: -3px;
685 936 transform: translateX( -50% );
686 937 width: 4px;
687 938 height: 4px;
688 939 border-radius: 50%;
689 - background: #fff;
940 + background: var( --os-dock-item-outline, #fff );
690 941 }
691 942
692 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
693 - .desktop-mode-dock__item--focused::before {
943 +.os-dock[ data-os-dock-placement="bottom" ]
944 + .os-dock__item--focused::before {
694 945 width: 16px;
695 946 height: 4px;
696 947 border-radius: 2px;
697 948 }
@@ -700,53 +951,50 @@
700 951 * "All instances minimized" on the bottom dock — solid dot becomes
701 952 * a hollow ring. Matches the vertical-dock treatment so the cue is
702 953 * consistent across placements.
703 954 */
704 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
705 - .desktop-mode-dock__item--all-minimized::before {
955 +.os-dock[ data-os-dock-placement="bottom" ]
956 + .os-dock__item--all-minimized::before {
706 957 width: 6px;
707 958 height: 6px;
708 959 border-radius: 50%;
709 960 background: transparent;
710 - border: 1px solid rgba( 255, 255, 255, 0.85 );
961 + border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
711 962 }
712 963
713 964 /*
714 - * Global "Show Desktop" indicator — set on `<body>` by the dock when
715 - * every live window on the active desktop is in the minimized state.
716 - * The bottom-dock pill picks up a soft theme-color ring so the user
717 - * has a global cue that the windows haven't disappeared, they're just
718 - * minimized. ArrowUp / a wallpaper click / clicking any dock tile
719 - * brings them back; without this the empty wallpaper looks identical
720 - * to a freshly-loaded desktop with no windows open.
965 + * "Show Desktop" — every live window on the active desktop minimized.
966 + *
967 + * The dock does not change when this happens. It used to: the bottom
968 + * pill and the vertical rails each picked up a 1px inset ring, on the
969 + * reasoning that an empty wallpaper otherwise looks identical to a
970 + * desktop that never had anything on it.
971 + *
972 + * The reasoning was sound and the ring was still the wrong answer. It
973 + * outlined the whole dock — the one surface on screen that is always
974 + * present and never the subject — to say something about windows that
975 + * are not on the dock at all. Every tile that has minimized windows
976 + * already says so, precisely and locally, by swapping its solid
977 + * indicator dot for a hollow ring. That is the cue: it points at the
978 + * tiles the windows are under, in the place the user will click to get
979 + * them back.
980 + *
981 + * `body.os-show-desktop-active` is still set (see `src/dock.ts`) and
982 + * still public, so a theme or plugin that wants a global cue can paint
983 + * one. Core does not.
721 984 */
722 -body.desktop-mode-show-desktop-active
723 - .desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ] {
724 - box-shadow:
725 - inset 0 0 0 1px var( --wp-admin-theme-color, rgba( 255, 255, 255, 0.45 ) ),
726 - inset 0 1px 0 rgba( 255, 255, 255, 0.08 ),
727 - 0 10px 32px rgba( 0, 0, 0, 0.45 );
728 -}
729 985
730 -/* Vertical docks get the same treatment along their inner edge. */
731 -body.desktop-mode-show-desktop-active
732 - .desktop-mode-dock[ data-desktop-mode-dock-placement="left" ],
733 -body.desktop-mode-show-desktop-active
734 - .desktop-mode-dock[ data-desktop-mode-dock-placement="right" ] {
735 - box-shadow: inset 0 0 0 1px var( --wp-admin-theme-color, rgba( 255, 255, 255, 0.45 ) );
736 -}
737 -
738 986 /* "Open another" chip — floats above the tile's top-right corner so
739 987 * it doesn't collide with neighbouring tiles in the horizontal row. */
740 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
741 - .desktop-mode-dock__item-new {
988 +.os-dock[ data-os-dock-placement="bottom" ]
989 + .os-dock__item-new {
742 990 top: -6px;
743 991 inset-inline-end: -6px;
744 992 transform: none;
745 993 }
746 994
747 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
748 - .desktop-mode-dock__item-new:hover {
995 +.os-dock[ data-os-dock-placement="bottom" ]
996 + .os-dock__item-new:hover {
749 997 transform: scale( 1.1 );
750 998 }
751 999
752 1000 /*
@@ -751,11 +999,21 @@
751 999
752 1000 /*
753 1001 * Bottom-dock inner wrappers — `__scroll` carries the menu tiles and
754 1002 * absorbs horizontal overflow; `__pinned` carries the system tiles and
755 - * stays anchored to the trailing edge. Padding-top on `__scroll` gives
756 - * the badge (top: -3px on the tile) room so `overflow-y: hidden`
757 - * doesn't crop it. `min-width: 0` lets the wrapper shrink below its
1003 + * stays anchored to the trailing edge.
1004 + *
1005 + * **The padding around `__scroll` is what keeps badges visible, and it
1006 + * has to be on both axes.** `overflow-x: auto` below cannot coexist
1007 + * with `overflow-y: visible`: per spec the visible axis computes to
1008 + * `auto`, so this wrapper is a scroll container in BOTH directions
1009 + * however it is authored, and anything a child hangs outside its
1010 + * padding box is clipped. A badge sits at `top: -3px; right: -3px` on
1011 + * its tile, and the LAST tile's edge is the wrapper's edge once the
1012 + * content is wide enough to scroll — which is why this showed up as
1013 + * "the badge on the last plugin looks cut off" rather than as a
1014 + * general problem. The inline padding is symmetric so the tile cluster
1015 + * stays centred in the pill. `min-width: 0` lets the wrapper shrink below its
758 1016 * content's natural width, so the dock pill's `max-width` is what
759 1017 * decides when scroll kicks in instead of the content forcing the
760 1018 * pill wider. `justify-content: center` keeps tiles balanced inside
761 1019 * the pill when content fits; when it overflows, scroll engages and
@@ -760,9 +1018,9 @@
760 1018 * pill wider. `justify-content: center` keeps tiles balanced inside
761 1019 * the pill when content fits; when it overflows, scroll engages and
762 1020 * items extend past the visible area.
763 1021 */
764 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ] .desktop-mode-dock__scroll {
1022 +.os-dock[ data-os-dock-placement="bottom" ] .os-dock__scroll {
765 1023 display: flex;
766 1024 flex-direction: row;
767 1025 align-items: center;
768 1026 justify-content: center;
@@ -769,18 +1027,26 @@
769 1027 gap: 6px;
770 1028 flex: 1 1 auto;
771 1029 min-width: 0;
772 1030 padding-top: 4px;
1031 + padding-bottom: 6px;
1032 + padding-inline: 4px;
773 1033 overflow-x: auto;
774 - overflow-y: hidden;
1034 + /*
1035 + * Authored `visible`, computed `auto` — see the note above. Kept as
1036 + * the honest statement of intent: nothing here wants a vertical
1037 + * scrollbar, and `scrollbar-width: none` plus the padding is what
1038 + * makes that true in practice.
1039 + */
1040 + overflow-y: visible;
775 1041 scrollbar-width: none;
776 1042 }
777 1043
778 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ] .desktop-mode-dock__scroll::-webkit-scrollbar {
1044 +.os-dock[ data-os-dock-placement="bottom" ] .os-dock__scroll::-webkit-scrollbar {
779 1045 display: none;
780 1046 }
781 1047
782 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ] .desktop-mode-dock__pinned {
1048 +.os-dock[ data-os-dock-placement="bottom" ] .os-dock__pinned {
783 1049 display: flex;
784 1050 flex-direction: row;
785 1051 align-items: center;
786 1052 gap: 6px;
@@ -787,60 +1053,58 @@
787 1053 flex-shrink: 0;
788 1054 /* Match the scroll wrapper's badge clearance so system tiles sit
789 1055 * on the same vertical baseline as the menu tiles. */
790 1056 padding-top: 4px;
1057 + padding-bottom: 6px;
791 1058 }
792 1059
793 -/* Separator — vertical hairline between menu tiles and system tiles.
794 - * Lives at the leading edge of `__pinned`; flex flow handles the
1060 +/* Separator — the line between two groups of tiles. The system one
1061 + * lives at the leading edge of `__pinned` (flex flow handles the
795 1062 * "pinned at the trailing edge" placement, no margin-inline-start:
796 - * auto needed. The `--group` variant keeps the hairline styling for
797 - * the inline core→plugin boundary inside `__scroll` and sits where
798 - * inserted. */
799 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
800 - .desktop-mode-dock__separator {
801 - width: 1px;
1063 + * auto needed); the `--group` one sits inline inside `__scroll` where
1064 + * it was inserted. Both resolve through this single rule — see the
1065 + * note on the vertical placements above for why there is only one. */
1066 +.os-dock[ data-os-dock-placement="bottom" ]
1067 + .os-dock__separator {
1068 + position: relative;
1069 + width: 2px;
802 1070 /* Fixed height — `60%` collapses against the floating pill's
803 - intrinsic height, leaving the hairline effectively invisible.
804 - 28px matches the dock tile inner glyph area. */
805 - height: 28px;
1071 + intrinsic height, leaving the line effectively invisible. */
1072 + height: 34px;
806 1073 /* Symmetric horizontal margin so the divider sits centered in the
807 1074 * inter-tile gap: the 6px flex gap on each side (`__scroll`→`__pinned`
808 - * on the left, `__pinned`'s own gap on the right) plus an equal 6px
1075 + * on the left, `__pinned`'s own gap on the right) plus an equal
809 1076 * margin per side balances out. An asymmetric margin here nudges the
810 1077 * divider — and the whole menu cluster with it — off the pill's
811 - * center. */
812 - margin: auto 6px;
813 - background: var( --desktop-mode-dock-border );
1078 + * center. Wider than the tile gap on purpose: the clusters need to
1079 + * read as groups before the line between them means anything. */
1080 + margin: auto 14px;
1081 + border-radius: 2px;
1082 + background: linear-gradient(
1083 + to bottom,
1084 + transparent 0%,
1085 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 28%,
1086 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 72%,
1087 + transparent 100%
1088 + );
1089 + box-shadow: 0 0 10px
1090 + color-mix(
1091 + in srgb,
1092 + var( --os-dock-divider, rgba( 217, 46, 227, 0.7 ) ) 45%,
1093 + transparent
1094 + );
814 1095 flex-shrink: 0;
815 1096 }
816 1097
817 -/*
818 - * Group separator — sits inline between the core cluster and the
819 - * plugin cluster. Brighter than the menu-vs-system separator because
820 - * it carries user meaning ("this is where your installed apps
821 - * begin"), and flanked by extra gap so the boundary reads at a
822 - * glance rather than disappearing into the dock tint.
823 - */
824 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
825 - .desktop-mode-dock__separator--group {
826 - width: 1px;
827 - height: 60%;
828 - margin: auto 10px;
829 - margin-inline-start: 10px;
830 - background: rgba( 255, 255, 255, 0.22 );
831 - flex-shrink: 0;
832 -}
833 -
834 1098 /* Tooltip — above the hovered tile. JS writes the horizontal center
835 1099 * + a top near the tile's top edge; CSS translates the tooltip up by
836 1100 * its own height so it clears the tile. */
837 -.desktop-mode-dock__tooltip--above {
1101 +.os-dock__tooltip--above {
838 1102 inset-inline-start: auto;
839 1103 transform: translate( -50%, calc( -100% - 4px ) );
840 1104 }
841 1105
842 -.desktop-mode-dock__tooltip--above.desktop-mode-dock__tooltip--visible {
1106 +.os-dock__tooltip--above.os-dock__tooltip--visible {
843 1107 transform: translate( -50%, -100% );
844 1108 }
845 1109
846 1110
@@ -854,9 +1118,9 @@
854 1118 * underneath the cursor. Sibling tiles animate via FLIP — the JS
855 1119 * writes inline `transform` + `transition` per leg, so no CSS
856 1120 * transition is declared here (it would race with the per-leg
857 1121 * inline transition). */
858 -.desktop-mode-dock__item--dragging {
1122 +.os-dock__item--dragging {
859 1123 pointer-events: none;
860 1124 z-index: 50;
861 1125 opacity: 0.92;
862 1126 cursor: grabbing;
@@ -865,11 +1129,311 @@
865 1129 * the dock surface it floats above. */
866 1130 filter: drop-shadow( 0 6px 12px rgba( 0, 0, 0, 0.35 ) );
867 1131 }
868 1132
869 -.desktop-mode-dock__item:not( .desktop-mode-dock__item--system ) {
1133 +.os-dock__item:not( .os-dock__item--system ) {
870 1134 cursor: grab;
871 1135 }
872 1136
873 -.desktop-mode-dock__item:not( .desktop-mode-dock__item--system ):active {
1137 +.os-dock__item:not( .os-dock__item--system ):active {
874 1138 cursor: grabbing;
1139 +}
1140 +
1141 +/* ------------------------------------------------------------------
1142 + * The way out — `Exit OpenStation`
1143 + *
1144 + * Every other system tile opens something you can close again. This
1145 + * one leaves the desktop, and with the admin bar hidden by default it
1146 + * is the only route back to classic admin. Drawn like its neighbours
1147 + * it read as one more launcher, which is the same "some tiles do a
1148 + * different kind of thing" confusion the single dock set out to fix,
1149 + * just moved to the other end of the rail.
1150 + *
1151 + * Three signals, none of them colour. Danger red would overstate it
1152 + * (nothing is destroyed, the session is saved and the desktop is one
1153 + * click away) and Pulse is already spent on the seam:
1154 + *
1155 + * 1. **Last, always.** `order: 1` rather than registration order —
1156 + * native-window tiles from plugins sync in after boot and would
1157 + * otherwise land behind it.
1158 + * 2. **Its own gap**, wider than the rail's 6px, so it reads as
1159 + * sitting outside the set rather than at the end of it. A second
1160 + * divider would have said this too, and would have cost the rail
1161 + * the one-structural-line rule it just earned.
1162 + * 3. **A different silhouette** — a ring rather than a plate. Shape
1163 + * survives every theme, every dock texture, greyscale and a
1164 + * colour-blind reading of the rail; a tint survives none of them
1165 + * reliably.
1166 + *
1167 + * And it leans toward the edge it leads to on hover instead of lifting
1168 + * toward the pointer, because every other tile's lift means "I will
1169 + * come to you" and this one means the opposite.
1170 + *
1171 + * Keyed on the tile id, the same idiom the Recycle Bin badge uses
1172 + * above. `os-exit` is frozen in `src/exit-openstation.ts`
1173 + * (`EXIT_OPENSTATION_TILE_ID`) and `dock-exit-tile.test.ts` pins the
1174 + * attribute this selector depends on.
1175 + * ------------------------------------------------------------------ */
1176 +
1177 +.os-dock__item[ data-system-id="os-exit" ] {
1178 + order: 1;
1179 +}
1180 +
1181 +.os-dock__item[ data-system-id="os-exit" ] .os-dock__item-primary {
1182 + border-radius: 50%;
1183 + background-color: transparent;
1184 + box-shadow: inset 0 0 0 1px var(
1185 + --os-dock-exit-ring,
1186 + var( --os-dock-border, rgba( 255, 255, 255, 0.18 ) )
1187 + );
1188 + color: var( --os-dock-exit-icon, rgba( 255, 255, 255, 0.55 ) );
1189 +}
1190 +
1191 +/*
1192 + * The gap runs along the rail's own axis, so it follows the placement:
1193 + * inline for the horizontal pill, block for the vertical pillars.
1194 + */
1195 +.os-dock[ data-os-dock-placement="bottom" ]
1196 + .os-dock__item[ data-system-id="os-exit" ] {
1197 + margin-inline-start: 10px;
1198 +}
1199 +
1200 +.os-dock[ data-os-dock-placement="left" ]
1201 + .os-dock__item[ data-system-id="os-exit" ],
1202 +.os-dock[ data-os-dock-placement="right" ]
1203 + .os-dock__item[ data-system-id="os-exit" ] {
1204 + margin-block-start: 8px;
1205 +}
1206 +
1207 +/*
1208 + * Hover: the ring closes up and the tile moves TOWARD its edge. The
1209 + * generic tile rule scales up by 1.1, so these have to out-specify it
1210 + * rather than sit alongside it.
1211 + */
1212 +.os-dock__item[ data-system-id="os-exit" ] .os-dock__item-primary:hover,
1213 +.os-dock__item[ data-system-id="os-exit" ]
1214 + .os-dock__item-primary:focus-visible {
1215 + background-color: transparent;
1216 + box-shadow: inset 0 0 0 1px var(
1217 + --os-dock-exit-ring-hover,
1218 + var( --os-dock-item-outline, rgba( 255, 255, 255, 0.7 ) )
1219 + );
1220 + color: var( --os-dock-icon-color-hover, var( --os-ui-fg-on-accent, #fff ) );
1221 +}
1222 +
1223 +.os-dock[ data-os-dock-placement="bottom" ]
1224 + .os-dock__item[ data-system-id="os-exit" ]
1225 + .os-dock__item-primary:hover {
1226 + transform: translateY( 2px );
1227 +}
1228 +
1229 +.os-dock[ data-os-dock-placement="left" ]
1230 + .os-dock__item[ data-system-id="os-exit" ]
1231 + .os-dock__item-primary:hover {
1232 + transform: translateX( -2px );
1233 +}
1234 +
1235 +.os-dock[ data-os-dock-placement="right" ]
1236 + .os-dock__item[ data-system-id="os-exit" ]
1237 + .os-dock__item-primary:hover {
1238 + transform: translateX( 2px );
1239 +}
1240 +
1241 +/* The press keeps the shared cue: every tile in the rail dips on
1242 + * :active, and this one should not feel unresponsive by comparison. */
1243 +.os-dock__item[ data-system-id="os-exit" ] .os-dock__item-primary:active {
1244 + transform: scale( 0.95 );
1245 +}
1246 +
1247 +@media ( prefers-reduced-motion: reduce ) {
1248 + .os-dock[ data-os-dock-placement="bottom" ]
1249 + .os-dock__item[ data-system-id="os-exit" ]
1250 + .os-dock__item-primary:hover,
1251 + .os-dock[ data-os-dock-placement="left" ]
1252 + .os-dock__item[ data-system-id="os-exit" ]
1253 + .os-dock__item-primary:hover,
1254 + .os-dock[ data-os-dock-placement="right" ]
1255 + .os-dock__item[ data-system-id="os-exit" ]
1256 + .os-dock__item-primary:hover {
1257 + transform: none;
1258 + }
1259 +}
1260 +
1261 +/* ------------------------------------------------------------------
1262 + * Dynamic dock behavior — OpenStation Preferences → Appearance →
1263 + * Desktop layout, persisted as `dockBehavior` (the dock) and
1264 + * `sideDockBehavior` (the Split sidebar) and worn by each rail as
1265 + * `data-os-dock-behavior` — an attribute PER RAIL rather than a body
1266 + * class, because Split's two rails answer independently. PHP stamps
1267 + * the dock on first paint; the apply pass and `src/dock-behavior.ts`
1268 + * re-stamp on every change and rebuild. `static` is the absence of
1269 + * rules.
1270 + *
1271 + * A dynamic rail that is not `os-dock--revealed` is PARKED: it
1272 + * collapses into a thin indicator line hugging its edge — the iOS
1273 + * home indicator, one line that says "there is a dock here" — and
1274 + * expands back into the full rail when the pointer comes for it.
1275 + * `src/dock-behavior.ts` owns the flip (a full-width edge zone, the
1276 + * rail's own box, its flyouts, keyboard focus) and runs it through
1277 + * the View Transitions API, so the line morphs into the pill and
1278 + * back; the rules below only describe the two resting states and
1279 + * tune the morph. There is deliberately no `:hover` here — a state
1280 + * CSS flipped on its own would jump instead of morphing.
1281 + *
1282 + * The line is the rail itself, not a separate element: same node,
1283 + * same `view-transition-name`, which is what lets the browser animate
1284 + * one box into the other. Its children are hidden while parked so
1285 + * the line is a line. A side rail leaves the flex row for the
1286 + * duration, so the area grows to the full width and the rail rides
1287 + * over it when summoned; the work area reserves nothing for a
1288 + * dynamic rail (`src/work-area/index.ts`).
1289 + * ------------------------------------------------------------------ */
1290 +
1291 +/*
1292 + * Side rails overlay the area in both states — the flex row must not
1293 + * shrink for a rail that is only sometimes there.
1294 + */
1295 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ],
1296 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ] {
1297 + position: absolute;
1298 + inset-block: 0;
1299 +}
1300 +
1301 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ] {
1302 + inset-inline-start: 0;
1303 +}
1304 +
1305 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ] {
1306 + inset-inline-end: 0;
1307 +}
1308 +
1309 +/*
1310 + * Parked — the indicator line. Everything that makes the pill a
1311 + * pill (padding, border, blur, texture, tile gap) comes off, and the
1312 + * box is the line's size. `overflow: hidden` clips whatever a tile
1313 + * paints past its box during the morph.
1314 + */
1315 +.os-dock[ data-os-dock-behavior="dynamic" ]:not( .os-dock--revealed ) {
1316 + min-width: 0;
1317 + min-height: 0;
1318 + max-width: none;
1319 + padding: 0;
1320 + gap: 0;
1321 + border: 0;
1322 + border-radius: 999px;
1323 + overflow: hidden;
1324 + background-color: var(
1325 + --os-dock-indicator-bg,
1326 + var( --os-dock-item-outline, rgba( 255, 255, 255, 0.55 ) )
1327 + );
1328 + background-image: none;
1329 + backdrop-filter: none;
1330 + -webkit-backdrop-filter: none;
1331 + box-shadow: none;
1332 + cursor: pointer;
1333 +}
1334 +
1335 +.os-dock[ data-os-dock-behavior="dynamic" ]:not( .os-dock--revealed ) > * {
1336 + display: none;
1337 +}
1338 +
1339 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="bottom" ]:not( .os-dock--revealed ) {
1340 + width: var( --os-dock-indicator-length, 180px );
1341 + height: var( --os-dock-indicator-thickness, 5px );
1342 + bottom: 8px;
1343 +}
1344 +
1345 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ]:not( .os-dock--revealed ),
1346 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ]:not( .os-dock--revealed ) {
1347 + width: var( --os-dock-indicator-thickness, 5px );
1348 + height: var( --os-dock-indicator-length, 180px );
1349 + inset-block: calc( 50% - var( --os-dock-indicator-length, 180px ) / 2 );
1350 +}
1351 +
1352 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="left" ]:not( .os-dock--revealed ) {
1353 + inset-inline-start: 8px;
1354 +}
1355 +
1356 +.os-dock[ data-os-dock-behavior="dynamic" ][ data-os-dock-placement="right" ]:not( .os-dock--revealed ) {
1357 + inset-inline-end: 8px;
1358 +}
1359 +
1360 +/*
1361 + * The morph. With the API, `src/dock-behavior.ts` names the rail for
1362 + * the duration of one flip and the browser animates the old box into
1363 + * the new one; the rail's contents cross-fade underneath. The root is
1364 + * opted out of the snapshot so the rest of the desktop keeps
1365 + * running while the line becomes a pill — a frozen page for 260ms
1366 + * on every reveal would be the animation costing more than it
1367 + * gives. Without the API, the properties that can transition do.
1368 + */
1369 +@supports ( view-transition-name: none ) {
1370 + html.os-dock-vt {
1371 + view-transition-name: none;
1372 + }
1373 +
1374 + html.os-dock-vt::view-transition-old( root ),
1375 + html.os-dock-vt::view-transition-new( root ) {
1376 + animation: none;
1377 + }
1378 +
1379 + /*
1380 + * The transition layer must not catch the pointer. By default it
1381 + * does — the whole page reads as inert until the morph settles —
1382 + * and with the root opted out of the snapshot that is worse than
1383 + * pointless: the live desktop is right there under a layer that
1384 + * swallows every mouseup and every pointerenter for 260ms. The
1385 + * behavior module already holds a park back while a button is
1386 + * down; this is the other half, so a hover that lands on a tile
1387 + * while the rail is still morphing out starts its hover-intent
1388 + * timer instead of waiting for the next pointer movement.
1389 + */
1390 + html.os-dock-vt::view-transition {
1391 + pointer-events: none;
1392 + }
1393 +
1394 + ::view-transition-group( os-dock-os-dock ),
1395 + ::view-transition-group( os-dock-os-side-dock ) {
1396 + animation-duration: 260ms;
1397 + animation-timing-function: cubic-bezier( 0.22, 1, 0.36, 1 );
1398 + }
1399 +
1400 + ::view-transition-old( os-dock-os-dock ),
1401 + ::view-transition-old( os-dock-os-side-dock ) {
1402 + animation: os-dock-vt-out 120ms ease-out both;
1403 + }
1404 +
1405 + ::view-transition-new( os-dock-os-dock ),
1406 + ::view-transition-new( os-dock-os-side-dock ) {
1407 + animation: os-dock-vt-in 180ms ease-out 80ms both;
1408 + }
1409 +
1410 + @keyframes os-dock-vt-out {
1411 + to {
1412 + opacity: 0;
1413 + }
1414 + }
1415 +
1416 + @keyframes os-dock-vt-in {
1417 + from {
1418 + opacity: 0;
1419 + }
1420 + }
1421 +}
1422 +
1423 +@supports not ( view-transition-name: none ) {
1424 + .os-dock[ data-os-dock-behavior="dynamic" ] {
1425 + transition:
1426 + width 240ms ease,
1427 + height 240ms ease,
1428 + opacity 240ms ease,
1429 + border-radius 240ms ease,
1430 + bottom 240ms ease,
1431 + inset-block 240ms ease;
1432 + }
1433 +}
1434 +
1435 +@media ( prefers-reduced-motion: reduce ) {
1436 + .os-dock[ data-os-dock-behavior="dynamic" ] {
1437 + transition: none;
1438 + }
875 1439 }