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 +890 -207 1.0.0 → 1.1.13 View file →
@@ -156,8 +156,27 @@
156 156 inset-inline-end: 0 !important;
157 157 }
158 158
159 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 +/*
160 179 * Admin-bar presentation modes — OS Settings → Appearance → Admin bar,
161 180 * persisted as `adminBarMode` and emitted as a
162 181 * `os-admin-bar-<mode>` body class by PHP on first paint and
163 182 * re-written by the shell's apply pass on every change.
@@ -166,9 +185,11 @@
166 185 * starts below the bar. The two modes below both take the bar out of
167 186 * the shell's way, so the shell reclaims the full viewport in each.
168 187 *
169 188 * Every selector here carries two body classes, so it outranks the
170 - * 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.
171 192 */
172 193
173 194 /*
174 195 * Dynamic — the bar parks off the top edge leaving a peek strip, and
@@ -173,25 +194,42 @@
173 194 /*
174 195 * Dynamic — the bar parks off the top edge leaving a peek strip, and
175 196 * slides back in when the pointer reaches the top of the viewport or
176 197 * something inside it takes keyboard focus. The classic Windows
177 - * auto-hide taskbar, and the reason `transform` is the right tool: a
178 - * transformed element keeps hit-testing at its PAINTED position, so
179 - * the parked remainder stops catching the pointer the moment it goes
180 - * off-screen. What DOES catch it is the peek strip plus the reveal
181 - * zone added below.
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.
182 218 */
183 219 body.os-active.os-admin-bar-dynamic #wpadminbar {
184 - transform: translateY(
185 - calc(-100% + var(--os-admin-bar-peek, 4px))
186 - );
187 - transition: transform 180ms ease;
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;
188 226 }
189 227
190 228 body.os-active.os-admin-bar-dynamic #wpadminbar:hover,
191 229 body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within,
192 230 body.os-active.os-admin-bar-dynamic #wpadminbar:active {
193 - transform: translateY(0);
231 + inset-block-start: 0 !important;
194 232 }
195 233
196 234 /*
197 235 * Reveal zone — an invisible extension of the bar's hit area, hanging
@@ -211,8 +249,25 @@
211 249 position: absolute;
212 250 inset-inline: 0;
213 251 inset-block-start: 100%;
214 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);
215 270 }
216 271
217 272 /*
218 273 * ...but only while the bar is parked. Left standing once the bar is
@@ -220,9 +275,10 @@
220 275 * clicks on whatever window is up there.
221 276 *
222 277 * Collapsing it is also what keeps the reveal stable: the pointer
223 278 * that tripped the zone is, post-reveal, inside the bar's real box,
224 - * 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).
225 281 */
226 282 body.os-active.os-admin-bar-dynamic #wpadminbar:hover::after,
227 283 body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within::after,
228 284 body.os-active.os-admin-bar-dynamic #wpadminbar:active::after {
@@ -242,9 +298,11 @@
242 298 }
243 299 }
244 300
245 301 @media (prefers-reduced-motion: reduce) {
246 - body.os-active.os-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. */
247 305 transition: none;
248 306 }
249 307 }
250 308
@@ -280,8 +338,132 @@
280 338 padding-top: 0 !important;
281 339 }
282 340
283 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 +/*
284 466 * Align the admin-bar W logo with the dock below it.
285 467 *
286 468 * Without this rule the admin bar's leftmost item (the WordPress W)
287 469 * renders at its Core-dictated ~44 px width, while the dock below it
@@ -293,8 +475,12 @@
293 475 * W inside, so the two columns share an axis. `--os-dock-width`
294 476 * is now set on :root (see settings.ts → apply()) so it reaches
295 477 * siblings of #os-shell like the admin bar.
296 478 */
479 +body.os-active #wpadminbar #wp-admin-bar-menu-toggle {
480 + display: none;
481 +}
482 +
297 483 body.os-active #wpadminbar #wp-admin-bar-wp-logo {
298 484 width: var( --os-dock-width, 56px );
299 485 }
300 486
@@ -314,8 +500,16 @@
314 500 margin: 0;
315 501 }
316 502
317 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 +/*
318 512 * Classic wpcontent carries a `margin-left: 160px` for the sidebar. With
319 513 * the sidebar hidden we let wpcontent fall back to flush — the shell is
320 514 * fixed-positioned anyway so this doesn't change the shell's layout, it
321 515 * just prevents a phantom sidebar gutter in any stray visible content.
@@ -328,12 +522,26 @@
328 522 /*
329 523 * Desktop shell — fills the viewport below the admin bar.
330 524 * Uses position: fixed so it's unaffected by any containing-block
331 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.
332 537 */
333 538 .os-shell {
334 539 position: fixed;
335 - 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 + );
336 544 inset-inline-start: 0;
337 545 inset-inline-end: 0;
338 546 inset-block-end: 0;
339 547 display: flex;
@@ -349,8 +557,14 @@
349 557 * See `--os-backstop`.
350 558 */
351 559 background: var(--os-backstop, #1d2327);
352 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 + /*
353 567 * Shell typeface. Every piece of chrome that doesn't set its own
354 568 * face inherits from here — dock labels, desktop icon labels,
355 569 * widgets, the overview. Windows override with `--os-ui-font` on
356 570 * their body, so a theme can run a display face on the desk and a
@@ -548,9 +762,12 @@
548 762
549 763 /* Below 783px WP collapses the admin bar to 46px. */
550 764 @media screen and (max-width: 782px) {
551 765 .os-shell {
552 - inset-block-start: var(--wp-admin--admin-bar--height, 46px);
766 + inset-block-start: var(
767 + --os-admin-bar-height,
768 + var(--wp-admin--admin-bar--height, 46px)
769 + );
553 770 }
554 771 }
555 772
556 773 /*
@@ -586,11 +803,17 @@
586 803 * because the wallpaper renders in the shell-level layer behind
587 804 * both this area and the dock. Stacking context via position: relative
588 805 * ensures children (windows) sit above the wallpaper.
589 806 *
590 - * `padding-bottom` reserves room for the floating bottom dock pill
591 - * that every layout (Classic, Unified, Spatial) puts at the bottom
592 - * — 40px icons + 16px padding + 12px gap below + 8px breathing room.
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.
593 816 */
594 817 .os-area {
595 818 flex: 1;
596 819 position: relative;
@@ -595,9 +818,12 @@
595 818 flex: 1;
596 819 position: relative;
597 820 overflow: hidden;
598 821 background: transparent;
599 - 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 );
600 826 transition: opacity 180ms ease;
601 827 }
602 828
603 829 /*
@@ -669,12 +895,23 @@
669 895 * --------------------------------------------------------------- */
670 896
671 897 .os-icons {
672 898 position: absolute;
673 - inset-block-start: 16px;
674 - inset-block-end: 16px;
675 - inset-inline-start: 16px;
676 - 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 );
677 914 display: grid;
678 915 /*
679 916 * Column-major flow: icons fill the first column top-to-bottom,
680 917 * then start the next column. Matches the macOS / Finder
@@ -725,9 +962,10 @@
725 962 background-image: var(--os-tile-image, none);
726 963 background-repeat: var(--os-tile-image-repeat, no-repeat);
727 964 background-size: var(--os-tile-image-size, auto);
728 965 background-position: var(--os-tile-image-position, center);
729 - color: var(--os-fg, #fff);
966 + /* Also the glyph: silhouette art is masked with `currentColor`. */
967 + color: var(--os-desk-fg, var(--os-fg, #fff));
730 968 cursor: pointer;
731 969 border-radius: 8px;
732 970 transition: background-color 0.15s ease;
733 971 /* Positioning context for `.os-icon__badge`. */
@@ -789,9 +1027,9 @@
789 1027 .os-icon:focus-visible {
790 1028 /* background-COLOR, not the shorthand: the shorthand would reset
791 1029 * the ICON_TILE texture declared above, so a themed tile would
792 1030 * vanish on hover. */
793 - background-color: rgba(255, 255, 255, 0.12);
1031 + background-color: var(--os-icon-hover-bg, rgba(255, 255, 255, 0.12));
794 1032 outline: none;
795 1033 }
796 1034
797 1035 .os-icon:focus-visible {
@@ -815,157 +1053,25 @@
815 1053 max-height: 48px;
816 1054 object-fit: contain;
817 1055 }
818 1056
1057 +.os-icon__image {
1058 + filter: var(--os-icon-glyph-shadow, none);
1059 +}
1060 +
819 1061 .os-icon__label {
820 - font-size: 12px;
1062 + font-size: var(--os-icon-label-font-size, 12px);
821 1063 text-align: center;
822 - 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));
823 1069 line-height: 1.2;
824 1070 word-break: break-word;
825 1071 }
826 1072
827 1073 /* ---------------------------------------------------------------
828 - * Sticky notes — Gutenberg `wp_guideline` artifacts tagged with the
829 - * sticky guideline type. Notes sit above wallpaper icons/widgets and
830 - * below windows, so they behave like desktop objects rather than app
831 - * chrome.
832 - * --------------------------------------------------------------- */
833 -
834 -.os-sticky-notes {
835 - position: absolute;
836 - inset: 0;
837 - z-index: 2;
838 - pointer-events: none;
839 - transition:
840 - opacity 0.22s ease,
841 - transform 0.28s cubic-bezier( 0.2, 0, 0.2, 1 );
842 -}
843 -
844 -.os-area--overview .os-sticky-notes {
845 - opacity: 0;
846 - transform: translateY( 12px );
847 - pointer-events: none;
848 -}
849 -
850 -.os-sticky-note {
851 - position: absolute;
852 - display: flex;
853 - flex-direction: column;
854 - pointer-events: auto;
855 - background: #fff09a;
856 - color: #2a2513;
857 - border: 1px solid rgba( 84, 67, 0, 0.24 );
858 - border-radius: 4px;
859 - box-shadow:
860 - 0 12px 28px rgba( 0, 0, 0, 0.22 ),
861 - 0 2px 5px rgba( 0, 0, 0, 0.16 ),
862 - inset 0 1px 0 rgba( 255, 255, 255, 0.55 );
863 - overflow: hidden;
864 - resize: both;
865 -}
866 -
867 -.os-sticky-note--dragging {
868 - opacity: 0.92;
869 - resize: none;
870 - user-select: none;
871 -}
872 -
873 -.os-sticky-note__header {
874 - display: flex;
875 - align-items: center;
876 - gap: 5px;
877 - min-height: 30px;
878 - padding: 4px 6px 4px 8px;
879 - background: rgba( 82, 64, 0, 0.06 );
880 - border-bottom: 1px solid rgba( 82, 64, 0, 0.14 );
881 - cursor: grab;
882 - touch-action: none;
883 - user-select: none;
884 -}
885 -
886 -.os-sticky-note--dragging .os-sticky-note__header {
887 - cursor: grabbing;
888 -}
889 -
890 -.os-sticky-note__grip {
891 - flex: 0 0 auto;
892 - width: 8px;
893 - height: 14px;
894 - background-image: radial-gradient(
895 - circle,
896 - rgba( 60, 48, 0, 0.5 ) 1.2px,
897 - transparent 1.5px
898 - );
899 - background-size: 4px 4px;
900 - background-position: 0 1px;
901 - background-repeat: space;
902 - opacity: 0.42;
903 -}
904 -
905 -.os-sticky-note__title {
906 - flex: 1;
907 - min-width: 0;
908 - font-size: 11px;
909 - font-weight: 650;
910 - overflow: hidden;
911 - text-overflow: ellipsis;
912 - white-space: nowrap;
913 -}
914 -
915 -.os-sticky-note__status {
916 - flex: 0 0 auto;
917 -}
918 -
919 -.os-sticky-note os-window-button {
920 - --os-ui-btn-color: rgba( 42, 37, 19, 0.7 );
921 - --os-ui-btn-color-hover: #2a2513;
922 - --os-ui-btn-bg-hover: rgba( 42, 37, 19, 0.1 );
923 - --os-ui-btn-bg-active: rgba( 42, 37, 19, 0.16 );
924 - --os-ui-btn-danger-hover: rgba( 214, 54, 56, 0.18 );
925 - --os-ui-btn-outline: var( --wp-admin-theme-color, #2271b1 );
926 -}
927 -
928 -.os-sticky-note__open.is-disabled {
929 - opacity: 0.34;
930 - pointer-events: none;
931 -}
932 -
933 -.os-sticky-note__editor {
934 - flex: 1;
935 - min-height: 0;
936 - --os-window-bg: transparent;
937 - --os-ui-border: transparent;
938 - --os-ui-fg: #2a2513;
939 - --os-ui-fg-muted: rgba( 42, 37, 19, 0.62 );
940 - font-family:
941 - -apple-system,
942 - BlinkMacSystemFont,
943 - "Segoe UI",
944 - sans-serif;
945 -}
946 -
947 -.os-sticky-note__editor::part(textarea) {
948 - height: 100%;
949 - min-height: 100%;
950 - padding: 10px 12px 12px;
951 - background: transparent;
952 - border: 0;
953 - border-radius: 0;
954 - box-shadow: none;
955 - color: #2a2513;
956 - font-size: 13px;
957 - line-height: 1.4;
958 - resize: none;
959 -}
960 -
961 -.os-sticky-note__editor::part(textarea):hover,
962 -.os-sticky-note__editor::part(textarea):focus-visible {
963 - border-color: transparent;
964 - box-shadow: none;
965 -}
966 -
967 -/* ---------------------------------------------------------------
968 1074 * Widgets column — right-edge glass strip that paints above the
969 1075 * wallpaper but beneath windows. Hosts stacked widget cards and a
970 1076 * trailing "Add widget" tile. Interactive only on its own cards +
971 1077 * the add tile; every other pixel is pointer-transparent so window
@@ -972,14 +1078,38 @@
972 1078 * drag / resize / click-through behaviour is unaffected.
973 1079 * --------------------------------------------------------------- */
974 1080
975 1081 .os-widgets {
976 - border-radius: 14px;
977 1082 position: absolute;
978 - top: 16px;
979 - bottom: 16px;
980 - 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;
981 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 );
982 1112 display: flex;
983 1113 flex-direction: column;
984 1114 gap: 12px;
985 1115 z-index: 1;
@@ -1029,10 +1159,16 @@
1029 1159 * spills past the card's rounded bottom edge. */
1030 1160 display: flex;
1031 1161 flex-direction: column;
1032 1162 pointer-events: auto;
1033 - background-color: rgba( 20, 20, 22, 0.55 );
1034 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 + /*
1035 1171 * WIDGET texture slot — layered over the frosted card colour, so a
1036 1172 * translucent texture still gets the blur underneath.
1037 1173 */
1038 1174 background-image: var( --os-widget-image, none );
@@ -1038,16 +1174,20 @@
1038 1174 background-image: var( --os-widget-image, none );
1039 1175 background-repeat: var( --os-widget-image-repeat, repeat );
1040 1176 background-size: var( --os-widget-image-size, auto );
1041 1177 background-position: var( --os-widget-image-position, center );
1042 - backdrop-filter: blur( 18px ) saturate( 140% );
1043 - -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
1044 - border: 1px solid rgba( 255, 255, 255, 0.08 );
1045 - border-radius: 14px;
1046 - color: var( --os-ui-fg-on-accent, #fff );
1047 - box-shadow:
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,
1048 1187 0 8px 28px rgba( 0, 0, 0, 0.35 ),
1049 - inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
1188 + inset 0 0 0 1px rgba( 255, 255, 255, 0.04 )
1189 + );
1050 1190 transition:
1051 1191 box-shadow 0.18s ease,
1052 1192 opacity 0.18s ease;
1053 1193 }
@@ -1055,11 +1195,13 @@
1055 1195 /* Hover feedback is shadow-only — no transform. Cards must feel
1056 1196 * anchored; a hover nudge reads as the widget drifting under the
1057 1197 * pointer (and fought the user's chosen position on floating cards). */
1058 1198 .os-widgets__card:hover {
1059 - box-shadow:
1199 + box-shadow: var(
1200 + --os-widget-card-shadow-hover,
1060 1201 0 12px 36px rgba( 0, 0, 0, 0.45 ),
1061 - inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
1202 + inset 0 0 0 1px rgba( 255, 255, 255, 0.08 )
1203 + );
1062 1204 }
1063 1205
1064 1206 .os-widgets__card-body {
1065 1207 padding: 16px;
@@ -1073,10 +1215,12 @@
1073 1215 .os-widgets__card-close {
1074 1216 position: absolute;
1075 1217 top: 6px;
1076 1218 inset-inline-end: 6px;
1077 - width: 22px;
1078 - 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;
1079 1223 display: flex;
1080 1224 align-items: center;
1081 1225 justify-content: center;
1082 1226 padding: 0;
@@ -1096,8 +1240,21 @@
1096 1240 .os-widgets__card-close:focus-visible {
1097 1241 opacity: 1;
1098 1242 }
1099 1243
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 + }
1255 +}
1256 +
1100 1257 .os-widgets__card-close:hover {
1101 1258 background: var( --os-ui-danger, #d63638 );
1102 1259 color: var( --os-ui-fg-on-accent, #fff );
1103 1260 }
@@ -1161,10 +1318,10 @@
1161 1318 corner-absolute — reset position + always-visible opacity. */
1162 1319 .os-widgets__chrome .os-widgets__card-close {
1163 1320 position: static;
1164 1321 opacity: 0.7;
1165 - width: 20px;
1166 - height: 20px;
1322 + width: 24px;
1323 + height: 24px;
1167 1324 }
1168 1325
1169 1326 .os-widgets__chrome .os-widgets__card-close:hover,
1170 1327 .os-widgets__chrome .os-widgets__card-close:focus-visible {
@@ -1178,9 +1335,10 @@
1178 1335 * expose it (tapping it on a docked card would be a confusing
1179 1336 * no-op). The DOM node stays mounted either way so state flips
1180 1337 * don't rebuild chrome.
1181 1338 *
1182 - * 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),
1183 1341 * dimmed by default, full-opacity on hover / focus. Distinct hover
1184 1342 * color (theme accent, not red) signals "put back" vs. "remove."
1185 1343 */
1186 1344 .os-widgets__card-redock {
@@ -1191,10 +1349,10 @@
1191 1349 border: 0;
1192 1350 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
1193 1351 color: rgba( 255, 255, 255, 0.75 );
1194 1352 border-radius: 50%;
1195 - width: 20px;
1196 - height: 20px;
1353 + width: 24px;
1354 + height: 24px;
1197 1355 cursor: pointer;
1198 1356 opacity: 0.7;
1199 1357 transition:
1200 1358 opacity 0.15s ease,
@@ -1209,9 +1367,9 @@
1209 1367 .os-widgets__card-redock:hover,
1210 1368 .os-widgets__card-redock:focus-visible {
1211 1369 opacity: 1;
1212 1370 background: var( --wp-admin-theme-color, #2271b1 );
1213 - color: var( --os-ui-fg-on-accent, #fff );
1371 + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) );
1214 1372 }
1215 1373
1216 1374 /*
1217 1375 * Floating widgets — absolute-positioned, inline top/left/width/height
@@ -1221,8 +1379,18 @@
1221 1379 position: absolute;
1222 1380 margin: 0;
1223 1381 }
1224 1382
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 +
1225 1393 .os-widgets__card--dragging,
1226 1394 .os-widgets__card--resizing {
1227 1395 transition: none;
1228 1396 z-index: 2;
@@ -1316,38 +1484,85 @@
1316 1484 display: none;
1317 1485 }
1318 1486
1319 1487 /*
1320 - * Add-widget tile — matches the "+" tile in the overview top bar.
1321 - * Always visible at the bottom of the column; promoted to center
1322 - * when the column is otherwise empty so first-run users see a
1323 - * friendly target rather than an empty strip.
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()`.
1324 1499 */
1325 1500 .os-widgets__add {
1326 - margin-top: auto;
1327 - padding: 14px 12px;
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;
1328 1510 display: flex;
1329 1511 align-items: center;
1330 1512 justify-content: center;
1331 - gap: 8px;
1332 - pointer-events: auto;
1333 - background: transparent;
1334 - border: 2px dashed rgba( 255, 255, 255, 0.22 );
1335 - border-radius: 14px;
1336 - color: rgba( 255, 255, 255, 0.72 );
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 ) );
1337 1524 font: inherit;
1338 1525 font-weight: 500;
1339 1526 cursor: pointer;
1527 + opacity: 0;
1340 1528 transition:
1529 + opacity 0.15s ease,
1341 1530 border-color 0.15s ease,
1342 1531 background-color 0.15s ease,
1343 1532 color 0.15s ease;
1344 1533 }
1345 1534
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 +
1346 1561 .os-widgets__add:hover {
1347 1562 border-color: rgba( 255, 255, 255, 0.4 );
1348 - background: rgba( 255, 255, 255, 0.06 );
1349 - color: var( --os-ui-fg-on-accent, #fff );
1563 + background: var( --os-widgets-add-bg-hover, rgba( 255, 255, 255, 0.06 ) );
1564 + color: var( --os-fg, #fff );
1350 1565 }
1351 1566
1352 1567 .os-widgets__add:focus-visible {
1353 1568 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
@@ -1354,11 +1569,11 @@
1354 1569 outline-offset: 2px;
1355 1570 }
1356 1571
1357 1572 .os-widgets__add-plus {
1358 - font-size: 20px;
1573 + font-size: 15px;
1359 1574 line-height: 1;
1360 - font-weight: 200;
1575 + font-weight: 300;
1361 1576 }
1362 1577
1363 1578 .os-widgets__add-label {
1364 1579 font-size: 13px;
@@ -1385,9 +1600,9 @@
1385 1600
1386 1601 .os-widget-clock__date {
1387 1602 margin-top: 4px;
1388 1603 font-size: 13px;
1389 - color: rgba( 255, 255, 255, 0.75 );
1604 + color: var( --os-ui-color-text-subtle, rgba( 255, 255, 255, 0.75 ) );
1390 1605 letter-spacing: 0.02em;
1391 1606 }
1392 1607
1393 1608 /* ---------------------------------------------------------------
@@ -1397,9 +1612,13 @@
1397 1612 * --------------------------------------------------------------- */
1398 1613
1399 1614 .os-widget-picker {
1400 1615 position: fixed;
1401 - 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;
1402 1621 max-height: 60vh;
1403 1622 overflow-y: auto;
1404 1623 padding: 10px;
1405 1624 border-radius: 14px;
@@ -1574,5 +1793,469 @@
1574 1793 os-notice a:hover,
1575 1794 os-notice a:focus,
1576 1795 os-notice a:active {
1577 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;
1578 2261 }