PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 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 All 37 releases
← All changes | assets/css/desktop.css +1286 -439 0.9.8 → 1.1.13 View file →
@@ -1,8 +1,8 @@
1 1 /**
2 - * Desktop Mode — Shell Layout.
2 + * OpenStation — Shell Layout.
3 3 *
4 - * When desktop mode is active, the classic admin chrome still renders
4 + * When OpenStation is active, the classic admin chrome still renders
5 5 * server-side (for hook compatibility) but is hidden with CSS. The
6 6 * plugin-injected shell floats above it, positioned just below the
7 7 * fixed admin bar, covering the rest of the viewport.
8 8 *
@@ -8,17 +8,57 @@
8 8 *
9 9 * @since 0.1.0
10 10 */
11 11
12 -/* Solid backdrop behind the shell. */
13 -body.desktop-mode-active {
14 - background: #1d2327;
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 );
15 20 overflow: hidden;
16 21 }
17 22
18 23 /*
19 - * Cursor policy — default arrow everywhere in desktop mode.
24 + * Text selection.
20 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 + *
21 61 * Per user preference (since 0.20.0): no hand/pointer/grab/grabbing/
22 62 * copy/no-drop cursors on icons, tiles, dock items, dock peek,
23 63 * window chrome, or any other clickable surface. The default arrow
24 64 * is the only "neutral" cursor we render.
@@ -38,46 +78,46 @@
38 78 * drag-state declarations elsewhere in the codebase have the same or
39 79 * higher specificity. The resize-handle exceptions use `!important`
40 80 * for the same reason.
41 81 *
42 - * Shadow-DOM web components (`wpd-*`) define their own cursors
82 + * Shadow-DOM web components (`os-*`) define their own cursors
43 83 * inside their shadow trees; those internal rules win regardless of
44 84 * what we set on the host. Anything visible on the desktop shell
45 85 * (light DOM) is covered by this policy.
46 86 */
47 -body.desktop-mode-active,
48 -body.desktop-mode-active * {
87 +body.os-active,
88 +body.os-active * {
49 89 cursor: default !important;
50 90 }
51 91
52 -body.desktop-mode-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"]),
53 -body.desktop-mode-active textarea,
54 -body.desktop-mode-active [contenteditable=""],
55 -body.desktop-mode-active [contenteditable="true"] {
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"] {
56 96 cursor: text !important;
57 97 }
58 98
59 -body.desktop-mode-active .desktop-mode-window__resize-handle--se,
60 -body.desktop-mode-active .desktop-mode-window__resize-handle--nw,
61 -body.desktop-mode-active .desktop-mode-widgets__resize--se,
62 -body.desktop-mode-active .desktop-mode-widgets__resize--nw {
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 {
63 103 cursor: nwse-resize !important;
64 104 }
65 105
66 -body.desktop-mode-active .desktop-mode-window__resize-handle--sw,
67 -body.desktop-mode-active .desktop-mode-window__resize-handle--ne,
68 -body.desktop-mode-active .desktop-mode-widgets__resize--sw,
69 -body.desktop-mode-active .desktop-mode-widgets__resize--ne {
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 {
70 110 cursor: nesw-resize !important;
71 111 }
72 112
73 -body.desktop-mode-active .desktop-mode-widgets__resize--n,
74 -body.desktop-mode-active .desktop-mode-widgets__resize--s {
113 +body.os-active .os-widgets__resize--n,
114 +body.os-active .os-widgets__resize--s {
75 115 cursor: ns-resize !important;
76 116 }
77 117
78 -body.desktop-mode-active .desktop-mode-widgets__resize--e,
79 -body.desktop-mode-active .desktop-mode-widgets__resize--w {
118 +body.os-active .os-widgets__resize--e,
119 +body.os-active .os-widgets__resize--w {
80 120 cursor: ew-resize !important;
81 121 }
82 122
83 123 /*
@@ -86,19 +126,19 @@
86 126 * The markup still exists in the DOM so server-side action hooks that
87 127 * target these regions (admin_notices, in_admin_footer, etc.) continue
88 128 * to fire — we just take them out of the visual flow.
89 129 */
90 -body.desktop-mode-active #adminmenuwrap,
91 -body.desktop-mode-active #adminmenuback,
92 -body.desktop-mode-active #wpbody,
93 -body.desktop-mode-active #wpfooter,
94 -body.desktop-mode-active .wp-responsive-toggle,
95 -body.desktop-mode-active #collapse-menu {
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 {
96 136 display: none !important;
97 137 }
98 138
99 139 /*
100 - * The admin bar is the ONLY escape hatch in desktop mode — it hosts the
140 + * The admin bar is the ONLY escape hatch in OpenStation — it hosts the
101 141 * "Switch to Classic Admin" toggle. Any plugin CSS, Gutenberg state,
102 142 * user preference, or stray `display:none` that hides it would strand
103 143 * the user inside the shell with no way out. Hard-pin it visible.
104 144 *
@@ -105,9 +145,9 @@
105 145 * The exception: while a window is in immersive fullscreen, we *want*
106 146 * the bar hidden (macOS convention). That rule lives below and wins on
107 147 * specificity because it also targets #wpadminbar under a body class.
108 148 */
109 -body.desktop-mode-active #wpadminbar {
149 +body.os-active #wpadminbar {
110 150 display: block !important;
111 151 visibility: visible !important;
112 152 opacity: 1 !important;
113 153 position: fixed !important;
@@ -116,11 +156,30 @@
116 156 inset-inline-end: 0 !important;
117 157 }
118 158
119 159 /*
160 + * No top-level item may be taller than the bar.
161 + *
162 + * Core lays the two item groups out as floats, and a float that
163 + * overflows the 32px bar overflows invisibly. A host may lay a group
164 + * out as a flex row instead — WordPress.com's Debug Bar does, to
165 + * order its own item first — and then every item stretches to the
166 + * tallest one and the group's background paints the difference under
167 + * the windows' title bars. Our own items were that tallest one once
168 + * (an `inline-flex` item on a 32px line box grew to 37px; fixed at
169 + * the source in `includes/admin-bar.php`), but any item from any
170 + * plugin can be, so the cap lives here: the bar's own height token,
171 + * which is also the 46px mobile bar. Submenus are absolutely
172 + * positioned and unaffected.
173 + */
174 +body.os-active #wpadminbar .ab-top-menu > li {
175 + max-height: var(--wp-admin--admin-bar--height, 32px);
176 +}
177 +
178 +/*
120 179 * Admin-bar presentation modes — OS Settings → Appearance → Admin bar,
121 180 * persisted as `adminBarMode` and emitted as a
122 - * `desktop-mode-admin-bar-<mode>` body class by PHP on first paint and
181 + * `os-admin-bar-<mode>` body class by PHP on first paint and
123 182 * re-written by the shell's apply pass on every change.
124 183 *
125 184 * `static` is the absence of rules: the pin above stands and the shell
126 185 * starts below the bar. The two modes below both take the bar out of
@@ -126,9 +185,11 @@
126 185 * starts below the bar. The two modes below both take the bar out of
127 186 * the shell's way, so the shell reclaims the full viewport in each.
128 187 *
129 188 * Every selector here carries two body classes, so it outranks the
130 - * one-class pin rule above without needing to escalate anything.
189 + * one-class pin rule above on specificity; the one property the pin
190 + * marks `!important` (`inset-block-start`) is re-declared `!important`
191 + * where a mode moves the bar, and wins the same way.
131 192 */
132 193
133 194 /*
134 195 * Dynamic — the bar parks off the top edge leaving a peek strip, and
@@ -133,25 +194,42 @@
133 194 /*
134 195 * Dynamic — the bar parks off the top edge leaving a peek strip, and
135 196 * slides back in when the pointer reaches the top of the viewport or
136 197 * something inside it takes keyboard focus. The classic Windows
137 - * auto-hide taskbar, and the reason `transform` is the right tool: a
138 - * transformed element keeps hit-testing at its PAINTED position, so
139 - * the parked remainder stops catching the pointer the moment it goes
140 - * off-screen. What DOES catch it is the peek strip plus the reveal
141 - * zone added below.
198 + * auto-hide taskbar.
199 + *
200 + * It parks by moving its `inset-block-start`, NOT by a `transform`,
201 + * and that is load-bearing. A transformed element becomes the
202 + * containing block for every `position: fixed` descendant, and the
203 + * bar has descendants like that which it does not own: the
204 + * WordPress.com notifications panel is `position: fixed; top: 32px;
205 + * bottom: 0` inside `#wp-admin-bar-notes`. Measured against a
206 + * transformed 32px bar instead of the viewport, that panel computes to
207 + * zero height — the button "did nothing" in this mode and worked in
208 + * `static`. `translate`, `filter`, `perspective`, `contain` and
209 + * `will-change: transform` all create the same containing block;
210 + * `tests/vitest/admin-bar-dynamic.test.ts` keeps every one of them off
211 + * this rule. Moving `inset-block-start` hit-tests the same way — the
212 + * part above the viewport is simply outside it — so what catches the
213 + * pointer is still only the peek strip plus the reveal zone below.
214 + *
215 + * The pin above sets `inset-block-start: 0 !important`; these carry
216 + * `!important` too and win on specificity. The bar's height is Core's
217 + * own token, so the parked offset follows the 46px mobile bar as well.
142 218 */
143 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar {
144 - transform: translateY(
145 - calc(-100% + var(--desktop-mode-admin-bar-peek, 4px))
146 - );
147 - transition: transform 180ms ease;
219 +body.os-active.os-admin-bar-dynamic #wpadminbar {
220 + /* One number for the slide; the reveal zone below waits on it. */
221 + --os-admin-bar-slide: 180ms;
222 + inset-block-start: calc(
223 + var(--os-admin-bar-peek, 4px) - var(--wp-admin--admin-bar--height, 32px)
224 + ) !important;
225 + transition: inset-block-start var(--os-admin-bar-slide, 180ms) ease;
148 226 }
149 227
150 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar:hover,
151 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar:focus-within,
152 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar:active {
153 - transform: translateY(0);
228 +body.os-active.os-admin-bar-dynamic #wpadminbar:hover,
229 +body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within,
230 +body.os-active.os-admin-bar-dynamic #wpadminbar:active {
231 + inset-block-start: 0 !important;
154 232 }
155 233
156 234 /*
157 235 * Reveal zone — an invisible extension of the bar's hit area, hanging
@@ -160,19 +238,36 @@
160 238 * Hovering a pseudo-element counts as hovering its originating
161 239 * element, so this widens what `:hover` above responds to WITHOUT
162 240 * widening the visible seam. The two wants are genuinely different:
163 241 * the seam should be a hairline, the target should be forgiving, and
164 - * `--desktop-mode-admin-bar-peek` alone can't be both.
242 + * `--os-admin-bar-peek` alone can't be both.
165 243 *
166 244 * Core ships no bare `#wpadminbar::before/::after` rule, so this
167 245 * claims the slot rather than fighting one.
168 246 */
169 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar::after {
247 +body.os-active.os-admin-bar-dynamic #wpadminbar::after {
170 248 content: "";
171 249 position: absolute;
172 250 inset-inline: 0;
173 251 inset-block-start: 100%;
174 - height: var(--desktop-mode-admin-bar-reveal-zone, 16px);
252 + height: var(--os-admin-bar-reveal-zone, 16px);
253 + /*
254 + * The zone changes size only once the bar has come to rest — a
255 + * step, not a slide, held back for the length of the bar's own
256 + * slide. That delay is what keeps the reveal stable. Collapsed the
257 + * instant hover began, the zone was gone while the bar was still
258 + * on its way down: a pointer resting in the band the zone had
259 + * covered was outside the bar's box for the rest of the slide, so
260 + * hover dropped, the bar turned back, the zone returned under the
261 + * pointer, hover resumed — the bar flickered up and down for as
262 + * long as the pointer stayed near the edge. Held for the slide,
263 + * the zone still hangs off the moving bar and the two together
264 + * always cover the pointer that tripped it; by the time the zone
265 + * goes, the bar's own box is over that pointer. The same delay on
266 + * the way back keeps the zone from reappearing under a pointer
267 + * that just left the bar while the bar is still retracting.
268 + */
269 + transition: height 0s linear var(--os-admin-bar-slide, 180ms);
175 270 }
176 271
177 272 /*
178 273 * ...but only while the bar is parked. Left standing once the bar is
@@ -180,13 +275,14 @@
180 275 * clicks on whatever window is up there.
181 276 *
182 277 * Collapsing it is also what keeps the reveal stable: the pointer
183 278 * that tripped the zone is, post-reveal, inside the bar's real box,
184 - * so `:hover` holds without the zone's help.
279 + * so `:hover` holds without the zone's help — and it collapses only
280 + * once the bar is there (the transition above).
185 281 */
186 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar:hover::after,
187 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar:focus-within::after,
188 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar:active::after {
282 +body.os-active.os-admin-bar-dynamic #wpadminbar:hover::after,
283 +body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within::after,
284 +body.os-active.os-admin-bar-dynamic #wpadminbar:active::after {
189 285 height: 0;
190 286 }
191 287
192 288 /*
@@ -195,16 +291,18 @@
195 291 * seam and the zone — `:active` above is what actually reveals the bar
196 292 * there. Kept under the 32px bar height, same ceiling as the default.
197 293 */
198 294 @media (hover: none) {
199 - body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar {
200 - --desktop-mode-admin-bar-peek: 10px;
201 - --desktop-mode-admin-bar-reveal-zone: 20px;
295 + body.os-active.os-admin-bar-dynamic #wpadminbar {
296 + --os-admin-bar-peek: 10px;
297 + --os-admin-bar-reveal-zone: 20px;
202 298 }
203 299 }
204 300
205 301 @media (prefers-reduced-motion: reduce) {
206 - body.desktop-mode-active.desktop-mode-admin-bar-dynamic #wpadminbar {
302 + body.os-active.os-admin-bar-dynamic #wpadminbar,
303 + body.os-active.os-admin-bar-dynamic #wpadminbar::after {
304 + /* The bar jumps, so the zone has nothing to wait for. */
207 305 transition: none;
208 306 }
209 307 }
210 308
@@ -210,22 +308,22 @@
210 308
211 309 /*
212 310 * Hidden — the bar is gone. This removes the "Switch to Classic Admin"
213 311 * toggle, which is why it is safe: the dock's core rail always carries
214 - * an "Exit Desktop Mode" tile (src/exit-desktop-mode.ts) hitting the
312 + * an "Exit OpenStation" tile (src/exit-os.ts) hitting the
215 313 * same endpoint. Do not add a mode that removes both.
216 314 */
217 -body.desktop-mode-active.desktop-mode-admin-bar-hidden #wpadminbar {
315 +body.os-active.os-admin-bar-hidden #wpadminbar {
218 316 display: none !important;
219 317 }
220 318
221 319 /*
222 320 * Both non-static modes: the shell owns the whole viewport. Two body
223 - * classes beat the `.desktop-mode-shell` base rule AND its <783px
321 + * classes beat the `.os-shell` base rule AND its <783px
224 322 * media-query variant on specificity, so source order doesn't matter.
225 323 */
226 -body.desktop-mode-active.desktop-mode-admin-bar-dynamic .desktop-mode-shell,
227 -body.desktop-mode-active.desktop-mode-admin-bar-hidden .desktop-mode-shell {
324 +body.os-active.os-admin-bar-dynamic .os-shell,
325 +body.os-active.os-admin-bar-hidden .os-shell {
228 326 inset-block-start: 0;
229 327 }
230 328
231 329 /*
@@ -234,14 +332,138 @@
234 332 * covers the viewport anyway, so the reserve is dead space — release
235 333 * it so any classic content behind the shell isn't pushed down by a
236 334 * bar that isn't sitting there. Same idiom as chromeless.css.
237 335 */
238 -html.wp-toolbar:has(body.desktop-mode-admin-bar-dynamic),
239 -html.wp-toolbar:has(body.desktop-mode-admin-bar-hidden) {
336 +html.wp-toolbar:has(body.os-admin-bar-dynamic),
337 +html.wp-toolbar:has(body.os-admin-bar-hidden) {
240 338 padding-top: 0 !important;
241 339 }
242 340
243 341 /*
342 + * A desktop theme that draws the toolbar.
343 + *
344 + * The toolbar's colours normally belong to the user's admin colour
345 + * scheme, whose own stylesheet paints it. A theme takes them over only
346 + * when it declares an `--os-toolbar-*` token, which is what puts
347 + * `os-toolbar-themed` on the body (PHP on first paint, the theme apply
348 + * pass on a live switch). Without that class none of this matches, so
349 + * a scheme's toolbar stays the scheme's under every other theme.
350 + *
351 + * The fallbacks are Core's own values for the default scheme.
352 + */
353 +body.os-active.os-toolbar-themed #wpadminbar {
354 + background: var(--os-toolbar-bg, #1d2327);
355 + box-shadow: var(--os-toolbar-shadow, none);
356 + -webkit-font-smoothing: var(--os-font-smoothing, subpixel-antialiased);
357 +}
358 +
359 +body.os-active.os-toolbar-themed #wpadminbar :not(.ab-icon):not([class*="dashicons"]) {
360 + font-family: var(--os-toolbar-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif);
361 + font-size: var(--os-toolbar-font-size, 13px);
362 +}
363 +
364 +/*
365 + * Core sets the bar's line heights in ems against its 13px type. A
366 + * face at another size keeps Core's pixel lines (the bar's height for a
367 + * top-level item, 26px for a menu row), or its labels ride high or low
368 + * against the icons. A face whose metrics sit differently from the
369 + * system font's nudges its labels with `--os-toolbar-line-height`.
370 + */
371 +body.os-active.os-toolbar-themed #wpadminbar .ab-top-menu > li > .ab-item,
372 +body.os-active.os-toolbar-themed #wpadminbar .ab-top-menu > li > .ab-item :not(.ab-icon):not(img) {
373 + line-height: var(--os-toolbar-line-height, var(--wp-admin--admin-bar--height, 32px));
374 +}
375 +
376 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop ul li .ab-item {
377 + line-height: 26px;
378 +}
379 +
380 +body.os-active.os-toolbar-themed #wpadminbar .ab-empty-item,
381 +body.os-active.os-toolbar-themed #wpadminbar a.ab-item,
382 +body.os-active.os-toolbar-themed #wpadminbar > #wp-toolbar span.ab-label,
383 +body.os-active.os-toolbar-themed #wpadminbar > #wp-toolbar span.noticon {
384 + color: var(--os-toolbar-fg, #f0f0f1);
385 +}
386 +
387 +body.os-active.os-toolbar-themed #wpadminbar .ab-icon::before,
388 +body.os-active.os-toolbar-themed #wpadminbar .ab-item::before,
389 +body.os-active.os-toolbar-themed #wpadminbar #adminbarsearch::before {
390 + color: var(--os-toolbar-icon, rgba(240, 246, 252, 0.6));
391 +}
392 +
393 +body.os-active.os-toolbar-themed #wpadminbar:not(.mobile) .ab-top-menu > li > .ab-item:focus,
394 +body.os-active.os-toolbar-themed #wpadminbar:not(.mobile) .ab-top-menu > li:hover > .ab-item,
395 +body.os-active.os-toolbar-themed #wpadminbar .ab-top-menu > li.hover > .ab-item {
396 + background: var(--os-toolbar-hover-bg, #2c3338);
397 + color: var(--os-toolbar-hover-fg, #72aee6);
398 +}
399 +
400 +body.os-active.os-toolbar-themed #wpadminbar:not(.mobile) > #wp-toolbar li:hover span.ab-label,
401 +body.os-active.os-toolbar-themed #wpadminbar > #wp-toolbar li.hover span.ab-label,
402 +body.os-active.os-toolbar-themed #wpadminbar li:hover .ab-icon::before,
403 +body.os-active.os-toolbar-themed #wpadminbar li:hover .ab-item::before,
404 +body.os-active.os-toolbar-themed #wpadminbar li.hover .ab-icon::before,
405 +body.os-active.os-toolbar-themed #wpadminbar li.hover .ab-item::before {
406 + color: var(--os-toolbar-hover-fg, #72aee6);
407 +}
408 +
409 +body.os-active.os-toolbar-themed #wpadminbar .menupop .ab-sub-wrapper {
410 + background: var(--os-toolbar-menu-bg, #2c3338);
411 + box-shadow: var(--os-toolbar-menu-shadow, 0 3px 5px rgba(0, 0, 0, 0.2));
412 +}
413 +
414 +/* The WordPress menu's lower group sits on a darker panel of its own. */
415 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop ul.ab-sub-secondary,
416 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop ul.ab-sub-secondary .ab-submenu {
417 + background: var(--os-toolbar-menu-bg, #3c434a);
418 +}
419 +
420 +body.os-active.os-toolbar-themed #wpadminbar .ab-submenu .ab-item,
421 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop ul li a,
422 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop ul li a strong {
423 + color: var(--os-toolbar-menu-fg, rgba(240, 246, 252, 0.7));
424 +}
425 +
426 +/*
427 + * The account menu's name lines. Core colours them by ID, which
428 + * out-ranks the rows above, so they are restated here, hover included.
429 + */
430 +body.os-active.os-toolbar-themed #wpadminbar #wp-admin-bar-user-info .display-name {
431 + color: var(--os-toolbar-menu-fg, #fff);
432 +}
433 +
434 +body.os-active.os-toolbar-themed #wpadminbar #wp-admin-bar-user-info .username {
435 + color: var(--os-toolbar-menu-fg-muted, #a7aaad);
436 +}
437 +
438 +body.os-active.os-toolbar-themed #wpadminbar #wp-admin-bar-user-info a:hover span,
439 +body.os-active.os-toolbar-themed #wpadminbar #wp-admin-bar-user-info a:focus span {
440 + color: var(--os-toolbar-menu-hover-fg, #72aee6);
441 +}
442 +
443 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop ul li a:hover,
444 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop ul li a:focus,
445 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop.hover ul li a:hover,
446 +body.os-active.os-toolbar-themed #wpadminbar .quicklinks .menupop.hover ul li a:focus {
447 + background: var(--os-toolbar-menu-hover-bg, transparent);
448 + color: var(--os-toolbar-menu-hover-fg, #72aee6);
449 +}
450 +
451 +/*
452 + * The time, for a theme that draws the toolbar as a menu bar
453 + * (`src/desktop-themes/toolbar-clock.ts`). It is not a control, so it
454 + * does not light up under the pointer.
455 + */
456 +body.os-active.os-toolbar-themed #wpadminbar #wp-admin-bar-os-clock {
457 + display: var(--os-toolbar-clock-display, none);
458 +}
459 +
460 +body.os-active.os-toolbar-themed #wpadminbar #wp-admin-bar-os-clock > .ab-item {
461 + background: none;
462 + color: var(--os-toolbar-fg, #f0f0f1);
463 +}
464 +
465 +/*
244 466 * Align the admin-bar W logo with the dock below it.
245 467 *
246 468 * Without this rule the admin bar's leftmost item (the WordPress W)
247 469 * renders at its Core-dictated ~44 px width, while the dock below it
@@ -249,17 +471,21 @@
249 471 * preference. The misalignment reads as "two disconnected left
250 472 * regions" — visually scruffy on an otherwise tight shell.
251 473 *
252 474 * We size the logo's <li> to match the dock's width and center the
253 - * W inside, so the two columns share an axis. `--desktop-mode-dock-width`
475 + * W inside, so the two columns share an axis. `--os-dock-width`
254 476 * is now set on :root (see settings.ts → apply()) so it reaches
255 - * siblings of #desktop-mode-shell like the admin bar.
477 + * siblings of #os-shell like the admin bar.
256 478 */
257 -body.desktop-mode-active #wpadminbar #wp-admin-bar-wp-logo {
258 - width: var( --desktop-mode-dock-width, 56px );
479 +body.os-active #wpadminbar #wp-admin-bar-menu-toggle {
480 + display: none;
259 481 }
260 482
261 -body.desktop-mode-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item {
483 +body.os-active #wpadminbar #wp-admin-bar-wp-logo {
484 + width: var( --os-dock-width, 56px );
485 +}
486 +
487 +body.os-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item {
262 488 display: flex !important;
263 489 align-items: center;
264 490 justify-content: center;
265 491 padding-inline: 0 !important;
@@ -269,19 +495,27 @@
269 495 * The W glyph is painted via a ::before pseudo on `.ab-icon`. Center
270 496 * it within the flex cell so varying dock widths don't leave it
271 497 * visually offset.
272 498 */
273 -body.desktop-mode-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item > .ab-icon {
499 +body.os-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item > .ab-icon {
274 500 margin: 0;
275 501 }
276 502
277 503 /*
504 + * Core nudges the W down 2px to centre it from the top of its cell.
505 + * The flex cell above already centres it, so the nudge would sit it low.
506 + */
507 +body.os-active #wpadminbar #wp-admin-bar-wp-logo > .ab-item > .ab-icon::before {
508 + top: 0;
509 +}
510 +
511 +/*
278 512 * Classic wpcontent carries a `margin-left: 160px` for the sidebar. With
279 513 * the sidebar hidden we let wpcontent fall back to flush — the shell is
280 514 * fixed-positioned anyway so this doesn't change the shell's layout, it
281 515 * just prevents a phantom sidebar gutter in any stray visible content.
282 516 */
283 -body.desktop-mode-active #wpcontent {
517 +body.os-active #wpcontent {
284 518 margin-inline-start: 0;
285 519 padding-inline-start: 0;
286 520 }
287 521
@@ -288,12 +522,26 @@
288 522 /*
289 523 * Desktop shell — fills the viewport below the admin bar.
290 524 * Uses position: fixed so it's unaffected by any containing-block
291 525 * quirks in #wpwrap/#wpcontent.
526 + *
527 + * The inset is the bar's MEASURED bottom edge, `--os-admin-bar-height`
528 + * (written on <html> by `src/admin-bar-height.ts`), with Core's own
529 + * token behind it for the first paint. Core's token is Core's promise
530 + * about Core's bar; a host can make the bar taller, give it a border
531 + * or push it down under a strip of its own — WordPress.com's staff
532 + * debug chrome does — and the shell then started under the bar by
533 + * exactly the difference. The measured token is absent whenever the
534 + * bar has no box at the top edge (hidden, mobile, solo, fullscreen,
535 + * parked in the dynamic mode), so every mode that rebinds Core's
536 + * token below still resolves through it.
292 537 */
293 -.desktop-mode-shell {
538 +.os-shell {
294 539 position: fixed;
295 - inset-block-start: var(--wp-admin--admin-bar--height, 32px);
540 + inset-block-start: var(
541 + --os-admin-bar-height,
542 + var(--wp-admin--admin-bar--height, 32px)
543 + );
296 544 inset-inline-start: 0;
297 545 inset-inline-end: 0;
298 546 inset-block-end: 0;
299 547 display: flex;
@@ -298,18 +546,33 @@
298 546 inset-block-end: 0;
299 547 display: flex;
300 548 flex-direction: column;
301 549 overflow: hidden;
302 - z-index: var(--desktop-mode-z-base);
550 + z-index: var(--os-z-base);
303 551 /*
552 + * Opaque floor. The wallpaper paints over this and is what anyone
553 + * actually sees — but the wallpaper is one layer among several,
554 + * and behind this shell is the classic admin page, which is WHITE.
555 + * Without a background here, any frame in which the wallpaper
556 + * doesn't paint flashes that white through the whole viewport.
557 + * See `--os-backstop`.
558 + */
559 + background: var(--os-backstop, #1d2327);
560 + /*
561 + * A bitmap face drawn on whole pixels wants plain greyscale
562 + * smoothing; the fallback is what the shell inherits from Core's
563 + * `#wpwrap` anyway.
564 + */
565 + -webkit-font-smoothing: var(--os-font-smoothing, subpixel-antialiased);
566 + /*
304 567 * Shell typeface. Every piece of chrome that doesn't set its own
305 568 * face inherits from here — dock labels, desktop icon labels,
306 - * widgets, the overview. Windows override with `--wpd-font` on
569 + * widgets, the overview. Windows override with `--os-ui-font` on
307 570 * their body, so a theme can run a display face on the desk and a
308 571 * text face inside windows. `inherit` is the no-theme value, i.e.
309 572 * identical to not declaring the property at all.
310 573 */
311 - font-family: var(--desktop-mode-font, inherit);
574 + font-family: var(--os-font, inherit);
312 575 }
313 576
314 577 /*
315 578 * Wallpaper layer — first child of the shell, sits behind the dock
@@ -314,9 +577,9 @@
314 577 /*
315 578 * Wallpaper layer — first child of the shell, sits behind the dock
316 579 * and desktop area so a translucent dock shows through to the
317 580 * background (macOS pattern). CSS wallpapers paint via the
318 - * `--desktop-mode-bg` custom property; canvas wallpapers mount their
581 + * `--os-bg` custom property; canvas wallpapers mount their
319 582 * own DOM (typically a <canvas>) into this element via JS.
320 583 *
321 584 * z-index: 0 keeps it below the shell body flex row which has no
322 585 * explicit z-index of its own but establishes its own local
@@ -322,28 +585,150 @@
322 585 * explicit z-index of its own but establishes its own local
323 586 * stacking context via `position: relative`, so the dock + windows
324 587 * always render above the wallpaper regardless of DOM order.
325 588 */
326 -.desktop-mode-wallpaper {
589 +.os-wallpaper {
327 590 position: absolute;
328 591 inset: 0;
329 592 z-index: 0;
330 - background: var(--desktop-mode-bg);
593 + background: var(--os-bg);
331 594 overflow: hidden;
332 595 pointer-events: none;
333 596 }
334 597
335 598 /*
599 + * Mio layer — the desk companion.
600 + *
601 + * A sibling of the wallpaper inside the shell, but at the far end of
602 + * the stack: Mio paints ABOVE windows (it perches on them)
603 + * and below the dock (it must never cover navigation). Created on
604 + * demand by `src/mio/controller.ts`. A shell whose user has never
605 + * switched Mio on has no extra element at all; switching it off
606 + * hides this one rather than removing it, because the instance is
607 + * parked (WebGL context intact) rather than destroyed — see the
608 + * `parked` field there.
609 + *
610 + * The layer and its <canvas> are permanently inert. Making a
611 + * full-shell canvas interactive — even conditionally, from a
612 + * per-frame hit test — would swallow clicks meant for the window
613 + * underneath. Only the small round `__handle` inside it takes
614 + * pointer events, and it rides on the blob.
615 + */
616 +.os-mio {
617 + position: absolute;
618 + inset: 0;
619 + z-index: var(--os-z-mio);
620 + overflow: hidden;
621 + pointer-events: none;
622 +}
623 +
624 +.os-mio__handle {
625 + position: absolute;
626 + /*
627 + * PHYSICAL `top` / `left`, deliberately — the one place in this
628 + * sheet that isn't logical. The handle is positioned every frame
629 + * by a `translate3d()` carrying the body's canvas coordinates,
630 + * and `transform` is always physical. Anchoring with
631 + * `inset-inline-start` would flip the origin to the right edge
632 + * under RTL while the translation kept pushing rightwards, and
633 + * the handle would walk off screen.
634 + */
635 + top: 0;
636 + left: 0;
637 + border-radius: 50%;
638 + pointer-events: auto;
639 + cursor: grab;
640 + /* Promote to its own layer: the handle is re-positioned via
641 + * `transform` on every animation frame. */
642 + will-change: transform;
643 + touch-action: none;
644 +}
645 +
646 +.os-mio__handle.is-dragging {
647 + cursor: grabbing;
648 +}
649 +
650 +/*
651 + * "Make it yours" panel — a live-preview dialog, not a modal.
652 + *
653 + * `<os-modal>` is built for the usual case: dim the page, blur it
654 + * slightly, put the decision in front of everything. Every one of
655 + * those defaults is wrong here, because the thing being edited is
656 + * ON the page and the whole point is watching it change.
657 + *
658 + * - No scrim and no `backdrop-filter`, or the companion the sliders
659 + * are driving is a blurred grey smudge behind them.
660 + * - `pointer-events: none` on the scrim so the desk stays live: Mio
661 + * can still be picked up and thrown while the panel is open, and a
662 + * click on the wallpaper doesn't dismiss the panel mid-adjustment.
663 + * `::part(dialog)` puts them back for the box itself.
664 + * - Parked against the inline end rather than centred, so it isn't
665 + * sitting on top of the subject.
666 + *
667 + * Mio never treats the panel as an obstacle: the collision set comes
668 + * from `getWallpaperSurfaces()`, which seeds windows, the shell floor,
669 + * docks and widget cards — a dialog on `document.body` is none of
670 + * those, so there is nothing to bump into.
671 + */
672 +.os-mio-panel {
673 + background: transparent;
674 + background-image: none;
675 + backdrop-filter: none;
676 + pointer-events: none;
677 + justify-content: flex-end;
678 + padding-inline-end: clamp(16px, 4vw, 64px);
679 +}
680 +
681 +/*
682 + * Both halves are required, and the second one is easy to miss.
683 + *
684 + * `::part(dialog)` reaches the box in the modal's SHADOW tree — its
685 + * header, title and close button inherit from there. But the controls
686 + * are slotted LIGHT-DOM children of `<os-modal>`, and slotted content
687 + * inherits from its light-DOM parent (the host) rather than from the
688 + * shadow ancestor it is projected into. So the host's
689 + * `pointer-events: none` reaches them regardless of what the part
690 + * says, and without this second rule every slider and button in the
691 + * panel is inert while looking perfectly normal.
692 + */
693 +.os-mio-panel::part(dialog),
694 +.os-mio-panel > * {
695 + pointer-events: auto;
696 +}
697 +
698 +/*
699 + * Overview (Exposé) hides the desk furniture so the window grid
700 + * reads cleanly — Mio goes with the widgets and the notes.
701 + * `:has()` rather than a descendant selector because Mio
702 + * layer is a sibling of `.os-shell__body`, not a child of
703 + * the desktop area.
704 + */
705 +.os-mio {
706 + transition: opacity 160ms ease;
707 +}
708 +
709 +.os-shell:has( .os-area--overview ) .os-mio {
710 + opacity: 0;
711 +}
712 +
713 +/*
714 + * Reduced motion is handled in the simulation, not here: the runtime
715 + * zeroes the idle float and the hue drift so Mio holds still
716 + * until the user actually interacts with it. Hiding it would be
717 + * wrong — the user switched it on deliberately.
718 + */
719 +
720 +/*
336 721 * Desktop-theme wallpaper texture (DESKTOP slot).
337 722 *
338 723 * On a `::before` rather than this element's own background, because
339 - * `--desktop-mode-bg` is set by the wallpaper picker and can be a
724 + * `--os-bg` is set by the wallpaper picker and can be a
340 725 * gradient — i.e. a background-IMAGE. Overriding `background-image`
341 726 * here would silently erase every gradient wallpaper the moment a
342 727 * theme was active. The pseudo-element layers instead of replacing.
343 728 *
344 729 * Resulting precedence, bottom to top:
345 - * CSS wallpaper (`--desktop-mode-bg`)
730 + * CSS wallpaper (`--os-bg`)
346 731 * → theme DESKTOP texture (this rule)
347 732 * → canvas wallpapers, which mount as CHILDREN of the layer and
348 733 * therefore paint above both.
349 734 *
@@ -350,17 +735,17 @@
350 735 * A theme that wants the user's wallpaper to remain visible ships a
351 736 * texture with transparency; an opaque one takes the desk over.
352 737 * With no theme active the image is `none` and this paints nothing.
353 738 */
354 -.desktop-mode-wallpaper::before {
739 +.os-wallpaper::before {
355 740 content: "";
356 741 position: absolute;
357 742 inset: 0;
358 743 pointer-events: none;
359 - background-image: var(--desktop-mode-desktop-image, none);
360 - background-repeat: var(--desktop-mode-desktop-image-repeat, repeat);
361 - background-size: var(--desktop-mode-desktop-image-size, auto);
362 - background-position: var(--desktop-mode-desktop-image-position, center);
744 + background-image: var(--os-desktop-image, none);
745 + background-repeat: var(--os-desktop-image-repeat, repeat);
746 + background-size: var(--os-desktop-image-size, auto);
747 + background-position: var(--os-desktop-image-position, center);
363 748 }
364 749
365 750 /*
366 751 * Canvas children fill the layer and ignore pointer events by
@@ -367,10 +752,10 @@
367 752 * default — a wallpaper is presentational chrome, not an interactive
368 753 * element. Plugins that genuinely need a clickable wallpaper can
369 754 * override `pointer-events` on their own mounted node.
370 755 */
371 -.desktop-mode-wallpaper > canvas,
372 -.desktop-mode-wallpaper > * {
756 +.os-wallpaper > canvas,
757 +.os-wallpaper > * {
373 758 width: 100%;
374 759 height: 100%;
375 760 display: block;
376 761 }
@@ -376,10 +761,13 @@
376 761 }
377 762
378 763 /* Below 783px WP collapses the admin bar to 46px. */
379 764 @media screen and (max-width: 782px) {
380 - .desktop-mode-shell {
381 - inset-block-start: var(--wp-admin--admin-bar--height, 46px);
765 + .os-shell {
766 + inset-block-start: var(
767 + --os-admin-bar-height,
768 + var(--wp-admin--admin-bar--height, 46px)
769 + );
382 770 }
383 771 }
384 772
385 773 /*
@@ -393,18 +781,18 @@
393 781 * The window's title bar remains visible because the window itself is
394 782 * position: fixed at top: 0, so the focus/exit-fullscreen button is
395 783 * still reachable.
396 784 */
397 -body.desktop-mode-has-fullscreen-window #wpadminbar {
785 +body.os-has-fullscreen-window #wpadminbar {
398 786 display: none !important;
399 787 }
400 788
401 -body.desktop-mode-has-fullscreen-window .desktop-mode-shell {
789 +body.os-has-fullscreen-window .os-shell {
402 790 inset-block-start: 0;
403 791 }
404 792
405 793 /* Shell body: contains dock + desktop area side by side. */
406 -.desktop-mode-shell__body {
794 +.os-shell__body {
407 795 flex: 1;
408 796 display: flex;
409 797 overflow: hidden;
410 798 position: relative;
@@ -415,18 +803,27 @@
415 803 * because the wallpaper renders in the shell-level layer behind
416 804 * both this area and the dock. Stacking context via position: relative
417 805 * ensures children (windows) sit above the wallpaper.
418 806 *
419 - * `padding-bottom` reserves room for the floating bottom dock pill
420 - * that every layout (Classic, Unified, Spatial) puts at the bottom
421 - * — 40px icons + 16px padding + 12px gap below + 8px breathing room.
807 + * The padding reserves the band the floating bottom dock pill covers.
808 + * It reads the work-area insets `src/work-area/index.ts` writes on
809 + * `#os-shell` from the pill's LIVE geometry, so it is 0 with a side
810 + * dock (which is a flex sibling and already narrows the area) and
811 + * tracks the dock-size preference with the pill at the bottom. The
812 + * `80px` literal is the pre-measurement floor: 40px tile + 16px pill
813 + * padding + 12px gap below + 8px breathing room, what the rule
814 + * hardcoded before the work area existed, in force until the shell
815 + * has measured once.
422 816 */
423 -.desktop-mode-area {
817 +.os-area {
424 818 flex: 1;
425 819 position: relative;
426 820 overflow: hidden;
427 821 background: transparent;
428 - padding-bottom: 80px;
822 + padding-top: var( --os-work-area-inset-top, 0px );
823 + padding-right: var( --os-work-area-inset-right, 0px );
824 + padding-bottom: var( --os-work-area-inset-bottom, 80px );
825 + padding-left: var( --os-work-area-inset-left, 0px );
429 826 transition: opacity 180ms ease;
430 827 }
431 828
432 829 /*
@@ -433,9 +830,9 @@
433 830 * Boot reveal gate.
434 831 *
435 832 * On F5 the shell paints in stages: the dispatcher's
436 833 * `repaintIcons()` lands the server-rendered wallpaper icons
437 - * synchronously from `desktopModeConfig.desktopIcons`, then the
834 + * synchronously from `openStationConfig.desktopIcons`, then the
438 835 * files-layer mounts and momentarily renders an empty bucket while
439 836 * REST `listPlacements(0)` is in flight, then folders/posts/links
440 837 * slot in on the next paint. The user reads that as "plugins show
441 838 * first, then everything blinks and the rest arrives."
@@ -448,9 +845,9 @@
448 845 *
449 846 * `desktop.ts` removes the class inside a `requestAnimationFrame`
450 847 * after the root files-layer's `hydrated` promise resolves (with a
451 848 * 2 s JS safety timeout). The opacity transition declared on
452 - * `.desktop-mode-area` smooths the reveal.
849 + * `.os-area` smooths the reveal.
453 850 *
454 851 * The fallback animation is a resilience layer for the case where
455 852 * the shell JS never runs (network error mid-load, ad-blocker
456 853 * tearing down a bundle, …). The animation holds the area
@@ -464,15 +861,15 @@
464 861 * a tile that's about to reposition once REST returns.
465 862 *
466 863 * @since 0.18.x
467 864 */
468 -.desktop-mode-area--booting {
865 +.os-area--booting {
469 866 opacity: 0;
470 867 pointer-events: none;
471 - animation: desktop-mode-area-boot-fallback 3s forwards;
868 + animation: os-area-boot-fallback 3s forwards;
472 869 }
473 870
474 -@keyframes desktop-mode-area-boot-fallback {
871 +@keyframes os-area-boot-fallback {
475 872 0%,
476 873 90% {
477 874 opacity: 0;
478 875 pointer-events: none;
@@ -496,14 +893,25 @@
496 893 * click) while individual tiles re-enable pointer events so they
497 894 * remain clickable.
498 895 * --------------------------------------------------------------- */
499 896
500 -.desktop-mode-icons {
897 +.os-icons {
501 898 position: absolute;
502 - inset-block-start: 16px;
503 - inset-block-end: 16px;
504 - inset-inline-start: 16px;
505 - inset-inline-end: 16px;
899 + /*
900 + * 16px of gutter inside the WORK AREA, not inside the desktop
901 + * area: an absolutely positioned box ignores its parent's
902 + * padding, so without the insets the grid's last row sat under
903 + * the bottom dock pill. Same tokens `.os-area` reserves with.
904 + *
905 + * Physical `left` / `right`, not the logical pair: the insets are
906 + * measured from rects, so `-left` is the visual left whichever
907 + * way the locale reads. The gutter is 16px on both sides, so
908 + * nothing is lost by going physical here.
909 + */
910 + top: calc( var( --os-work-area-inset-top, 0px ) + 16px );
911 + bottom: calc( var( --os-work-area-inset-bottom, 80px ) + 16px );
912 + left: calc( var( --os-work-area-inset-left, 0px ) + 16px );
913 + right: calc( var( --os-work-area-inset-right, 0px ) + 16px );
506 914 display: grid;
507 915 /*
508 916 * Column-major flow: icons fill the first column top-to-bottom,
509 917 * then start the next column. Matches the macOS / Finder
@@ -533,9 +941,9 @@
533 941 */
534 942 overflow: auto;
535 943 }
536 944
537 -.desktop-mode-icon {
945 +.os-icon {
538 946 pointer-events: auto;
539 947 display: flex;
540 948 flex-direction: column;
541 949 align-items: center;
@@ -550,25 +958,26 @@
550 958 * Painted under the icon image and its label, so a theme can give
551 959 * desktop shortcuts a physical tile the way a phone home screen
552 960 * does. Unset = `none` = the transparent tile we always had.
553 961 */
554 - background-image: var(--desktop-mode-tile-image, none);
555 - background-repeat: var(--desktop-mode-tile-image-repeat, no-repeat);
556 - background-size: var(--desktop-mode-tile-image-size, auto);
557 - background-position: var(--desktop-mode-tile-image-position, center);
558 - color: var(--desktop-mode-fg, #fff);
962 + background-image: var(--os-tile-image, none);
963 + background-repeat: var(--os-tile-image-repeat, no-repeat);
964 + background-size: var(--os-tile-image-size, auto);
965 + background-position: var(--os-tile-image-position, center);
966 + /* Also the glyph: silhouette art is masked with `currentColor`. */
967 + color: var(--os-desk-fg, var(--os-fg, #fff));
559 968 cursor: pointer;
560 969 border-radius: 8px;
561 970 transition: background-color 0.15s ease;
562 - /* Positioning context for `.desktop-mode-icon__badge`. */
971 + /* Positioning context for `.os-icon__badge`. */
563 972 position: relative;
564 973 }
565 974
566 975 /*
567 - * Icon badge — symmetric to `.desktop-mode-dock__badge` but anchored
976 + * Icon badge — symmetric to `.os-dock__badge` but anchored
568 977 * to the wallpaper-icon image. Same gradient, same ring, slightly
569 978 * larger so it reads at the longer wallpaper viewing distance.
570 - * Painted by `wp.desktop.icons.setBadge( id, count )` — see
979 + * Painted by `wp.os.icons.setBadge( id, count )` — see
571 980 * `src/desktop-icons.ts` for the imperative API and
572 981 * `src/recycle-bin/badge.ts` for the canonical multi-rail
573 982 * consumer. The class name is part of the stable contract; the
574 983 * positioning, color, and shadow are not — themes or future
@@ -573,9 +982,9 @@
573 982 * consumer. The class name is part of the stable contract; the
574 983 * positioning, color, and shadow are not — themes or future
575 984 * shell builds may restyle freely.
576 985 */
577 -.desktop-mode-icon__badge {
986 +.os-icon__badge {
578 987 position: absolute;
579 988 top: 4px;
580 989 inset-inline-end: 14px;
581 990 /* Same derivation as the dock badge, sized off the wallpaper icon
@@ -580,33 +989,33 @@
580 989 inset-inline-end: 14px;
581 990 /* Same derivation as the dock badge, sized off the wallpaper icon
582 991 * rather than the dock icon. The default resolves to 18px / 11px —
583 992 * exactly the old values. */
584 - --desktop-mode-icon-badge-computed-size: var(
585 - --desktop-mode-icon-badge-size,
586 - calc( var( --desktop-mode-icon-image-size, 48px ) * 0.375 )
993 + --os-icon-badge-computed-size: var(
994 + --os-icon-badge-size,
995 + calc( var( --os-icon-image-size, 48px ) * 0.375 )
587 996 );
588 - min-width: var( --desktop-mode-icon-badge-computed-size );
589 - height: var( --desktop-mode-icon-badge-computed-size );
997 + min-width: var( --os-icon-badge-computed-size );
998 + height: var( --os-icon-badge-computed-size );
590 999 padding: 0 var(
591 - --desktop-mode-icon-badge-padding,
592 - calc( var( --desktop-mode-icon-badge-computed-size ) * 0.28 )
1000 + --os-icon-badge-padding,
1001 + calc( var( --os-icon-badge-computed-size ) * 0.28 )
593 1002 );
594 1003 box-sizing: border-box;
595 1004 border-radius: 999px;
596 1005 background: var(
597 - --desktop-mode-icon-badge-bg,
598 - linear-gradient( 180deg, #ff5a5a 0%, var( --wpd-danger, #d63638 ) 100% )
1006 + --os-icon-badge-bg,
1007 + linear-gradient( 180deg, #ff5a5a 0%, var( --os-ui-danger, #d63638 ) 100% )
599 1008 );
600 - color: var( --desktop-mode-icon-badge-fg, var( --wpd-fg-on-accent, #fff ) );
1009 + color: var( --os-icon-badge-fg, var( --os-ui-fg-on-accent, #fff ) );
601 1010 display: inline-flex;
602 1011 align-items: center;
603 1012 justify-content: center;
604 1013 font-size: var(
605 - --desktop-mode-icon-badge-font-size,
606 - calc( var( --desktop-mode-icon-badge-computed-size ) * 0.611 )
1014 + --os-icon-badge-font-size,
1015 + calc( var( --os-icon-badge-computed-size ) * 0.611 )
607 1016 );
608 - font-weight: var( --desktop-mode-icon-badge-font-weight, 700 );
1017 + font-weight: var( --os-icon-badge-font-weight, 700 );
609 1018 line-height: 1;
610 1019 font-variant-numeric: tabular-nums;
611 1020 box-shadow:
612 1021 0 0 0 1.5px rgba( 0, 0, 0, 0.4 ),
@@ -613,230 +1022,56 @@
613 1022 0 1px 3px rgba( 0, 0, 0, 0.35 );
614 1023 pointer-events: none;
615 1024 }
616 1025
617 -.desktop-mode-icon[ data-icon-id="desktop-mode-recycle-bin" ] > .desktop-mode-icon__badge {
618 - /*
619 - * The bin's count is tucked ONTO the lid rather than floating off
620 - * the corner, so it gets its own smaller geometry. Every value
621 - * derives from the icon image size, which keeps the badge on the
622 - * lid when a theme scales wallpaper icons — hardcoded offsets slid
623 - * off the artwork entirely.
624 - *
625 - * Override chain: bin-specific token → the generic icon-badge
626 - * token (so a theme that says "badges are 22px" gets the bin too)
627 - * → the derivation. All three resolve to the historical 15px / 9px
628 - * at the default 48px icon.
629 - */
630 - --desktop-mode-recycle-badge-computed-size: var(
631 - --desktop-mode-recycle-badge-size,
632 - var(
633 - --desktop-mode-icon-badge-size,
634 - calc( var( --desktop-mode-icon-image-size, 48px ) * 0.3125 )
635 - )
636 - );
637 - top: calc( var( --desktop-mode-icon-image-size, 48px ) * 0.8125 );
638 - inset-inline-end: calc( var( --desktop-mode-icon-image-size, 48px ) * 0.4167 );
639 - min-width: var( --desktop-mode-recycle-badge-computed-size );
640 - height: var( --desktop-mode-recycle-badge-computed-size );
641 - padding: 0 calc( var( --desktop-mode-recycle-badge-computed-size ) * 0.267 );
642 - border-radius: 999px;
643 - background: var( --desktop-mode-recycle-badge-bg, rgba( 29, 35, 39, 0.74 ) );
644 - color: var( --desktop-mode-recycle-badge-fg, rgba( 255, 255, 255, 0.96 ) );
645 - font-size: var(
646 - --desktop-mode-recycle-badge-font-size,
647 - var(
648 - --desktop-mode-icon-badge-font-size,
649 - calc( var( --desktop-mode-recycle-badge-computed-size ) * 0.6 )
650 - )
651 - );
652 - font-weight: 700;
653 - letter-spacing: 0;
654 - box-shadow:
655 - inset 0 0 0 1px rgba( 255, 255, 255, 0.28 ),
656 - 0 1px 2px rgba( 0, 0, 0, 0.28 );
657 -}
658 -
659 -.desktop-mode-icon:hover,
660 -.desktop-mode-icon:focus-visible {
1026 +.os-icon:hover,
1027 +.os-icon:focus-visible {
661 1028 /* background-COLOR, not the shorthand: the shorthand would reset
662 1029 * the ICON_TILE texture declared above, so a themed tile would
663 1030 * vanish on hover. */
664 - background-color: rgba(255, 255, 255, 0.12);
1031 + background-color: var(--os-icon-hover-bg, rgba(255, 255, 255, 0.12));
665 1032 outline: none;
666 1033 }
667 1034
668 -.desktop-mode-icon:focus-visible {
1035 +.os-icon:focus-visible {
669 1036 box-shadow: 0 0 0 2px var(--wp-admin-theme-color, #2271b1);
670 1037 }
671 1038
672 -.desktop-mode-icon__image {
1039 +.os-icon__image {
673 1040 display: inline-flex;
674 1041 align-items: center;
675 1042 justify-content: center;
676 1043 /* Tokenized so a theme can scale wallpaper icons — and so the
677 1044 * badge above has something to derive its own size from. */
678 - width: var( --desktop-mode-icon-image-size, 48px );
679 - height: var( --desktop-mode-icon-image-size, 48px );
680 - font-size: calc( var( --desktop-mode-icon-image-size, 48px ) * 0.667 );
1045 + width: var( --os-icon-image-size, 48px );
1046 + height: var( --os-icon-image-size, 48px );
1047 + font-size: calc( var( --os-icon-image-size, 48px ) * 0.667 );
681 1048 line-height: 1;
682 1049 }
683 1050
684 -.desktop-mode-icon__image img {
1051 +.os-icon__image img {
685 1052 max-width: 48px;
686 1053 max-height: 48px;
687 1054 object-fit: contain;
688 1055 }
689 1056
690 -.desktop-mode-icon__label {
691 - font-size: 12px;
1057 +.os-icon__image {
1058 + filter: var(--os-icon-glyph-shadow, none);
1059 +}
1060 +
1061 +.os-icon__label {
1062 + font-size: var(--os-icon-label-font-size, 12px);
692 1063 text-align: center;
693 - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
1064 + background: var(--os-icon-label-bg, transparent);
1065 + border-radius: var(--os-icon-label-radius, 6px);
1066 + /* Inline only: a two-line caption already fills the 96px grid row. */
1067 + padding: var(--os-icon-label-padding, 0 6px);
1068 + text-shadow: var(--os-icon-label-shadow, 0 1px 2px rgba(0, 0, 0, 0.45));
694 1069 line-height: 1.2;
695 1070 word-break: break-word;
696 1071 }
697 1072
698 1073 /* ---------------------------------------------------------------
699 - * Sticky notes — Gutenberg `wp_guideline` artifacts tagged with the
700 - * sticky guideline type. Notes sit above wallpaper icons/widgets and
701 - * below windows, so they behave like desktop objects rather than app
702 - * chrome.
703 - * --------------------------------------------------------------- */
704 -
705 -.desktop-mode-sticky-notes {
706 - position: absolute;
707 - inset: 0;
708 - z-index: 2;
709 - pointer-events: none;
710 - transition:
711 - opacity 0.22s ease,
712 - transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
713 -}
714 -
715 -.desktop-mode-area--overview .desktop-mode-sticky-notes {
716 - opacity: 0;
717 - transform: translateY( 12px );
718 - pointer-events: none;
719 -}
720 -
721 -.desktop-mode-sticky-note {
722 - position: absolute;
723 - display: flex;
724 - flex-direction: column;
725 - pointer-events: auto;
726 - background: #fff09a;
727 - color: #2a2513;
728 - border: 1px solid rgba( 84, 67, 0, 0.24 );
729 - border-radius: 4px;
730 - box-shadow:
731 - 0 12px 28px rgba( 0, 0, 0, 0.22 ),
732 - 0 2px 5px rgba( 0, 0, 0, 0.16 ),
733 - inset 0 1px 0 rgba( 255, 255, 255, 0.55 );
734 - overflow: hidden;
735 - resize: both;
736 -}
737 -
738 -.desktop-mode-sticky-note--dragging {
739 - opacity: 0.92;
740 - resize: none;
741 - user-select: none;
742 -}
743 -
744 -.desktop-mode-sticky-note__header {
745 - display: flex;
746 - align-items: center;
747 - gap: 5px;
748 - min-height: 30px;
749 - padding: 4px 6px 4px 8px;
750 - background: rgba( 82, 64, 0, 0.06 );
751 - border-bottom: 1px solid rgba( 82, 64, 0, 0.14 );
752 - cursor: grab;
753 - touch-action: none;
754 - user-select: none;
755 -}
756 -
757 -.desktop-mode-sticky-note--dragging .desktop-mode-sticky-note__header {
758 - cursor: grabbing;
759 -}
760 -
761 -.desktop-mode-sticky-note__grip {
762 - flex: 0 0 auto;
763 - width: 8px;
764 - height: 14px;
765 - background-image: radial-gradient(
766 - circle,
767 - rgba( 60, 48, 0, 0.5 ) 1.2px,
768 - transparent 1.5px
769 - );
770 - background-size: 4px 4px;
771 - background-position: 0 1px;
772 - background-repeat: space;
773 - opacity: 0.42;
774 -}
775 -
776 -.desktop-mode-sticky-note__title {
777 - flex: 1;
778 - min-width: 0;
779 - font-size: 11px;
780 - font-weight: 650;
781 - overflow: hidden;
782 - text-overflow: ellipsis;
783 - white-space: nowrap;
784 -}
785 -
786 -.desktop-mode-sticky-note__status {
787 - flex: 0 0 auto;
788 -}
789 -
790 -.desktop-mode-sticky-note wpd-window-button {
791 - --wpd-btn-color: rgba( 42, 37, 19, 0.7 );
792 - --wpd-btn-color-hover: #2a2513;
793 - --wpd-btn-bg-hover: rgba( 42, 37, 19, 0.1 );
794 - --wpd-btn-bg-active: rgba( 42, 37, 19, 0.16 );
795 - --wpd-btn-danger-hover: rgba( 214, 54, 56, 0.18 );
796 - --wpd-btn-outline: var( --wp-admin-theme-color, #2271b1 );
797 -}
798 -
799 -.desktop-mode-sticky-note__open.is-disabled {
800 - opacity: 0.34;
801 - pointer-events: none;
802 -}
803 -
804 -.desktop-mode-sticky-note__editor {
805 - flex: 1;
806 - min-height: 0;
807 - --desktop-mode-window-bg: transparent;
808 - --wpd-border: transparent;
809 - --wpd-fg: #2a2513;
810 - --wpd-fg-muted: rgba( 42, 37, 19, 0.62 );
811 - font-family:
812 - -apple-system,
813 - BlinkMacSystemFont,
814 - "Segoe UI",
815 - sans-serif;
816 -}
817 -
818 -.desktop-mode-sticky-note__editor::part(textarea) {
819 - height: 100%;
820 - min-height: 100%;
821 - padding: 10px 12px 12px;
822 - background: transparent;
823 - border: 0;
824 - border-radius: 0;
825 - box-shadow: none;
826 - color: #2a2513;
827 - font-size: 13px;
828 - line-height: 1.4;
829 - resize: none;
830 -}
831 -
832 -.desktop-mode-sticky-note__editor::part(textarea):hover,
833 -.desktop-mode-sticky-note__editor::part(textarea):focus-visible {
834 - border-color: transparent;
835 - box-shadow: none;
836 -}
837 -
838 -/* ---------------------------------------------------------------
839 1074 * Widgets column — right-edge glass strip that paints above the
840 1075 * wallpaper but beneath windows. Hosts stacked widget cards and a
841 1076 * trailing "Add widget" tile. Interactive only on its own cards +
842 1077 * the add tile; every other pixel is pointer-transparent so window
@@ -842,15 +1077,39 @@
842 1077 * the add tile; every other pixel is pointer-transparent so window
843 1078 * drag / resize / click-through behaviour is unaffected.
844 1079 * --------------------------------------------------------------- */
845 1080
846 -.desktop-mode-widgets {
847 - border-radius: 14px;
1081 +.os-widgets {
848 1082 position: absolute;
849 - top: 16px;
850 - bottom: 16px;
851 - inset-inline-end: 16px;
1083 + /*
1084 + * Inside the work area vertically — see `.os-icons` for why the
1085 + * insets are here. The inline edge is logical: the column hugs
1086 + * the trailing side, while the side insets are physical; and a
1087 + * side rail never claims one anyway (it is a flex sibling, the
1088 + * area is already narrower).
1089 + *
1090 + * The cards sit 16px in from the area's top and trailing edges
1091 + * and 16px above the work area's foot, but the box reaches
1092 + * further than that and pads back in. The column scrolls, and a
1093 + * scroll container clips everything inside it to its padding
1094 + * box, the cards' drop shadow included: with the box hugging the
1095 + * cards, the shadow ended in a hard rectangle the size of the
1096 + * column, easy to see on a light wallpaper. The padding is the
1097 + * room the shadow needs (it reaches 28px out and 36px down, more
1098 + * on hover). The trailing side only gets the 16px to the area's
1099 + * edge, since the area clips there anyway. The column is
1100 + * pointer-transparent, so the extra room catches nothing; the
1101 + * add-widget pill measures the content box, not this one.
1102 + */
1103 + box-sizing: content-box;
1104 + top: var( --os-work-area-inset-top, 0px );
1105 + bottom: calc( var( --os-work-area-inset-bottom, 80px ) - 24px );
1106 + inset-inline-end: 0;
852 1107 width: 320px;
1108 + --_os-widgets-room-start: 40px;
1109 + --_os-widgets-room-end: 16px;
1110 + padding-block: 16px 40px;
1111 + padding-inline: var( --_os-widgets-room-start ) var( --_os-widgets-room-end );
853 1112 display: flex;
854 1113 flex-direction: column;
855 1114 gap: 12px;
856 1115 z-index: 1;
@@ -868,9 +1127,9 @@
868 1127 opacity 0.22s ease,
869 1128 transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
870 1129 }
871 1130
872 -.desktop-mode-area--overview .desktop-mode-widgets {
1131 +.os-area--overview .os-widgets {
873 1132 opacity: 0;
874 1133 /* Slight slide-off-right so the fade reads as "the column is
875 1134 * stepping back out of the way" rather than flat-dimming. */
876 1135 transform: translateX( 16px );
@@ -876,9 +1135,9 @@
876 1135 transform: translateX( 16px );
877 1136 pointer-events: none;
878 1137 }
879 1138
880 -.desktop-mode-widgets__list {
1139 +.os-widgets__list {
881 1140 display: flex;
882 1141 flex-direction: column;
883 1142 gap: 12px;
884 1143 padding: 4px;
@@ -890,9 +1149,9 @@
890 1149 * content + the remove X land; the column itself stays
891 1150 * pointer-transparent so a drag that grazes the column's margin
892 1151 * falls through to the window beneath.
893 1152 */
894 -.desktop-mode-widgets__card {
1153 +.os-widgets__card {
895 1154 position: relative;
896 1155 padding: 0;
897 1156 /* Column layout so the body can flex-shrink and scroll when the
898 1157 * frame writes a fixed inline height (floating / resized cards).
@@ -900,25 +1159,35 @@
900 1159 * spills past the card's rounded bottom edge. */
901 1160 display: flex;
902 1161 flex-direction: column;
903 1162 pointer-events: auto;
904 - background-color: rgba( 20, 20, 22, 0.55 );
905 1163 /*
1164 + * The glass is also `--os-ui-color-surface`, the one name in the
1165 + * widget contract that describes the card itself. Declared in
1166 + * `variables.css` so a widget reading it and the card painting it
1167 + * cannot drift apart.
1168 + */
1169 + background-color: var( --os-ui-color-surface, rgba( 20, 20, 22, 0.55 ) );
1170 + /*
906 1171 * WIDGET texture slot — layered over the frosted card colour, so a
907 1172 * translucent texture still gets the blur underneath.
908 1173 */
909 - background-image: var( --desktop-mode-widget-image, none );
910 - background-repeat: var( --desktop-mode-widget-image-repeat, repeat );
911 - background-size: var( --desktop-mode-widget-image-size, auto );
912 - background-position: var( --desktop-mode-widget-image-position, center );
913 - backdrop-filter: blur( 18px ) saturate( 140% );
914 - -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
915 - border: 1px solid rgba( 255, 255, 255, 0.08 );
916 - border-radius: 14px;
917 - color: var( --wpd-fg-on-accent, #fff );
918 - box-shadow:
1174 + background-image: var( --os-widget-image, none );
1175 + background-repeat: var( --os-widget-image-repeat, repeat );
1176 + background-size: var( --os-widget-image-size, auto );
1177 + background-position: var( --os-widget-image-position, center );
1178 + backdrop-filter: var( --os-widget-card-backdrop, blur( 18px ) saturate( 140% ) );
1179 + -webkit-backdrop-filter: var( --os-widget-card-backdrop, blur( 18px ) saturate( 140% ) );
1180 + border: var( --os-widget-card-border, 1px solid rgba( 255, 255, 255, 0.08 ) );
1181 + border-radius: var( --os-widget-card-radius, 14px );
1182 + /* The widget contract's text colour, which is what a widget's own
1183 + * stylesheet is told to read. */
1184 + color: var( --os-ui-color-text, var( --os-ui-fg-on-accent, #fff ) );
1185 + box-shadow: var(
1186 + --os-widget-card-shadow,
919 1187 0 8px 28px rgba( 0, 0, 0, 0.35 ),
920 - inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
1188 + inset 0 0 0 1px rgba( 255, 255, 255, 0.04 )
1189 + );
921 1190 transition:
922 1191 box-shadow 0.18s ease,
923 1192 opacity 0.18s ease;
924 1193 }
@@ -925,15 +1194,17 @@
925 1194
926 1195 /* Hover feedback is shadow-only — no transform. Cards must feel
927 1196 * anchored; a hover nudge reads as the widget drifting under the
928 1197 * pointer (and fought the user's chosen position on floating cards). */
929 -.desktop-mode-widgets__card:hover {
930 - box-shadow:
1198 +.os-widgets__card:hover {
1199 + box-shadow: var(
1200 + --os-widget-card-shadow-hover,
931 1201 0 12px 36px rgba( 0, 0, 0, 0.45 ),
932 - inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
1202 + inset 0 0 0 1px rgba( 255, 255, 255, 0.08 )
1203 + );
933 1204 }
934 1205
935 -.desktop-mode-widgets__card-body {
1206 +.os-widgets__card-body {
936 1207 padding: 16px;
937 1208 min-height: 48px;
938 1209 flex: 1 1 auto;
939 1210 overflow-x: hidden;
@@ -940,20 +1211,22 @@
940 1211 overflow-y: auto;
941 1212 scrollbar-width: thin;
942 1213 }
943 1214
944 -.desktop-mode-widgets__card-close {
1215 +.os-widgets__card-close {
945 1216 position: absolute;
946 1217 top: 6px;
947 1218 inset-inline-end: 6px;
948 - width: 22px;
949 - height: 22px;
1219 + /* 24px is the WCAG 2.2 SC 2.5.8 floor. The glyph inside is unchanged;
1220 + only the target grows. */
1221 + width: 24px;
1222 + height: 24px;
950 1223 display: flex;
951 1224 align-items: center;
952 1225 justify-content: center;
953 1226 padding: 0;
954 1227 border: 0;
955 - background: var( --wpd-scrim, rgba( 0, 0, 0, 0.55 ) );
1228 + background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
956 1229 color: rgba( 255, 255, 255, 0.75 );
957 1230 border-radius: 50%;
958 1231 cursor: pointer;
959 1232 opacity: 0;
@@ -962,18 +1235,31 @@
962 1235 background-color 0.15s ease,
963 1236 color 0.15s ease;
964 1237 }
965 1238
966 -.desktop-mode-widgets__card:hover .desktop-mode-widgets__card-close,
967 -.desktop-mode-widgets__card-close:focus-visible {
1239 +.os-widgets__card:hover .os-widgets__card-close,
1240 +.os-widgets__card-close:focus-visible {
968 1241 opacity: 1;
969 1242 }
970 1243
971 -.desktop-mode-widgets__card-close:hover {
972 - background: var( --wpd-danger, #d63638 );
973 - color: var( --wpd-fg-on-accent, #fff );
1244 +/*
1245 + * The corner close button rests at `opacity: 0` and only the card's
1246 + * hover reveals it, so on touch it is invisible and the widget cannot
1247 + * be dismissed at all. Rest it at the same 0.7 the chrome header's
1248 + * close button uses there, rather than a full 1, so a card that is
1249 + * only being read does not wear a hard × in its corner.
1250 + */
1251 +@media ( hover: none ) {
1252 + .os-widgets__card-close {
1253 + opacity: 0.7;
1254 + }
974 1255 }
975 1256
1257 +.os-widgets__card-close:hover {
1258 + background: var( --os-ui-danger, #d63638 );
1259 + color: var( --os-ui-fg-on-accent, #fff );
1260 +}
1261 +
976 1262 /*
977 1263 * Movable widgets — a thin chrome header at the top of the card holds
978 1264 * the drag grip, the widget's own label, and the × button. The whole
979 1265 * header is the drag handle (except for interactive children — those
@@ -979,9 +1265,9 @@
979 1265 * header is the drag handle (except for interactive children — those
980 1266 * remain clickable as normal). Non-movable widgets keep today's
981 1267 * corner-× layout unchanged.
982 1268 */
983 -.desktop-mode-widgets__chrome {
1269 +.os-widgets__chrome {
984 1270 display: flex;
985 1271 flex-shrink: 0;
986 1272 align-items: center;
987 1273 gap: 8px;
@@ -992,13 +1278,13 @@
992 1278 -webkit-user-select: none;
993 1279 touch-action: none;
994 1280 }
995 1281
996 -.desktop-mode-widgets__card--dragging .desktop-mode-widgets__chrome {
1282 +.os-widgets__card--dragging .os-widgets__chrome {
997 1283 cursor: grabbing;
998 1284 }
999 1285
1000 -.desktop-mode-widgets__grip {
1286 +.os-widgets__grip {
1001 1287 flex-shrink: 0;
1002 1288 width: 10px;
1003 1289 height: 16px;
1004 1290 background-image: radial-gradient(
@@ -1012,13 +1298,13 @@
1012 1298 opacity: 0.45;
1013 1299 transition: opacity 0.12s ease;
1014 1300 }
1015 1301
1016 -.desktop-mode-widgets__card:hover .desktop-mode-widgets__grip {
1302 +.os-widgets__card:hover .os-widgets__grip {
1017 1303 opacity: 0.85;
1018 1304 }
1019 1305
1020 -.desktop-mode-widgets__title {
1306 +.os-widgets__title {
1021 1307 flex: 1;
1022 1308 font-size: 12px;
1023 1309 font-weight: 500;
1024 1310 letter-spacing: 0.01em;
@@ -1029,17 +1315,17 @@
1029 1315 }
1030 1316
1031 1317 /* When the close button sits inside the chrome it's inline, not
1032 1318 corner-absolute — reset position + always-visible opacity. */
1033 -.desktop-mode-widgets__chrome .desktop-mode-widgets__card-close {
1319 +.os-widgets__chrome .os-widgets__card-close {
1034 1320 position: static;
1035 1321 opacity: 0.7;
1036 - width: 20px;
1037 - height: 20px;
1322 + width: 24px;
1323 + height: 24px;
1038 1324 }
1039 1325
1040 -.desktop-mode-widgets__chrome .desktop-mode-widgets__card-close:hover,
1041 -.desktop-mode-widgets__chrome .desktop-mode-widgets__card-close:focus-visible {
1326 +.os-widgets__chrome .os-widgets__card-close:hover,
1327 +.os-widgets__chrome .os-widgets__card-close:focus-visible {
1042 1328 opacity: 1;
1043 1329 }
1044 1330
1045 1331 /*
@@ -1049,23 +1335,24 @@
1049 1335 * expose it (tapping it on a docked card would be a confusing
1050 1336 * no-op). The DOM node stays mounted either way so state flips
1051 1337 * don't rebuild chrome.
1052 1338 *
1053 - * Styling mirrors the inline close button: 20×20 round tile,
1339 + * Styling mirrors the inline close button: 24×24 round tile
1340 + * (the WCAG 2.2 SC 2.5.8 target-size floor),
1054 1341 * dimmed by default, full-opacity on hover / focus. Distinct hover
1055 1342 * color (theme accent, not red) signals "put back" vs. "remove."
1056 1343 */
1057 -.desktop-mode-widgets__card-redock {
1344 +.os-widgets__card-redock {
1058 1345 display: none;
1059 1346 align-items: center;
1060 1347 justify-content: center;
1061 1348 padding: 0;
1062 1349 border: 0;
1063 - background: var( --wpd-scrim, rgba( 0, 0, 0, 0.55 ) );
1350 + background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
1064 1351 color: rgba( 255, 255, 255, 0.75 );
1065 1352 border-radius: 50%;
1066 - width: 20px;
1067 - height: 20px;
1353 + width: 24px;
1354 + height: 24px;
1068 1355 cursor: pointer;
1069 1356 opacity: 0.7;
1070 1357 transition:
1071 1358 opacity 0.15s ease,
@@ -1072,17 +1359,17 @@
1072 1359 background-color 0.15s ease,
1073 1360 color 0.15s ease;
1074 1361 }
1075 1362
1076 -.desktop-mode-widgets__card--floating .desktop-mode-widgets__card-redock {
1363 +.os-widgets__card--floating .os-widgets__card-redock {
1077 1364 display: flex;
1078 1365 }
1079 1366
1080 -.desktop-mode-widgets__card-redock:hover,
1081 -.desktop-mode-widgets__card-redock:focus-visible {
1367 +.os-widgets__card-redock:hover,
1368 +.os-widgets__card-redock:focus-visible {
1082 1369 opacity: 1;
1083 1370 background: var( --wp-admin-theme-color, #2271b1 );
1084 - color: var( --wpd-fg-on-accent, #fff );
1371 + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) );
1085 1372 }
1086 1373
1087 1374 /*
1088 1375 * Floating widgets — absolute-positioned, inline top/left/width/height
@@ -1087,15 +1374,25 @@
1087 1374 /*
1088 1375 * Floating widgets — absolute-positioned, inline top/left/width/height
1089 1376 * written by the frame.
1090 1377 */
1091 -.desktop-mode-widgets__card--floating {
1378 +.os-widgets__card--floating {
1092 1379 position: absolute;
1093 1380 margin: 0;
1094 1381 }
1095 1382
1096 -.desktop-mode-widgets__card--dragging,
1097 -.desktop-mode-widgets__card--resizing {
1383 +/*
1384 + * A liberated card is reparented to the desktop area, so the column's
1385 + * overview fade above no longer reaches it. Direct children only: a
1386 + * card inside a window body belongs to that window's thumbnail.
1387 + */
1388 +.os-area--overview > .os-widgets__card--floating {
1389 + opacity: 0;
1390 + pointer-events: none;
1391 +}
1392 +
1393 +.os-widgets__card--dragging,
1394 +.os-widgets__card--resizing {
1098 1395 transition: none;
1099 1396 z-index: 2;
1100 1397 }
1101 1398
@@ -1106,14 +1403,14 @@
1106 1403 * every handle; a non-movable widget ignores everything except the
1107 1404 * `s` (bottom-edge, height-only) handle at the pointer layer, so we
1108 1405 * visually hide the mismatched handles via the modifier class.
1109 1406 */
1110 -.desktop-mode-widgets__resize {
1407 +.os-widgets__resize {
1111 1408 position: absolute;
1112 1409 z-index: 1;
1113 1410 }
1114 1411
1115 -.desktop-mode-widgets__resize--n {
1412 +.os-widgets__resize--n {
1116 1413 top: -3px;
1117 1414 left: 8px;
1118 1415 right: 8px;
1119 1416 height: 6px;
@@ -1119,9 +1416,9 @@
1119 1416 height: 6px;
1120 1417 cursor: ns-resize;
1121 1418 }
1122 1419
1123 -.desktop-mode-widgets__resize--s {
1420 +.os-widgets__resize--s {
1124 1421 bottom: -3px;
1125 1422 left: 8px;
1126 1423 right: 8px;
1127 1424 height: 6px;
@@ -1127,9 +1424,9 @@
1127 1424 height: 6px;
1128 1425 cursor: ns-resize;
1129 1426 }
1130 1427
1131 -.desktop-mode-widgets__resize--e {
1428 +.os-widgets__resize--e {
1132 1429 top: 8px;
1133 1430 bottom: 8px;
1134 1431 right: -3px;
1135 1432 width: 6px;
@@ -1135,9 +1432,9 @@
1135 1432 width: 6px;
1136 1433 cursor: ew-resize;
1137 1434 }
1138 1435
1139 -.desktop-mode-widgets__resize--w {
1436 +.os-widgets__resize--w {
1140 1437 top: 8px;
1141 1438 bottom: 8px;
1142 1439 left: -3px;
1143 1440 width: 6px;
@@ -1143,9 +1440,9 @@
1143 1440 width: 6px;
1144 1441 cursor: ew-resize;
1145 1442 }
1146 1443
1147 -.desktop-mode-widgets__resize--ne {
1444 +.os-widgets__resize--ne {
1148 1445 top: -4px;
1149 1446 right: -4px;
1150 1447 width: 12px;
1151 1448 height: 12px;
@@ -1151,9 +1448,9 @@
1151 1448 height: 12px;
1152 1449 cursor: nesw-resize;
1153 1450 }
1154 1451
1155 -.desktop-mode-widgets__resize--nw {
1452 +.os-widgets__resize--nw {
1156 1453 top: -4px;
1157 1454 left: -4px;
1158 1455 width: 12px;
1159 1456 height: 12px;
@@ -1159,9 +1456,9 @@
1159 1456 height: 12px;
1160 1457 cursor: nwse-resize;
1161 1458 }
1162 1459
1163 -.desktop-mode-widgets__resize--se {
1460 +.os-widgets__resize--se {
1164 1461 bottom: -4px;
1165 1462 right: -4px;
1166 1463 width: 12px;
1167 1464 height: 12px;
@@ -1167,9 +1464,9 @@
1167 1464 height: 12px;
1168 1465 cursor: nwse-resize;
1169 1466 }
1170 1467
1171 -.desktop-mode-widgets__resize--sw {
1468 +.os-widgets__resize--sw {
1172 1469 bottom: -4px;
1173 1470 left: -4px;
1174 1471 width: 12px;
1175 1472 height: 12px;
@@ -1182,63 +1479,110 @@
1182 1479 * resizes on the height axis alone (the frame ignores every other
1183 1480 * direction while docked), so hide all handles except the bottom
1184 1481 * edge and the cursor never lies about what's interactive.
1185 1482 */
1186 -.desktop-mode-widgets__card--resizable:not( .desktop-mode-widgets__card--floating ) .desktop-mode-widgets__resize:not( .desktop-mode-widgets__resize--s ) {
1483 +.os-widgets__card--resizable:not( .os-widgets__card--floating ) .os-widgets__resize:not( .os-widgets__resize--s ) {
1187 1484 display: none;
1188 1485 }
1189 1486
1190 1487 /*
1191 - * Add-widget tile — matches the "+" tile in the overview top bar.
1192 - * Always visible at the bottom of the column; promoted to center
1193 - * when the column is otherwise empty so first-run users see a
1194 - * friendly target rather than an empty strip.
1488 + * Add-widget tile — a compact pill that trails the widget stack, not
1489 + * a full-width drop zone. Hidden until the pointer comes near the
1490 + * column (the layer toggles `--hovered`), so the desktop stays clean
1491 + * for people who never touch widgets.
1492 + *
1493 + * Absolute rather than in flow because "the bottom of the stack" is
1494 + * not something the column's layout knows: a widget dragged out of
1495 + * the column becomes a floating card parented to the desktop, so it
1496 + * leaves the flex flow while still sitting visually in the column.
1497 + * The layer measures those too and writes `top` — see
1498 + * `positionAddTile()`.
1195 1499 */
1196 -.desktop-mode-widgets__add {
1197 - margin-top: auto;
1198 - padding: 14px 12px;
1500 +.os-widgets__add {
1501 + position: absolute;
1502 + top: 0;
1503 + /* Centred over the cards, not over the column's box: the column
1504 + * pads out unevenly to give the card shadows room (see
1505 + * `.os-widgets`), so the box's own middle is off to one side. */
1506 + inset-inline: var( --_os-widgets-room-start, 0px ) var( --_os-widgets-room-end, 0px );
1507 + width: fit-content;
1508 + margin-inline: auto;
1509 + padding: 8px 22px;
1199 1510 display: flex;
1200 1511 align-items: center;
1201 1512 justify-content: center;
1202 - gap: 8px;
1203 - pointer-events: auto;
1204 - background: transparent;
1205 - border: 2px dashed rgba( 255, 255, 255, 0.22 );
1206 - border-radius: 14px;
1207 - color: rgba( 255, 255, 255, 0.72 );
1513 + gap: 7px;
1514 + pointer-events: none;
1515 + /* `.os-widgets__card`'s glass, to the number: the pill trails a
1516 + stack of those cards. Its dashed edge is what makes it a slot. */
1517 + background: var( --os-widgets-add-bg, transparent );
1518 + backdrop-filter: blur( 18px ) saturate( 140% );
1519 + -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
1520 + box-shadow: 0 8px 28px var( --os-widgets-add-shadow, rgba( 0, 0, 0, 0.35 ) );
1521 + border: 1px dashed var( --os-widgets-add-border, rgba( 255, 255, 255, 0.22 ) );
1522 + border-radius: 10px;
1523 + color: var( --os-ui-color-text, var( --os-fg, #fff ) );
1208 1524 font: inherit;
1209 1525 font-weight: 500;
1210 1526 cursor: pointer;
1527 + opacity: 0;
1211 1528 transition:
1529 + opacity 0.15s ease,
1212 1530 border-color 0.15s ease,
1213 1531 background-color 0.15s ease,
1214 1532 color 0.15s ease;
1215 1533 }
1216 1534
1217 -.desktop-mode-widgets__add:hover {
1535 +/*
1536 + * Reveal conditions. An empty column is no exception — approaching
1537 + * the right side is the gesture, whether or not there's a card there
1538 + * to aim at. `:focus-visible` keeps it reachable by keyboard, where
1539 + * there is no pointer to bring near.
1540 + */
1541 +.os-widgets--hovered .os-widgets__add,
1542 +.os-widgets--picking .os-widgets__add,
1543 +.os-widgets__add:focus-visible {
1544 + opacity: 1;
1545 + pointer-events: auto;
1546 +}
1547 +
1548 +/*
1549 + * Touch has no hover to approach the column with. The wallpaper's
1550 + * right-click menu carries an "Add widget" entry, but a long-press
1551 + * to reach it is a lot to ask of someone who just wants a clock, so
1552 + * on those devices the pill stays put.
1553 + */
1554 +@media ( hover: none ) {
1555 + .os-widgets__add {
1556 + opacity: 1;
1557 + pointer-events: auto;
1558 + }
1559 +}
1560 +
1561 +.os-widgets__add:hover {
1218 1562 border-color: rgba( 255, 255, 255, 0.4 );
1219 - background: rgba( 255, 255, 255, 0.06 );
1220 - color: var( --wpd-fg-on-accent, #fff );
1563 + background: var( --os-widgets-add-bg-hover, rgba( 255, 255, 255, 0.06 ) );
1564 + color: var( --os-fg, #fff );
1221 1565 }
1222 1566
1223 -.desktop-mode-widgets__add:focus-visible {
1567 +.os-widgets__add:focus-visible {
1224 1568 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
1225 1569 outline-offset: 2px;
1226 1570 }
1227 1571
1228 -.desktop-mode-widgets__add-plus {
1229 - font-size: 20px;
1572 +.os-widgets__add-plus {
1573 + font-size: 15px;
1230 1574 line-height: 1;
1231 - font-weight: 200;
1575 + font-weight: 300;
1232 1576 }
1233 1577
1234 -.desktop-mode-widgets__add-label {
1578 +.os-widgets__add-label {
1235 1579 font-size: 13px;
1236 1580 letter-spacing: 0.02em;
1237 1581 }
1238 1582
1239 1583 /* Built-in clock widget */
1240 -.desktop-mode-widget-clock {
1584 +.os-widget-clock {
1241 1585 text-align: center;
1242 1586 /* The time + date are passive glyphs, not content users would
1243 1587 * ever reasonably select — turning it off avoids the accidental-
1244 1588 * highlight mess that happens when a drag starts on the clock. */
@@ -1245,9 +1589,9 @@
1245 1589 user-select: none;
1246 1590 -webkit-user-select: none;
1247 1591 }
1248 1592
1249 -.desktop-mode-widget-clock__time {
1593 +.os-widget-clock__time {
1250 1594 font-size: 34px;
1251 1595 font-weight: 600;
1252 1596 line-height: 1.1;
1253 1597 font-variant-numeric: tabular-nums;
@@ -1253,12 +1597,12 @@
1253 1597 font-variant-numeric: tabular-nums;
1254 1598 letter-spacing: 0.01em;
1255 1599 }
1256 1600
1257 -.desktop-mode-widget-clock__date {
1601 +.os-widget-clock__date {
1258 1602 margin-top: 4px;
1259 1603 font-size: 13px;
1260 - color: rgba( 255, 255, 255, 0.75 );
1604 + color: var( --os-ui-color-text-subtle, rgba( 255, 255, 255, 0.75 ) );
1261 1605 letter-spacing: 0.02em;
1262 1606 }
1263 1607
1264 1608 /* ---------------------------------------------------------------
@@ -1266,11 +1610,15 @@
1266 1610 * registered widget. Positioned fixed so it can extend outside
1267 1611 * the desktop-area clip.
1268 1612 * --------------------------------------------------------------- */
1269 1613
1270 -.desktop-mode-widget-picker {
1614 +.os-widget-picker {
1271 1615 position: fixed;
1272 - width: 320px;
1616 + /* Border-box so the picker script can cap max-height to the
1617 + * room it measured, frame included. 342 = the 320px list plus
1618 + * the padding and border it always carried. */
1619 + box-sizing: border-box;
1620 + width: 342px;
1273 1621 max-height: 60vh;
1274 1622 overflow-y: auto;
1275 1623 padding: 10px;
1276 1624 border-radius: 14px;
@@ -1278,14 +1626,14 @@
1278 1626 backdrop-filter: blur( 22px ) saturate( 160% );
1279 1627 -webkit-backdrop-filter: blur( 22px ) saturate( 160% );
1280 1628 border: 1px solid rgba( 255, 255, 255, 0.1 );
1281 1629 box-shadow: 0 18px 54px rgba( 0, 0, 0, 0.55 );
1282 - color: var( --wpd-fg-on-accent, #fff );
1630 + color: var( --os-ui-fg-on-accent, #fff );
1283 1631 z-index: 9999;
1284 - animation: desktop-mode-widget-picker-in 0.14s ease-out;
1632 + animation: os-widget-picker-in 0.14s ease-out;
1285 1633 }
1286 1634
1287 -@keyframes desktop-mode-widget-picker-in {
1635 +@keyframes os-widget-picker-in {
1288 1636 from {
1289 1637 opacity: 0;
1290 1638 transform: translateY( 4px );
1291 1639 }
@@ -1290,9 +1638,9 @@
1290 1638 transform: translateY( 4px );
1291 1639 }
1292 1640 }
1293 1641
1294 -.desktop-mode-widget-picker__title {
1642 +.os-widget-picker__title {
1295 1643 padding: 4px 8px 8px;
1296 1644 font-size: 11px;
1297 1645 font-weight: 600;
1298 1646 letter-spacing: 0.08em;
@@ -1299,15 +1647,15 @@
1299 1647 text-transform: uppercase;
1300 1648 color: rgba( 255, 255, 255, 0.55 );
1301 1649 }
1302 1650
1303 -.desktop-mode-widget-picker__list {
1651 +.os-widget-picker__list {
1304 1652 display: flex;
1305 1653 flex-direction: column;
1306 1654 gap: 4px;
1307 1655 }
1308 1656
1309 -.desktop-mode-widget-picker__entry {
1657 +.os-widget-picker__entry {
1310 1658 display: flex;
1311 1659 align-items: center;
1312 1660 gap: 12px;
1313 1661 padding: 10px;
@@ -1313,9 +1661,9 @@
1313 1661 padding: 10px;
1314 1662 background: transparent;
1315 1663 border: 0;
1316 1664 border-radius: 10px;
1317 - color: var( --wpd-fg-on-accent, #fff );
1665 + color: var( --os-ui-fg-on-accent, #fff );
1318 1666 cursor: pointer;
1319 1667 text-align: start;
1320 1668 font: inherit;
1321 1669 transition: background-color 0.12s ease;
@@ -1320,23 +1668,23 @@
1320 1668 font: inherit;
1321 1669 transition: background-color 0.12s ease;
1322 1670 }
1323 1671
1324 -.desktop-mode-widget-picker__entry:hover:not( :disabled ) {
1672 +.os-widget-picker__entry:hover:not( :disabled ) {
1325 1673 background: rgba( 255, 255, 255, 0.08 );
1326 1674 }
1327 1675
1328 -.desktop-mode-widget-picker__entry:focus-visible {
1676 +.os-widget-picker__entry:focus-visible {
1329 1677 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
1330 1678 outline-offset: 1px;
1331 1679 }
1332 1680
1333 -.desktop-mode-widget-picker__entry--added {
1681 +.os-widget-picker__entry--added {
1334 1682 opacity: 0.55;
1335 1683 cursor: default;
1336 1684 }
1337 1685
1338 -.desktop-mode-widget-picker__entry-icon {
1686 +.os-widget-picker__entry-icon {
1339 1687 flex-shrink: 0;
1340 1688 font-size: 20px;
1341 1689 width: 20px;
1342 1690 height: 20px;
@@ -1342,9 +1690,9 @@
1342 1690 height: 20px;
1343 1691 line-height: 1;
1344 1692 }
1345 1693
1346 -.desktop-mode-widget-picker__entry-text {
1694 +.os-widget-picker__entry-text {
1347 1695 display: flex;
1348 1696 flex-direction: column;
1349 1697 gap: 2px;
1350 1698 flex: 1;
@@ -1350,9 +1698,9 @@
1350 1698 flex: 1;
1351 1699 min-width: 0;
1352 1700 }
1353 1701
1354 -.desktop-mode-widget-picker__entry-label {
1702 +.os-widget-picker__entry-label {
1355 1703 font-size: 13px;
1356 1704 font-weight: 500;
1357 1705 white-space: nowrap;
1358 1706 overflow: hidden;
@@ -1358,9 +1706,9 @@
1358 1706 overflow: hidden;
1359 1707 text-overflow: ellipsis;
1360 1708 }
1361 1709
1362 -.desktop-mode-widget-picker__entry-description {
1710 +.os-widget-picker__entry-description {
1363 1711 font-size: 11px;
1364 1712 color: rgba( 255, 255, 255, 0.6 );
1365 1713 line-height: 1.35;
1366 1714 white-space: normal;
@@ -1365,9 +1713,9 @@
1365 1713 line-height: 1.35;
1366 1714 white-space: normal;
1367 1715 }
1368 1716
1369 -.desktop-mode-widget-picker__entry-status {
1717 +.os-widget-picker__entry-status {
1370 1718 flex-shrink: 0;
1371 1719 font-size: 11px;
1372 1720 color: rgba( 255, 255, 255, 0.5 );
1373 1721 text-transform: uppercase;
@@ -1373,9 +1721,9 @@
1373 1721 text-transform: uppercase;
1374 1722 letter-spacing: 0.08em;
1375 1723 }
1376 1724
1377 -.desktop-mode-widget-picker__empty {
1725 +.os-widget-picker__empty {
1378 1726 padding: 12px 8px;
1379 1727 font-size: 12px;
1380 1728 color: rgba( 255, 255, 255, 0.6 );
1381 1729 line-height: 1.4;
@@ -1384,12 +1732,12 @@
1384 1732 /**
1385 1733 * Chromeless mode: pages loaded inside iframes.
1386 1734 * Remove all shell chrome and let content fill the frame.
1387 1735 */
1388 -.desktop-mode-chromeless {
1736 +.os-chromeless {
1389 1737 margin: 0;
1390 1738 padding: 0;
1391 - background: var(--desktop-mode-window-bg);
1739 + background: var(--os-window-bg);
1392 1740 }
1393 1741
1394 1742 /*
1395 1743 * Chromeless #wpbody-content, #adminmenuwrap, #wpfooter overrides are in
@@ -1397,9 +1745,9 @@
1397 1745 */
1398 1746
1399 1747 /*
1400 1748 * Hide WordPress core's built-in command palette (`@wordpress/commands`).
1401 - * Desktop mode force-enqueues `wp_enqueue_command_palette_assets()` so
1749 + * OpenStation force-enqueues `wp_enqueue_command_palette_assets()` so
1402 1750 * the `core/commands` data store is populated for our shell harvester
1403 1751 * (see `src/commands/shell-harvester.ts`); the side effect is that WP
1404 1752 * also mounts its own `<CommandPalette>` to the document body. We
1405 1753 * already block its Cmd+K handler in `src/palette-registry.ts`, but if
@@ -1410,5 +1758,504 @@
1410 1758 */
1411 1759 .commands-command-menu,
1412 1760 .commands-command-menu__overlay {
1413 1761 display: none !important;
1762 +}
1763 +
1764 +/*
1765 + * ---- Links slotted into <os-notice> ---------------------------
1766 + *
1767 + * `<os-notice>` styles its slotted links from inside its shadow root,
1768 + * through `::slotted( a )` and the `--os-ui-notice-link` tokens. That
1769 + * rule cannot win here, and no amount of specificity on it would help.
1770 + *
1771 + * A slotted element lives in the DOCUMENT tree; the shadow root only
1772 + * borrows it for rendering. When declarations from two trees collide,
1773 + * CSS Scoping resolves normal (non-`!important`) ones in favour of the
1774 + * OUTER tree regardless of specificity — so `wp-admin/css/common.css`,
1775 + * which sets `a { color: #2271b1 }` and `a:hover { color: #135e96 }` on
1776 + * every anchor in the admin, beats `::slotted( a )` every time. The
1777 + * shell renders inside wp-admin, so it always loses: the link painted
1778 + * WordPress Blue at rest and a darker blue on hover, the second of
1779 + * which is close to illegible on a notice's dark wash.
1780 + *
1781 + * Hence a document-tree rule. It reads the same tokens the component
1782 + * documents, so a theme still restyles the link by setting them; this
1783 + * only moves the declaration into a tree that can win. `a:hover` is
1784 + * (0,1,1), so the hover selector has to carry its own pseudo-class to
1785 + * outrank it — matching `os-notice a` alone would lose the hover state
1786 + * while winning the base one, which is exactly the inert-looking link
1787 + * the component's own styles already went out of their way to avoid.
1788 + */
1789 +os-notice a {
1790 + color: var(--os-ui-notice-link, #ec9bff);
1791 +}
1792 +
1793 +os-notice a:hover,
1794 +os-notice a:focus,
1795 +os-notice a:active {
1796 + color: var(--os-ui-notice-link-hover, #fffbff);
1797 +}
1798 +
1799 +/* --------------------------------------------------------------------
1800 + * The workspace hop — cross-document view transitions between shells.
1801 + *
1802 + * Loaded only on the SHELL screen (this sheet never reaches chromeless
1803 + * iframes), so only shell→shell navigations transition: hopping between
1804 + * a site's desktop and the network admin's crossfades the two desktops
1805 + * instead of hard-cutting, which is what makes the per-admin sessions
1806 + * read as workspaces. Classic admin pages never opt in, so entering or
1807 + * leaving the shell stays a plain navigation, and reloads are excluded
1808 + * by the spec. Browsers without cross-document view transitions ignore
1809 + * all of this and navigate plainly. See docs/multisite.md.
1810 + */
1811 +@view-transition {
1812 + navigation: auto;
1813 +}
1814 +
1815 +/* Reduced motion swaps instantly — the hop still happens, the
1816 + * animation does not. */
1817 +@media ( prefers-reduced-motion: reduce ) {
1818 + @view-transition {
1819 + navigation: none;
1820 + }
1821 +}
1822 +
1823 +/* A gentle zoom-through: the desktop being left recedes as the one
1824 + * arriving settles in. Subtle on purpose — this runs on a full page
1825 + * swap, and anything louder reads as a reload effect. */
1826 +::view-transition-old(root) {
1827 + animation: os-workspace-hop-out 220ms cubic-bezier( 0.4, 0, 1, 1 ) both;
1828 +}
1829 +
1830 +::view-transition-new(root) {
1831 + animation: os-workspace-hop-in 280ms cubic-bezier( 0, 0, 0.2, 1 ) both;
1832 +}
1833 +
1834 +@keyframes os-workspace-hop-out {
1835 + to {
1836 + opacity: 0;
1837 + transform: scale( 0.985 );
1838 + }
1839 +}
1840 +
1841 +@keyframes os-workspace-hop-in {
1842 + from {
1843 + opacity: 0;
1844 + transform: scale( 1.015 );
1845 + }
1846 +}
1847 +
1848 +/* The instance hop's two halves, on either side of that page swap.
1849 + * The switcher slides this desk out towards the site it picked
1850 + * (`os-shell--hop-out-next` / `-prev`, for the beat before it
1851 + * navigates), and a shell asked to boot into overview arrives with its
1852 + * desk hidden (`os-shell--arriving`, stamped server-side) until overview
1853 + * is up, then slides it in from the same side (`-next` / `-prev`, the
1854 + * hint the switcher left in sessionStorage; no hint, a plain fade). The
1855 + * wallpaper never moves, which is what makes it read as the tiles
1856 + * changing rather than the page. `src/multisite/instance-transition.ts`
1857 + * drives the classes. */
1858 +.os-shell--hop-out-next #os-area,
1859 +.os-shell--hop-out-prev #os-area {
1860 + opacity: 0;
1861 + transition:
1862 + opacity 220ms cubic-bezier( 0.4, 0, 1, 1 ),
1863 + transform 220ms cubic-bezier( 0.4, 0, 1, 1 );
1864 +}
1865 +
1866 +.os-shell--hop-out-next #os-area {
1867 + transform: translateX( -48px );
1868 +}
1869 +
1870 +.os-shell--hop-out-prev #os-area {
1871 + transform: translateX( 48px );
1872 +}
1873 +
1874 +.os-shell--hop-out-next .os-dock,
1875 +.os-shell--hop-out-prev .os-dock,
1876 +.os-shell--arriving .os-dock {
1877 + opacity: 0;
1878 + transition: opacity 220ms ease;
1879 +}
1880 +
1881 +.os-shell--arriving #os-area {
1882 + opacity: 0;
1883 + transition: none;
1884 +}
1885 +
1886 +.os-shell--arriving-next #os-area {
1887 + transform: translateX( 48px );
1888 +}
1889 +
1890 +.os-shell--arriving-prev #os-area {
1891 + transform: translateX( -48px );
1892 +}
1893 +
1894 +/* The reveal: the arriving classes come off and this goes on in the
1895 + * same frame, so the desk transitions from hidden to its resting
1896 + * place. */
1897 +.os-shell--revealing #os-area {
1898 + transition:
1899 + opacity 360ms cubic-bezier( 0, 0, 0.2, 1 ),
1900 + transform 360ms cubic-bezier( 0, 0, 0.2, 1 );
1901 +}
1902 +
1903 +.os-shell--revealing .os-dock {
1904 + transition: opacity 360ms ease;
1905 +}
1906 +
1907 +/* A desk must never stay hidden: if the boot that reveals it never
1908 + * runs, the area and the dock come back on their own a few seconds
1909 + * in. */
1910 +@keyframes os-instance-arrive-fallback {
1911 + to {
1912 + opacity: 1;
1913 + transform: none;
1914 + }
1915 +}
1916 +
1917 +.os-shell--arriving #os-area,
1918 +.os-shell--arriving .os-dock {
1919 + animation: os-instance-arrive-fallback 1ms linear 6s forwards;
1920 +}
1921 +
1922 +@media ( prefers-reduced-motion: reduce ) {
1923 + .os-shell--hop-out-next #os-area,
1924 + .os-shell--hop-out-prev #os-area,
1925 + .os-shell--arriving-next #os-area,
1926 + .os-shell--arriving-prev #os-area {
1927 + transform: none;
1928 + }
1929 +
1930 + .os-shell--hop-out-next #os-area,
1931 + .os-shell--hop-out-prev #os-area,
1932 + .os-shell--hop-out-next .os-dock,
1933 + .os-shell--hop-out-prev .os-dock,
1934 + .os-shell--revealing #os-area,
1935 + .os-shell--revealing .os-dock {
1936 + transition: none;
1937 + }
1938 +}
1939 +
1940 +/* A residency frame follows the window body without scrolling with its contents. */
1941 +.os-mio-residence {
1942 + position: absolute;
1943 + z-index: 50;
1944 + pointer-events: none;
1945 + overflow: hidden;
1946 + container-type: inline-size;
1947 +}
1948 +
1949 +.os-mio-residence[hidden],
1950 +.os-mio-chat-launcher[hidden] {
1951 + display: none;
1952 +}
1953 +
1954 +.os-mio-chat-launcher {
1955 + position: absolute;
1956 + inset-inline-end: 20px;
1957 + inset-block-end: 18px;
1958 + pointer-events: auto;
1959 +}
1960 +
1961 +.os-mio-chat {
1962 + position: absolute;
1963 + inset-inline-end: 16px;
1964 + inset-block-end: 16px;
1965 + inline-size: min(380px, calc(100% - 32px));
1966 + max-block-size: calc(100% - 32px);
1967 + box-sizing: border-box;
1968 + display: flex;
1969 + flex-direction: column;
1970 + gap: 12px;
1971 + padding: 18px;
1972 + pointer-events: auto;
1973 + color: var(--os-mio-chat-fg, #1d2327);
1974 + background: var(--os-mio-chat-bg, #fff);
1975 + border: 1px solid var(--os-mio-chat-border, #c3c4c7);
1976 + border-radius: var(--os-mio-chat-radius, 16px);
1977 + box-shadow: 0 8px 40px var(--os-mio-chat-glow, #00000020);
1978 + animation: os-mio-chat-enter 180ms ease-out;
1979 +}
1980 +
1981 +.os-mio-chat header {
1982 + display: flex;
1983 + align-items: center;
1984 + gap: 12px;
1985 +}
1986 +
1987 +.os-mio-chat header strong {
1988 + letter-spacing: .14em;
1989 +}
1990 +
1991 +.os-mio-chat header span {
1992 + flex: 1;
1993 + font-size: 11px;
1994 + color: var(--os-mio-chat-muted, #646970);
1995 +}
1996 +
1997 +.os-mio-chat__log {
1998 + position: relative;
1999 + min-block-size: 60px;
2000 + max-block-size: 300px;
2001 + overflow: auto;
2002 + overscroll-behavior: contain;
2003 +}
2004 +
2005 +.os-mio-chat__message {
2006 + margin-block: 0 10px;
2007 + padding: 10px 12px;
2008 + line-height: 1.6;
2009 + white-space: pre-wrap;
2010 + overflow-wrap: anywhere;
2011 +}
2012 +
2013 +.os-mio-chat__message--user {
2014 + border-radius: 16px 16px 4px 16px;
2015 + background: var(--os-mio-chat-user-bg, #f0f0f1);
2016 +}
2017 +
2018 +.os-mio-chat__composer {
2019 + display: flex;
2020 + align-items: center;
2021 + gap: 8px;
2022 +}
2023 +
2024 +.os-mio-chat__composer os-textarea {
2025 + flex: 1;
2026 + min-inline-size: 0;
2027 +}
2028 +
2029 +.os-mio-chat__status,
2030 +.os-mio-chat small {
2031 + margin: 0;
2032 + font-size: 11px;
2033 + line-height: 1.5;
2034 + color: var(--os-mio-chat-muted, #646970);
2035 +}
2036 +
2037 +@keyframes os-mio-chat-enter {
2038 + from { opacity: 0; transform: translateY(6px) scale(.98); }
2039 + to { opacity: 1; transform: translateY(0) scale(1); }
2040 +}
2041 +
2042 +@media (prefers-reduced-motion: reduce) {
2043 + .os-mio-chat { animation: none; }
2044 +}
2045 +
2046 +.os-mio-residence > .os-mio { z-index: 0; }
2047 +.os-mio-chat, .os-mio-chat-launcher { z-index: 1; }
2048 +
2049 +.os-mio-chat [hidden] {
2050 + display: none !important;
2051 +}
2052 +
2053 +.os-mio-chat__message--assistant {
2054 + white-space: normal;
2055 +}
2056 +
2057 +.os-mio-chat__message p {
2058 + margin-block: 0 8px;
2059 +}
2060 +
2061 +.os-mio-chat__message ul,
2062 +.os-mio-chat__message ol {
2063 + margin-block: 8px;
2064 + padding-inline-start: 20px;
2065 +}
2066 +
2067 +.os-mio-chat__message a {
2068 + color: inherit;
2069 + text-decoration: underline;
2070 +}
2071 +
2072 +/* Shared MIO chrome: the same portrait with a softly revealed off-state slash. */
2073 +.os-mio-window-toggle svg {
2074 + width: 20px;
2075 + height: 20px;
2076 + overflow: visible;
2077 +}
2078 +
2079 +.os-mio-window-toggle__slash {
2080 + fill: none;
2081 + stroke: currentColor;
2082 + stroke-width: 2.4;
2083 + stroke-linecap: round;
2084 + opacity: 0;
2085 + stroke-dasharray: 26;
2086 + stroke-dashoffset: 26;
2087 + transition: opacity 240ms ease, stroke-dashoffset 240ms ease;
2088 +}
2089 +
2090 +.os-mio-window-toggle[data-mio-disabled="true"] .os-mio-window-toggle__slash {
2091 + opacity: 1;
2092 + stroke-dashoffset: 0;
2093 +}
2094 +
2095 +.os-mio-chat__activity {
2096 + display: flex;
2097 + align-items: center;
2098 + gap: 8px;
2099 + min-height: 18px;
2100 +}
2101 +
2102 +.os-mio-chat__thinking {
2103 + display: inline-flex;
2104 + align-items: center;
2105 + gap: 3px;
2106 + width: 24px;
2107 + flex-shrink: 0;
2108 + opacity: 0;
2109 + color: var(--os-mio-chat-muted, #646970);
2110 + transition: opacity 240ms ease;
2111 +}
2112 +
2113 +.os-mio-chat[data-thinking="true"] .os-mio-chat__thinking {
2114 + opacity: 1;
2115 +}
2116 +
2117 +.os-mio-chat__thinking i {
2118 + width: 4px;
2119 + height: 4px;
2120 + border-radius: 50%;
2121 + background: currentColor;
2122 + animation: os-mio-thinking 1.6s ease-in-out infinite;
2123 + animation-play-state: paused;
2124 +}
2125 +
2126 +.os-mio-chat__thinking i:nth-child(2) { animation-delay: 160ms; }
2127 +.os-mio-chat__thinking i:nth-child(3) { animation-delay: 320ms; }
2128 +.os-mio-chat[data-thinking="true"] .os-mio-chat__thinking i { animation-play-state: running; }
2129 +.os-mio-window-toggle[data-mio-thinking="true"] circle { animation: os-mio-thinking 2.4s ease-in-out infinite; }
2130 +
2131 +@keyframes os-mio-thinking {
2132 + 0%, 100% { opacity: 0.45; }
2133 + 50% { opacity: 1; }
2134 +}
2135 +
2136 +@media (prefers-reduced-motion: reduce) {
2137 + .os-mio-window-toggle__slash,
2138 + .os-mio-chat__thinking { transition: none; }
2139 + .os-mio-chat__thinking i,
2140 + .os-mio-window-toggle[data-mio-thinking="true"] circle { animation: none; }
2141 +}
2142 +
2143 +.os-mio-window-toggle {
2144 + max-inline-size: 30px;
2145 + opacity: 1;
2146 + visibility: visible;
2147 + transition: opacity 180ms ease, max-inline-size 240ms ease, visibility 0s;
2148 +}
2149 +
2150 +.os-mio-window-toggle[data-mio-available="false"] {
2151 + max-inline-size: 0;
2152 + opacity: 0;
2153 + visibility: hidden;
2154 + overflow: hidden;
2155 + pointer-events: none;
2156 + transition: opacity 180ms ease, max-inline-size 240ms ease, visibility 0s 180ms;
2157 +}
2158 +
2159 +@media (prefers-reduced-motion: reduce) {
2160 + .os-mio-window-toggle,
2161 + .os-mio-window-toggle[data-mio-available="false"] { transition: none; }
2162 +}
2163 +
2164 +/* The launcher stays available; the companion only speaks when invited. */
2165 +.os-mio > canvas,
2166 +.os-mio > .os-mio__handle {
2167 + transition: opacity 180ms ease, visibility 180ms;
2168 +}
2169 +
2170 +.os-mio[data-mio-visible="false"] > canvas,
2171 +.os-mio[data-mio-visible="false"] > .os-mio__handle {
2172 + opacity: 0;
2173 + visibility: hidden;
2174 + pointer-events: none;
2175 +}
2176 +
2177 +.os-mio-callout {
2178 + position: absolute;
2179 + z-index: 2;
2180 + display: flex;
2181 + align-items: center;
2182 + gap: 10px;
2183 + max-inline-size: calc(100% - 16px);
2184 + box-sizing: border-box;
2185 + padding-block: 6px;
2186 + padding-inline: 15px 6px;
2187 + border: 1px solid var(--os-mio-callout-border, #d6b8cb);
2188 + border-radius: 22px;
2189 + background: var(--os-mio-callout-bg, #ffe0cf);
2190 + color: var(--os-mio-callout-fg, #382b38);
2191 + box-shadow: 0 4px 16px var(--os-mio-chat-glow, #00000020);
2192 + font-size: 12px;
2193 + line-height: 1.5;
2194 + font-weight: 500;
2195 + pointer-events: auto;
2196 + animation: os-mio-chat-enter 180ms ease-out;
2197 +}
2198 +
2199 +.os-mio-callout os-button::part(button) {
2200 + inline-size: 26px;
2201 + block-size: 26px;
2202 + font-size: 17px;
2203 + line-height: 1;
2204 +}
2205 +
2206 +/* A small rounded tail joins the companion without a second outlined shape. */
2207 +.os-mio-callout::after {
2208 + content: "";
2209 + position: absolute;
2210 + inset-block-end: -4px;
2211 + inset-inline-end: 18px;
2212 + inline-size: 8px;
2213 + block-size: 8px;
2214 + background: inherit;
2215 + border-inline-end: inherit;
2216 + border-block-end: inherit;
2217 + border-end-end-radius: 3px;
2218 + transform: rotate(45deg);
2219 +}
2220 +
2221 +.os-mio-callout[hidden] { display: none; }
2222 +
2223 +@media (prefers-reduced-motion: reduce) {
2224 + .os-mio > canvas,
2225 + .os-mio > .os-mio__handle { transition: none; }
2226 + .os-mio-callout { animation: none; }
2227 +}
2228 +
2229 +/* Response controls belong to their message; all layout follows inline flow. */
2230 +.os-mio-chat__actions {
2231 + display: flex;
2232 + flex-wrap: wrap;
2233 + gap: 8px;
2234 + margin-block-start: 12px;
2235 +}
2236 +
2237 +.os-mio-chat__action {
2238 + display: flex;
2239 + flex-direction: column;
2240 + align-items: flex-start;
2241 + gap: 4px;
2242 + min-inline-size: 0;
2243 + max-inline-size: 100%;
2244 +}
2245 +
2246 +.os-mio-chat__action os-button::part(button) {
2247 + white-space: normal;
2248 + overflow-wrap: anywhere;
2249 +}
2250 +
2251 +.os-mio-chat__action-status {
2252 + font-size: 12px;
2253 + color: var(--os-mio-chat-muted, #646970);
2254 + overflow-wrap: anywhere;
2255 +}
2256 +
2257 +.os-mio-chat__composer os-textarea::part(textarea) {
2258 + white-space: pre-wrap;
2259 + overflow-wrap: anywhere;
2260 + overflow-x: hidden;
1414 2261 }