PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / trunk
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin vtrunk
1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / assets / css / dock.css

dock.css in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin trunk, at assets/css/dock.css

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