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 +655 -62 1.1.6 → 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,33 +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 -body.os-active.os-admin-bar-dynamic #wpadminbar:active,
193 -/*
194 - * …and while the notch is being used. The notch hangs from the same
195 - * edge and steps down to sit under the bar when it appears (see
196 - * notch.css), so letting the bar retract out from over it mid-reach
197 - * would drop the notch back up under the pointer.
198 - */
199 -body.os-active.os-admin-bar-dynamic:has( .os-notch:hover ) #wpadminbar,
200 -body.os-active.os-admin-bar-dynamic:has( .os-notch:focus-visible ) #wpadminbar {
201 - transform: translateY(0);
230 +body.os-active.os-admin-bar-dynamic #wpadminbar:active {
231 + inset-block-start: 0 !important;
202 232 }
203 233
204 234 /*
205 235 * Reveal zone — an invisible extension of the bar's hit area, hanging
@@ -219,8 +249,25 @@
219 249 position: absolute;
220 250 inset-inline: 0;
221 251 inset-block-start: 100%;
222 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);
223 270 }
224 271
225 272 /*
226 273 * ...but only while the bar is parked. Left standing once the bar is
@@ -228,9 +275,10 @@
228 275 * clicks on whatever window is up there.
229 276 *
230 277 * Collapsing it is also what keeps the reveal stable: the pointer
231 278 * that tripped the zone is, post-reveal, inside the bar's real box,
232 - * 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).
233 281 */
234 282 body.os-active.os-admin-bar-dynamic #wpadminbar:hover::after,
235 283 body.os-active.os-admin-bar-dynamic #wpadminbar:focus-within::after,
236 284 body.os-active.os-admin-bar-dynamic #wpadminbar:active::after {
@@ -250,9 +298,11 @@
250 298 }
251 299 }
252 300
253 301 @media (prefers-reduced-motion: reduce) {
254 - 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. */
255 305 transition: none;
256 306 }
257 307 }
258 308
@@ -288,8 +338,132 @@
288 338 padding-top: 0 !important;
289 339 }
290 340
291 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 +/*
292 466 * Align the admin-bar W logo with the dock below it.
293 467 *
294 468 * Without this rule the admin bar's leftmost item (the WordPress W)
295 469 * renders at its Core-dictated ~44 px width, while the dock below it
@@ -301,8 +475,12 @@
301 475 * W inside, so the two columns share an axis. `--os-dock-width`
302 476 * is now set on :root (see settings.ts → apply()) so it reaches
303 477 * siblings of #os-shell like the admin bar.
304 478 */
479 +body.os-active #wpadminbar #wp-admin-bar-menu-toggle {
480 + display: none;
481 +}
482 +
305 483 body.os-active #wpadminbar #wp-admin-bar-wp-logo {
306 484 width: var( --os-dock-width, 56px );
307 485 }
308 486
@@ -322,8 +500,16 @@
322 500 margin: 0;
323 501 }
324 502
325 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 +/*
326 512 * Classic wpcontent carries a `margin-left: 160px` for the sidebar. With
327 513 * the sidebar hidden we let wpcontent fall back to flush — the shell is
328 514 * fixed-positioned anyway so this doesn't change the shell's layout, it
329 515 * just prevents a phantom sidebar gutter in any stray visible content.
@@ -336,12 +522,26 @@
336 522 /*
337 523 * Desktop shell — fills the viewport below the admin bar.
338 524 * Uses position: fixed so it's unaffected by any containing-block
339 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.
340 537 */
341 538 .os-shell {
342 539 position: fixed;
343 - 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 + );
344 544 inset-inline-start: 0;
345 545 inset-inline-end: 0;
346 546 inset-block-end: 0;
347 547 display: flex;
@@ -357,8 +557,14 @@
357 557 * See `--os-backstop`.
358 558 */
359 559 background: var(--os-backstop, #1d2327);
360 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 + /*
361 567 * Shell typeface. Every piece of chrome that doesn't set its own
362 568 * face inherits from here — dock labels, desktop icon labels,
363 569 * widgets, the overview. Windows override with `--os-ui-font` on
364 570 * their body, so a theme can run a display face on the desk and a
@@ -556,9 +762,12 @@
556 762
557 763 /* Below 783px WP collapses the admin bar to 46px. */
558 764 @media screen and (max-width: 782px) {
559 765 .os-shell {
560 - 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 + );
561 770 }
562 771 }
563 772
564 773 /*
@@ -753,9 +962,10 @@
753 962 background-image: var(--os-tile-image, none);
754 963 background-repeat: var(--os-tile-image-repeat, no-repeat);
755 964 background-size: var(--os-tile-image-size, auto);
756 965 background-position: var(--os-tile-image-position, center);
757 - 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));
758 968 cursor: pointer;
759 969 border-radius: 8px;
760 970 transition: background-color 0.15s ease;
761 971 /* Positioning context for `.os-icon__badge`. */
@@ -817,9 +1027,9 @@
817 1027 .os-icon:focus-visible {
818 1028 /* background-COLOR, not the shorthand: the shorthand would reset
819 1029 * the ICON_TILE texture declared above, so a themed tile would
820 1030 * vanish on hover. */
821 - background-color: rgba(255, 255, 255, 0.12);
1031 + background-color: var(--os-icon-hover-bg, rgba(255, 255, 255, 0.12));
822 1032 outline: none;
823 1033 }
824 1034
825 1035 .os-icon:focus-visible {
@@ -843,12 +1053,20 @@
843 1053 max-height: 48px;
844 1054 object-fit: contain;
845 1055 }
846 1056
1057 +.os-icon__image {
1058 + filter: var(--os-icon-glyph-shadow, none);
1059 +}
1060 +
847 1061 .os-icon__label {
848 - font-size: 12px;
1062 + font-size: var(--os-icon-label-font-size, 12px);
849 1063 text-align: center;
850 - 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));
851 1069 line-height: 1.2;
852 1070 word-break: break-word;
853 1071 }
854 1072
@@ -860,21 +1078,38 @@
860 1078 * drag / resize / click-through behaviour is unaffected.
861 1079 * --------------------------------------------------------------- */
862 1080
863 1081 .os-widgets {
864 - border-radius: 14px;
865 1082 position: absolute;
866 1083 /*
867 1084 * Inside the work area vertically — see `.os-icons` for why the
868 - * insets are here. The inline edge stays a plain 16px: the column
869 - * hugs the trailing side, which is a logical choice, while the
870 - * side insets are physical; and a side rail never claims one
871 - * anyway (it is a flex sibling, the area is already narrower).
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.
872 1102 */
873 - top: calc( var( --os-work-area-inset-top, 0px ) + 16px );
874 - bottom: calc( var( --os-work-area-inset-bottom, 80px ) + 16px );
875 - inset-inline-end: 16px;
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;
876 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 );
877 1112 display: flex;
878 1113 flex-direction: column;
879 1114 gap: 12px;
880 1115 z-index: 1;
@@ -939,16 +1174,20 @@
939 1174 background-image: var( --os-widget-image, none );
940 1175 background-repeat: var( --os-widget-image-repeat, repeat );
941 1176 background-size: var( --os-widget-image-size, auto );
942 1177 background-position: var( --os-widget-image-position, center );
943 - backdrop-filter: blur( 18px ) saturate( 140% );
944 - -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
945 - border: 1px solid rgba( 255, 255, 255, 0.08 );
946 - border-radius: 14px;
947 - color: var( --os-ui-fg-on-accent, #fff );
948 - 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,
949 1187 0 8px 28px rgba( 0, 0, 0, 0.35 ),
950 - inset 0 0 0 1px rgba( 255, 255, 255, 0.04 );
1188 + inset 0 0 0 1px rgba( 255, 255, 255, 0.04 )
1189 + );
951 1190 transition:
952 1191 box-shadow 0.18s ease,
953 1192 opacity 0.18s ease;
954 1193 }
@@ -956,11 +1195,13 @@
956 1195 /* Hover feedback is shadow-only — no transform. Cards must feel
957 1196 * anchored; a hover nudge reads as the widget drifting under the
958 1197 * pointer (and fought the user's chosen position on floating cards). */
959 1198 .os-widgets__card:hover {
960 - box-shadow:
1199 + box-shadow: var(
1200 + --os-widget-card-shadow-hover,
961 1201 0 12px 36px rgba( 0, 0, 0, 0.45 ),
962 - inset 0 0 0 1px rgba( 255, 255, 255, 0.08 );
1202 + inset 0 0 0 1px rgba( 255, 255, 255, 0.08 )
1203 + );
963 1204 }
964 1205
965 1206 .os-widgets__card-body {
966 1207 padding: 16px;
@@ -974,10 +1215,12 @@
974 1215 .os-widgets__card-close {
975 1216 position: absolute;
976 1217 top: 6px;
977 1218 inset-inline-end: 6px;
978 - width: 22px;
979 - 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;
980 1223 display: flex;
981 1224 align-items: center;
982 1225 justify-content: center;
983 1226 padding: 0;
@@ -997,8 +1240,21 @@
997 1240 .os-widgets__card-close:focus-visible {
998 1241 opacity: 1;
999 1242 }
1000 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 +
1001 1257 .os-widgets__card-close:hover {
1002 1258 background: var( --os-ui-danger, #d63638 );
1003 1259 color: var( --os-ui-fg-on-accent, #fff );
1004 1260 }
@@ -1062,10 +1318,10 @@
1062 1318 corner-absolute — reset position + always-visible opacity. */
1063 1319 .os-widgets__chrome .os-widgets__card-close {
1064 1320 position: static;
1065 1321 opacity: 0.7;
1066 - width: 20px;
1067 - height: 20px;
1322 + width: 24px;
1323 + height: 24px;
1068 1324 }
1069 1325
1070 1326 .os-widgets__chrome .os-widgets__card-close:hover,
1071 1327 .os-widgets__chrome .os-widgets__card-close:focus-visible {
@@ -1079,9 +1335,10 @@
1079 1335 * expose it (tapping it on a docked card would be a confusing
1080 1336 * no-op). The DOM node stays mounted either way so state flips
1081 1337 * don't rebuild chrome.
1082 1338 *
1083 - * 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),
1084 1341 * dimmed by default, full-opacity on hover / focus. Distinct hover
1085 1342 * color (theme accent, not red) signals "put back" vs. "remove."
1086 1343 */
1087 1344 .os-widgets__card-redock {
@@ -1092,10 +1349,10 @@
1092 1349 border: 0;
1093 1350 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
1094 1351 color: rgba( 255, 255, 255, 0.75 );
1095 1352 border-radius: 50%;
1096 - width: 20px;
1097 - height: 20px;
1353 + width: 24px;
1354 + height: 24px;
1098 1355 cursor: pointer;
1099 1356 opacity: 0.7;
1100 1357 transition:
1101 1358 opacity 0.15s ease,
@@ -1110,9 +1367,9 @@
1110 1367 .os-widgets__card-redock:hover,
1111 1368 .os-widgets__card-redock:focus-visible {
1112 1369 opacity: 1;
1113 1370 background: var( --wp-admin-theme-color, #2271b1 );
1114 - color: var( --os-ui-fg-on-accent, #fff );
1371 + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) );
1115 1372 }
1116 1373
1117 1374 /*
1118 1375 * Floating widgets — absolute-positioned, inline top/left/width/height
@@ -1242,10 +1499,14 @@
1242 1499 */
1243 1500 .os-widgets__add {
1244 1501 position: absolute;
1245 1502 top: 0;
1246 - left: 50%;
1247 - transform: translateX( -50% );
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;
1248 1509 padding: 8px 22px;
1249 1510 display: flex;
1250 1511 align-items: center;
1251 1512 justify-content: center;
@@ -1250,12 +1511,17 @@
1250 1511 align-items: center;
1251 1512 justify-content: center;
1252 1513 gap: 7px;
1253 1514 pointer-events: none;
1254 - background: transparent;
1255 - border: 1px dashed rgba( 255, 255, 255, 0.22 );
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 ) );
1256 1522 border-radius: 10px;
1257 - color: rgba( 255, 255, 255, 0.72 );
1523 + color: var( --os-ui-color-text, var( --os-fg, #fff ) );
1258 1524 font: inherit;
1259 1525 font-weight: 500;
1260 1526 cursor: pointer;
1261 1527 opacity: 0;
@@ -1293,10 +1559,10 @@
1293 1559 }
1294 1560
1295 1561 .os-widgets__add:hover {
1296 1562 border-color: rgba( 255, 255, 255, 0.4 );
1297 - background: rgba( 255, 255, 255, 0.06 );
1298 - 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 );
1299 1565 }
1300 1566
1301 1567 .os-widgets__add:focus-visible {
1302 1568 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
@@ -1334,9 +1600,9 @@
1334 1600
1335 1601 .os-widget-clock__date {
1336 1602 margin-top: 4px;
1337 1603 font-size: 13px;
1338 - color: rgba( 255, 255, 255, 0.75 );
1604 + color: var( --os-ui-color-text-subtle, rgba( 255, 255, 255, 0.75 ) );
1339 1605 letter-spacing: 0.02em;
1340 1606 }
1341 1607
1342 1608 /* ---------------------------------------------------------------
@@ -1346,9 +1612,13 @@
1346 1612 * --------------------------------------------------------------- */
1347 1613
1348 1614 .os-widget-picker {
1349 1615 position: fixed;
1350 - 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;
1351 1621 max-height: 60vh;
1352 1622 overflow-y: auto;
1353 1623 padding: 10px;
1354 1624 border-radius: 14px;
@@ -1664,5 +1934,328 @@
1664 1934 .os-shell--revealing #os-area,
1665 1935 .os-shell--revealing .os-dock {
1666 1936 transition: none;
1667 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;
1668 2261 }