PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.6
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.6
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 / desktop.css

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

1,669 lines 51.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OpenStation — Shell Layout.
3 *
4 * When OpenStation is active, the classic admin chrome still renders
5 * server-side (for hook compatibility) but is hidden with CSS. The
6 * plugin-injected shell floats above it, positioned just below the
7 * fixed admin bar, covering the rest of the viewport.
8 *
9 * @since 0.1.0
10 */
11
12 /*
13 * Solid backdrop behind the shell. Reads the backstop token rather
14 * than a literal so it moves with the desk: this is the colour a
15 * dropped frame shows, and it has to be the same one the shell paints
16 * or the seam is visible at exactly the wrong moment.
17 */
18 body.os-active {
19 background: var( --os-backstop, #1d2327 );
20 overflow: hidden;
21 }
22
23 /*
24 * Text selection.
25 *
26 * The shell had no ::selection rule at all, so every selection in it
27 * was the browser default — a colour chosen for a white page, landing
28 * on a near-black one. In Chrome that is a dark translucent blue over
29 * Obsidian, which is roughly 1.3:1 against the surface and leaves the
30 * selected text at a contrast the UA never checked, because the UA
31 * assumed a light background. Dragging across a paragraph produced no
32 * visible change at all on some displays.
33 *
34 * Both halves are set on purpose. `background` alone is the common
35 * half-fix: the UA then keeps its own selected-text colour, which is
36 * frequently forced to black and lands on this violet at ~2:1.
37 *
38 * Scoped to the shell document. `variables.css` is a dependency of
39 * `chromeless.css` and so loads inside every iframe window, but this
40 * file does not — a wp-admin page in a window keeps the selection
41 * colour it has outside one, which is the same promise the palette
42 * makes about everything else.
43 *
44 * `::-moz-selection` is a separate rule rather than a second selector
45 * on this one: an unrecognised pseudo-element invalidates the entire
46 * selector list, so pairing them would leave Chrome unstyled too.
47 */
48 body.os-active ::selection {
49 background: var( --os-ui-selection-bg, rgba( 159, 152, 255, 0.6 ) );
50 color: var( --os-ui-selection-fg, #fffbff );
51 }
52
53 body.os-active ::-moz-selection {
54 background: var( --os-ui-selection-bg, rgba( 159, 152, 255, 0.6 ) );
55 color: var( --os-ui-selection-fg, #fffbff );
56 }
57
58 /*
59 * Cursor policy — default arrow everywhere in OpenStation.
60 *
61 * Per user preference (since 0.20.0): no hand/pointer/grab/grabbing/
62 * copy/no-drop cursors on icons, tiles, dock items, dock peek,
63 * window chrome, or any other clickable surface. The default arrow
64 * is the only "neutral" cursor we render.
65 *
66 * Exceptions:
67 * - Window + widget resize handles keep their direction-specific
68 * cursors (ns-resize, ew-resize, nesw-resize, nwse-resize) —
69 * they're a functional cue, not a hover affordance, and removing
70 * them would make corner-grabs guesswork.
71 * - Text inputs / textareas / contenteditable surfaces keep the
72 * browser-default I-beam.
73 * - The AI assistant's inline "AI Settings" recovery link keeps a
74 * pointer so it reads as a genuine link (rule lives in
75 * ai-assistant.css).
76 *
77 * `!important` is needed because the specific `cursor: pointer` and
78 * drag-state declarations elsewhere in the codebase have the same or
79 * higher specificity. The resize-handle exceptions use `!important`
80 * for the same reason.
81 *
82 * Shadow-DOM web components (`os-*`) define their own cursors
83 * inside their shadow trees; those internal rules win regardless of
84 * what we set on the host. Anything visible on the desktop shell
85 * (light DOM) is covered by this policy.
86 */
87 body.os-active,
88 body.os-active * {
89 cursor: default !important;
90 }
91
92 body.os-active input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"]):not([type="file"]):not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="image"]),
93 body.os-active textarea,
94 body.os-active [contenteditable=""],
95 body.os-active [contenteditable="true"] {
96 cursor: text !important;
97 }
98
99 body.os-active .os-window__resize-handle--se,
100 body.os-active .os-window__resize-handle--nw,
101 body.os-active .os-widgets__resize--se,
102 body.os-active .os-widgets__resize--nw {
103 cursor: nwse-resize !important;
104 }
105
106 body.os-active .os-window__resize-handle--sw,
107 body.os-active .os-window__resize-handle--ne,
108 body.os-active .os-widgets__resize--sw,
109 body.os-active .os-widgets__resize--ne {
110 cursor: nesw-resize !important;
111 }
112
113 body.os-active .os-widgets__resize--n,
114 body.os-active .os-widgets__resize--s {
115 cursor: ns-resize !important;
116 }
117
118 body.os-active .os-widgets__resize--e,
119 body.os-active .os-widgets__resize--w {
120 cursor: ew-resize !important;
121 }
122
123 /*
124 * Hide the classic admin chrome while the shell is active.
125 *
126 * The markup still exists in the DOM so server-side action hooks that
127 * target these regions (admin_notices, in_admin_footer, etc.) continue
128 * to fire — we just take them out of the visual flow.
129 */
130 body.os-active #adminmenuwrap,
131 body.os-active #adminmenuback,
132 body.os-active #wpbody,
133 body.os-active #wpfooter,
134 body.os-active .wp-responsive-toggle,
135 body.os-active #collapse-menu {
136 display: none !important;
137 }
138
139 /*
140 * The admin bar is the ONLY escape hatch in OpenStation — it hosts the
141 * "Switch to Classic Admin" toggle. Any plugin CSS, Gutenberg state,
142 * user preference, or stray `display:none` that hides it would strand
143 * the user inside the shell with no way out. Hard-pin it visible.
144 *
145 * The exception: while a window is in immersive fullscreen, we *want*
146 * the bar hidden (macOS convention). That rule lives below and wins on
147 * specificity because it also targets #wpadminbar under a body class.
148 */
149 body.os-active #wpadminbar {
150 display: block !important;
151 visibility: visible !important;
152 opacity: 1 !important;
153 position: fixed !important;
154 inset-block-start: 0 !important;
155 inset-inline-start: 0 !important;
156 inset-inline-end: 0 !important;
157 }
158
159 /*
160 * Admin-bar presentation modes — OS Settings → Appearance → Admin bar,
161 * persisted as `adminBarMode` and emitted as a
162 * `os-admin-bar-<mode>` body class by PHP on first paint and
163 * re-written by the shell's apply pass on every change.
164 *
165 * `static` is the absence of rules: the pin above stands and the shell
166 * starts below the bar. The two modes below both take the bar out of
167 * the shell's way, so the shell reclaims the full viewport in each.
168 *
169 * Every selector here carries two body classes, so it outranks the
170 * one-class pin rule above without needing to escalate anything.
171 */
172
173 /*
174 * Dynamic — the bar parks off the top edge leaving a peek strip, and
175 * slides back in when the pointer reaches the top of the viewport or
176 * something inside it takes keyboard focus. The classic Windows
177 * auto-hide taskbar, and the reason `transform` is the right tool: a
178 * transformed element keeps hit-testing at its PAINTED position, so
179 * the parked remainder stops catching the pointer the moment it goes
180 * off-screen. What DOES catch it is the peek strip plus the reveal
181 * zone added below.
182 */
183 body.os-active.os-admin-bar-dynamic #wpadminbar {
184 transform: translateY(
185 calc(-100% + var(--os-admin-bar-peek, 4px))
186 );
187 transition: transform 180ms ease;
188 }
189
190 body.os-active.os-admin-bar-dynamic #wpadminbar:hover,
191 body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within,
192 body.os-active.os-admin-bar-dynamic #wpadminbar:active,
193 /*
194 * …and while the notch is being used. The notch hangs from the same
195 * edge and steps down to sit under the bar when it appears (see
196 * notch.css), so letting the bar retract out from over it mid-reach
197 * would drop the notch back up under the pointer.
198 */
199 body.os-active.os-admin-bar-dynamic:has( .os-notch:hover ) #wpadminbar,
200 body.os-active.os-admin-bar-dynamic:has( .os-notch:focus-visible ) #wpadminbar {
201 transform: translateY(0);
202 }
203
204 /*
205 * Reveal zone — an invisible extension of the bar's hit area, hanging
206 * below the parked peek strip.
207 *
208 * Hovering a pseudo-element counts as hovering its originating
209 * element, so this widens what `:hover` above responds to WITHOUT
210 * widening the visible seam. The two wants are genuinely different:
211 * the seam should be a hairline, the target should be forgiving, and
212 * `--os-admin-bar-peek` alone can't be both.
213 *
214 * Core ships no bare `#wpadminbar::before/::after` rule, so this
215 * claims the slot rather than fighting one.
216 */
217 body.os-active.os-admin-bar-dynamic #wpadminbar::after {
218 content: "";
219 position: absolute;
220 inset-inline: 0;
221 inset-block-start: 100%;
222 height: var(--os-admin-bar-reveal-zone, 16px);
223 }
224
225 /*
226 * ...but only while the bar is parked. Left standing once the bar is
227 * out, the zone would hang over the top of the desktop and swallow
228 * clicks on whatever window is up there.
229 *
230 * Collapsing it is also what keeps the reveal stable: the pointer
231 * that tripped the zone is, post-reveal, inside the bar's real box,
232 * so `:hover` holds without the zone's help.
233 */
234 body.os-active.os-admin-bar-dynamic #wpadminbar:hover::after,
235 body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within::after,
236 body.os-active.os-admin-bar-dynamic #wpadminbar:active::after {
237 height: 0;
238 }
239
240 /*
241 * Coarse pointers have no hover state and a much fatter contact patch,
242 * so a 4px seam is neither discoverable nor tappable. Widen both the
243 * seam and the zone — `:active` above is what actually reveals the bar
244 * there. Kept under the 32px bar height, same ceiling as the default.
245 */
246 @media (hover: none) {
247 body.os-active.os-admin-bar-dynamic #wpadminbar {
248 --os-admin-bar-peek: 10px;
249 --os-admin-bar-reveal-zone: 20px;
250 }
251 }
252
253 @media (prefers-reduced-motion: reduce) {
254 body.os-active.os-admin-bar-dynamic #wpadminbar {
255 transition: none;
256 }
257 }
258
259 /*
260 * Hidden — the bar is gone. This removes the "Switch to Classic Admin"
261 * toggle, which is why it is safe: the dock's core rail always carries
262 * an "Exit OpenStation" tile (src/exit-os.ts) hitting the
263 * same endpoint. Do not add a mode that removes both.
264 */
265 body.os-active.os-admin-bar-hidden #wpadminbar {
266 display: none !important;
267 }
268
269 /*
270 * Both non-static modes: the shell owns the whole viewport. Two body
271 * classes beat the `.os-shell` base rule AND its <783px
272 * media-query variant on specificity, so source order doesn't matter.
273 */
274 body.os-active.os-admin-bar-dynamic .os-shell,
275 body.os-active.os-admin-bar-hidden .os-shell {
276 inset-block-start: 0;
277 }
278
279 /*
280 * Core reserves the bar's height as `padding-top` on <html>. In both
281 * non-static modes the bar is out of flow (or absent) and the shell
282 * covers the viewport anyway, so the reserve is dead space — release
283 * it so any classic content behind the shell isn't pushed down by a
284 * bar that isn't sitting there. Same idiom as chromeless.css.
285 */
286 html.wp-toolbar:has(body.os-admin-bar-dynamic),
287 html.wp-toolbar:has(body.os-admin-bar-hidden) {
288 padding-top: 0 !important;
289 }
290
291 /*
292 * Align the admin-bar W logo with the dock below it.
293 *
294 * Without this rule the admin bar's leftmost item (the WordPress W)
295 * renders at its Core-dictated ~44 px width, while the dock below it
296 * can be 48, 56, or 72 px wide depending on the user's OS-Settings
297 * preference. The misalignment reads as "two disconnected left
298 * regions" — visually scruffy on an otherwise tight shell.
299 *
300 * We size the logo's <li> to match the dock's width and center the
301 * W inside, so the two columns share an axis. `--os-dock-width`
302 * is now set on :root (see settings.ts → apply()) so it reaches
303 * siblings of #os-shell like the admin bar.
304 */
305 body.os-active #wpadminbar #wp-admin-bar-wp-logo {
306 width: var( --os-dock-width, 56px );
307 }
308
309 body.os-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item {
310 display: flex !important;
311 align-items: center;
312 justify-content: center;
313 padding-inline: 0 !important;
314 }
315
316 /*
317 * The W glyph is painted via a ::before pseudo on `.ab-icon`. Center
318 * it within the flex cell so varying dock widths don't leave it
319 * visually offset.
320 */
321 body.os-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item > .ab-icon {
322 margin: 0;
323 }
324
325 /*
326 * Classic wpcontent carries a `margin-left: 160px` for the sidebar. With
327 * the sidebar hidden we let wpcontent fall back to flush — the shell is
328 * fixed-positioned anyway so this doesn't change the shell's layout, it
329 * just prevents a phantom sidebar gutter in any stray visible content.
330 */
331 body.os-active #wpcontent {
332 margin-inline-start: 0;
333 padding-inline-start: 0;
334 }
335
336 /*
337 * Desktop shell — fills the viewport below the admin bar.
338 * Uses position: fixed so it's unaffected by any containing-block
339 * quirks in #wpwrap/#wpcontent.
340 */
341 .os-shell {
342 position: fixed;
343 inset-block-start: var(--wp-admin--admin-bar--height, 32px);
344 inset-inline-start: 0;
345 inset-inline-end: 0;
346 inset-block-end: 0;
347 display: flex;
348 flex-direction: column;
349 overflow: hidden;
350 z-index: var(--os-z-base);
351 /*
352 * Opaque floor. The wallpaper paints over this and is what anyone
353 * actually sees — but the wallpaper is one layer among several,
354 * and behind this shell is the classic admin page, which is WHITE.
355 * Without a background here, any frame in which the wallpaper
356 * doesn't paint flashes that white through the whole viewport.
357 * See `--os-backstop`.
358 */
359 background: var(--os-backstop, #1d2327);
360 /*
361 * Shell typeface. Every piece of chrome that doesn't set its own
362 * face inherits from here — dock labels, desktop icon labels,
363 * widgets, the overview. Windows override with `--os-ui-font` on
364 * their body, so a theme can run a display face on the desk and a
365 * text face inside windows. `inherit` is the no-theme value, i.e.
366 * identical to not declaring the property at all.
367 */
368 font-family: var(--os-font, inherit);
369 }
370
371 /*
372 * Wallpaper layer — first child of the shell, sits behind the dock
373 * and desktop area so a translucent dock shows through to the
374 * background (macOS pattern). CSS wallpapers paint via the
375 * `--os-bg` custom property; canvas wallpapers mount their
376 * own DOM (typically a <canvas>) into this element via JS.
377 *
378 * z-index: 0 keeps it below the shell body flex row which has no
379 * explicit z-index of its own but establishes its own local
380 * stacking context via `position: relative`, so the dock + windows
381 * always render above the wallpaper regardless of DOM order.
382 */
383 .os-wallpaper {
384 position: absolute;
385 inset: 0;
386 z-index: 0;
387 background: var(--os-bg);
388 overflow: hidden;
389 pointer-events: none;
390 }
391
392 /*
393 * Mio layer — the desk companion.
394 *
395 * A sibling of the wallpaper inside the shell, but at the far end of
396 * the stack: Mio paints ABOVE windows (it perches on them)
397 * and below the dock (it must never cover navigation). Created on
398 * demand by `src/mio/controller.ts`. A shell whose user has never
399 * switched Mio on has no extra element at all; switching it off
400 * hides this one rather than removing it, because the instance is
401 * parked (WebGL context intact) rather than destroyed — see the
402 * `parked` field there.
403 *
404 * The layer and its <canvas> are permanently inert. Making a
405 * full-shell canvas interactive — even conditionally, from a
406 * per-frame hit test — would swallow clicks meant for the window
407 * underneath. Only the small round `__handle` inside it takes
408 * pointer events, and it rides on the blob.
409 */
410 .os-mio {
411 position: absolute;
412 inset: 0;
413 z-index: var(--os-z-mio);
414 overflow: hidden;
415 pointer-events: none;
416 }
417
418 .os-mio__handle {
419 position: absolute;
420 /*
421 * PHYSICAL `top` / `left`, deliberately — the one place in this
422 * sheet that isn't logical. The handle is positioned every frame
423 * by a `translate3d()` carrying the body's canvas coordinates,
424 * and `transform` is always physical. Anchoring with
425 * `inset-inline-start` would flip the origin to the right edge
426 * under RTL while the translation kept pushing rightwards, and
427 * the handle would walk off screen.
428 */
429 top: 0;
430 left: 0;
431 border-radius: 50%;
432 pointer-events: auto;
433 cursor: grab;
434 /* Promote to its own layer: the handle is re-positioned via
435 * `transform` on every animation frame. */
436 will-change: transform;
437 touch-action: none;
438 }
439
440 .os-mio__handle.is-dragging {
441 cursor: grabbing;
442 }
443
444 /*
445 * "Make it yours" panel — a live-preview dialog, not a modal.
446 *
447 * `<os-modal>` is built for the usual case: dim the page, blur it
448 * slightly, put the decision in front of everything. Every one of
449 * those defaults is wrong here, because the thing being edited is
450 * ON the page and the whole point is watching it change.
451 *
452 * - No scrim and no `backdrop-filter`, or the companion the sliders
453 * are driving is a blurred grey smudge behind them.
454 * - `pointer-events: none` on the scrim so the desk stays live: Mio
455 * can still be picked up and thrown while the panel is open, and a
456 * click on the wallpaper doesn't dismiss the panel mid-adjustment.
457 * `::part(dialog)` puts them back for the box itself.
458 * - Parked against the inline end rather than centred, so it isn't
459 * sitting on top of the subject.
460 *
461 * Mio never treats the panel as an obstacle: the collision set comes
462 * from `getWallpaperSurfaces()`, which seeds windows, the shell floor,
463 * docks and widget cards — a dialog on `document.body` is none of
464 * those, so there is nothing to bump into.
465 */
466 .os-mio-panel {
467 background: transparent;
468 background-image: none;
469 backdrop-filter: none;
470 pointer-events: none;
471 justify-content: flex-end;
472 padding-inline-end: clamp(16px, 4vw, 64px);
473 }
474
475 /*
476 * Both halves are required, and the second one is easy to miss.
477 *
478 * `::part(dialog)` reaches the box in the modal's SHADOW tree — its
479 * header, title and close button inherit from there. But the controls
480 * are slotted LIGHT-DOM children of `<os-modal>`, and slotted content
481 * inherits from its light-DOM parent (the host) rather than from the
482 * shadow ancestor it is projected into. So the host's
483 * `pointer-events: none` reaches them regardless of what the part
484 * says, and without this second rule every slider and button in the
485 * panel is inert while looking perfectly normal.
486 */
487 .os-mio-panel::part(dialog),
488 .os-mio-panel > * {
489 pointer-events: auto;
490 }
491
492 /*
493 * Overview (Exposé) hides the desk furniture so the window grid
494 * reads cleanly — Mio goes with the widgets and the notes.
495 * `:has()` rather than a descendant selector because Mio
496 * layer is a sibling of `.os-shell__body`, not a child of
497 * the desktop area.
498 */
499 .os-mio {
500 transition: opacity 160ms ease;
501 }
502
503 .os-shell:has( .os-area--overview ) .os-mio {
504 opacity: 0;
505 }
506
507 /*
508 * Reduced motion is handled in the simulation, not here: the runtime
509 * zeroes the idle float and the hue drift so Mio holds still
510 * until the user actually interacts with it. Hiding it would be
511 * wrong — the user switched it on deliberately.
512 */
513
514 /*
515 * Desktop-theme wallpaper texture (DESKTOP slot).
516 *
517 * On a `::before` rather than this element's own background, because
518 * `--os-bg` is set by the wallpaper picker and can be a
519 * gradient — i.e. a background-IMAGE. Overriding `background-image`
520 * here would silently erase every gradient wallpaper the moment a
521 * theme was active. The pseudo-element layers instead of replacing.
522 *
523 * Resulting precedence, bottom to top:
524 * CSS wallpaper (`--os-bg`)
525 * → theme DESKTOP texture (this rule)
526 * → canvas wallpapers, which mount as CHILDREN of the layer and
527 * therefore paint above both.
528 *
529 * A theme that wants the user's wallpaper to remain visible ships a
530 * texture with transparency; an opaque one takes the desk over.
531 * With no theme active the image is `none` and this paints nothing.
532 */
533 .os-wallpaper::before {
534 content: "";
535 position: absolute;
536 inset: 0;
537 pointer-events: none;
538 background-image: var(--os-desktop-image, none);
539 background-repeat: var(--os-desktop-image-repeat, repeat);
540 background-size: var(--os-desktop-image-size, auto);
541 background-position: var(--os-desktop-image-position, center);
542 }
543
544 /*
545 * Canvas children fill the layer and ignore pointer events by
546 * default — a wallpaper is presentational chrome, not an interactive
547 * element. Plugins that genuinely need a clickable wallpaper can
548 * override `pointer-events` on their own mounted node.
549 */
550 .os-wallpaper > canvas,
551 .os-wallpaper > * {
552 width: 100%;
553 height: 100%;
554 display: block;
555 }
556
557 /* Below 783px WP collapses the admin bar to 46px. */
558 @media screen and (max-width: 782px) {
559 .os-shell {
560 inset-block-start: var(--wp-admin--admin-bar--height, 46px);
561 }
562 }
563
564 /*
565 * Immersive fullscreen: when any window is in fullscreen state, hide the
566 * admin bar and let the shell cover the full viewport. This keeps the
567 * fullscreen window visually above all chrome without fighting the shell's
568 * stacking context (a fullscreen window lives inside the shell, so its
569 * z-index is always bounded by the shell's — moving it out of the shell
570 * to raise it would break focus, drag, and session snapshotting).
571 *
572 * The window's title bar remains visible because the window itself is
573 * position: fixed at top: 0, so the focus/exit-fullscreen button is
574 * still reachable.
575 */
576 body.os-has-fullscreen-window #wpadminbar {
577 display: none !important;
578 }
579
580 body.os-has-fullscreen-window .os-shell {
581 inset-block-start: 0;
582 }
583
584 /* Shell body: contains dock + desktop area side by side. */
585 .os-shell__body {
586 flex: 1;
587 display: flex;
588 overflow: hidden;
589 position: relative;
590 }
591
592 /*
593 * Desktop area — where windows float. Background is now transparent
594 * because the wallpaper renders in the shell-level layer behind
595 * both this area and the dock. Stacking context via position: relative
596 * ensures children (windows) sit above the wallpaper.
597 *
598 * The padding reserves the band the floating bottom dock pill covers.
599 * It reads the work-area insets `src/work-area/index.ts` writes on
600 * `#os-shell` from the pill's LIVE geometry, so it is 0 with a side
601 * dock (which is a flex sibling and already narrows the area) and
602 * tracks the dock-size preference with the pill at the bottom. The
603 * `80px` literal is the pre-measurement floor: 40px tile + 16px pill
604 * padding + 12px gap below + 8px breathing room, what the rule
605 * hardcoded before the work area existed, in force until the shell
606 * has measured once.
607 */
608 .os-area {
609 flex: 1;
610 position: relative;
611 overflow: hidden;
612 background: transparent;
613 padding-top: var( --os-work-area-inset-top, 0px );
614 padding-right: var( --os-work-area-inset-right, 0px );
615 padding-bottom: var( --os-work-area-inset-bottom, 80px );
616 padding-left: var( --os-work-area-inset-left, 0px );
617 transition: opacity 180ms ease;
618 }
619
620 /*
621 * Boot reveal gate.
622 *
623 * On F5 the shell paints in stages: the dispatcher's
624 * `repaintIcons()` lands the server-rendered wallpaper icons
625 * synchronously from `openStationConfig.desktopIcons`, then the
626 * files-layer mounts and momentarily renders an empty bucket while
627 * REST `listPlacements(0)` is in flight, then folders/posts/links
628 * slot in on the next paint. The user reads that as "plugins show
629 * first, then everything blinks and the rest arrives."
630 *
631 * The `--booting` modifier is applied BY PHP (see
632 * `includes/render/shell.php`) on the initial HTML so the area
633 * starts invisible BEFORE any JS runs — otherwise the dispatcher
634 * gets to paint the wallpaper icons into a visible area first and
635 * the user sees them flash before our gate kicks in.
636 *
637 * `desktop.ts` removes the class inside a `requestAnimationFrame`
638 * after the root files-layer's `hydrated` promise resolves (with a
639 * 2 s JS safety timeout). The opacity transition declared on
640 * `.os-area` smooths the reveal.
641 *
642 * The fallback animation is a resilience layer for the case where
643 * the shell JS never runs (network error mid-load, ad-blocker
644 * tearing down a bundle, …). The animation holds the area
645 * invisible for 2.7 s then flips it to opacity 1; the
646 * `animation-fill-mode: forwards` keeps the revealed state held
647 * after the animation ends. In the normal case JS removes the
648 * class long before 2.7 s and the animation is discarded.
649 *
650 * `pointer-events: none` while booting prevents a stray click on
651 * the still-hidden surface from triggering Show-Desktop or opening
652 * a tile that's about to reposition once REST returns.
653 *
654 * @since 0.18.x
655 */
656 .os-area--booting {
657 opacity: 0;
658 pointer-events: none;
659 animation: os-area-boot-fallback 3s forwards;
660 }
661
662 @keyframes os-area-boot-fallback {
663 0%,
664 90% {
665 opacity: 0;
666 pointer-events: none;
667 }
668 100% {
669 opacity: 1;
670 pointer-events: auto;
671 }
672 }
673
674 /* ---------------------------------------------------------------
675 * Desktop icons — wallpaper shortcut tiles registered via the
676 * `wp_register_desktop_icon()` PHP API. Rendered inside the
677 * desktop area as the first child after the wallpaper, so they
678 * paint above the wallpaper but beneath any window.
679 *
680 * Grid layout is fixed-width columns; tiles wrap onto multiple
681 * rows when the desktop is wide enough to host more than one
682 * column's worth. `pointer-events: none` on the container keeps
683 * the background clickable (for "minimize all" on wallpaper
684 * click) while individual tiles re-enable pointer events so they
685 * remain clickable.
686 * --------------------------------------------------------------- */
687
688 .os-icons {
689 position: absolute;
690 /*
691 * 16px of gutter inside the WORK AREA, not inside the desktop
692 * area: an absolutely positioned box ignores its parent's
693 * padding, so without the insets the grid's last row sat under
694 * the bottom dock pill. Same tokens `.os-area` reserves with.
695 *
696 * Physical `left` / `right`, not the logical pair: the insets are
697 * measured from rects, so `-left` is the visual left whichever
698 * way the locale reads. The gutter is 16px on both sides, so
699 * nothing is lost by going physical here.
700 */
701 top: calc( var( --os-work-area-inset-top, 0px ) + 16px );
702 bottom: calc( var( --os-work-area-inset-bottom, 80px ) + 16px );
703 left: calc( var( --os-work-area-inset-left, 0px ) + 16px );
704 right: calc( var( --os-work-area-inset-right, 0px ) + 16px );
705 display: grid;
706 /*
707 * Column-major flow: icons fill the first column top-to-bottom,
708 * then start the next column. Matches the macOS / Finder
709 * convention. `grid-auto-flow: column` plus `auto-fill` rows
710 * (sized to the container height) yields exactly that layout.
711 *
712 * All four insets are pinned so the container has a definite
713 * height — `repeat(auto-fill, 96px)` needs that to compute the
714 * row count. 96px is one icon button (48px image + 6px gap +
715 * up-to-two-line label + 8px vertical padding); a slightly
716 * generous figure leaves room for descenders without clipping.
717 */
718 grid-auto-flow: column;
719 grid-template-rows: repeat(auto-fill, 96px);
720 grid-auto-columns: 88px;
721 align-content: start;
722 justify-content: start;
723 gap: 12px;
724 z-index: 1;
725 pointer-events: none;
726 /*
727 * Spill protection: if a user ever pins more icons than fit
728 * vertically, the grid scrolls horizontally so the unreachable
729 * tiles can be reached without clipping the visible ones.
730 * Individual tiles re-enable pointer events so they stay
731 * clickable inside the scroll surface.
732 */
733 overflow: auto;
734 }
735
736 .os-icon {
737 pointer-events: auto;
738 display: flex;
739 flex-direction: column;
740 align-items: center;
741 justify-content: flex-start;
742 gap: 6px;
743 width: 88px;
744 padding: 8px 4px;
745 border: 0;
746 background-color: transparent;
747 /*
748 * ICON_TILE texture slot — a plate behind each wallpaper icon.
749 * Painted under the icon image and its label, so a theme can give
750 * desktop shortcuts a physical tile the way a phone home screen
751 * does. Unset = `none` = the transparent tile we always had.
752 */
753 background-image: var(--os-tile-image, none);
754 background-repeat: var(--os-tile-image-repeat, no-repeat);
755 background-size: var(--os-tile-image-size, auto);
756 background-position: var(--os-tile-image-position, center);
757 color: var(--os-fg, #fff);
758 cursor: pointer;
759 border-radius: 8px;
760 transition: background-color 0.15s ease;
761 /* Positioning context for `.os-icon__badge`. */
762 position: relative;
763 }
764
765 /*
766 * Icon badge — symmetric to `.os-dock__badge` but anchored
767 * to the wallpaper-icon image. Same gradient, same ring, slightly
768 * larger so it reads at the longer wallpaper viewing distance.
769 * Painted by `wp.os.icons.setBadge( id, count )` — see
770 * `src/desktop-icons.ts` for the imperative API and
771 * `src/recycle-bin/badge.ts` for the canonical multi-rail
772 * consumer. The class name is part of the stable contract; the
773 * positioning, color, and shadow are not — themes or future
774 * shell builds may restyle freely.
775 */
776 .os-icon__badge {
777 position: absolute;
778 top: 4px;
779 inset-inline-end: 14px;
780 /* Same derivation as the dock badge, sized off the wallpaper icon
781 * rather than the dock icon. The default resolves to 18px / 11px —
782 * exactly the old values. */
783 --os-icon-badge-computed-size: var(
784 --os-icon-badge-size,
785 calc( var( --os-icon-image-size, 48px ) * 0.375 )
786 );
787 min-width: var( --os-icon-badge-computed-size );
788 height: var( --os-icon-badge-computed-size );
789 padding: 0 var(
790 --os-icon-badge-padding,
791 calc( var( --os-icon-badge-computed-size ) * 0.28 )
792 );
793 box-sizing: border-box;
794 border-radius: 999px;
795 background: var(
796 --os-icon-badge-bg,
797 linear-gradient( 180deg, #ff5a5a 0%, var( --os-ui-danger, #d63638 ) 100% )
798 );
799 color: var( --os-icon-badge-fg, var( --os-ui-fg-on-accent, #fff ) );
800 display: inline-flex;
801 align-items: center;
802 justify-content: center;
803 font-size: var(
804 --os-icon-badge-font-size,
805 calc( var( --os-icon-badge-computed-size ) * 0.611 )
806 );
807 font-weight: var( --os-icon-badge-font-weight, 700 );
808 line-height: 1;
809 font-variant-numeric: tabular-nums;
810 box-shadow:
811 0 0 0 1.5px rgba( 0, 0, 0, 0.4 ),
812 0 1px 3px rgba( 0, 0, 0, 0.35 );
813 pointer-events: none;
814 }
815
816 .os-icon:hover,
817 .os-icon:focus-visible {
818 /* background-COLOR, not the shorthand: the shorthand would reset
819 * the ICON_TILE texture declared above, so a themed tile would
820 * vanish on hover. */
821 background-color: rgba(255, 255, 255, 0.12);
822 outline: none;
823 }
824
825 .os-icon:focus-visible {
826 box-shadow: 0 0 0 2px var(--wp-admin-theme-color, #2271b1);
827 }
828
829 .os-icon__image {
830 display: inline-flex;
831 align-items: center;
832 justify-content: center;
833 /* Tokenized so a theme can scale wallpaper icons — and so the
834 * badge above has something to derive its own size from. */
835 width: var( --os-icon-image-size, 48px );
836 height: var( --os-icon-image-size, 48px );
837 font-size: calc( var( --os-icon-image-size, 48px ) * 0.667 );
838 line-height: 1;
839 }
840
841 .os-icon__image img {
842 max-width: 48px;
843 max-height: 48px;
844 object-fit: contain;
845 }
846
847 .os-icon__label {
848 font-size: 12px;
849 text-align: center;
850 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
851 line-height: 1.2;
852 word-break: break-word;
853 }
854
855 /* ---------------------------------------------------------------
856 * Widgets column — right-edge glass strip that paints above the
857 * wallpaper but beneath windows. Hosts stacked widget cards and a
858 * trailing "Add widget" tile. Interactive only on its own cards +
859 * the add tile; every other pixel is pointer-transparent so window
860 * drag / resize / click-through behaviour is unaffected.
861 * --------------------------------------------------------------- */
862
863 .os-widgets {
864 border-radius: 14px;
865 position: absolute;
866 /*
867 * Inside the work area vertically — see `.os-icons` for why the
868 * insets are here. The inline edge stays a plain 16px: the column
869 * hugs the trailing side, which is a logical choice, while the
870 * side insets are physical; and a side rail never claims one
871 * anyway (it is a flex sibling, the area is already narrower).
872 */
873 top: calc( var( --os-work-area-inset-top, 0px ) + 16px );
874 bottom: calc( var( --os-work-area-inset-bottom, 80px ) + 16px );
875 inset-inline-end: 16px;
876 width: 320px;
877 display: flex;
878 flex-direction: column;
879 gap: 12px;
880 z-index: 1;
881 pointer-events: none;
882 overflow-y: auto;
883 /* Hide the scrollbar chrome unless the user actually overflows —
884 * most setups never will. */
885 scrollbar-width: thin;
886 /* Fade alongside the overview backdrop. The class flip that
887 * drives this is on the parent area, set at t=0 of enter and
888 * removed at t=0 of exit, so widgets fade in parallel with the
889 * dock collapse / return — never sequential. */
890 opacity: 1;
891 transition:
892 opacity 0.22s ease,
893 transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
894 }
895
896 .os-area--overview .os-widgets {
897 opacity: 0;
898 /* Slight slide-off-right so the fade reads as "the column is
899 * stepping back out of the way" rather than flat-dimming. */
900 transform: translateX( 16px );
901 pointer-events: none;
902 }
903
904 .os-widgets__list {
905 display: flex;
906 flex-direction: column;
907 gap: 12px;
908 padding: 4px;
909 }
910
911 /*
912 * Individual widget card — glass backdrop matching the dock /
913 * overview top bar. Card root is pointer-events: auto so clicks on
914 * content + the remove X land; the column itself stays
915 * pointer-transparent so a drag that grazes the column's margin
916 * falls through to the window beneath.
917 */
918 .os-widgets__card {
919 position: relative;
920 padding: 0;
921 /* Column layout so the body can flex-shrink and scroll when the
922 * frame writes a fixed inline height (floating / resized cards).
923 * Without this the body keeps its natural height and content
924 * spills past the card's rounded bottom edge. */
925 display: flex;
926 flex-direction: column;
927 pointer-events: auto;
928 /*
929 * The glass is also `--os-ui-color-surface`, the one name in the
930 * widget contract that describes the card itself. Declared in
931 * `variables.css` so a widget reading it and the card painting it
932 * cannot drift apart.
933 */
934 background-color: var( --os-ui-color-surface, rgba( 20, 20, 22, 0.55 ) );
935 /*
936 * WIDGET texture slot — layered over the frosted card colour, so a
937 * translucent texture still gets the blur underneath.
938 */
939 background-image: var( --os-widget-image, none );
940 background-repeat: var( --os-widget-image-repeat, repeat );
941 background-size: var( --os-widget-image-size, auto );
942 background-position: var( --os-widget-image-position, center );
943 backdrop-filter: blur( 18px ) saturate( 140% );
944 -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
945 border: 1px solid rgba( 255, 255, 255, 0.08 );
946 border-radius: 14px;
947 color: var( --os-ui-fg-on-accent, #fff );
948 box-shadow:
949 0 8px 28px rgba( 0, 0, 0, 0.35 ),
950 inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
951 transition:
952 box-shadow 0.18s ease,
953 opacity 0.18s ease;
954 }
955
956 /* Hover feedback is shadow-only — no transform. Cards must feel
957 * anchored; a hover nudge reads as the widget drifting under the
958 * pointer (and fought the user's chosen position on floating cards). */
959 .os-widgets__card:hover {
960 box-shadow:
961 0 12px 36px rgba( 0, 0, 0, 0.45 ),
962 inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
963 }
964
965 .os-widgets__card-body {
966 padding: 16px;
967 min-height: 48px;
968 flex: 1 1 auto;
969 overflow-x: hidden;
970 overflow-y: auto;
971 scrollbar-width: thin;
972 }
973
974 .os-widgets__card-close {
975 position: absolute;
976 top: 6px;
977 inset-inline-end: 6px;
978 width: 22px;
979 height: 22px;
980 display: flex;
981 align-items: center;
982 justify-content: center;
983 padding: 0;
984 border: 0;
985 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
986 color: rgba( 255, 255, 255, 0.75 );
987 border-radius: 50%;
988 cursor: pointer;
989 opacity: 0;
990 transition:
991 opacity 0.15s ease,
992 background-color 0.15s ease,
993 color 0.15s ease;
994 }
995
996 .os-widgets__card:hover .os-widgets__card-close,
997 .os-widgets__card-close:focus-visible {
998 opacity: 1;
999 }
1000
1001 .os-widgets__card-close:hover {
1002 background: var( --os-ui-danger, #d63638 );
1003 color: var( --os-ui-fg-on-accent, #fff );
1004 }
1005
1006 /*
1007 * Movable widgets — a thin chrome header at the top of the card holds
1008 * the drag grip, the widget's own label, and the × button. The whole
1009 * header is the drag handle (except for interactive children — those
1010 * remain clickable as normal). Non-movable widgets keep today's
1011 * corner-× layout unchanged.
1012 */
1013 .os-widgets__chrome {
1014 display: flex;
1015 flex-shrink: 0;
1016 align-items: center;
1017 gap: 8px;
1018 padding: 6px 8px;
1019 border-bottom: 1px solid rgba( 255, 255, 255, 0.06 );
1020 cursor: grab;
1021 user-select: none;
1022 -webkit-user-select: none;
1023 touch-action: none;
1024 }
1025
1026 .os-widgets__card--dragging .os-widgets__chrome {
1027 cursor: grabbing;
1028 }
1029
1030 .os-widgets__grip {
1031 flex-shrink: 0;
1032 width: 10px;
1033 height: 16px;
1034 background-image: radial-gradient(
1035 circle,
1036 rgba( 255, 255, 255, 0.55 ) 1.2px,
1037 transparent 1.5px
1038 );
1039 background-size: 5px 5px;
1040 background-position: 0 1px;
1041 background-repeat: space;
1042 opacity: 0.45;
1043 transition: opacity 0.12s ease;
1044 }
1045
1046 .os-widgets__card:hover .os-widgets__grip {
1047 opacity: 0.85;
1048 }
1049
1050 .os-widgets__title {
1051 flex: 1;
1052 font-size: 12px;
1053 font-weight: 500;
1054 letter-spacing: 0.01em;
1055 color: rgba( 255, 255, 255, 0.85 );
1056 overflow: hidden;
1057 text-overflow: ellipsis;
1058 white-space: nowrap;
1059 }
1060
1061 /* When the close button sits inside the chrome it's inline, not
1062 corner-absolute — reset position + always-visible opacity. */
1063 .os-widgets__chrome .os-widgets__card-close {
1064 position: static;
1065 opacity: 0.7;
1066 width: 20px;
1067 height: 20px;
1068 }
1069
1070 .os-widgets__chrome .os-widgets__card-close:hover,
1071 .os-widgets__chrome .os-widgets__card-close:focus-visible {
1072 opacity: 1;
1073 }
1074
1075 /*
1076 * Re-dock button — sits in the chrome next to the close button and
1077 * returns a floating widget to the right column. Visibility is
1078 * gated on the `--floating` modifier class so docked widgets never
1079 * expose it (tapping it on a docked card would be a confusing
1080 * no-op). The DOM node stays mounted either way so state flips
1081 * don't rebuild chrome.
1082 *
1083 * Styling mirrors the inline close button: 20×20 round tile,
1084 * dimmed by default, full-opacity on hover / focus. Distinct hover
1085 * color (theme accent, not red) signals "put back" vs. "remove."
1086 */
1087 .os-widgets__card-redock {
1088 display: none;
1089 align-items: center;
1090 justify-content: center;
1091 padding: 0;
1092 border: 0;
1093 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
1094 color: rgba( 255, 255, 255, 0.75 );
1095 border-radius: 50%;
1096 width: 20px;
1097 height: 20px;
1098 cursor: pointer;
1099 opacity: 0.7;
1100 transition:
1101 opacity 0.15s ease,
1102 background-color 0.15s ease,
1103 color 0.15s ease;
1104 }
1105
1106 .os-widgets__card--floating .os-widgets__card-redock {
1107 display: flex;
1108 }
1109
1110 .os-widgets__card-redock:hover,
1111 .os-widgets__card-redock:focus-visible {
1112 opacity: 1;
1113 background: var( --wp-admin-theme-color, #2271b1 );
1114 color: var( --os-ui-fg-on-accent, #fff );
1115 }
1116
1117 /*
1118 * Floating widgets — absolute-positioned, inline top/left/width/height
1119 * written by the frame.
1120 */
1121 .os-widgets__card--floating {
1122 position: absolute;
1123 margin: 0;
1124 }
1125
1126 /*
1127 * A liberated card is reparented to the desktop area, so the column's
1128 * overview fade above no longer reaches it. Direct children only: a
1129 * card inside a window body belongs to that window's thumbnail.
1130 */
1131 .os-area--overview > .os-widgets__card--floating {
1132 opacity: 0;
1133 pointer-events: none;
1134 }
1135
1136 .os-widgets__card--dragging,
1137 .os-widgets__card--resizing {
1138 transition: none;
1139 z-index: 2;
1140 }
1141
1142 /*
1143 * Resize handles — 8 in all, with the 4 edges as hair-line strips and
1144 * the 4 corners as small squares. Invisible by default; cursor change
1145 * telegraphs them on hover. The frame attaches pointer listeners to
1146 * every handle; a non-movable widget ignores everything except the
1147 * `s` (bottom-edge, height-only) handle at the pointer layer, so we
1148 * visually hide the mismatched handles via the modifier class.
1149 */
1150 .os-widgets__resize {
1151 position: absolute;
1152 z-index: 1;
1153 }
1154
1155 .os-widgets__resize--n {
1156 top: -3px;
1157 left: 8px;
1158 right: 8px;
1159 height: 6px;
1160 cursor: ns-resize;
1161 }
1162
1163 .os-widgets__resize--s {
1164 bottom: -3px;
1165 left: 8px;
1166 right: 8px;
1167 height: 6px;
1168 cursor: ns-resize;
1169 }
1170
1171 .os-widgets__resize--e {
1172 top: 8px;
1173 bottom: 8px;
1174 right: -3px;
1175 width: 6px;
1176 cursor: ew-resize;
1177 }
1178
1179 .os-widgets__resize--w {
1180 top: 8px;
1181 bottom: 8px;
1182 left: -3px;
1183 width: 6px;
1184 cursor: ew-resize;
1185 }
1186
1187 .os-widgets__resize--ne {
1188 top: -4px;
1189 right: -4px;
1190 width: 12px;
1191 height: 12px;
1192 cursor: nesw-resize;
1193 }
1194
1195 .os-widgets__resize--nw {
1196 top: -4px;
1197 left: -4px;
1198 width: 12px;
1199 height: 12px;
1200 cursor: nwse-resize;
1201 }
1202
1203 .os-widgets__resize--se {
1204 bottom: -4px;
1205 right: -4px;
1206 width: 12px;
1207 height: 12px;
1208 cursor: nwse-resize;
1209 }
1210
1211 .os-widgets__resize--sw {
1212 bottom: -4px;
1213 left: -4px;
1214 width: 12px;
1215 height: 12px;
1216 cursor: nesw-resize;
1217 }
1218
1219 /*
1220 * Only floating cards expose the full 8-direction handle set. A
1221 * column-docked card — non-movable OR movable-but-not-yet-liberated —
1222 * resizes on the height axis alone (the frame ignores every other
1223 * direction while docked), so hide all handles except the bottom
1224 * edge and the cursor never lies about what's interactive.
1225 */
1226 .os-widgets__card--resizable:not( .os-widgets__card--floating ) .os-widgets__resize:not( .os-widgets__resize--s ) {
1227 display: none;
1228 }
1229
1230 /*
1231 * Add-widget tile — a compact pill that trails the widget stack, not
1232 * a full-width drop zone. Hidden until the pointer comes near the
1233 * column (the layer toggles `--hovered`), so the desktop stays clean
1234 * for people who never touch widgets.
1235 *
1236 * Absolute rather than in flow because "the bottom of the stack" is
1237 * not something the column's layout knows: a widget dragged out of
1238 * the column becomes a floating card parented to the desktop, so it
1239 * leaves the flex flow while still sitting visually in the column.
1240 * The layer measures those too and writes `top` — see
1241 * `positionAddTile()`.
1242 */
1243 .os-widgets__add {
1244 position: absolute;
1245 top: 0;
1246 left: 50%;
1247 transform: translateX( -50% );
1248 padding: 8px 22px;
1249 display: flex;
1250 align-items: center;
1251 justify-content: center;
1252 gap: 7px;
1253 pointer-events: none;
1254 background: transparent;
1255 border: 1px dashed rgba( 255, 255, 255, 0.22 );
1256 border-radius: 10px;
1257 color: rgba( 255, 255, 255, 0.72 );
1258 font: inherit;
1259 font-weight: 500;
1260 cursor: pointer;
1261 opacity: 0;
1262 transition:
1263 opacity 0.15s ease,
1264 border-color 0.15s ease,
1265 background-color 0.15s ease,
1266 color 0.15s ease;
1267 }
1268
1269 /*
1270 * Reveal conditions. An empty column is no exception — approaching
1271 * the right side is the gesture, whether or not there's a card there
1272 * to aim at. `:focus-visible` keeps it reachable by keyboard, where
1273 * there is no pointer to bring near.
1274 */
1275 .os-widgets--hovered .os-widgets__add,
1276 .os-widgets--picking .os-widgets__add,
1277 .os-widgets__add:focus-visible {
1278 opacity: 1;
1279 pointer-events: auto;
1280 }
1281
1282 /*
1283 * Touch has no hover to approach the column with. The wallpaper's
1284 * right-click menu carries an "Add widget" entry, but a long-press
1285 * to reach it is a lot to ask of someone who just wants a clock, so
1286 * on those devices the pill stays put.
1287 */
1288 @media ( hover: none ) {
1289 .os-widgets__add {
1290 opacity: 1;
1291 pointer-events: auto;
1292 }
1293 }
1294
1295 .os-widgets__add:hover {
1296 border-color: rgba( 255, 255, 255, 0.4 );
1297 background: rgba( 255, 255, 255, 0.06 );
1298 color: var( --os-ui-fg-on-accent, #fff );
1299 }
1300
1301 .os-widgets__add:focus-visible {
1302 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
1303 outline-offset: 2px;
1304 }
1305
1306 .os-widgets__add-plus {
1307 font-size: 15px;
1308 line-height: 1;
1309 font-weight: 300;
1310 }
1311
1312 .os-widgets__add-label {
1313 font-size: 13px;
1314 letter-spacing: 0.02em;
1315 }
1316
1317 /* Built-in clock widget */
1318 .os-widget-clock {
1319 text-align: center;
1320 /* The time + date are passive glyphs, not content users would
1321 * ever reasonably select — turning it off avoids the accidental-
1322 * highlight mess that happens when a drag starts on the clock. */
1323 user-select: none;
1324 -webkit-user-select: none;
1325 }
1326
1327 .os-widget-clock__time {
1328 font-size: 34px;
1329 font-weight: 600;
1330 line-height: 1.1;
1331 font-variant-numeric: tabular-nums;
1332 letter-spacing: 0.01em;
1333 }
1334
1335 .os-widget-clock__date {
1336 margin-top: 4px;
1337 font-size: 13px;
1338 color: rgba( 255, 255, 255, 0.75 );
1339 letter-spacing: 0.02em;
1340 }
1341
1342 /* ---------------------------------------------------------------
1343 * Widget picker popover — opens from the add tile, lists every
1344 * registered widget. Positioned fixed so it can extend outside
1345 * the desktop-area clip.
1346 * --------------------------------------------------------------- */
1347
1348 .os-widget-picker {
1349 position: fixed;
1350 width: 320px;
1351 max-height: 60vh;
1352 overflow-y: auto;
1353 padding: 10px;
1354 border-radius: 14px;
1355 background: rgba( 20, 20, 22, 0.88 );
1356 backdrop-filter: blur( 22px ) saturate( 160% );
1357 -webkit-backdrop-filter: blur( 22px ) saturate( 160% );
1358 border: 1px solid rgba( 255, 255, 255, 0.1 );
1359 box-shadow: 0 18px 54px rgba( 0, 0, 0, 0.55 );
1360 color: var( --os-ui-fg-on-accent, #fff );
1361 z-index: 9999;
1362 animation: os-widget-picker-in 0.14s ease-out;
1363 }
1364
1365 @keyframes os-widget-picker-in {
1366 from {
1367 opacity: 0;
1368 transform: translateY( 4px );
1369 }
1370 }
1371
1372 .os-widget-picker__title {
1373 padding: 4px 8px 8px;
1374 font-size: 11px;
1375 font-weight: 600;
1376 letter-spacing: 0.08em;
1377 text-transform: uppercase;
1378 color: rgba( 255, 255, 255, 0.55 );
1379 }
1380
1381 .os-widget-picker__list {
1382 display: flex;
1383 flex-direction: column;
1384 gap: 4px;
1385 }
1386
1387 .os-widget-picker__entry {
1388 display: flex;
1389 align-items: center;
1390 gap: 12px;
1391 padding: 10px;
1392 background: transparent;
1393 border: 0;
1394 border-radius: 10px;
1395 color: var( --os-ui-fg-on-accent, #fff );
1396 cursor: pointer;
1397 text-align: start;
1398 font: inherit;
1399 transition: background-color 0.12s ease;
1400 }
1401
1402 .os-widget-picker__entry:hover:not( :disabled ) {
1403 background: rgba( 255, 255, 255, 0.08 );
1404 }
1405
1406 .os-widget-picker__entry:focus-visible {
1407 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
1408 outline-offset: 1px;
1409 }
1410
1411 .os-widget-picker__entry--added {
1412 opacity: 0.55;
1413 cursor: default;
1414 }
1415
1416 .os-widget-picker__entry-icon {
1417 flex-shrink: 0;
1418 font-size: 20px;
1419 width: 20px;
1420 height: 20px;
1421 line-height: 1;
1422 }
1423
1424 .os-widget-picker__entry-text {
1425 display: flex;
1426 flex-direction: column;
1427 gap: 2px;
1428 flex: 1;
1429 min-width: 0;
1430 }
1431
1432 .os-widget-picker__entry-label {
1433 font-size: 13px;
1434 font-weight: 500;
1435 white-space: nowrap;
1436 overflow: hidden;
1437 text-overflow: ellipsis;
1438 }
1439
1440 .os-widget-picker__entry-description {
1441 font-size: 11px;
1442 color: rgba( 255, 255, 255, 0.6 );
1443 line-height: 1.35;
1444 white-space: normal;
1445 }
1446
1447 .os-widget-picker__entry-status {
1448 flex-shrink: 0;
1449 font-size: 11px;
1450 color: rgba( 255, 255, 255, 0.5 );
1451 text-transform: uppercase;
1452 letter-spacing: 0.08em;
1453 }
1454
1455 .os-widget-picker__empty {
1456 padding: 12px 8px;
1457 font-size: 12px;
1458 color: rgba( 255, 255, 255, 0.6 );
1459 line-height: 1.4;
1460 }
1461
1462 /**
1463 * Chromeless mode: pages loaded inside iframes.
1464 * Remove all shell chrome and let content fill the frame.
1465 */
1466 .os-chromeless {
1467 margin: 0;
1468 padding: 0;
1469 background: var(--os-window-bg);
1470 }
1471
1472 /*
1473 * Chromeless #wpbody-content, #adminmenuwrap, #wpfooter overrides are in
1474 * chromeless.css to keep all legacy page tweaks in one place.
1475 */
1476
1477 /*
1478 * Hide WordPress core's built-in command palette (`@wordpress/commands`).
1479 * OpenStation force-enqueues `wp_enqueue_command_palette_assets()` so
1480 * the `core/commands` data store is populated for our shell harvester
1481 * (see `src/commands/shell-harvester.ts`); the side effect is that WP
1482 * also mounts its own `<CommandPalette>` to the document body. We
1483 * already block its Cmd+K handler in `src/palette-registry.ts`, but if
1484 * a third-party script opens it programmatically (`wp.data.dispatch(
1485 * 'core/commands' ).open()`), the dialog would float over the desktop
1486 * with admin-menu navigation callbacks whose `document.location = url`
1487 * would unload the shell. Keep it permanently hidden.
1488 */
1489 .commands-command-menu,
1490 .commands-command-menu__overlay {
1491 display: none !important;
1492 }
1493
1494 /*
1495 * ---- Links slotted into <os-notice> ---------------------------
1496 *
1497 * `<os-notice>` styles its slotted links from inside its shadow root,
1498 * through `::slotted( a )` and the `--os-ui-notice-link` tokens. That
1499 * rule cannot win here, and no amount of specificity on it would help.
1500 *
1501 * A slotted element lives in the DOCUMENT tree; the shadow root only
1502 * borrows it for rendering. When declarations from two trees collide,
1503 * CSS Scoping resolves normal (non-`!important`) ones in favour of the
1504 * OUTER tree regardless of specificity — so `wp-admin/css/common.css`,
1505 * which sets `a { color: #2271b1 }` and `a:hover { color: #135e96 }` on
1506 * every anchor in the admin, beats `::slotted( a )` every time. The
1507 * shell renders inside wp-admin, so it always loses: the link painted
1508 * WordPress Blue at rest and a darker blue on hover, the second of
1509 * which is close to illegible on a notice's dark wash.
1510 *
1511 * Hence a document-tree rule. It reads the same tokens the component
1512 * documents, so a theme still restyles the link by setting them; this
1513 * only moves the declaration into a tree that can win. `a:hover` is
1514 * (0,1,1), so the hover selector has to carry its own pseudo-class to
1515 * outrank it — matching `os-notice a` alone would lose the hover state
1516 * while winning the base one, which is exactly the inert-looking link
1517 * the component's own styles already went out of their way to avoid.
1518 */
1519 os-notice a {
1520 color: var(--os-ui-notice-link, #ec9bff);
1521 }
1522
1523 os-notice a:hover,
1524 os-notice a:focus,
1525 os-notice a:active {
1526 color: var(--os-ui-notice-link-hover, #fffbff);
1527 }
1528
1529 /* --------------------------------------------------------------------
1530 * The workspace hop — cross-document view transitions between shells.
1531 *
1532 * Loaded only on the SHELL screen (this sheet never reaches chromeless
1533 * iframes), so only shell→shell navigations transition: hopping between
1534 * a site's desktop and the network admin's crossfades the two desktops
1535 * instead of hard-cutting, which is what makes the per-admin sessions
1536 * read as workspaces. Classic admin pages never opt in, so entering or
1537 * leaving the shell stays a plain navigation, and reloads are excluded
1538 * by the spec. Browsers without cross-document view transitions ignore
1539 * all of this and navigate plainly. See docs/multisite.md.
1540 */
1541 @view-transition {
1542 navigation: auto;
1543 }
1544
1545 /* Reduced motion swaps instantly — the hop still happens, the
1546 * animation does not. */
1547 @media ( prefers-reduced-motion: reduce ) {
1548 @view-transition {
1549 navigation: none;
1550 }
1551 }
1552
1553 /* A gentle zoom-through: the desktop being left recedes as the one
1554 * arriving settles in. Subtle on purpose — this runs on a full page
1555 * swap, and anything louder reads as a reload effect. */
1556 ::view-transition-old(root) {
1557 animation: os-workspace-hop-out 220ms cubic-bezier( 0.4, 0, 1, 1 ) both;
1558 }
1559
1560 ::view-transition-new(root) {
1561 animation: os-workspace-hop-in 280ms cubic-bezier( 0, 0, 0.2, 1 ) both;
1562 }
1563
1564 @keyframes os-workspace-hop-out {
1565 to {
1566 opacity: 0;
1567 transform: scale( 0.985 );
1568 }
1569 }
1570
1571 @keyframes os-workspace-hop-in {
1572 from {
1573 opacity: 0;
1574 transform: scale( 1.015 );
1575 }
1576 }
1577
1578 /* The instance hop's two halves, on either side of that page swap.
1579 * The switcher slides this desk out towards the site it picked
1580 * (`os-shell--hop-out-next` / `-prev`, for the beat before it
1581 * navigates), and a shell asked to boot into overview arrives with its
1582 * desk hidden (`os-shell--arriving`, stamped server-side) until overview
1583 * is up, then slides it in from the same side (`-next` / `-prev`, the
1584 * hint the switcher left in sessionStorage; no hint, a plain fade). The
1585 * wallpaper never moves, which is what makes it read as the tiles
1586 * changing rather than the page. `src/multisite/instance-transition.ts`
1587 * drives the classes. */
1588 .os-shell--hop-out-next #os-area,
1589 .os-shell--hop-out-prev #os-area {
1590 opacity: 0;
1591 transition:
1592 opacity 220ms cubic-bezier( 0.4, 0, 1, 1 ),
1593 transform 220ms cubic-bezier( 0.4, 0, 1, 1 );
1594 }
1595
1596 .os-shell--hop-out-next #os-area {
1597 transform: translateX( -48px );
1598 }
1599
1600 .os-shell--hop-out-prev #os-area {
1601 transform: translateX( 48px );
1602 }
1603
1604 .os-shell--hop-out-next .os-dock,
1605 .os-shell--hop-out-prev .os-dock,
1606 .os-shell--arriving .os-dock {
1607 opacity: 0;
1608 transition: opacity 220ms ease;
1609 }
1610
1611 .os-shell--arriving #os-area {
1612 opacity: 0;
1613 transition: none;
1614 }
1615
1616 .os-shell--arriving-next #os-area {
1617 transform: translateX( 48px );
1618 }
1619
1620 .os-shell--arriving-prev #os-area {
1621 transform: translateX( -48px );
1622 }
1623
1624 /* The reveal: the arriving classes come off and this goes on in the
1625 * same frame, so the desk transitions from hidden to its resting
1626 * place. */
1627 .os-shell--revealing #os-area {
1628 transition:
1629 opacity 360ms cubic-bezier( 0, 0, 0.2, 1 ),
1630 transform 360ms cubic-bezier( 0, 0, 0.2, 1 );
1631 }
1632
1633 .os-shell--revealing .os-dock {
1634 transition: opacity 360ms ease;
1635 }
1636
1637 /* A desk must never stay hidden: if the boot that reveals it never
1638 * runs, the area and the dock come back on their own a few seconds
1639 * in. */
1640 @keyframes os-instance-arrive-fallback {
1641 to {
1642 opacity: 1;
1643 transform: none;
1644 }
1645 }
1646
1647 .os-shell--arriving #os-area,
1648 .os-shell--arriving .os-dock {
1649 animation: os-instance-arrive-fallback 1ms linear 6s forwards;
1650 }
1651
1652 @media ( prefers-reduced-motion: reduce ) {
1653 .os-shell--hop-out-next #os-area,
1654 .os-shell--hop-out-prev #os-area,
1655 .os-shell--arriving-next #os-area,
1656 .os-shell--arriving-prev #os-area {
1657 transform: none;
1658 }
1659
1660 .os-shell--hop-out-next #os-area,
1661 .os-shell--hop-out-prev #os-area,
1662 .os-shell--hop-out-next .os-dock,
1663 .os-shell--hop-out-prev .os-dock,
1664 .os-shell--revealing #os-area,
1665 .os-shell--revealing .os-dock {
1666 transition: none;
1667 }
1668 }
1669