PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.3
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.3
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.3, at assets/css/desktop.css

1,495 lines 45.5 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 * `padding-bottom` reserves room for the floating bottom dock pill
599 * that every layout (Classic, Unified, Spatial) puts at the bottom
600 * — 40px icons + 16px padding + 12px gap below + 8px breathing room.
601 */
602 .os-area {
603 flex: 1;
604 position: relative;
605 overflow: hidden;
606 background: transparent;
607 padding-bottom: 80px;
608 transition: opacity 180ms ease;
609 }
610
611 /*
612 * Boot reveal gate.
613 *
614 * On F5 the shell paints in stages: the dispatcher's
615 * `repaintIcons()` lands the server-rendered wallpaper icons
616 * synchronously from `openStationConfig.desktopIcons`, then the
617 * files-layer mounts and momentarily renders an empty bucket while
618 * REST `listPlacements(0)` is in flight, then folders/posts/links
619 * slot in on the next paint. The user reads that as "plugins show
620 * first, then everything blinks and the rest arrives."
621 *
622 * The `--booting` modifier is applied BY PHP (see
623 * `includes/render/shell.php`) on the initial HTML so the area
624 * starts invisible BEFORE any JS runs — otherwise the dispatcher
625 * gets to paint the wallpaper icons into a visible area first and
626 * the user sees them flash before our gate kicks in.
627 *
628 * `desktop.ts` removes the class inside a `requestAnimationFrame`
629 * after the root files-layer's `hydrated` promise resolves (with a
630 * 2 s JS safety timeout). The opacity transition declared on
631 * `.os-area` smooths the reveal.
632 *
633 * The fallback animation is a resilience layer for the case where
634 * the shell JS never runs (network error mid-load, ad-blocker
635 * tearing down a bundle, …). The animation holds the area
636 * invisible for 2.7 s then flips it to opacity 1; the
637 * `animation-fill-mode: forwards` keeps the revealed state held
638 * after the animation ends. In the normal case JS removes the
639 * class long before 2.7 s and the animation is discarded.
640 *
641 * `pointer-events: none` while booting prevents a stray click on
642 * the still-hidden surface from triggering Show-Desktop or opening
643 * a tile that's about to reposition once REST returns.
644 *
645 * @since 0.18.x
646 */
647 .os-area--booting {
648 opacity: 0;
649 pointer-events: none;
650 animation: os-area-boot-fallback 3s forwards;
651 }
652
653 @keyframes os-area-boot-fallback {
654 0%,
655 90% {
656 opacity: 0;
657 pointer-events: none;
658 }
659 100% {
660 opacity: 1;
661 pointer-events: auto;
662 }
663 }
664
665 /* ---------------------------------------------------------------
666 * Desktop icons — wallpaper shortcut tiles registered via the
667 * `wp_register_desktop_icon()` PHP API. Rendered inside the
668 * desktop area as the first child after the wallpaper, so they
669 * paint above the wallpaper but beneath any window.
670 *
671 * Grid layout is fixed-width columns; tiles wrap onto multiple
672 * rows when the desktop is wide enough to host more than one
673 * column's worth. `pointer-events: none` on the container keeps
674 * the background clickable (for "minimize all" on wallpaper
675 * click) while individual tiles re-enable pointer events so they
676 * remain clickable.
677 * --------------------------------------------------------------- */
678
679 .os-icons {
680 position: absolute;
681 inset-block-start: 16px;
682 inset-block-end: 16px;
683 inset-inline-start: 16px;
684 inset-inline-end: 16px;
685 display: grid;
686 /*
687 * Column-major flow: icons fill the first column top-to-bottom,
688 * then start the next column. Matches the macOS / Finder
689 * convention. `grid-auto-flow: column` plus `auto-fill` rows
690 * (sized to the container height) yields exactly that layout.
691 *
692 * All four insets are pinned so the container has a definite
693 * height — `repeat(auto-fill, 96px)` needs that to compute the
694 * row count. 96px is one icon button (48px image + 6px gap +
695 * up-to-two-line label + 8px vertical padding); a slightly
696 * generous figure leaves room for descenders without clipping.
697 */
698 grid-auto-flow: column;
699 grid-template-rows: repeat(auto-fill, 96px);
700 grid-auto-columns: 88px;
701 align-content: start;
702 justify-content: start;
703 gap: 12px;
704 z-index: 1;
705 pointer-events: none;
706 /*
707 * Spill protection: if a user ever pins more icons than fit
708 * vertically, the grid scrolls horizontally so the unreachable
709 * tiles can be reached without clipping the visible ones.
710 * Individual tiles re-enable pointer events so they stay
711 * clickable inside the scroll surface.
712 */
713 overflow: auto;
714 }
715
716 .os-icon {
717 pointer-events: auto;
718 display: flex;
719 flex-direction: column;
720 align-items: center;
721 justify-content: flex-start;
722 gap: 6px;
723 width: 88px;
724 padding: 8px 4px;
725 border: 0;
726 background-color: transparent;
727 /*
728 * ICON_TILE texture slot — a plate behind each wallpaper icon.
729 * Painted under the icon image and its label, so a theme can give
730 * desktop shortcuts a physical tile the way a phone home screen
731 * does. Unset = `none` = the transparent tile we always had.
732 */
733 background-image: var(--os-tile-image, none);
734 background-repeat: var(--os-tile-image-repeat, no-repeat);
735 background-size: var(--os-tile-image-size, auto);
736 background-position: var(--os-tile-image-position, center);
737 color: var(--os-fg, #fff);
738 cursor: pointer;
739 border-radius: 8px;
740 transition: background-color 0.15s ease;
741 /* Positioning context for `.os-icon__badge`. */
742 position: relative;
743 }
744
745 /*
746 * Icon badge — symmetric to `.os-dock__badge` but anchored
747 * to the wallpaper-icon image. Same gradient, same ring, slightly
748 * larger so it reads at the longer wallpaper viewing distance.
749 * Painted by `wp.os.icons.setBadge( id, count )` — see
750 * `src/desktop-icons.ts` for the imperative API and
751 * `src/recycle-bin/badge.ts` for the canonical multi-rail
752 * consumer. The class name is part of the stable contract; the
753 * positioning, color, and shadow are not — themes or future
754 * shell builds may restyle freely.
755 */
756 .os-icon__badge {
757 position: absolute;
758 top: 4px;
759 inset-inline-end: 14px;
760 /* Same derivation as the dock badge, sized off the wallpaper icon
761 * rather than the dock icon. The default resolves to 18px / 11px —
762 * exactly the old values. */
763 --os-icon-badge-computed-size: var(
764 --os-icon-badge-size,
765 calc( var( --os-icon-image-size, 48px ) * 0.375 )
766 );
767 min-width: var( --os-icon-badge-computed-size );
768 height: var( --os-icon-badge-computed-size );
769 padding: 0 var(
770 --os-icon-badge-padding,
771 calc( var( --os-icon-badge-computed-size ) * 0.28 )
772 );
773 box-sizing: border-box;
774 border-radius: 999px;
775 background: var(
776 --os-icon-badge-bg,
777 linear-gradient( 180deg, #ff5a5a 0%, var( --os-ui-danger, #d63638 ) 100% )
778 );
779 color: var( --os-icon-badge-fg, var( --os-ui-fg-on-accent, #fff ) );
780 display: inline-flex;
781 align-items: center;
782 justify-content: center;
783 font-size: var(
784 --os-icon-badge-font-size,
785 calc( var( --os-icon-badge-computed-size ) * 0.611 )
786 );
787 font-weight: var( --os-icon-badge-font-weight, 700 );
788 line-height: 1;
789 font-variant-numeric: tabular-nums;
790 box-shadow:
791 0 0 0 1.5px rgba( 0, 0, 0, 0.4 ),
792 0 1px 3px rgba( 0, 0, 0, 0.35 );
793 pointer-events: none;
794 }
795
796 .os-icon:hover,
797 .os-icon:focus-visible {
798 /* background-COLOR, not the shorthand: the shorthand would reset
799 * the ICON_TILE texture declared above, so a themed tile would
800 * vanish on hover. */
801 background-color: rgba(255, 255, 255, 0.12);
802 outline: none;
803 }
804
805 .os-icon:focus-visible {
806 box-shadow: 0 0 0 2px var(--wp-admin-theme-color, #2271b1);
807 }
808
809 .os-icon__image {
810 display: inline-flex;
811 align-items: center;
812 justify-content: center;
813 /* Tokenized so a theme can scale wallpaper icons — and so the
814 * badge above has something to derive its own size from. */
815 width: var( --os-icon-image-size, 48px );
816 height: var( --os-icon-image-size, 48px );
817 font-size: calc( var( --os-icon-image-size, 48px ) * 0.667 );
818 line-height: 1;
819 }
820
821 .os-icon__image img {
822 max-width: 48px;
823 max-height: 48px;
824 object-fit: contain;
825 }
826
827 .os-icon__label {
828 font-size: 12px;
829 text-align: center;
830 text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
831 line-height: 1.2;
832 word-break: break-word;
833 }
834
835 /* ---------------------------------------------------------------
836 * Widgets column — right-edge glass strip that paints above the
837 * wallpaper but beneath windows. Hosts stacked widget cards and a
838 * trailing "Add widget" tile. Interactive only on its own cards +
839 * the add tile; every other pixel is pointer-transparent so window
840 * drag / resize / click-through behaviour is unaffected.
841 * --------------------------------------------------------------- */
842
843 .os-widgets {
844 border-radius: 14px;
845 position: absolute;
846 top: 16px;
847 bottom: 16px;
848 inset-inline-end: 16px;
849 width: 320px;
850 display: flex;
851 flex-direction: column;
852 gap: 12px;
853 z-index: 1;
854 pointer-events: none;
855 overflow-y: auto;
856 /* Hide the scrollbar chrome unless the user actually overflows —
857 * most setups never will. */
858 scrollbar-width: thin;
859 /* Fade alongside the overview backdrop. The class flip that
860 * drives this is on the parent area, set at t=0 of enter and
861 * removed at t=0 of exit, so widgets fade in parallel with the
862 * dock collapse / return — never sequential. */
863 opacity: 1;
864 transition:
865 opacity 0.22s ease,
866 transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
867 }
868
869 .os-area--overview .os-widgets {
870 opacity: 0;
871 /* Slight slide-off-right so the fade reads as "the column is
872 * stepping back out of the way" rather than flat-dimming. */
873 transform: translateX( 16px );
874 pointer-events: none;
875 }
876
877 .os-widgets__list {
878 display: flex;
879 flex-direction: column;
880 gap: 12px;
881 padding: 4px;
882 }
883
884 /*
885 * Individual widget card — glass backdrop matching the dock /
886 * overview top bar. Card root is pointer-events: auto so clicks on
887 * content + the remove X land; the column itself stays
888 * pointer-transparent so a drag that grazes the column's margin
889 * falls through to the window beneath.
890 */
891 .os-widgets__card {
892 position: relative;
893 padding: 0;
894 /* Column layout so the body can flex-shrink and scroll when the
895 * frame writes a fixed inline height (floating / resized cards).
896 * Without this the body keeps its natural height and content
897 * spills past the card's rounded bottom edge. */
898 display: flex;
899 flex-direction: column;
900 pointer-events: auto;
901 background-color: rgba( 20, 20, 22, 0.55 );
902 /*
903 * WIDGET texture slot — layered over the frosted card colour, so a
904 * translucent texture still gets the blur underneath.
905 */
906 background-image: var( --os-widget-image, none );
907 background-repeat: var( --os-widget-image-repeat, repeat );
908 background-size: var( --os-widget-image-size, auto );
909 background-position: var( --os-widget-image-position, center );
910 backdrop-filter: blur( 18px ) saturate( 140% );
911 -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
912 border: 1px solid rgba( 255, 255, 255, 0.08 );
913 border-radius: 14px;
914 color: var( --os-ui-fg-on-accent, #fff );
915 box-shadow:
916 0 8px 28px rgba( 0, 0, 0, 0.35 ),
917 inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
918 transition:
919 box-shadow 0.18s ease,
920 opacity 0.18s ease;
921 }
922
923 /* Hover feedback is shadow-only — no transform. Cards must feel
924 * anchored; a hover nudge reads as the widget drifting under the
925 * pointer (and fought the user's chosen position on floating cards). */
926 .os-widgets__card:hover {
927 box-shadow:
928 0 12px 36px rgba( 0, 0, 0, 0.45 ),
929 inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
930 }
931
932 .os-widgets__card-body {
933 padding: 16px;
934 min-height: 48px;
935 flex: 1 1 auto;
936 overflow-x: hidden;
937 overflow-y: auto;
938 scrollbar-width: thin;
939 }
940
941 .os-widgets__card-close {
942 position: absolute;
943 top: 6px;
944 inset-inline-end: 6px;
945 width: 22px;
946 height: 22px;
947 display: flex;
948 align-items: center;
949 justify-content: center;
950 padding: 0;
951 border: 0;
952 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
953 color: rgba( 255, 255, 255, 0.75 );
954 border-radius: 50%;
955 cursor: pointer;
956 opacity: 0;
957 transition:
958 opacity 0.15s ease,
959 background-color 0.15s ease,
960 color 0.15s ease;
961 }
962
963 .os-widgets__card:hover .os-widgets__card-close,
964 .os-widgets__card-close:focus-visible {
965 opacity: 1;
966 }
967
968 .os-widgets__card-close:hover {
969 background: var( --os-ui-danger, #d63638 );
970 color: var( --os-ui-fg-on-accent, #fff );
971 }
972
973 /*
974 * Movable widgets — a thin chrome header at the top of the card holds
975 * the drag grip, the widget's own label, and the × button. The whole
976 * header is the drag handle (except for interactive children — those
977 * remain clickable as normal). Non-movable widgets keep today's
978 * corner-× layout unchanged.
979 */
980 .os-widgets__chrome {
981 display: flex;
982 flex-shrink: 0;
983 align-items: center;
984 gap: 8px;
985 padding: 6px 8px;
986 border-bottom: 1px solid rgba( 255, 255, 255, 0.06 );
987 cursor: grab;
988 user-select: none;
989 -webkit-user-select: none;
990 touch-action: none;
991 }
992
993 .os-widgets__card--dragging .os-widgets__chrome {
994 cursor: grabbing;
995 }
996
997 .os-widgets__grip {
998 flex-shrink: 0;
999 width: 10px;
1000 height: 16px;
1001 background-image: radial-gradient(
1002 circle,
1003 rgba( 255, 255, 255, 0.55 ) 1.2px,
1004 transparent 1.5px
1005 );
1006 background-size: 5px 5px;
1007 background-position: 0 1px;
1008 background-repeat: space;
1009 opacity: 0.45;
1010 transition: opacity 0.12s ease;
1011 }
1012
1013 .os-widgets__card:hover .os-widgets__grip {
1014 opacity: 0.85;
1015 }
1016
1017 .os-widgets__title {
1018 flex: 1;
1019 font-size: 12px;
1020 font-weight: 500;
1021 letter-spacing: 0.01em;
1022 color: rgba( 255, 255, 255, 0.85 );
1023 overflow: hidden;
1024 text-overflow: ellipsis;
1025 white-space: nowrap;
1026 }
1027
1028 /* When the close button sits inside the chrome it's inline, not
1029 corner-absolute — reset position + always-visible opacity. */
1030 .os-widgets__chrome .os-widgets__card-close {
1031 position: static;
1032 opacity: 0.7;
1033 width: 20px;
1034 height: 20px;
1035 }
1036
1037 .os-widgets__chrome .os-widgets__card-close:hover,
1038 .os-widgets__chrome .os-widgets__card-close:focus-visible {
1039 opacity: 1;
1040 }
1041
1042 /*
1043 * Re-dock button — sits in the chrome next to the close button and
1044 * returns a floating widget to the right column. Visibility is
1045 * gated on the `--floating` modifier class so docked widgets never
1046 * expose it (tapping it on a docked card would be a confusing
1047 * no-op). The DOM node stays mounted either way so state flips
1048 * don't rebuild chrome.
1049 *
1050 * Styling mirrors the inline close button: 20×20 round tile,
1051 * dimmed by default, full-opacity on hover / focus. Distinct hover
1052 * color (theme accent, not red) signals "put back" vs. "remove."
1053 */
1054 .os-widgets__card-redock {
1055 display: none;
1056 align-items: center;
1057 justify-content: center;
1058 padding: 0;
1059 border: 0;
1060 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
1061 color: rgba( 255, 255, 255, 0.75 );
1062 border-radius: 50%;
1063 width: 20px;
1064 height: 20px;
1065 cursor: pointer;
1066 opacity: 0.7;
1067 transition:
1068 opacity 0.15s ease,
1069 background-color 0.15s ease,
1070 color 0.15s ease;
1071 }
1072
1073 .os-widgets__card--floating .os-widgets__card-redock {
1074 display: flex;
1075 }
1076
1077 .os-widgets__card-redock:hover,
1078 .os-widgets__card-redock:focus-visible {
1079 opacity: 1;
1080 background: var( --wp-admin-theme-color, #2271b1 );
1081 color: var( --os-ui-fg-on-accent, #fff );
1082 }
1083
1084 /*
1085 * Floating widgets — absolute-positioned, inline top/left/width/height
1086 * written by the frame.
1087 */
1088 .os-widgets__card--floating {
1089 position: absolute;
1090 margin: 0;
1091 }
1092
1093 /*
1094 * A liberated card is reparented to the desktop area, so the column's
1095 * overview fade above no longer reaches it. Direct children only: a
1096 * card inside a window body belongs to that window's thumbnail.
1097 */
1098 .os-area--overview > .os-widgets__card--floating {
1099 opacity: 0;
1100 pointer-events: none;
1101 }
1102
1103 .os-widgets__card--dragging,
1104 .os-widgets__card--resizing {
1105 transition: none;
1106 z-index: 2;
1107 }
1108
1109 /*
1110 * Resize handles — 8 in all, with the 4 edges as hair-line strips and
1111 * the 4 corners as small squares. Invisible by default; cursor change
1112 * telegraphs them on hover. The frame attaches pointer listeners to
1113 * every handle; a non-movable widget ignores everything except the
1114 * `s` (bottom-edge, height-only) handle at the pointer layer, so we
1115 * visually hide the mismatched handles via the modifier class.
1116 */
1117 .os-widgets__resize {
1118 position: absolute;
1119 z-index: 1;
1120 }
1121
1122 .os-widgets__resize--n {
1123 top: -3px;
1124 left: 8px;
1125 right: 8px;
1126 height: 6px;
1127 cursor: ns-resize;
1128 }
1129
1130 .os-widgets__resize--s {
1131 bottom: -3px;
1132 left: 8px;
1133 right: 8px;
1134 height: 6px;
1135 cursor: ns-resize;
1136 }
1137
1138 .os-widgets__resize--e {
1139 top: 8px;
1140 bottom: 8px;
1141 right: -3px;
1142 width: 6px;
1143 cursor: ew-resize;
1144 }
1145
1146 .os-widgets__resize--w {
1147 top: 8px;
1148 bottom: 8px;
1149 left: -3px;
1150 width: 6px;
1151 cursor: ew-resize;
1152 }
1153
1154 .os-widgets__resize--ne {
1155 top: -4px;
1156 right: -4px;
1157 width: 12px;
1158 height: 12px;
1159 cursor: nesw-resize;
1160 }
1161
1162 .os-widgets__resize--nw {
1163 top: -4px;
1164 left: -4px;
1165 width: 12px;
1166 height: 12px;
1167 cursor: nwse-resize;
1168 }
1169
1170 .os-widgets__resize--se {
1171 bottom: -4px;
1172 right: -4px;
1173 width: 12px;
1174 height: 12px;
1175 cursor: nwse-resize;
1176 }
1177
1178 .os-widgets__resize--sw {
1179 bottom: -4px;
1180 left: -4px;
1181 width: 12px;
1182 height: 12px;
1183 cursor: nesw-resize;
1184 }
1185
1186 /*
1187 * Only floating cards expose the full 8-direction handle set. A
1188 * column-docked card — non-movable OR movable-but-not-yet-liberated —
1189 * resizes on the height axis alone (the frame ignores every other
1190 * direction while docked), so hide all handles except the bottom
1191 * edge and the cursor never lies about what's interactive.
1192 */
1193 .os-widgets__card--resizable:not( .os-widgets__card--floating ) .os-widgets__resize:not( .os-widgets__resize--s ) {
1194 display: none;
1195 }
1196
1197 /*
1198 * Add-widget tile — a compact pill that trails the widget stack, not
1199 * a full-width drop zone. Hidden until the pointer comes near the
1200 * column (the layer toggles `--hovered`), so the desktop stays clean
1201 * for people who never touch widgets.
1202 *
1203 * Absolute rather than in flow because "the bottom of the stack" is
1204 * not something the column's layout knows: a widget dragged out of
1205 * the column becomes a floating card parented to the desktop, so it
1206 * leaves the flex flow while still sitting visually in the column.
1207 * The layer measures those too and writes `top` — see
1208 * `positionAddTile()`.
1209 */
1210 .os-widgets__add {
1211 position: absolute;
1212 top: 0;
1213 left: 50%;
1214 transform: translateX( -50% );
1215 padding: 8px 22px;
1216 display: flex;
1217 align-items: center;
1218 justify-content: center;
1219 gap: 7px;
1220 pointer-events: none;
1221 background: transparent;
1222 border: 1px dashed rgba( 255, 255, 255, 0.22 );
1223 border-radius: 10px;
1224 color: rgba( 255, 255, 255, 0.72 );
1225 font: inherit;
1226 font-weight: 500;
1227 cursor: pointer;
1228 opacity: 0;
1229 transition:
1230 opacity 0.15s ease,
1231 border-color 0.15s ease,
1232 background-color 0.15s ease,
1233 color 0.15s ease;
1234 }
1235
1236 /*
1237 * Reveal conditions. An empty column is no exception — approaching
1238 * the right side is the gesture, whether or not there's a card there
1239 * to aim at. `:focus-visible` keeps it reachable by keyboard, where
1240 * there is no pointer to bring near.
1241 */
1242 .os-widgets--hovered .os-widgets__add,
1243 .os-widgets--picking .os-widgets__add,
1244 .os-widgets__add:focus-visible {
1245 opacity: 1;
1246 pointer-events: auto;
1247 }
1248
1249 /*
1250 * Touch has no hover to approach the column with. The wallpaper's
1251 * right-click menu carries an "Add widget" entry, but a long-press
1252 * to reach it is a lot to ask of someone who just wants a clock, so
1253 * on those devices the pill stays put.
1254 */
1255 @media ( hover: none ) {
1256 .os-widgets__add {
1257 opacity: 1;
1258 pointer-events: auto;
1259 }
1260 }
1261
1262 .os-widgets__add:hover {
1263 border-color: rgba( 255, 255, 255, 0.4 );
1264 background: rgba( 255, 255, 255, 0.06 );
1265 color: var( --os-ui-fg-on-accent, #fff );
1266 }
1267
1268 .os-widgets__add:focus-visible {
1269 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
1270 outline-offset: 2px;
1271 }
1272
1273 .os-widgets__add-plus {
1274 font-size: 15px;
1275 line-height: 1;
1276 font-weight: 300;
1277 }
1278
1279 .os-widgets__add-label {
1280 font-size: 13px;
1281 letter-spacing: 0.02em;
1282 }
1283
1284 /* Built-in clock widget */
1285 .os-widget-clock {
1286 text-align: center;
1287 /* The time + date are passive glyphs, not content users would
1288 * ever reasonably select — turning it off avoids the accidental-
1289 * highlight mess that happens when a drag starts on the clock. */
1290 user-select: none;
1291 -webkit-user-select: none;
1292 }
1293
1294 .os-widget-clock__time {
1295 font-size: 34px;
1296 font-weight: 600;
1297 line-height: 1.1;
1298 font-variant-numeric: tabular-nums;
1299 letter-spacing: 0.01em;
1300 }
1301
1302 .os-widget-clock__date {
1303 margin-top: 4px;
1304 font-size: 13px;
1305 color: rgba( 255, 255, 255, 0.75 );
1306 letter-spacing: 0.02em;
1307 }
1308
1309 /* ---------------------------------------------------------------
1310 * Widget picker popover — opens from the add tile, lists every
1311 * registered widget. Positioned fixed so it can extend outside
1312 * the desktop-area clip.
1313 * --------------------------------------------------------------- */
1314
1315 .os-widget-picker {
1316 position: fixed;
1317 width: 320px;
1318 max-height: 60vh;
1319 overflow-y: auto;
1320 padding: 10px;
1321 border-radius: 14px;
1322 background: rgba( 20, 20, 22, 0.88 );
1323 backdrop-filter: blur( 22px ) saturate( 160% );
1324 -webkit-backdrop-filter: blur( 22px ) saturate( 160% );
1325 border: 1px solid rgba( 255, 255, 255, 0.1 );
1326 box-shadow: 0 18px 54px rgba( 0, 0, 0, 0.55 );
1327 color: var( --os-ui-fg-on-accent, #fff );
1328 z-index: 9999;
1329 animation: os-widget-picker-in 0.14s ease-out;
1330 }
1331
1332 @keyframes os-widget-picker-in {
1333 from {
1334 opacity: 0;
1335 transform: translateY( 4px );
1336 }
1337 }
1338
1339 .os-widget-picker__title {
1340 padding: 4px 8px 8px;
1341 font-size: 11px;
1342 font-weight: 600;
1343 letter-spacing: 0.08em;
1344 text-transform: uppercase;
1345 color: rgba( 255, 255, 255, 0.55 );
1346 }
1347
1348 .os-widget-picker__list {
1349 display: flex;
1350 flex-direction: column;
1351 gap: 4px;
1352 }
1353
1354 .os-widget-picker__entry {
1355 display: flex;
1356 align-items: center;
1357 gap: 12px;
1358 padding: 10px;
1359 background: transparent;
1360 border: 0;
1361 border-radius: 10px;
1362 color: var( --os-ui-fg-on-accent, #fff );
1363 cursor: pointer;
1364 text-align: start;
1365 font: inherit;
1366 transition: background-color 0.12s ease;
1367 }
1368
1369 .os-widget-picker__entry:hover:not( :disabled ) {
1370 background: rgba( 255, 255, 255, 0.08 );
1371 }
1372
1373 .os-widget-picker__entry:focus-visible {
1374 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
1375 outline-offset: 1px;
1376 }
1377
1378 .os-widget-picker__entry--added {
1379 opacity: 0.55;
1380 cursor: default;
1381 }
1382
1383 .os-widget-picker__entry-icon {
1384 flex-shrink: 0;
1385 font-size: 20px;
1386 width: 20px;
1387 height: 20px;
1388 line-height: 1;
1389 }
1390
1391 .os-widget-picker__entry-text {
1392 display: flex;
1393 flex-direction: column;
1394 gap: 2px;
1395 flex: 1;
1396 min-width: 0;
1397 }
1398
1399 .os-widget-picker__entry-label {
1400 font-size: 13px;
1401 font-weight: 500;
1402 white-space: nowrap;
1403 overflow: hidden;
1404 text-overflow: ellipsis;
1405 }
1406
1407 .os-widget-picker__entry-description {
1408 font-size: 11px;
1409 color: rgba( 255, 255, 255, 0.6 );
1410 line-height: 1.35;
1411 white-space: normal;
1412 }
1413
1414 .os-widget-picker__entry-status {
1415 flex-shrink: 0;
1416 font-size: 11px;
1417 color: rgba( 255, 255, 255, 0.5 );
1418 text-transform: uppercase;
1419 letter-spacing: 0.08em;
1420 }
1421
1422 .os-widget-picker__empty {
1423 padding: 12px 8px;
1424 font-size: 12px;
1425 color: rgba( 255, 255, 255, 0.6 );
1426 line-height: 1.4;
1427 }
1428
1429 /**
1430 * Chromeless mode: pages loaded inside iframes.
1431 * Remove all shell chrome and let content fill the frame.
1432 */
1433 .os-chromeless {
1434 margin: 0;
1435 padding: 0;
1436 background: var(--os-window-bg);
1437 }
1438
1439 /*
1440 * Chromeless #wpbody-content, #adminmenuwrap, #wpfooter overrides are in
1441 * chromeless.css to keep all legacy page tweaks in one place.
1442 */
1443
1444 /*
1445 * Hide WordPress core's built-in command palette (`@wordpress/commands`).
1446 * OpenStation force-enqueues `wp_enqueue_command_palette_assets()` so
1447 * the `core/commands` data store is populated for our shell harvester
1448 * (see `src/commands/shell-harvester.ts`); the side effect is that WP
1449 * also mounts its own `<CommandPalette>` to the document body. We
1450 * already block its Cmd+K handler in `src/palette-registry.ts`, but if
1451 * a third-party script opens it programmatically (`wp.data.dispatch(
1452 * 'core/commands' ).open()`), the dialog would float over the desktop
1453 * with admin-menu navigation callbacks whose `document.location = url`
1454 * would unload the shell. Keep it permanently hidden.
1455 */
1456 .commands-command-menu,
1457 .commands-command-menu__overlay {
1458 display: none !important;
1459 }
1460
1461 /*
1462 * ---- Links slotted into <os-notice> ---------------------------
1463 *
1464 * `<os-notice>` styles its slotted links from inside its shadow root,
1465 * through `::slotted( a )` and the `--os-ui-notice-link` tokens. That
1466 * rule cannot win here, and no amount of specificity on it would help.
1467 *
1468 * A slotted element lives in the DOCUMENT tree; the shadow root only
1469 * borrows it for rendering. When declarations from two trees collide,
1470 * CSS Scoping resolves normal (non-`!important`) ones in favour of the
1471 * OUTER tree regardless of specificity — so `wp-admin/css/common.css`,
1472 * which sets `a { color: #2271b1 }` and `a:hover { color: #135e96 }` on
1473 * every anchor in the admin, beats `::slotted( a )` every time. The
1474 * shell renders inside wp-admin, so it always loses: the link painted
1475 * WordPress Blue at rest and a darker blue on hover, the second of
1476 * which is close to illegible on a notice's dark wash.
1477 *
1478 * Hence a document-tree rule. It reads the same tokens the component
1479 * documents, so a theme still restyles the link by setting them; this
1480 * only moves the declaration into a tree that can win. `a:hover` is
1481 * (0,1,1), so the hover selector has to carry its own pseudo-class to
1482 * outrank it — matching `os-notice a` alone would lose the hover state
1483 * while winning the base one, which is exactly the inert-looking link
1484 * the component's own styles already went out of their way to avoid.
1485 */
1486 os-notice a {
1487 color: var(--os-ui-notice-link, #ec9bff);
1488 }
1489
1490 os-notice a:hover,
1491 os-notice a:focus,
1492 os-notice a:active {
1493 color: var(--os-ui-notice-link-hover, #fffbff);
1494 }
1495