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 +870 -277 0.9.6 → 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,79 +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
80 139 /* Stacked card hint on hover — a second card peeks from behind the
81 140 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 {
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 {
85 153 content: "";
86 154 position: absolute;
87 155 top: -4px;
88 156 inset-inline-start: -4px;
89 - width: 100%;
90 - height: 100%;
91 - border-radius: inherit;
157 + width: 40px;
158 + height: 40px;
159 + border-radius: 10px;
92 160 background: rgba( 255, 255, 255, 0.06 );
93 161 border: 1px solid rgba( 255, 255, 255, 0.10 );
94 - z-index: -1;
95 162 pointer-events: none;
96 163 }
97 164
98 165 /* 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 );
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 );
103 170 line-height: 1;
104 171 }
105 172
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 );
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 );
110 182 display: block;
111 183 fill: currentColor;
112 184 opacity: 0.7;
113 185 /* Some plugin SVGs ship hardcoded `fill="..."` attributes inside
@@ -112,23 +184,58 @@
112 184 opacity: 0.7;
113 185 /* Some plugin SVGs ship hardcoded `fill="..."` attributes inside
114 186 the markup, which override `fill: currentColor` and make the
115 187 icon keep its brand color. Force the monochrome white palette
116 - 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. */
117 191 filter: brightness(0) invert(1);
118 192 transition: opacity 0.15s ease;
119 193 }
120 194
121 -.desktop-mode-dock__item-primary:hover .desktop-mode-dock__item-svg {
195 +.os-dock__item-primary:hover .os-dock__item-svg {
122 196 opacity: 1;
123 197 }
124 198
125 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 +/*
126 233 * Letter-badge icon — rendered for items registered without a
127 234 * dashicon / SVG / image URL. Miniature "app placeholder": rounded
128 235 * square tinted with a title-derived hue, first letter centered.
129 236 */
130 -.desktop-mode-dock__item-letter {
237 +.os-dock__item-letter {
131 238 display: flex;
132 239 align-items: center;
133 240 justify-content: center;
134 241 width: 28px;
@@ -133,12 +240,15 @@
133 240 justify-content: center;
134 241 width: 28px;
135 242 height: 28px;
136 243 border-radius: 8px;
137 - color: #fff;
244 + color: var( --os-ui-fg-on-accent, #fff );
138 245 font-size: 15px;
139 246 font-weight: 700;
140 - 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 + );
141 251 line-height: 1;
142 252 letter-spacing: 0;
143 253 text-shadow: 0 1px 1px rgba( 0, 0, 0, 0.22 );
144 254 box-shadow:
@@ -145,17 +255,17 @@
145 255 inset 0 1px 0 rgba( 255, 255, 255, 0.2 ),
146 256 inset 0 -1px 0 rgba( 0, 0, 0, 0.15 );
147 257 }
148 258
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 );
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 );
152 262 display: block;
153 263 opacity: 0.7;
154 264 transition: opacity 0.15s ease;
155 265 }
156 266
157 -.desktop-mode-dock__item-primary:hover img.desktop-mode-dock__item-img {
267 +.os-dock__item-primary:hover img.os-dock__item-img {
158 268 opacity: 1;
159 269 }
160 270
161 271 /* "Open another" floating chip — shown on multi-capable tiles that
@@ -160,9 +270,9 @@
160 270
161 271 /* "Open another" floating chip — shown on multi-capable tiles that
162 272 * already have ≥1 open instance. Positioned inside each orientation
163 273 * block so the chip hugs the outward edge of the tile. */
164 -.desktop-mode-dock__item-new {
274 +.os-dock__item-new {
165 275 position: absolute;
166 276 display: flex;
167 277 align-items: center;
168 278 justify-content: center;
@@ -168,27 +278,27 @@
168 278 justify-content: center;
169 279 width: 20px;
170 280 height: 20px;
171 281 padding: 0;
172 - 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 ) );
173 283 border-radius: 50%;
174 284 background: var( --wp-admin-theme-color, #2271b1 );
175 - color: #fff;
285 + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) );
176 286 cursor: pointer;
177 287 transition: background-color 0.15s ease, transform 0.15s ease;
178 288 z-index: 1;
179 289 }
180 290
181 -.desktop-mode-dock__item-new[hidden] {
291 +.os-dock__item-new[hidden] {
182 292 display: none;
183 293 }
184 294
185 -.desktop-mode-dock__item-new:hover {
186 - background: #fff;
295 +.os-dock__item-new:hover {
296 + background: var( --os-ui-surface, #fff );
187 297 color: var( --wp-admin-theme-color, #2271b1 );
188 298 }
189 299
190 -.desktop-mode-dock__item-new:focus-visible {
300 +.os-dock__item-new:focus-visible {
191 301 outline: 2px solid rgba( 255, 255, 255, 0.85 );
192 302 outline-offset: 2px;
193 303 }
194 304
@@ -194,36 +304,75 @@
194 304
195 305 /* Notification badge — iOS/macOS-style pill straddling the tile
196 306 * corner. Ring matches the dock bg so it reads as "punched out."
197 307 * Badge positioning is placement-aware via the blocks below. */
198 -.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 + );
199 328 position: absolute;
200 - min-width: 16px;
201 - height: 16px;
202 - 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 + );
203 335 box-sizing: border-box;
204 336 border-radius: 999px;
205 - background: linear-gradient( 180deg, #ff5a5a 0%, #d63638 100% );
206 - 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 ) );
207 342 display: inline-flex;
208 343 align-items: center;
209 344 justify-content: center;
210 - font-size: 10px;
211 - 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 );
212 361 line-height: 1;
213 362 letter-spacing: 0.01em;
214 363 text-align: center;
215 364 font-variant-numeric: tabular-nums;
216 365 box-shadow:
217 - 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 ) ),
218 367 0 1px 3px rgba( 0, 0, 0, 0.35 );
219 368 pointer-events: none;
220 369 transition: transform 0.15s ease;
221 - top: -3px;
222 - inset-inline-end: -3px;
370 + top: var( --os-dock-badge-offset, -3px );
371 + inset-inline-end: var( --os-dock-badge-offset, -3px );
223 372 }
224 373
225 -.desktop-mode-dock__item:hover .desktop-mode-dock__badge {
374 +.os-dock__item:hover .os-dock__badge {
226 375 transform: scale( 1.05 );
227 376 top: -4px;
228 377 }
229 378
@@ -231,18 +380,51 @@
231 380 * Recycle Bin count — unlike unread/update badges, the trash count
232 381 * is ambient state. Keep it inside the bin glyph as a quiet neutral
233 382 * marker instead of a red alert on the tile corner.
234 383 */
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;
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 );
241 411 border-radius: 999px;
242 - background: rgba( 29, 35, 39, 0.72 );
243 - color: rgba( 255, 255, 255, 0.92 );
244 - 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 + );
245 427 font-weight: 700;
246 428 letter-spacing: 0;
247 429 box-shadow:
248 430 inset 0 0 0 1px rgba( 255, 255, 255, 0.26 ),
@@ -248,16 +430,25 @@
248 430 inset 0 0 0 1px rgba( 255, 255, 255, 0.26 ),
249 431 0 1px 2px rgba( 0, 0, 0, 0.24 );
250 432 }
251 433
252 -.desktop-mode-dock__item[ data-system-id="desktop-mode-recycle-bin" ]:hover .desktop-mode-dock__badge {
253 - 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 );
254 440 transform: scale( 1.03 );
255 441 }
256 442
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 );
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 ) );
260 451 }
261 452
262 453 /* -----------------------------------------------------------------------------
263 454 * Attention animations — driven by `Dock.setAttention()` /
@@ -269,9 +460,9 @@
269 460 * shows a static accent ring for the same duration so the affordance
270 461 * still works.
271 462 * ------------------------------------------------------------------------- */
272 463
273 -@keyframes wpd-dock-attention-pulse {
464 +@keyframes os-dock-attention-pulse {
274 465 0%, 100% {
275 466 transform: scale( 1 );
276 467 box-shadow: 0 0 0 0 rgba( 255, 90, 90, 0.55 );
277 468 }
@@ -280,9 +471,9 @@
280 471 box-shadow: 0 0 0 8px rgba( 255, 90, 90, 0 );
281 472 }
282 473 }
283 474
284 -@keyframes wpd-dock-attention-shake {
475 +@keyframes os-dock-attention-shake {
285 476 0%, 100% {
286 477 transform: translateX( 0 );
287 478 }
288 479 20%, 60% {
@@ -292,9 +483,9 @@
292 483 transform: translateX( 3px );
293 484 }
294 485 }
295 486
296 -@keyframes wpd-dock-attention-bounce {
487 +@keyframes os-dock-attention-bounce {
297 488 0%, 100% {
298 489 transform: translateY( 0 );
299 490 }
300 491 30% {
@@ -304,36 +495,36 @@
304 495 transform: translateY( -2px );
305 496 }
306 497 }
307 498
308 -.desktop-mode-dock__item--attention-pulse .desktop-mode-dock__item-primary {
309 - 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;
310 501 }
311 -.desktop-mode-dock__item--attention-shake .desktop-mode-dock__item-primary {
312 - 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;
313 504 }
314 -.desktop-mode-dock__item--attention-bounce .desktop-mode-dock__item-primary {
315 - 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;
316 507 }
317 508
318 509 /* Intensity modulates animation duration — strong = faster, more
319 510 urgent; subtle = slower, calmer. */
320 -.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 {
321 512 animation-duration: 1s;
322 513 }
323 -.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 {
324 515 animation-duration: 0.6s;
325 516 }
326 -.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 {
327 518 animation-duration: 0.9s;
328 519 }
329 -.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 {
330 521 animation-duration: 1.8s;
331 522 }
332 -.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 {
333 524 animation-duration: 1s;
334 525 }
335 -.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 {
336 527 animation-duration: 1.6s;
337 528 }
338 529
339 530 /* Reduced-motion: kill the animation, replace with a static accent ring
@@ -338,11 +529,11 @@
338 529
339 530 /* Reduced-motion: kill the animation, replace with a static accent ring
340 531 for the same visual prominence without the movement. */
341 532 @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 {
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 {
345 536 animation: none;
346 537 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
347 538 outline-offset: 2px;
348 539 border-radius: 8px;
@@ -349,25 +540,33 @@
349 540 }
350 541 }
351 542
352 543 /* Shared tooltip — placement-specific anchor modifiers applied by JS. */
353 -.desktop-mode-dock__tooltip {
544 +.os-dock__tooltip {
354 545 position: fixed;
355 - padding: 6px 12px;
356 - background: rgba( 0, 0, 0, 0.85 );
357 - color: #fff;
358 - 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 );
359 556 font-weight: 400;
360 557 line-height: 1.4;
361 558 white-space: nowrap;
362 - 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 );
363 562 pointer-events: none;
364 - z-index: calc( var( --desktop-mode-z-dock ) + 1 );
563 + z-index: calc( var( --os-z-dock ) + 1 );
365 564 opacity: 0;
366 565 transition: opacity 0.15s ease, transform 0.15s ease;
367 566 }
368 567
369 -.desktop-mode-dock__tooltip--visible {
568 +.os-dock__tooltip--visible {
370 569 opacity: 1;
371 570 }
372 571
373 572 /* ------------------------------------------------------------------
@@ -374,11 +573,11 @@
374 573 * Vertical placement (left + right) — dock runs the full shell body
375 574 * height along one edge, with items stacked vertically.
376 575 * ------------------------------------------------------------------ */
377 576
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 );
577 +.os-dock[ data-os-dock-placement="left" ],
578 +.os-dock[ data-os-dock-placement="right" ] {
579 + width: var( --os-dock-width );
381 580 flex-direction: column;
382 581 padding: 16px 0 12px;
383 582 gap: 6px;
384 583 /*
@@ -410,10 +609,10 @@
410 609 * Firefox via `scrollbar-width: none`, WebKit via the pseudo-element
411 610 * rule below — so the rail keeps its clean macOS look. Mouse wheel,
412 611 * touchpad, touch, and keyboard focus-into-view all still work.
413 612 */
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 {
613 +.os-dock[ data-os-dock-placement="left" ] .os-dock__scroll,
614 +.os-dock[ data-os-dock-placement="right" ] .os-dock__scroll {
416 615 display: flex;
417 616 flex-direction: column;
418 617 align-items: center;
419 618 gap: 6px;
@@ -424,10 +623,10 @@
424 623 overflow-x: clip;
425 624 scrollbar-width: none;
426 625 }
427 626
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 {
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 {
430 629 display: none;
431 630 }
432 631
433 632 /*
@@ -434,10 +633,10 @@
434 633 * Pinned area below `__scroll` — system tiles stay reachable here
435 634 * regardless of how the menu list scrolls. `flex-shrink: 0` so it's
436 635 * never compressed; sized to its content's natural height.
437 636 */
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 {
637 +.os-dock[ data-os-dock-placement="left" ] .os-dock__pinned,
638 +.os-dock[ data-os-dock-placement="right" ] .os-dock__pinned {
440 639 display: flex;
441 640 flex-direction: column;
442 641 align-items: center;
443 642 gap: 6px;
@@ -445,22 +644,22 @@
445 644 width: 100%;
446 645 }
447 646
448 647 /* 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 );
648 +.os-dock[ data-os-dock-placement="left" ] {
649 + border-inline-end: 1px solid var( --os-dock-border );
451 650 order: -1;
452 651 }
453 652
454 653 /* 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 );
654 +.os-dock[ data-os-dock-placement="right" ] {
655 + border-inline-start: 1px solid var( --os-dock-border );
457 656 order: 1;
458 657 }
459 658
460 659 /* 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 {
660 +.os-dock[ data-os-dock-placement="left" ]
661 + .os-dock__item--active::before {
463 662 content: "";
464 663 position: absolute;
465 664 inset-inline-start: -6px;
466 665 top: 50%;
@@ -467,13 +666,13 @@
467 666 transform: translateY( -50% );
468 667 width: 4px;
469 668 height: 4px;
470 669 border-radius: 50%;
471 - background: #fff;
670 + background: var( --os-dock-active-indicator, var( --os-dock-item-outline, #fff ) );
472 671 }
473 672
474 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
475 - .desktop-mode-dock__item--focused::before {
673 +.os-dock[ data-os-dock-placement="left" ]
674 + .os-dock__item--focused::before {
476 675 width: 4px;
477 676 height: 16px;
478 677 border-radius: 2px;
479 678 }
@@ -487,20 +686,20 @@
487 686 *
488 687 * Sized slightly larger than the solid dot so the ring is legible at
489 688 * dock size — a 4×4 outline reads as a dirty pixel.
490 689 */
491 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
492 - .desktop-mode-dock__item--all-minimized::before {
690 +.os-dock[ data-os-dock-placement="left" ]
691 + .os-dock__item--all-minimized::before {
493 692 width: 6px;
494 693 height: 6px;
495 694 border-radius: 50%;
496 695 background: transparent;
497 - 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 ) ) );
498 697 }
499 698
500 699 /* 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 {
700 +.os-dock[ data-os-dock-placement="right" ]
701 + .os-dock__item--active::before {
503 702 content: "";
504 703 position: absolute;
505 704 inset-inline-end: -6px;
506 705 top: 50%;
@@ -507,78 +706,95 @@
507 706 transform: translateY( -50% );
508 707 width: 4px;
509 708 height: 4px;
510 709 border-radius: 50%;
511 - background: #fff;
710 + background: var( --os-dock-active-indicator, var( --os-dock-item-outline, #fff ) );
512 711 }
513 712
514 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
515 - .desktop-mode-dock__item--focused::before {
713 +.os-dock[ data-os-dock-placement="right" ]
714 + .os-dock__item--focused::before {
516 715 width: 4px;
517 716 height: 16px;
518 717 border-radius: 2px;
519 718 }
520 719
521 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
522 - .desktop-mode-dock__item--all-minimized::before {
720 +.os-dock[ data-os-dock-placement="right" ]
721 + .os-dock__item--all-minimized::before {
523 722 width: 6px;
524 723 height: 6px;
525 724 border-radius: 50%;
526 725 background: transparent;
527 - 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 ) ) );
528 727 }
529 728
530 729 /* "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 {
730 +.os-dock[ data-os-dock-placement="left" ]
731 + .os-dock__item-new {
533 732 top: 50%;
534 733 inset-inline-end: -13px;
535 734 transform: translateY( -50% );
536 735 }
537 736
538 -.desktop-mode-dock[ data-desktop-mode-dock-placement="left" ]
539 - .desktop-mode-dock__item-new:hover {
737 +.os-dock[ data-os-dock-placement="left" ]
738 + .os-dock__item-new:hover {
540 739 transform: translateY( -50% ) scale( 1.1 );
541 740 }
542 741
543 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
544 - .desktop-mode-dock__item-new {
742 +.os-dock[ data-os-dock-placement="right" ]
743 + .os-dock__item-new {
545 744 top: 50%;
546 745 inset-inline-start: -13px;
547 746 transform: translateY( -50% );
548 747 }
549 748
550 -.desktop-mode-dock[ data-desktop-mode-dock-placement="right" ]
551 - .desktop-mode-dock__item-new:hover {
749 +.os-dock[ data-os-dock-placement="right" ]
750 + .os-dock__item-new:hover {
552 751 transform: translateY( -50% ) scale( 1.1 );
553 752 }
554 753
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 {
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;
566 777 width: 60%;
567 - height: 1px;
568 - margin: 8px auto 4px;
569 - 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 + );
570 794 flex-shrink: 0;
571 795 }
572 796
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 797 /*
582 798 * Hide the system separator when nothing precedes it — on a clean
583 799 * install (or a low-cap user with zero menu items) the separator
584 800 * would render as a stray hairline with nothing to divide.
@@ -588,18 +804,19 @@
588 804 * when it's the first such child.
589 805 * 2. Vertical docks — separator lives inside `__pinned`; hide when
590 806 * `__scroll` (its sibling above) is empty of menu tiles.
591 807 */
592 -.desktop-mode-dock > .desktop-mode-dock__separator:first-child {
808 +.os-dock > .os-dock__separator:first-child {
593 809 display: none;
594 810 }
595 811
596 -.desktop-mode-dock__scroll:not(:has( .desktop-mode-dock__item ))
597 - + .desktop-mode-dock__pinned
598 - .desktop-mode-dock__separator {
812 +.os-dock__scroll:not(:has( .os-dock__item ))
813 + + .os-dock__pinned
814 + .os-dock__separator {
599 815 display: none;
600 816 }
601 817
818 +
602 819 /*
603 820 * Drop an empty `__scroll` from the flex flow entirely. When only
604 821 * system tiles are present (e.g. just the Recycle Bin), the empty
605 822 * wrapper would otherwise still occupy the rail: its `flex-grow`
@@ -609,20 +826,20 @@
609 826 * it lets the dock center the lone system tile. As soon as a menu
610 827 * tile is added the `:has()` no longer matches and normal flow
611 828 * resumes.
612 829 *
613 - * The `[data-desktop-mode-dock-placement]` qualifier is load-bearing:
830 + * The `[data-os-dock-placement]` qualifier is load-bearing:
614 831 * the per-placement rules that set `display: flex` on `__scroll` are
615 832 * (0,3,0); this selector must out-specify them, so it carries the
616 833 * placement attribute to reach (0,4,0).
617 834 */
618 -.desktop-mode-dock[ data-desktop-mode-dock-placement ]
619 - .desktop-mode-dock__scroll:not(:has( .desktop-mode-dock__item )) {
835 +.os-dock[ data-os-dock-placement ]
836 + .os-dock__scroll:not(:has( .os-dock__item )) {
620 837 display: none;
621 838 }
622 839
623 840 /* Tooltip — anchor modifiers are applied directly to the tooltip
624 - * element (NOT via a descendant selector on `.desktop-mode-dock`)
841 + * element (NOT via a descendant selector on `.os-dock`)
625 842 * because the tooltip lives in `document.body`. JS writes the
626 843 * absolute `top` / `left` coords; CSS handles the slide-in
627 844 * animation direction per modifier.
628 845 *
@@ -632,17 +849,17 @@
632 849 */
633 850
634 851 /* Left dock — tooltip slides in from the left by 4px. JS sets
635 852 * `left` to `tile.right + 8px`; the transform animates the entry. */
636 -.desktop-mode-dock__tooltip--after {
853 +.os-dock__tooltip--after {
637 854 transform: translateX( -4px );
638 855 }
639 856
640 -.desktop-mode-dock__tooltip--after.desktop-mode-dock__tooltip--visible {
857 +.os-dock__tooltip--after.os-dock__tooltip--visible {
641 858 transform: translateX( 0 );
642 859 }
643 860
644 -.desktop-mode-dock__tooltip--before {
861 +.os-dock__tooltip--before {
645 862 /* Right-placed dock — tooltip sits to the left of the tile. JS
646 863 * writes the left coord to the tile's left edge; this translate
647 864 * pulls the tooltip further left by its own width. */
648 865 transform: translate( calc( -100% - 8px ), 0 );
@@ -647,9 +864,9 @@
647 864 * pulls the tooltip further left by its own width. */
648 865 transform: translate( calc( -100% - 8px ), 0 );
649 866 }
650 867
651 -.desktop-mode-dock__tooltip--before.desktop-mode-dock__tooltip--visible {
868 +.os-dock__tooltip--before.os-dock__tooltip--visible {
652 869 transform: translate( calc( -100% - 8px ), 0 );
653 870 opacity: 1;
654 871 }
655 872
@@ -658,16 +875,22 @@
658 875 * viewport floor. Absolutely positioned so it floats as pure chrome
659 876 * rather than stealing vertical space from the shell body.
660 877 * ------------------------------------------------------------------ */
661 878
662 -.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" ] {
663 886 position: absolute;
664 887 inset-inline-start: 0;
665 888 inset-inline-end: 0;
666 - bottom: 12px;
889 + bottom: var( --os-dock-floating-offset, 12px );
667 890 margin: 0 auto;
668 - width: fit-content;
669 - 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 ) );
670 893 flex-direction: row;
671 894 justify-content: center;
672 895 /* Top padding is 4px instead of 8px so that the inner `__scroll`
673 896 * wrapper can claim its own 4px padding-top for badge clearance
@@ -673,29 +896,68 @@
673 896 * wrapper can claim its own 4px padding-top for badge clearance
674 897 * (the tile's badge sits at top: -3px). Net vertical chrome stays
675 898 * symmetric: 4px pill padding + 4px wrapper padding above the
676 899 * tile, 8px pill padding below — tile is 8px from both edges. */
677 - padding: 4px 12px 8px;
678 - gap: 6px;
679 - 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 );
680 903
681 904 /* Warmer, more saturated glass tint than vertical placements so
682 905 * 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 );
685 - backdrop-filter: blur( 28px ) saturate( 170% );
686 - -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% ) );
687 922
688 - border: 1px solid rgba( 255, 255, 255, 0.12 );
689 - 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 + );
690 946 box-shadow:
691 - inset 0 1px 0 rgba( 255, 255, 255, 0.08 ),
692 - 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 + );
693 955 }
694 956
695 957 /* 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 {
958 +.os-dock[ data-os-dock-placement="bottom" ]
959 + .os-dock__item--active::before {
698 960 content: "";
699 961 position: absolute;
700 962 inset-inline-start: 50%;
701 963 top: auto;
@@ -703,13 +965,13 @@
703 965 transform: translateX( -50% );
704 966 width: 4px;
705 967 height: 4px;
706 968 border-radius: 50%;
707 - background: #fff;
969 + background: var( --os-dock-active-indicator, var( --os-dock-item-outline, #fff ) );
708 970 }
709 971
710 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
711 - .desktop-mode-dock__item--focused::before {
972 +.os-dock[ data-os-dock-placement="bottom" ]
973 + .os-dock__item--focused::before {
712 974 width: 16px;
713 975 height: 4px;
714 976 border-radius: 2px;
715 977 }
@@ -718,53 +980,50 @@
718 980 * "All instances minimized" on the bottom dock — solid dot becomes
719 981 * a hollow ring. Matches the vertical-dock treatment so the cue is
720 982 * consistent across placements.
721 983 */
722 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
723 - .desktop-mode-dock__item--all-minimized::before {
984 +.os-dock[ data-os-dock-placement="bottom" ]
985 + .os-dock__item--all-minimized::before {
724 986 width: 6px;
725 987 height: 6px;
726 988 border-radius: 50%;
727 989 background: transparent;
728 - 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 ) ) );
729 991 }
730 992
731 993 /*
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.
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.
739 1013 */
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 1014
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 1015 /* "Open another" chip — floats above the tile's top-right corner so
757 1016 * 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 {
1017 +.os-dock[ data-os-dock-placement="bottom" ]
1018 + .os-dock__item-new {
760 1019 top: -6px;
761 1020 inset-inline-end: -6px;
762 1021 transform: none;
763 1022 }
764 1023
765 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ]
766 - .desktop-mode-dock__item-new:hover {
1024 +.os-dock[ data-os-dock-placement="bottom" ]
1025 + .os-dock__item-new:hover {
767 1026 transform: scale( 1.1 );
768 1027 }
769 1028
770 1029 /*
@@ -769,98 +1028,132 @@
769 1028
770 1029 /*
771 1030 * Bottom-dock inner wrappers — `__scroll` carries the menu tiles and
772 1031 * 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
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
776 1045 * content's natural width, so the dock pill's `max-width` is what
777 1046 * 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.
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.
781 1058 */
782 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ] .desktop-mode-dock__scroll {
1059 +.os-dock[ data-os-dock-placement="bottom" ] .os-dock__scroll {
783 1060 display: flex;
784 1061 flex-direction: row;
785 1062 align-items: center;
786 - justify-content: center;
787 - gap: 6px;
1063 + justify-content: start;
1064 + justify-content: var( --os-dock-floating-justify, safe center );
1065 + gap: var( --os-dock-floating-gap, 6px );
788 1066 flex: 1 1 auto;
789 1067 min-width: 0;
790 - padding-top: 4px;
791 - padding-bottom: 6px;
1068 + padding: var( --os-dock-floating-track-padding, 4px 4px 6px );
792 1069 overflow-x: auto;
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 + */
793 1076 overflow-y: visible;
794 1077 scrollbar-width: none;
795 1078 }
796 1079
797 -.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 {
798 1081 display: none;
799 1082 }
800 1083
801 -.desktop-mode-dock[ data-desktop-mode-dock-placement="bottom" ] .desktop-mode-dock__pinned {
1084 +.os-dock[ data-os-dock-placement="bottom" ] .os-dock__pinned {
802 1085 display: flex;
803 1086 flex-direction: row;
804 1087 align-items: center;
805 - gap: 6px;
1088 + gap: var( --os-dock-floating-gap, 6px );
806 1089 flex-shrink: 0;
807 1090 /* Match the scroll wrapper's badge clearance so system tiles sit
808 1091 * on the same vertical baseline as the menu tiles. */
809 - padding-top: 4px;
810 - padding-bottom: 6px;
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 );
811 1096 }
812 1097
813 -/* Separator — vertical hairline between menu tiles and system tiles.
814 - * 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
815 1100 * "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;
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 );
822 1108 /* 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;
1109 + intrinsic height, leaving the line effectively invisible. */
1110 + height: var( --os-dock-separator-height, 34px );
826 1111 /* Symmetric horizontal margin so the divider sits centered in the
827 1112 * 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
1113 + * on the left, `__pinned`'s own gap on the right) plus an equal
829 1114 * margin per side balances out. An asymmetric margin here nudges the
830 1115 * 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 );
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 + );
834 1144 flex-shrink: 0;
835 1145 }
836 1146
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 1147 /* Tooltip — above the hovered tile. JS writes the horizontal center
855 1148 * + a top near the tile's top edge; CSS translates the tooltip up by
856 1149 * its own height so it clears the tile. */
857 -.desktop-mode-dock__tooltip--above {
1150 +.os-dock__tooltip--above {
858 1151 inset-inline-start: auto;
859 1152 transform: translate( -50%, calc( -100% - 4px ) );
860 1153 }
861 1154
862 -.desktop-mode-dock__tooltip--above.desktop-mode-dock__tooltip--visible {
1155 +.os-dock__tooltip--above.os-dock__tooltip--visible {
863 1156 transform: translate( -50%, -100% );
864 1157 }
865 1158
866 1159
@@ -874,9 +1167,9 @@
874 1167 * underneath the cursor. Sibling tiles animate via FLIP — the JS
875 1168 * writes inline `transform` + `transition` per leg, so no CSS
876 1169 * transition is declared here (it would race with the per-leg
877 1170 * inline transition). */
878 -.desktop-mode-dock__item--dragging {
1171 +.os-dock__item--dragging {
879 1172 pointer-events: none;
880 1173 z-index: 50;
881 1174 opacity: 0.92;
882 1175 cursor: grabbing;
@@ -885,11 +1178,311 @@
885 1178 * the dock surface it floats above. */
886 1179 filter: drop-shadow( 0 6px 12px rgba( 0, 0, 0, 0.35 ) );
887 1180 }
888 1181
889 -.desktop-mode-dock__item:not( .desktop-mode-dock__item--system ) {
1182 +.os-dock__item:not( .os-dock__item--system ) {
890 1183 cursor: grab;
891 1184 }
892 1185
893 -.desktop-mode-dock__item:not( .desktop-mode-dock__item--system ):active {
1186 +.os-dock__item:not( .os-dock__item--system ):active {
894 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 + }
895 1488 }