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