PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.1
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.0.1, at assets/css/dock.css

1,097 lines 35.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OpenStation — Dock.
3 *
4 * A rail that hosts admin menu tiles plus shell-level system tiles.
5 * Placement (left / right / bottom) is reflected on each dock element
6 * itself as `data-os-dock-placement`; this stylesheet keys
7 * off that attribute for layout, tooltip anchor, and indicator
8 * position. Two instances can coexist (Classic layout: a left side
9 * bar with core menus + a bottom dock with plugin menus); each
10 * carries its own placement attribute, so the two rails are styled
11 * independently without leaking selector state to one another.
12 *
13 * @since 6.9.0
14 */
15
16 /* ------------------------------------------------------------------
17 * Shared dock chrome — applies to every placement.
18 * ------------------------------------------------------------------ */
19
20 .os-dock {
21 display: flex;
22 align-items: center;
23 z-index: var( --os-z-dock );
24 background-color: var( --os-dock-bg );
25 /*
26 * Desktop-theme dock texture (DOCK slot). Layered OVER the dock
27 * background colour, so a theme can ship a semi-transparent
28 * texture and still get the translucent wash underneath. `none`
29 * when unset — no theme, no cost.
30 */
31 background-image: var( --os-dock-bg-image, none );
32 background-repeat: var( --os-dock-bg-image-repeat, repeat );
33 background-size: var( --os-dock-bg-image-size, auto );
34 background-position: var( --os-dock-bg-image-position, center );
35 backdrop-filter: blur( 20px );
36 -webkit-backdrop-filter: blur( 20px );
37 flex-shrink: 0;
38 /*
39 * overflow: visible so the per-tile "Open another" chip can float
40 * past the dock's outer edge. Real-world bottom-dock menus fit
41 * the viewport without a scrollbar; vertical placements route
42 * their overflow through the inner `__scroll` wrapper, leaving
43 * the dock itself non-scrolling.
44 */
45 overflow: visible;
46 }
47
48 /* A single dock tile — icon button (+ optional instance rail below). */
49 .os-dock__item {
50 position: relative;
51 display: flex;
52 flex-direction: column;
53 align-items: center;
54 width: 40px;
55 flex-shrink: 0;
56 }
57
58 /* Primary icon button — opens or focuses the page. */
59 .os-dock__item-primary {
60 position: relative;
61 display: flex;
62 align-items: center;
63 justify-content: center;
64 width: 40px;
65 height: 40px;
66 border: none;
67 border-radius: 10px;
68 cursor: pointer;
69 padding: 0;
70 background-color: transparent;
71 /*
72 * DOCK_ITEM texture slot — the face of a single dock tile, so a
73 * theme can give every launcher a physical key/plate instead of
74 * relying on the dock strip's texture showing through. Painted
75 * under the icon; the hover wash below is a background-COLOR so
76 * it composes with the texture rather than erasing it.
77 */
78 background-image: var( --os-dock-item-image, none );
79 background-repeat: var( --os-dock-item-image-repeat, no-repeat );
80 background-size: var( --os-dock-item-image-size, auto );
81 background-position: var( --os-dock-item-image-position, center );
82 /*
83 * Dock glyph colour. The literal is what this rule always said;
84 * the token is the name a theme aims at when its dock strip is
85 * not dark. See the "Dock glyphs" block in variables.css.
86 */
87 color: var( --os-dock-icon-color, rgba( 255, 255, 255, 0.7 ) );
88 transition: background-color 0.15s ease, transform 0.15s ease, color 0.15s ease;
89 }
90
91 .os-dock__item-primary:hover {
92 background-color: var(
93 --os-dock-item-bg-hover,
94 rgba( 255, 255, 255, 0.15 )
95 );
96 color: var(
97 --os-dock-icon-color-hover,
98 var( --os-ui-fg-on-accent, #fff )
99 );
100 transform: scale( 1.1 );
101 }
102
103 .os-dock__item-primary:active {
104 transform: scale( 0.95 );
105 }
106
107 .os-dock__item-primary:focus-visible {
108 outline: 2px solid var(
109 --os-dock-item-outline,
110 rgba( 255, 255, 255, 0.7 )
111 );
112 outline-offset: 2px;
113 }
114
115 /* Stacked card hint on hover — a second card peeks from behind the
116 icon when the tile has multiple open windows. Only visible on hover so
117 it never conflicts with focused tiles. */
118 .os-dock__item--stacked:hover .os-dock__item-primary::before,
119 .os-dock__item--stacked[data-peek-active] .os-dock__item-primary::before {
120 content: "";
121 position: absolute;
122 top: -4px;
123 inset-inline-start: -4px;
124 width: 100%;
125 height: 100%;
126 border-radius: inherit;
127 background: rgba( 255, 255, 255, 0.06 );
128 border: 1px solid rgba( 255, 255, 255, 0.10 );
129 z-index: -1;
130 pointer-events: none;
131 }
132
133 /* Dashicon inside dock item. */
134 .os-dock__item .dashicons {
135 font-size: var( --os-dock-icon-size, 20px );
136 width: var( --os-dock-icon-size, 20px );
137 height: var( --os-dock-icon-size, 20px );
138 line-height: 1;
139 }
140
141 /* SVG icon (for custom post types with data:image/svg+xml icons).
142 *
143 * The FALLBACK path now: `_makeSvgIcon()` paints these as a mask
144 * filled with `currentColor` (see `__item-mask` below) and only lands
145 * here when the mask refuses the URL — one carrying quotes, spaces or
146 * parens. The filter is what that fallback still needs. */
147 .os-dock__item-svg {
148 width: var( --os-dock-icon-size, 20px );
149 height: var( --os-dock-icon-size, 20px );
150 display: block;
151 fill: currentColor;
152 opacity: 0.7;
153 /* Some plugin SVGs ship hardcoded `fill="..."` attributes inside
154 the markup, which override `fill: currentColor` and make the
155 icon keep its brand color. Force the monochrome white palette
156 to match dashicons. Flattens to WHITE specifically, which is why
157 the mask path is preferred: a filter has no colour to name, so a
158 theme cannot reach it. */
159 filter: brightness(0) invert(1);
160 transition: opacity 0.15s ease;
161 }
162
163 .os-dock__item-primary:hover .os-dock__item-svg {
164 opacity: 1;
165 }
166
167 /*
168 * MASKED icon — the glyph is painted as a CSS mask filled with a
169 * colour, so only the source artwork's alpha is used.
170 *
171 * Two callers, one shape:
172 *
173 * - A desktop theme's tinted iconset, filled with the colour the
174 * theme named for that slot.
175 * - Every other plugin / CPT image icon, filled with
176 * `currentColor` — which is `--os-dock-icon-color`, the
177 * same token the dashicons follow.
178 *
179 * Deliberately NOT `__item-svg`: that class force-whitens its content
180 * with `filter: brightness(0) invert(1)`. Both mechanisms discard the
181 * artwork's colours and keep its alpha, but only one of them has a
182 * colour a theme can name.
183 *
184 * Full opacity, unlike `__item-svg`'s 0.7: the fill carries its own
185 * alpha. Unthemed that is `rgba( 255, 255, 255, 0.7 )` at rest and
186 * `#fff` on hover — exactly what the filter plus the opacity pair
187 * used to compute — and a theme that names a colour has already
188 * decided how prominent its glyphs should be, so dimming it again
189 * would silently mix it with the dock.
190 *
191 * @since 0.9.8
192 */
193 .os-dock__item-mask {
194 width: var( --os-dock-icon-size, 20px );
195 height: var( --os-dock-icon-size, 20px );
196 display: block;
197 flex-shrink: 0;
198 }
199
200 /*
201 * Letter-badge icon — rendered for items registered without a
202 * dashicon / SVG / image URL. Miniature "app placeholder": rounded
203 * square tinted with a title-derived hue, first letter centered.
204 */
205 .os-dock__item-letter {
206 display: flex;
207 align-items: center;
208 justify-content: center;
209 width: 28px;
210 height: 28px;
211 border-radius: 8px;
212 color: var( --os-ui-fg-on-accent, #fff );
213 font-size: 15px;
214 font-weight: 700;
215 font-family: var(
216 --os-font,
217 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
218 );
219 line-height: 1;
220 letter-spacing: 0;
221 text-shadow: 0 1px 1px rgba( 0, 0, 0, 0.22 );
222 box-shadow:
223 inset 0 1px 0 rgba( 255, 255, 255, 0.2 ),
224 inset 0 -1px 0 rgba( 0, 0, 0, 0.15 );
225 }
226
227 .os-dock__item img.os-dock__item-img {
228 width: var( --os-dock-icon-size, 20px );
229 height: var( --os-dock-icon-size, 20px );
230 display: block;
231 opacity: 0.7;
232 transition: opacity 0.15s ease;
233 }
234
235 .os-dock__item-primary:hover img.os-dock__item-img {
236 opacity: 1;
237 }
238
239 /* "Open another" floating chip — shown on multi-capable tiles that
240 * already have ≥1 open instance. Positioned inside each orientation
241 * block so the chip hugs the outward edge of the tile. */
242 .os-dock__item-new {
243 position: absolute;
244 display: flex;
245 align-items: center;
246 justify-content: center;
247 width: 20px;
248 height: 20px;
249 padding: 0;
250 border: 2px solid var( --os-dock-bg, rgba( 0, 0, 0, 0.4 ) );
251 border-radius: 50%;
252 background: var( --wp-admin-theme-color, #2271b1 );
253 color: var( --os-ui-fg-on-accent, #fff );
254 cursor: pointer;
255 transition: background-color 0.15s ease, transform 0.15s ease;
256 z-index: 1;
257 }
258
259 .os-dock__item-new[hidden] {
260 display: none;
261 }
262
263 .os-dock__item-new:hover {
264 background: var( --os-ui-surface, #fff );
265 color: var( --wp-admin-theme-color, #2271b1 );
266 }
267
268 .os-dock__item-new:focus-visible {
269 outline: 2px solid rgba( 255, 255, 255, 0.85 );
270 outline-offset: 2px;
271 }
272
273 /* Notification badge — iOS/macOS-style pill straddling the tile
274 * corner. Ring matches the dock bg so it reads as "punched out."
275 * Badge positioning is placement-aware via the blocks below. */
276 .os-dock__badge {
277 /*
278 * Badge geometry DERIVES from the dock's icon size instead of
279 * being pinned to 16px.
280 *
281 * The dock is user-sizable (18 / 20 / 26px icons via OS Settings →
282 * dock size). A fixed badge tracked none of that: at "Large" it
283 * read as a speck stuck to an oversized tile. The ratios below are
284 * chosen so the DEFAULT dock (20px icons) resolves to exactly the
285 * old numbers — 16px box, 10px text, ~5px padding — so nothing
286 * moves for anyone who hasn't changed the setting.
287 *
288 * `--os-dock-badge-size` overrides the whole derivation
289 * for themes that want a specific badge; the font size and padding
290 * then follow it, so one token is usually enough.
291 */
292 --os-dock-badge-computed-size: var(
293 --os-dock-badge-size,
294 calc( var( --os-dock-icon-size, 20px ) * 0.8 )
295 );
296 position: absolute;
297 min-width: var( --os-dock-badge-computed-size );
298 height: var( --os-dock-badge-computed-size );
299 padding: 0 var(
300 --os-dock-badge-padding,
301 calc( var( --os-dock-badge-computed-size ) * 0.3 )
302 );
303 box-sizing: border-box;
304 border-radius: 999px;
305 background: var(
306 --os-dock-badge-bg,
307 linear-gradient( 180deg, #ff5a5a 0%, var( --os-ui-danger, #d63638 ) 100% )
308 );
309 color: var( --os-dock-badge-fg, var( --os-ui-fg-on-accent, #fff ) );
310 display: inline-flex;
311 align-items: center;
312 justify-content: center;
313 /*
314 * 0.625 of the badge box — 10px on the default 16px badge, the
315 * value this rule carried before the geometry was derived. It is
316 * the right weight for a menu tile's alert count: these badges sit
317 * on the tile CORNER where the pill itself carries the signal, so
318 * the digit only has to confirm it.
319 *
320 * The Recycle Bin's badge is a different problem and has its own
321 * ratio below — it sits INSIDE the bin glyph, where the pill reads
322 * as part of the artwork and the number has to carry itself.
323 */
324 font-size: var(
325 --os-dock-badge-font-size,
326 calc( var( --os-dock-badge-computed-size ) * 0.625 )
327 );
328 font-weight: var( --os-dock-badge-font-weight, 700 );
329 line-height: 1;
330 letter-spacing: 0.01em;
331 text-align: center;
332 font-variant-numeric: tabular-nums;
333 box-shadow:
334 0 0 0 1.5px var( --os-dock-bg, rgba( 0, 0, 0, 0.4 ) ),
335 0 1px 3px rgba( 0, 0, 0, 0.35 );
336 pointer-events: none;
337 transition: transform 0.15s ease;
338 top: var( --os-dock-badge-offset, -3px );
339 inset-inline-end: var( --os-dock-badge-offset, -3px );
340 }
341
342 .os-dock__item:hover .os-dock__badge {
343 transform: scale( 1.05 );
344 top: -4px;
345 }
346
347 /*
348 * Recycle Bin count — unlike unread/update badges, the trash count
349 * is ambient state. Keep it inside the bin glyph as a quiet neutral
350 * marker instead of a red alert on the tile corner.
351 */
352 .os-dock__item[ data-system-id="desktop-mode-recycle-bin" ] .os-dock__badge {
353 /*
354 * Sized off the dock icon like every other badge, so it tracks the
355 * dock-size setting instead of sitting at a fixed 13px on an icon
356 * that may be 18, 20 or 26px.
357 *
358 * Deliberately quieter than the corner alert badge above — this is
359 * ambient state, not a notification — but it still has to be a
360 * READABLE number. It was 13px with an 8px digit, which is below
361 * the point where a count reads as a count.
362 *
363 * Override chain: bin-specific token → the generic dock-badge
364 * token → the derivation, so a theme that says "dock badges are
365 * 20px" reaches the bin without naming it.
366 */
367 --os-dock-recycle-badge-computed-size: var(
368 --os-dock-recycle-badge-size,
369 var(
370 --os-dock-badge-size,
371 calc( var( --os-dock-icon-size, 20px ) * 0.75 )
372 )
373 );
374 top: calc( var( --os-dock-icon-size, 20px ) * 1 );
375 inset-inline-end: calc( var( --os-dock-icon-size, 20px ) * 0.35 );
376 min-width: var( --os-dock-recycle-badge-computed-size );
377 height: var( --os-dock-recycle-badge-computed-size );
378 padding: 0 calc( var( --os-dock-recycle-badge-computed-size ) * 0.25 );
379 border-radius: 999px;
380 background: var(
381 --os-dock-recycle-badge-bg,
382 rgba( 29, 35, 39, 0.72 )
383 );
384 color: var(
385 --os-dock-recycle-badge-fg,
386 rgba( 255, 255, 255, 0.92 )
387 );
388 font-size: var(
389 --os-dock-recycle-badge-font-size,
390 var(
391 --os-dock-badge-font-size,
392 calc( var( --os-dock-recycle-badge-computed-size ) * 0.8 )
393 )
394 );
395 font-weight: 700;
396 letter-spacing: 0;
397 box-shadow:
398 inset 0 0 0 1px rgba( 255, 255, 255, 0.26 ),
399 0 1px 2px rgba( 0, 0, 0, 0.24 );
400 }
401
402 .os-dock__item[ data-system-id="desktop-mode-recycle-bin" ]:hover .os-dock__badge {
403 /* Re-assert the derived offset, not a literal: the base rule above
404 * is overridden by the generic `__item:hover .__badge` rule, and
405 * hardcoding 20px here would strand the badge the moment the dock
406 * icon size changed. */
407 top: calc( var( --os-dock-icon-size, 20px ) * 1 );
408 transform: scale( 1.03 );
409 }
410
411 /* System tiles — same footprint as menu tiles, no badges, no rails.
412 *
413 * Reads the SAME glyph token as a menu tile, with its own literal as
414 * the fallback: unthemed, system tiles keep sitting a notch brighter
415 * than the rest (0.8 vs 0.7); themed, one colour covers every glyph
416 * in the dock rather than leaving these four stranded white. */
417 .os-dock__item--system .os-dock__item-primary {
418 color: var( --os-dock-icon-color, rgba( 255, 255, 255, 0.8 ) );
419 }
420
421 /* -----------------------------------------------------------------------------
422 * Attention animations — driven by `Dock.setAttention()` /
423 * `Window.requestAttention()`. The class lands on the tile root,
424 * the animation runs on the primary icon button so the badge stays
425 * stable. Three modes (pulse / shake / bounce) and three intensities
426 * (subtle / normal / strong). All animations gated on
427 * `prefers-reduced-motion: no-preference`; the reduced-motion fallback
428 * shows a static accent ring for the same duration so the affordance
429 * still works.
430 * ------------------------------------------------------------------------- */
431
432 @keyframes os-dock-attention-pulse {
433 0%, 100% {
434 transform: scale( 1 );
435 box-shadow: 0 0 0 0 rgba( 255, 90, 90, 0.55 );
436 }
437 50% {
438 transform: scale( 1.06 );
439 box-shadow: 0 0 0 8px rgba( 255, 90, 90, 0 );
440 }
441 }
442
443 @keyframes os-dock-attention-shake {
444 0%, 100% {
445 transform: translateX( 0 );
446 }
447 20%, 60% {
448 transform: translateX( -3px );
449 }
450 40%, 80% {
451 transform: translateX( 3px );
452 }
453 }
454
455 @keyframes os-dock-attention-bounce {
456 0%, 100% {
457 transform: translateY( 0 );
458 }
459 30% {
460 transform: translateY( -6px );
461 }
462 60% {
463 transform: translateY( -2px );
464 }
465 }
466
467 .os-dock__item--attention-pulse .os-dock__item-primary {
468 animation: os-dock-attention-pulse 1.4s ease-in-out infinite;
469 }
470 .os-dock__item--attention-shake .os-dock__item-primary {
471 animation: os-dock-attention-shake 0.8s ease-in-out infinite;
472 }
473 .os-dock__item--attention-bounce .os-dock__item-primary {
474 animation: os-dock-attention-bounce 1.2s ease-in-out infinite;
475 }
476
477 /* Intensity modulates animation duration — strong = faster, more
478 urgent; subtle = slower, calmer. */
479 .os-dock__item--intensity-strong.os-dock__item--attention-pulse .os-dock__item-primary {
480 animation-duration: 1s;
481 }
482 .os-dock__item--intensity-strong.os-dock__item--attention-shake .os-dock__item-primary {
483 animation-duration: 0.6s;
484 }
485 .os-dock__item--intensity-strong.os-dock__item--attention-bounce .os-dock__item-primary {
486 animation-duration: 0.9s;
487 }
488 .os-dock__item--intensity-subtle.os-dock__item--attention-pulse .os-dock__item-primary {
489 animation-duration: 1.8s;
490 }
491 .os-dock__item--intensity-subtle.os-dock__item--attention-shake .os-dock__item-primary {
492 animation-duration: 1s;
493 }
494 .os-dock__item--intensity-subtle.os-dock__item--attention-bounce .os-dock__item-primary {
495 animation-duration: 1.6s;
496 }
497
498 /* Reduced-motion: kill the animation, replace with a static accent ring
499 for the same visual prominence without the movement. */
500 @media ( prefers-reduced-motion: reduce ) {
501 .os-dock__item--attention-pulse .os-dock__item-primary,
502 .os-dock__item--attention-shake .os-dock__item-primary,
503 .os-dock__item--attention-bounce .os-dock__item-primary {
504 animation: none;
505 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
506 outline-offset: 2px;
507 border-radius: 8px;
508 }
509 }
510
511 /* Shared tooltip — placement-specific anchor modifiers applied by JS. */
512 .os-dock__tooltip {
513 position: fixed;
514 padding: 6px 12px;
515 background: var(
516 --os-tooltip-bg,
517 var( --os-ui-scrim, rgba( 0, 0, 0, 0.85 ) )
518 );
519 color: var(
520 --os-tooltip-fg,
521 var( --os-ui-fg-on-accent, #fff )
522 );
523 font-size: 12px;
524 font-weight: 400;
525 line-height: 1.4;
526 white-space: nowrap;
527 border-radius: 6px;
528 pointer-events: none;
529 z-index: calc( var( --os-z-dock ) + 1 );
530 opacity: 0;
531 transition: opacity 0.15s ease, transform 0.15s ease;
532 }
533
534 .os-dock__tooltip--visible {
535 opacity: 1;
536 }
537
538 /* ------------------------------------------------------------------
539 * Vertical placement (left + right) — dock runs the full shell body
540 * height along one edge, with items stacked vertically.
541 * ------------------------------------------------------------------ */
542
543 .os-dock[ data-os-dock-placement="left" ],
544 .os-dock[ data-os-dock-placement="right" ] {
545 width: var( --os-dock-width );
546 flex-direction: column;
547 padding: 16px 0 12px;
548 gap: 6px;
549 /*
550 * Vertical docks split into two inner wrappers:
551 * - `__scroll` — flex: 1, scrollable, hosts menu tiles + the
552 * inline core/plugin separator. When the menu
553 * exceeds the dock's height, this is what
554 * scrolls; the outer dock stays a fixed pillar.
555 * - `__pinned` — sized to content, hosts system tiles
556 * (Recycle Bin, OS Settings, …) + their hairline
557 * separator. Always visible at the bottom edge
558 * regardless of scroll position.
559 *
560 * `min-height: 0` on the dock is defensive: even though `__scroll`
561 * absorbs the overflow, the dock is a flex item inside the shell
562 * body (a flex row), and the default `min-height: auto` would let
563 * a tall `__pinned` push the dock past the parent's height.
564 */
565 min-height: 0;
566 }
567
568 /*
569 * Scrollable menu-tile area inside vertical docks. `flex: 1 1 0` plus
570 * `min-height: 0` lets it shrink below its content's natural height so
571 * `overflow-y: auto` actually engages. `overflow-x: clip` keeps the
572 * active-dot / focused-pill indicators visible (they sit at x≈2px
573 * inside the wrapper's content box) while preventing `overflow-y: auto`
574 * from coercing horizontal scroll. Scrollbar is hidden visually —
575 * Firefox via `scrollbar-width: none`, WebKit via the pseudo-element
576 * rule below — so the rail keeps its clean macOS look. Mouse wheel,
577 * touchpad, touch, and keyboard focus-into-view all still work.
578 */
579 .os-dock[ data-os-dock-placement="left" ] .os-dock__scroll,
580 .os-dock[ data-os-dock-placement="right" ] .os-dock__scroll {
581 display: flex;
582 flex-direction: column;
583 align-items: center;
584 gap: 6px;
585 flex: 1 1 0;
586 min-height: 0;
587 width: 100%;
588 overflow-y: auto;
589 overflow-x: clip;
590 scrollbar-width: none;
591 }
592
593 .os-dock[ data-os-dock-placement="left" ] .os-dock__scroll::-webkit-scrollbar,
594 .os-dock[ data-os-dock-placement="right" ] .os-dock__scroll::-webkit-scrollbar {
595 display: none;
596 }
597
598 /*
599 * Pinned area below `__scroll` — system tiles stay reachable here
600 * regardless of how the menu list scrolls. `flex-shrink: 0` so it's
601 * never compressed; sized to its content's natural height.
602 */
603 .os-dock[ data-os-dock-placement="left" ] .os-dock__pinned,
604 .os-dock[ data-os-dock-placement="right" ] .os-dock__pinned {
605 display: flex;
606 flex-direction: column;
607 align-items: center;
608 gap: 6px;
609 flex-shrink: 0;
610 width: 100%;
611 }
612
613 /* Left: dock is the first flex item in __body (row layout). */
614 .os-dock[ data-os-dock-placement="left" ] {
615 border-inline-end: 1px solid var( --os-dock-border );
616 order: -1;
617 }
618
619 /* Right: push the dock to the end of __body and flip the border edge. */
620 .os-dock[ data-os-dock-placement="right" ] {
621 border-inline-start: 1px solid var( --os-dock-border );
622 order: 1;
623 }
624
625 /* Active-indicator dot — left dock: inside-edge vertical pill. */
626 .os-dock[ data-os-dock-placement="left" ]
627 .os-dock__item--active::before {
628 content: "";
629 position: absolute;
630 inset-inline-start: -6px;
631 top: 50%;
632 transform: translateY( -50% );
633 width: 4px;
634 height: 4px;
635 border-radius: 50%;
636 background: var( --os-dock-item-outline, #fff );
637 }
638
639 .os-dock[ data-os-dock-placement="left" ]
640 .os-dock__item--focused::before {
641 width: 4px;
642 height: 16px;
643 border-radius: 2px;
644 }
645
646 /*
647 * "All instances minimized" — the tile still has windows, they're
648 * just hidden. Swap the solid dot for a hollow ring so the user sees
649 * "I have something here, currently tucked away" instead of "I have
650 * a focused / visible window here." `--all-minimized` is layered on
651 * top of `--active`; the rule below wins on selector specificity.
652 *
653 * Sized slightly larger than the solid dot so the ring is legible at
654 * dock size — a 4×4 outline reads as a dirty pixel.
655 */
656 .os-dock[ data-os-dock-placement="left" ]
657 .os-dock__item--all-minimized::before {
658 width: 6px;
659 height: 6px;
660 border-radius: 50%;
661 background: transparent;
662 border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
663 }
664
665 /* Right dock: indicator on the opposite edge (inside-facing). */
666 .os-dock[ data-os-dock-placement="right" ]
667 .os-dock__item--active::before {
668 content: "";
669 position: absolute;
670 inset-inline-end: -6px;
671 top: 50%;
672 transform: translateY( -50% );
673 width: 4px;
674 height: 4px;
675 border-radius: 50%;
676 background: var( --os-dock-item-outline, #fff );
677 }
678
679 .os-dock[ data-os-dock-placement="right" ]
680 .os-dock__item--focused::before {
681 width: 4px;
682 height: 16px;
683 border-radius: 2px;
684 }
685
686 .os-dock[ data-os-dock-placement="right" ]
687 .os-dock__item--all-minimized::before {
688 width: 6px;
689 height: 6px;
690 border-radius: 50%;
691 background: transparent;
692 border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
693 }
694
695 /* "Open another" chip — left: right of tile; right: left of tile. */
696 .os-dock[ data-os-dock-placement="left" ]
697 .os-dock__item-new {
698 top: 50%;
699 inset-inline-end: -13px;
700 transform: translateY( -50% );
701 }
702
703 .os-dock[ data-os-dock-placement="left" ]
704 .os-dock__item-new:hover {
705 transform: translateY( -50% ) scale( 1.1 );
706 }
707
708 .os-dock[ data-os-dock-placement="right" ]
709 .os-dock__item-new {
710 top: 50%;
711 inset-inline-start: -13px;
712 transform: translateY( -50% );
713 }
714
715 .os-dock[ data-os-dock-placement="right" ]
716 .os-dock__item-new:hover {
717 transform: translateY( -50% ) scale( 1.1 );
718 }
719
720 /* Separator — horizontal hairline between menu tiles and system
721 * tiles. With the `__scroll` / `__pinned` split the system separator
722 * lives at the top of `__pinned`; flex-flow handles the "system
723 * tiles at the bottom" placement, no margin-top: auto needed. The
724 * `--group` variant is used inline between core and plugin menu
725 * tiles in `__scroll`; it keeps the hairline styling and sits
726 * exactly where it's inserted. */
727 .os-dock[ data-os-dock-placement="left" ]
728 .os-dock__separator,
729 .os-dock[ data-os-dock-placement="right" ]
730 .os-dock__separator {
731 width: 60%;
732 height: 1px;
733 margin: 8px auto 4px;
734 background: var( --os-dock-border );
735 flex-shrink: 0;
736 }
737
738 .os-dock[ data-os-dock-placement="left" ]
739 .os-dock__separator--group,
740 .os-dock[ data-os-dock-placement="right" ]
741 .os-dock__separator--group {
742 margin: 10px auto;
743 background: rgba( 255, 255, 255, 0.22 );
744 }
745
746 /*
747 * Hide the system separator when nothing precedes it — on a clean
748 * install (or a low-cap user with zero menu items) the separator
749 * would render as a stray hairline with nothing to divide.
750 *
751 * Two cases:
752 * 1. Bottom dock — separator is a direct child of the dock; hide
753 * when it's the first such child.
754 * 2. Vertical docks — separator lives inside `__pinned`; hide when
755 * `__scroll` (its sibling above) is empty of menu tiles.
756 */
757 .os-dock > .os-dock__separator:first-child {
758 display: none;
759 }
760
761 .os-dock__scroll:not(:has( .os-dock__item ))
762 + .os-dock__pinned
763 .os-dock__separator {
764 display: none;
765 }
766
767 /*
768 * Drop an empty `__scroll` from the flex flow entirely. When only
769 * system tiles are present (e.g. just the Recycle Bin), the empty
770 * wrapper would otherwise still occupy the rail: its `flex-grow`
771 * stretches it — shoving the lone `__pinned` tile to the far edge on
772 * vertical docks — and the inter-wrapper `gap` leaves a phantom
773 * offset that nudges the tile off-center on the bottom dock. Removing
774 * it lets the dock center the lone system tile. As soon as a menu
775 * tile is added the `:has()` no longer matches and normal flow
776 * resumes.
777 *
778 * The `[data-os-dock-placement]` qualifier is load-bearing:
779 * the per-placement rules that set `display: flex` on `__scroll` are
780 * (0,3,0); this selector must out-specify them, so it carries the
781 * placement attribute to reach (0,4,0).
782 */
783 .os-dock[ data-os-dock-placement ]
784 .os-dock__scroll:not(:has( .os-dock__item )) {
785 display: none;
786 }
787
788 /* Tooltip — anchor modifiers are applied directly to the tooltip
789 * element (NOT via a descendant selector on `.os-dock`)
790 * because the tooltip lives in `document.body`. JS writes the
791 * absolute `top` / `left` coords; CSS handles the slide-in
792 * animation direction per modifier.
793 *
794 * - `--after` : left-placed dock (tooltip to the RIGHT of tile)
795 * - `--before` : right-placed dock (tooltip to the LEFT of tile)
796 * - `--above` : bottom-placed dock (tooltip ABOVE the tile)
797 */
798
799 /* Left dock — tooltip slides in from the left by 4px. JS sets
800 * `left` to `tile.right + 8px`; the transform animates the entry. */
801 .os-dock__tooltip--after {
802 transform: translateX( -4px );
803 }
804
805 .os-dock__tooltip--after.os-dock__tooltip--visible {
806 transform: translateX( 0 );
807 }
808
809 .os-dock__tooltip--before {
810 /* Right-placed dock — tooltip sits to the left of the tile. JS
811 * writes the left coord to the tile's left edge; this translate
812 * pulls the tooltip further left by its own width. */
813 transform: translate( calc( -100% - 8px ), 0 );
814 }
815
816 .os-dock__tooltip--before.os-dock__tooltip--visible {
817 transform: translate( calc( -100% - 8px ), 0 );
818 opacity: 1;
819 }
820
821 /* ------------------------------------------------------------------
822 * Bottom placement — floating macOS-style pill centered above the
823 * viewport floor. Absolutely positioned so it floats as pure chrome
824 * rather than stealing vertical space from the shell body.
825 * ------------------------------------------------------------------ */
826
827 .os-dock[ data-os-dock-placement="bottom" ] {
828 position: absolute;
829 inset-inline-start: 0;
830 inset-inline-end: 0;
831 bottom: 12px;
832 margin: 0 auto;
833 width: fit-content;
834 max-width: calc( 100% - 32px );
835 flex-direction: row;
836 justify-content: center;
837 /* Top padding is 4px instead of 8px so that the inner `__scroll`
838 * wrapper can claim its own 4px padding-top for badge clearance
839 * (the tile's badge sits at top: -3px). Net vertical chrome stays
840 * symmetric: 4px pill padding + 4px wrapper padding above the
841 * tile, 8px pill padding below — tile is 8px from both edges. */
842 padding: 4px 12px 8px;
843 gap: 6px;
844 border-radius: 18px;
845
846 /* Warmer, more saturated glass tint than vertical placements so
847 * the floating pill reads as a distinct piece of chrome rather
848 * than a section of the sidebar.
849 *
850 * background-COLOR, not the shorthand: the base `.os-dock`
851 * rule declares the DOCK texture slot's background-image, and a
852 * shorthand here would reset it — a themed dock texture would
853 * simply never appear in the bottom placement.
854 *
855 * The tint is its own token rather than `--os-dock-bg`
856 * precisely because the two are meant to differ; a theme that
857 * wants them identical points this at the same value. */
858 background-color: var(
859 --os-dock-floating-bg,
860 rgba( 22, 22, 26, 0.42 )
861 );
862 backdrop-filter: blur( 28px ) saturate( 170% );
863 -webkit-backdrop-filter: blur( 28px ) saturate( 170% );
864
865 /*
866 * Hairline + inset highlight. These used to be hardcoded, which
867 * meant a theme could restyle every dock placement EXCEPT this
868 * one and had no way to remove the outline it saw floating over
869 * an empty desktop.
870 *
871 * The chain matters: the floating-specific token wins, then the
872 * general `--os-dock-border` (so a theme that tokenized
873 * its side docks gets the pill for free, which is what anyone
874 * setting that token expects), then the literal. Set either to
875 * `transparent` for a borderless pill.
876 */
877 border: 1px solid var(
878 --os-dock-floating-border,
879 var( --os-dock-border, rgba( 255, 255, 255, 0.12 ) )
880 );
881 border-block-start: 1px solid var(
882 --os-dock-floating-border-top,
883 var(
884 --os-dock-floating-border,
885 var( --os-dock-border, rgba( 255, 255, 255, 0.18 ) )
886 )
887 );
888 box-shadow:
889 inset 0 1px 0 var(
890 --os-dock-floating-highlight,
891 rgba( 255, 255, 255, 0.08 )
892 ),
893 0 10px 32px var(
894 --os-dock-floating-shadow,
895 rgba( 0, 0, 0, 0.45 )
896 );
897 }
898
899 /* Active-indicator dot — below the tile, macOS-style. */
900 .os-dock[ data-os-dock-placement="bottom" ]
901 .os-dock__item--active::before {
902 content: "";
903 position: absolute;
904 inset-inline-start: 50%;
905 top: auto;
906 bottom: -3px;
907 transform: translateX( -50% );
908 width: 4px;
909 height: 4px;
910 border-radius: 50%;
911 background: var( --os-dock-item-outline, #fff );
912 }
913
914 .os-dock[ data-os-dock-placement="bottom" ]
915 .os-dock__item--focused::before {
916 width: 16px;
917 height: 4px;
918 border-radius: 2px;
919 }
920
921 /*
922 * "All instances minimized" on the bottom dock — solid dot becomes
923 * a hollow ring. Matches the vertical-dock treatment so the cue is
924 * consistent across placements.
925 */
926 .os-dock[ data-os-dock-placement="bottom" ]
927 .os-dock__item--all-minimized::before {
928 width: 6px;
929 height: 6px;
930 border-radius: 50%;
931 background: transparent;
932 border: 1px solid var( --os-dock-item-outline, rgba( 255, 255, 255, 0.85 ) );
933 }
934
935 /*
936 * "Show Desktop" — every live window on the active desktop minimized.
937 *
938 * The dock does not change when this happens. It used to: the bottom
939 * pill and the vertical rails each picked up a 1px inset ring, on the
940 * reasoning that an empty wallpaper otherwise looks identical to a
941 * desktop that never had anything on it.
942 *
943 * The reasoning was sound and the ring was still the wrong answer. It
944 * outlined the whole dock — the one surface on screen that is always
945 * present and never the subject — to say something about windows that
946 * are not on the dock at all. Every tile that has minimized windows
947 * already says so, precisely and locally, by swapping its solid
948 * indicator dot for a hollow ring. That is the cue: it points at the
949 * tiles the windows are under, in the place the user will click to get
950 * them back.
951 *
952 * `body.os-show-desktop-active` is still set (see `src/dock.ts`) and
953 * still public, so a theme or plugin that wants a global cue can paint
954 * one. Core does not.
955 */
956
957 /* "Open another" chip — floats above the tile's top-right corner so
958 * it doesn't collide with neighbouring tiles in the horizontal row. */
959 .os-dock[ data-os-dock-placement="bottom" ]
960 .os-dock__item-new {
961 top: -6px;
962 inset-inline-end: -6px;
963 transform: none;
964 }
965
966 .os-dock[ data-os-dock-placement="bottom" ]
967 .os-dock__item-new:hover {
968 transform: scale( 1.1 );
969 }
970
971 /*
972 * Bottom-dock inner wrappers — `__scroll` carries the menu tiles and
973 * absorbs horizontal overflow; `__pinned` carries the system tiles and
974 * stays anchored to the trailing edge. Padding-top on `__scroll` gives
975 * the badge (top: -3px on the tile) room so `overflow-y: hidden`
976 * doesn't crop it. `min-width: 0` lets the wrapper shrink below its
977 * content's natural width, so the dock pill's `max-width` is what
978 * decides when scroll kicks in instead of the content forcing the
979 * pill wider. `justify-content: center` keeps tiles balanced inside
980 * the pill when content fits; when it overflows, scroll engages and
981 * items extend past the visible area.
982 */
983 .os-dock[ data-os-dock-placement="bottom" ] .os-dock__scroll {
984 display: flex;
985 flex-direction: row;
986 align-items: center;
987 justify-content: center;
988 gap: 6px;
989 flex: 1 1 auto;
990 min-width: 0;
991 padding-top: 4px;
992 padding-bottom: 6px;
993 overflow-x: auto;
994 overflow-y: visible;
995 scrollbar-width: none;
996 }
997
998 .os-dock[ data-os-dock-placement="bottom" ] .os-dock__scroll::-webkit-scrollbar {
999 display: none;
1000 }
1001
1002 .os-dock[ data-os-dock-placement="bottom" ] .os-dock__pinned {
1003 display: flex;
1004 flex-direction: row;
1005 align-items: center;
1006 gap: 6px;
1007 flex-shrink: 0;
1008 /* Match the scroll wrapper's badge clearance so system tiles sit
1009 * on the same vertical baseline as the menu tiles. */
1010 padding-top: 4px;
1011 padding-bottom: 6px;
1012 }
1013
1014 /* Separator — vertical hairline between menu tiles and system tiles.
1015 * Lives at the leading edge of `__pinned`; flex flow handles the
1016 * "pinned at the trailing edge" placement, no margin-inline-start:
1017 * auto needed. The `--group` variant keeps the hairline styling for
1018 * the inline core→plugin boundary inside `__scroll` and sits where
1019 * inserted. */
1020 .os-dock[ data-os-dock-placement="bottom" ]
1021 .os-dock__separator {
1022 width: 1px;
1023 /* Fixed height — `60%` collapses against the floating pill's
1024 intrinsic height, leaving the hairline effectively invisible.
1025 28px matches the dock tile inner glyph area. */
1026 height: 28px;
1027 /* Symmetric horizontal margin so the divider sits centered in the
1028 * inter-tile gap: the 6px flex gap on each side (`__scroll`→`__pinned`
1029 * on the left, `__pinned`'s own gap on the right) plus an equal 6px
1030 * margin per side balances out. An asymmetric margin here nudges the
1031 * divider — and the whole menu cluster with it — off the pill's
1032 * center. */
1033 margin: auto 6px;
1034 background: var( --os-dock-border );
1035 flex-shrink: 0;
1036 }
1037
1038 /*
1039 * Group separator — sits inline between the core cluster and the
1040 * plugin cluster. Brighter than the menu-vs-system separator because
1041 * it carries user meaning ("this is where your installed apps
1042 * begin"), and flanked by extra gap so the boundary reads at a
1043 * glance rather than disappearing into the dock tint.
1044 */
1045 .os-dock[ data-os-dock-placement="bottom" ]
1046 .os-dock__separator--group {
1047 width: 1px;
1048 height: 60%;
1049 margin: auto 10px;
1050 margin-inline-start: 10px;
1051 background: rgba( 255, 255, 255, 0.22 );
1052 flex-shrink: 0;
1053 }
1054
1055 /* Tooltip — above the hovered tile. JS writes the horizontal center
1056 * + a top near the tile's top edge; CSS translates the tooltip up by
1057 * its own height so it clears the tile. */
1058 .os-dock__tooltip--above {
1059 inset-inline-start: auto;
1060 transform: translate( -50%, calc( -100% - 4px ) );
1061 }
1062
1063 .os-dock__tooltip--above.os-dock__tooltip--visible {
1064 transform: translate( -50%, -100% );
1065 }
1066
1067
1068
1069 /* Drag-to-reorder (since 0.25.0).
1070 *
1071 * The dragged tile follows the cursor via inline `transform:
1072 * translate()` written from JS. It stays IN flow so flex doesn't
1073 * collapse the row — the visual lift is a z-index bump and
1074 * `pointer-events: none` so `elementFromPoint` finds siblings
1075 * underneath the cursor. Sibling tiles animate via FLIP — the JS
1076 * writes inline `transform` + `transition` per leg, so no CSS
1077 * transition is declared here (it would race with the per-leg
1078 * inline transition). */
1079 .os-dock__item--dragging {
1080 pointer-events: none;
1081 z-index: 50;
1082 opacity: 0.92;
1083 cursor: grabbing;
1084 will-change: transform;
1085 /* Subtle shadow so the dragged tile reads as "lifted" against
1086 * the dock surface it floats above. */
1087 filter: drop-shadow( 0 6px 12px rgba( 0, 0, 0, 0.35 ) );
1088 }
1089
1090 .os-dock__item:not( .os-dock__item--system ) {
1091 cursor: grab;
1092 }
1093
1094 .os-dock__item:not( .os-dock__item--system ):active {
1095 cursor: grabbing;
1096 }
1097