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

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