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