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

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