PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
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 0.8.9 All 36 releases
← All changes | assets/css/desktop.css +612 -41 1.1.4 → 1.1.12 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
@@ -301,8 +351,12 @@
301 351 * W inside, so the two columns share an axis. `--os-dock-width`
302 352 * is now set on :root (see settings.ts → apply()) so it reaches
303 353 * siblings of #os-shell like the admin bar.
304 354 */
355 +body.os-active #wpadminbar #wp-admin-bar-menu-toggle {
356 + display: none;
357 +}
358 +
305 359 body.os-active #wpadminbar #wp-admin-bar-wp-logo {
306 360 width: var( --os-dock-width, 56px );
307 361 }
308 362
@@ -336,12 +390,26 @@
336 390 /*
337 391 * Desktop shell — fills the viewport below the admin bar.
338 392 * Uses position: fixed so it's unaffected by any containing-block
339 393 * quirks in #wpwrap/#wpcontent.
394 + *
395 + * The inset is the bar's MEASURED bottom edge, `--os-admin-bar-height`
396 + * (written on <html> by `src/admin-bar-height.ts`), with Core's own
397 + * token behind it for the first paint. Core's token is Core's promise
398 + * about Core's bar; a host can make the bar taller, give it a border
399 + * or push it down under a strip of its own — WordPress.com's staff
400 + * debug chrome does — and the shell then started under the bar by
401 + * exactly the difference. The measured token is absent whenever the
402 + * bar has no box at the top edge (hidden, mobile, solo, fullscreen,
403 + * parked in the dynamic mode), so every mode that rebinds Core's
404 + * token below still resolves through it.
340 405 */
341 406 .os-shell {
342 407 position: fixed;
343 - inset-block-start: var(--wp-admin--admin-bar--height, 32px);
408 + inset-block-start: var(
409 + --os-admin-bar-height,
410 + var(--wp-admin--admin-bar--height, 32px)
411 + );
344 412 inset-inline-start: 0;
345 413 inset-inline-end: 0;
346 414 inset-block-end: 0;
347 415 display: flex;
@@ -556,9 +624,12 @@
556 624
557 625 /* Below 783px WP collapses the admin bar to 46px. */
558 626 @media screen and (max-width: 782px) {
559 627 .os-shell {
560 - inset-block-start: var(--wp-admin--admin-bar--height, 46px);
628 + inset-block-start: var(
629 + --os-admin-bar-height,
630 + var(--wp-admin--admin-bar--height, 46px)
631 + );
561 632 }
562 633 }
563 634
564 635 /*
@@ -753,9 +824,10 @@
753 824 background-image: var(--os-tile-image, none);
754 825 background-repeat: var(--os-tile-image-repeat, no-repeat);
755 826 background-size: var(--os-tile-image-size, auto);
756 827 background-position: var(--os-tile-image-position, center);
757 - color: var(--os-fg, #fff);
828 + /* Also the glyph: silhouette art is masked with `currentColor`. */
829 + color: var(--os-desk-fg, var(--os-fg, #fff));
758 830 cursor: pointer;
759 831 border-radius: 8px;
760 832 transition: background-color 0.15s ease;
761 833 /* Positioning context for `.os-icon__badge`. */
@@ -817,9 +889,9 @@
817 889 .os-icon:focus-visible {
818 890 /* background-COLOR, not the shorthand: the shorthand would reset
819 891 * the ICON_TILE texture declared above, so a themed tile would
820 892 * vanish on hover. */
821 - background-color: rgba(255, 255, 255, 0.12);
893 + background-color: var(--os-icon-hover-bg, rgba(255, 255, 255, 0.12));
822 894 outline: none;
823 895 }
824 896
825 897 .os-icon:focus-visible {
@@ -843,12 +915,20 @@
843 915 max-height: 48px;
844 916 object-fit: contain;
845 917 }
846 918
919 +.os-icon__image {
920 + filter: var(--os-icon-glyph-shadow, none);
921 +}
922 +
847 923 .os-icon__label {
848 924 font-size: 12px;
849 925 text-align: center;
850 - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45);
926 + background: var(--os-icon-label-bg, transparent);
927 + border-radius: 6px;
928 + /* Inline only: a two-line caption already fills the 96px grid row. */
929 + padding: 0 6px;
930 + text-shadow: var(--os-icon-label-shadow, 0 1px 2px rgba(0, 0, 0, 0.45));
851 931 line-height: 1.2;
852 932 word-break: break-word;
853 933 }
854 934
@@ -924,10 +1004,16 @@
924 1004 * spills past the card's rounded bottom edge. */
925 1005 display: flex;
926 1006 flex-direction: column;
927 1007 pointer-events: auto;
928 - background-color: rgba( 20, 20, 22, 0.55 );
929 1008 /*
1009 + * The glass is also `--os-ui-color-surface`, the one name in the
1010 + * widget contract that describes the card itself. Declared in
1011 + * `variables.css` so a widget reading it and the card painting it
1012 + * cannot drift apart.
1013 + */
1014 + background-color: var( --os-ui-color-surface, rgba( 20, 20, 22, 0.55 ) );
1015 + /*
930 1016 * WIDGET texture slot — layered over the frosted card colour, so a
931 1017 * translucent texture still gets the blur underneath.
932 1018 */
933 1019 background-image: var( --os-widget-image, none );
@@ -968,10 +1054,12 @@
968 1054 .os-widgets__card-close {
969 1055 position: absolute;
970 1056 top: 6px;
971 1057 inset-inline-end: 6px;
972 - width: 22px;
973 - height: 22px;
1058 + /* 24px is the WCAG 2.2 SC 2.5.8 floor. The glyph inside is unchanged;
1059 + only the target grows. */
1060 + width: 24px;
1061 + height: 24px;
974 1062 display: flex;
975 1063 align-items: center;
976 1064 justify-content: center;
977 1065 padding: 0;
@@ -991,8 +1079,21 @@
991 1079 .os-widgets__card-close:focus-visible {
992 1080 opacity: 1;
993 1081 }
994 1082
1083 +/*
1084 + * The corner close button rests at `opacity: 0` and only the card's
1085 + * hover reveals it, so on touch it is invisible and the widget cannot
1086 + * be dismissed at all. Rest it at the same 0.7 the chrome header's
1087 + * close button uses there, rather than a full 1, so a card that is
1088 + * only being read does not wear a hard × in its corner.
1089 + */
1090 +@media ( hover: none ) {
1091 + .os-widgets__card-close {
1092 + opacity: 0.7;
1093 + }
1094 +}
1095 +
995 1096 .os-widgets__card-close:hover {
996 1097 background: var( --os-ui-danger, #d63638 );
997 1098 color: var( --os-ui-fg-on-accent, #fff );
998 1099 }
@@ -1056,10 +1157,10 @@
1056 1157 corner-absolute — reset position + always-visible opacity. */
1057 1158 .os-widgets__chrome .os-widgets__card-close {
1058 1159 position: static;
1059 1160 opacity: 0.7;
1060 - width: 20px;
1061 - height: 20px;
1161 + width: 24px;
1162 + height: 24px;
1062 1163 }
1063 1164
1064 1165 .os-widgets__chrome .os-widgets__card-close:hover,
1065 1166 .os-widgets__chrome .os-widgets__card-close:focus-visible {
@@ -1073,9 +1174,10 @@
1073 1174 * expose it (tapping it on a docked card would be a confusing
1074 1175 * no-op). The DOM node stays mounted either way so state flips
1075 1176 * don't rebuild chrome.
1076 1177 *
1077 - * Styling mirrors the inline close button: 20×20 round tile,
1178 + * Styling mirrors the inline close button: 24×24 round tile
1179 + * (the WCAG 2.2 SC 2.5.8 target-size floor),
1078 1180 * dimmed by default, full-opacity on hover / focus. Distinct hover
1079 1181 * color (theme accent, not red) signals "put back" vs. "remove."
1080 1182 */
1081 1183 .os-widgets__card-redock {
@@ -1086,10 +1188,10 @@
1086 1188 border: 0;
1087 1189 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
1088 1190 color: rgba( 255, 255, 255, 0.75 );
1089 1191 border-radius: 50%;
1090 - width: 20px;
1091 - height: 20px;
1192 + width: 24px;
1193 + height: 24px;
1092 1194 cursor: pointer;
1093 1195 opacity: 0.7;
1094 1196 transition:
1095 1197 opacity 0.15s ease,
@@ -1104,9 +1206,9 @@
1104 1206 .os-widgets__card-redock:hover,
1105 1207 .os-widgets__card-redock:focus-visible {
1106 1208 opacity: 1;
1107 1209 background: var( --wp-admin-theme-color, #2271b1 );
1108 - color: var( --os-ui-fg-on-accent, #fff );
1210 + color: var( --os-ui-accent-ink, var( --os-ui-fg-on-accent, #fff ) );
1109 1211 }
1110 1212
1111 1213 /*
1112 1214 * Floating widgets — absolute-positioned, inline top/left/width/height
@@ -1244,12 +1346,17 @@
1244 1346 align-items: center;
1245 1347 justify-content: center;
1246 1348 gap: 7px;
1247 1349 pointer-events: none;
1248 - background: transparent;
1249 - border: 1px dashed rgba( 255, 255, 255, 0.22 );
1350 + /* `.os-widgets__card`'s glass, to the number: the pill trails a
1351 + stack of those cards. Its dashed edge is what makes it a slot. */
1352 + background: var( --os-widgets-add-bg, transparent );
1353 + backdrop-filter: blur( 18px ) saturate( 140% );
1354 + -webkit-backdrop-filter: blur( 18px ) saturate( 140% );
1355 + box-shadow: 0 8px 28px var( --os-widgets-add-shadow, rgba( 0, 0, 0, 0.35 ) );
1356 + border: 1px dashed var( --os-widgets-add-border, rgba( 255, 255, 255, 0.22 ) );
1250 1357 border-radius: 10px;
1251 - color: rgba( 255, 255, 255, 0.72 );
1358 + color: var( --os-ui-color-text, var( --os-fg, #fff ) );
1252 1359 font: inherit;
1253 1360 font-weight: 500;
1254 1361 cursor: pointer;
1255 1362 opacity: 0;
@@ -1287,10 +1394,10 @@
1287 1394 }
1288 1395
1289 1396 .os-widgets__add:hover {
1290 1397 border-color: rgba( 255, 255, 255, 0.4 );
1291 - background: rgba( 255, 255, 255, 0.06 );
1292 - color: var( --os-ui-fg-on-accent, #fff );
1398 + background: var( --os-widgets-add-bg-hover, rgba( 255, 255, 255, 0.06 ) );
1399 + color: var( --os-fg, #fff );
1293 1400 }
1294 1401
1295 1402 .os-widgets__add:focus-visible {
1296 1403 outline: 2px solid var( --wp-admin-theme-color, #2271b1 );
@@ -1517,5 +1624,469 @@
1517 1624 os-notice a:hover,
1518 1625 os-notice a:focus,
1519 1626 os-notice a:active {
1520 1627 color: var(--os-ui-notice-link-hover, #fffbff);
1628 +}
1629 +
1630 +/* --------------------------------------------------------------------
1631 + * The workspace hop — cross-document view transitions between shells.
1632 + *
1633 + * Loaded only on the SHELL screen (this sheet never reaches chromeless
1634 + * iframes), so only shell→shell navigations transition: hopping between
1635 + * a site's desktop and the network admin's crossfades the two desktops
1636 + * instead of hard-cutting, which is what makes the per-admin sessions
1637 + * read as workspaces. Classic admin pages never opt in, so entering or
1638 + * leaving the shell stays a plain navigation, and reloads are excluded
1639 + * by the spec. Browsers without cross-document view transitions ignore
1640 + * all of this and navigate plainly. See docs/multisite.md.
1641 + */
1642 +@view-transition {
1643 + navigation: auto;
1644 +}
1645 +
1646 +/* Reduced motion swaps instantly — the hop still happens, the
1647 + * animation does not. */
1648 +@media ( prefers-reduced-motion: reduce ) {
1649 + @view-transition {
1650 + navigation: none;
1651 + }
1652 +}
1653 +
1654 +/* A gentle zoom-through: the desktop being left recedes as the one
1655 + * arriving settles in. Subtle on purpose — this runs on a full page
1656 + * swap, and anything louder reads as a reload effect. */
1657 +::view-transition-old(root) {
1658 + animation: os-workspace-hop-out 220ms cubic-bezier( 0.4, 0, 1, 1 ) both;
1659 +}
1660 +
1661 +::view-transition-new(root) {
1662 + animation: os-workspace-hop-in 280ms cubic-bezier( 0, 0, 0.2, 1 ) both;
1663 +}
1664 +
1665 +@keyframes os-workspace-hop-out {
1666 + to {
1667 + opacity: 0;
1668 + transform: scale( 0.985 );
1669 + }
1670 +}
1671 +
1672 +@keyframes os-workspace-hop-in {
1673 + from {
1674 + opacity: 0;
1675 + transform: scale( 1.015 );
1676 + }
1677 +}
1678 +
1679 +/* The instance hop's two halves, on either side of that page swap.
1680 + * The switcher slides this desk out towards the site it picked
1681 + * (`os-shell--hop-out-next` / `-prev`, for the beat before it
1682 + * navigates), and a shell asked to boot into overview arrives with its
1683 + * desk hidden (`os-shell--arriving`, stamped server-side) until overview
1684 + * is up, then slides it in from the same side (`-next` / `-prev`, the
1685 + * hint the switcher left in sessionStorage; no hint, a plain fade). The
1686 + * wallpaper never moves, which is what makes it read as the tiles
1687 + * changing rather than the page. `src/multisite/instance-transition.ts`
1688 + * drives the classes. */
1689 +.os-shell--hop-out-next #os-area,
1690 +.os-shell--hop-out-prev #os-area {
1691 + opacity: 0;
1692 + transition:
1693 + opacity 220ms cubic-bezier( 0.4, 0, 1, 1 ),
1694 + transform 220ms cubic-bezier( 0.4, 0, 1, 1 );
1695 +}
1696 +
1697 +.os-shell--hop-out-next #os-area {
1698 + transform: translateX( -48px );
1699 +}
1700 +
1701 +.os-shell--hop-out-prev #os-area {
1702 + transform: translateX( 48px );
1703 +}
1704 +
1705 +.os-shell--hop-out-next .os-dock,
1706 +.os-shell--hop-out-prev .os-dock,
1707 +.os-shell--arriving .os-dock {
1708 + opacity: 0;
1709 + transition: opacity 220ms ease;
1710 +}
1711 +
1712 +.os-shell--arriving #os-area {
1713 + opacity: 0;
1714 + transition: none;
1715 +}
1716 +
1717 +.os-shell--arriving-next #os-area {
1718 + transform: translateX( 48px );
1719 +}
1720 +
1721 +.os-shell--arriving-prev #os-area {
1722 + transform: translateX( -48px );
1723 +}
1724 +
1725 +/* The reveal: the arriving classes come off and this goes on in the
1726 + * same frame, so the desk transitions from hidden to its resting
1727 + * place. */
1728 +.os-shell--revealing #os-area {
1729 + transition:
1730 + opacity 360ms cubic-bezier( 0, 0, 0.2, 1 ),
1731 + transform 360ms cubic-bezier( 0, 0, 0.2, 1 );
1732 +}
1733 +
1734 +.os-shell--revealing .os-dock {
1735 + transition: opacity 360ms ease;
1736 +}
1737 +
1738 +/* A desk must never stay hidden: if the boot that reveals it never
1739 + * runs, the area and the dock come back on their own a few seconds
1740 + * in. */
1741 +@keyframes os-instance-arrive-fallback {
1742 + to {
1743 + opacity: 1;
1744 + transform: none;
1745 + }
1746 +}
1747 +
1748 +.os-shell--arriving #os-area,
1749 +.os-shell--arriving .os-dock {
1750 + animation: os-instance-arrive-fallback 1ms linear 6s forwards;
1751 +}
1752 +
1753 +@media ( prefers-reduced-motion: reduce ) {
1754 + .os-shell--hop-out-next #os-area,
1755 + .os-shell--hop-out-prev #os-area,
1756 + .os-shell--arriving-next #os-area,
1757 + .os-shell--arriving-prev #os-area {
1758 + transform: none;
1759 + }
1760 +
1761 + .os-shell--hop-out-next #os-area,
1762 + .os-shell--hop-out-prev #os-area,
1763 + .os-shell--hop-out-next .os-dock,
1764 + .os-shell--hop-out-prev .os-dock,
1765 + .os-shell--revealing #os-area,
1766 + .os-shell--revealing .os-dock {
1767 + transition: none;
1768 + }
1769 +}
1770 +
1771 +/* A residency frame follows the window body without scrolling with its contents. */
1772 +.os-mio-residence {
1773 + position: absolute;
1774 + z-index: 50;
1775 + pointer-events: none;
1776 + overflow: hidden;
1777 + container-type: inline-size;
1778 +}
1779 +
1780 +.os-mio-residence[hidden],
1781 +.os-mio-chat-launcher[hidden] {
1782 + display: none;
1783 +}
1784 +
1785 +.os-mio-chat-launcher {
1786 + position: absolute;
1787 + inset-inline-end: 20px;
1788 + inset-block-end: 18px;
1789 + pointer-events: auto;
1790 +}
1791 +
1792 +.os-mio-chat {
1793 + position: absolute;
1794 + inset-inline-end: 16px;
1795 + inset-block-end: 16px;
1796 + inline-size: min(380px, calc(100% - 32px));
1797 + max-block-size: calc(100% - 32px);
1798 + box-sizing: border-box;
1799 + display: flex;
1800 + flex-direction: column;
1801 + gap: 12px;
1802 + padding: 18px;
1803 + pointer-events: auto;
1804 + color: var(--os-mio-chat-fg, #1d2327);
1805 + background: var(--os-mio-chat-bg, #fff);
1806 + border: 1px solid var(--os-mio-chat-border, #c3c4c7);
1807 + border-radius: var(--os-mio-chat-radius, 16px);
1808 + box-shadow: 0 8px 40px var(--os-mio-chat-glow, #00000020);
1809 + animation: os-mio-chat-enter 180ms ease-out;
1810 +}
1811 +
1812 +.os-mio-chat header {
1813 + display: flex;
1814 + align-items: center;
1815 + gap: 12px;
1816 +}
1817 +
1818 +.os-mio-chat header strong {
1819 + letter-spacing: .14em;
1820 +}
1821 +
1822 +.os-mio-chat header span {
1823 + flex: 1;
1824 + font-size: 11px;
1825 + color: var(--os-mio-chat-muted, #646970);
1826 +}
1827 +
1828 +.os-mio-chat__log {
1829 + position: relative;
1830 + min-block-size: 60px;
1831 + max-block-size: 300px;
1832 + overflow: auto;
1833 + overscroll-behavior: contain;
1834 +}
1835 +
1836 +.os-mio-chat__message {
1837 + margin-block: 0 10px;
1838 + padding: 10px 12px;
1839 + line-height: 1.6;
1840 + white-space: pre-wrap;
1841 + overflow-wrap: anywhere;
1842 +}
1843 +
1844 +.os-mio-chat__message--user {
1845 + border-radius: 16px 16px 4px 16px;
1846 + background: var(--os-mio-chat-user-bg, #f0f0f1);
1847 +}
1848 +
1849 +.os-mio-chat__composer {
1850 + display: flex;
1851 + align-items: center;
1852 + gap: 8px;
1853 +}
1854 +
1855 +.os-mio-chat__composer os-textarea {
1856 + flex: 1;
1857 + min-inline-size: 0;
1858 +}
1859 +
1860 +.os-mio-chat__status,
1861 +.os-mio-chat small {
1862 + margin: 0;
1863 + font-size: 11px;
1864 + line-height: 1.5;
1865 + color: var(--os-mio-chat-muted, #646970);
1866 +}
1867 +
1868 +@keyframes os-mio-chat-enter {
1869 + from { opacity: 0; transform: translateY(6px) scale(.98); }
1870 + to { opacity: 1; transform: translateY(0) scale(1); }
1871 +}
1872 +
1873 +@media (prefers-reduced-motion: reduce) {
1874 + .os-mio-chat { animation: none; }
1875 +}
1876 +
1877 +.os-mio-residence > .os-mio { z-index: 0; }
1878 +.os-mio-chat, .os-mio-chat-launcher { z-index: 1; }
1879 +
1880 +.os-mio-chat [hidden] {
1881 + display: none !important;
1882 +}
1883 +
1884 +.os-mio-chat__message--assistant {
1885 + white-space: normal;
1886 +}
1887 +
1888 +.os-mio-chat__message p {
1889 + margin-block: 0 8px;
1890 +}
1891 +
1892 +.os-mio-chat__message ul,
1893 +.os-mio-chat__message ol {
1894 + margin-block: 8px;
1895 + padding-inline-start: 20px;
1896 +}
1897 +
1898 +.os-mio-chat__message a {
1899 + color: inherit;
1900 + text-decoration: underline;
1901 +}
1902 +
1903 +/* Shared MIO chrome: the same portrait with a softly revealed off-state slash. */
1904 +.os-mio-window-toggle svg {
1905 + width: 20px;
1906 + height: 20px;
1907 + overflow: visible;
1908 +}
1909 +
1910 +.os-mio-window-toggle__slash {
1911 + fill: none;
1912 + stroke: currentColor;
1913 + stroke-width: 2.4;
1914 + stroke-linecap: round;
1915 + opacity: 0;
1916 + stroke-dasharray: 26;
1917 + stroke-dashoffset: 26;
1918 + transition: opacity 240ms ease, stroke-dashoffset 240ms ease;
1919 +}
1920 +
1921 +.os-mio-window-toggle[data-mio-disabled="true"] .os-mio-window-toggle__slash {
1922 + opacity: 1;
1923 + stroke-dashoffset: 0;
1924 +}
1925 +
1926 +.os-mio-chat__activity {
1927 + display: flex;
1928 + align-items: center;
1929 + gap: 8px;
1930 + min-height: 18px;
1931 +}
1932 +
1933 +.os-mio-chat__thinking {
1934 + display: inline-flex;
1935 + align-items: center;
1936 + gap: 3px;
1937 + width: 24px;
1938 + flex-shrink: 0;
1939 + opacity: 0;
1940 + color: var(--os-mio-chat-muted, #646970);
1941 + transition: opacity 240ms ease;
1942 +}
1943 +
1944 +.os-mio-chat[data-thinking="true"] .os-mio-chat__thinking {
1945 + opacity: 1;
1946 +}
1947 +
1948 +.os-mio-chat__thinking i {
1949 + width: 4px;
1950 + height: 4px;
1951 + border-radius: 50%;
1952 + background: currentColor;
1953 + animation: os-mio-thinking 1.6s ease-in-out infinite;
1954 + animation-play-state: paused;
1955 +}
1956 +
1957 +.os-mio-chat__thinking i:nth-child(2) { animation-delay: 160ms; }
1958 +.os-mio-chat__thinking i:nth-child(3) { animation-delay: 320ms; }
1959 +.os-mio-chat[data-thinking="true"] .os-mio-chat__thinking i { animation-play-state: running; }
1960 +.os-mio-window-toggle[data-mio-thinking="true"] circle { animation: os-mio-thinking 2.4s ease-in-out infinite; }
1961 +
1962 +@keyframes os-mio-thinking {
1963 + 0%, 100% { opacity: 0.45; }
1964 + 50% { opacity: 1; }
1965 +}
1966 +
1967 +@media (prefers-reduced-motion: reduce) {
1968 + .os-mio-window-toggle__slash,
1969 + .os-mio-chat__thinking { transition: none; }
1970 + .os-mio-chat__thinking i,
1971 + .os-mio-window-toggle[data-mio-thinking="true"] circle { animation: none; }
1972 +}
1973 +
1974 +.os-mio-window-toggle {
1975 + max-inline-size: 30px;
1976 + opacity: 1;
1977 + visibility: visible;
1978 + transition: opacity 180ms ease, max-inline-size 240ms ease, visibility 0s;
1979 +}
1980 +
1981 +.os-mio-window-toggle[data-mio-available="false"] {
1982 + max-inline-size: 0;
1983 + opacity: 0;
1984 + visibility: hidden;
1985 + overflow: hidden;
1986 + pointer-events: none;
1987 + transition: opacity 180ms ease, max-inline-size 240ms ease, visibility 0s 180ms;
1988 +}
1989 +
1990 +@media (prefers-reduced-motion: reduce) {
1991 + .os-mio-window-toggle,
1992 + .os-mio-window-toggle[data-mio-available="false"] { transition: none; }
1993 +}
1994 +
1995 +/* The launcher stays available; the companion only speaks when invited. */
1996 +.os-mio > canvas,
1997 +.os-mio > .os-mio__handle {
1998 + transition: opacity 180ms ease, visibility 180ms;
1999 +}
2000 +
2001 +.os-mio[data-mio-visible="false"] > canvas,
2002 +.os-mio[data-mio-visible="false"] > .os-mio__handle {
2003 + opacity: 0;
2004 + visibility: hidden;
2005 + pointer-events: none;
2006 +}
2007 +
2008 +.os-mio-callout {
2009 + position: absolute;
2010 + z-index: 2;
2011 + display: flex;
2012 + align-items: center;
2013 + gap: 10px;
2014 + max-inline-size: calc(100% - 16px);
2015 + box-sizing: border-box;
2016 + padding-block: 6px;
2017 + padding-inline: 15px 6px;
2018 + border: 1px solid var(--os-mio-callout-border, #d6b8cb);
2019 + border-radius: 22px;
2020 + background: var(--os-mio-callout-bg, #ffe0cf);
2021 + color: var(--os-mio-callout-fg, #382b38);
2022 + box-shadow: 0 4px 16px var(--os-mio-chat-glow, #00000020);
2023 + font-size: 12px;
2024 + line-height: 1.5;
2025 + font-weight: 500;
2026 + pointer-events: auto;
2027 + animation: os-mio-chat-enter 180ms ease-out;
2028 +}
2029 +
2030 +.os-mio-callout os-button::part(button) {
2031 + inline-size: 26px;
2032 + block-size: 26px;
2033 + font-size: 17px;
2034 + line-height: 1;
2035 +}
2036 +
2037 +/* A small rounded tail joins the companion without a second outlined shape. */
2038 +.os-mio-callout::after {
2039 + content: "";
2040 + position: absolute;
2041 + inset-block-end: -4px;
2042 + inset-inline-end: 18px;
2043 + inline-size: 8px;
2044 + block-size: 8px;
2045 + background: inherit;
2046 + border-inline-end: inherit;
2047 + border-block-end: inherit;
2048 + border-end-end-radius: 3px;
2049 + transform: rotate(45deg);
2050 +}
2051 +
2052 +.os-mio-callout[hidden] { display: none; }
2053 +
2054 +@media (prefers-reduced-motion: reduce) {
2055 + .os-mio > canvas,
2056 + .os-mio > .os-mio__handle { transition: none; }
2057 + .os-mio-callout { animation: none; }
2058 +}
2059 +
2060 +/* Response controls belong to their message; all layout follows inline flow. */
2061 +.os-mio-chat__actions {
2062 + display: flex;
2063 + flex-wrap: wrap;
2064 + gap: 8px;
2065 + margin-block-start: 12px;
2066 +}
2067 +
2068 +.os-mio-chat__action {
2069 + display: flex;
2070 + flex-direction: column;
2071 + align-items: flex-start;
2072 + gap: 4px;
2073 + min-inline-size: 0;
2074 + max-inline-size: 100%;
2075 +}
2076 +
2077 +.os-mio-chat__action os-button::part(button) {
2078 + white-space: normal;
2079 + overflow-wrap: anywhere;
2080 +}
2081 +
2082 +.os-mio-chat__action-status {
2083 + font-size: 12px;
2084 + color: var(--os-mio-chat-muted, #646970);
2085 + overflow-wrap: anywhere;
2086 +}
2087 +
2088 +.os-mio-chat__composer os-textarea::part(textarea) {
2089 + white-space: pre-wrap;
2090 + overflow-wrap: anywhere;
2091 + overflow-x: hidden;
1521 2092 }