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