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

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

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