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

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

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