PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 0.9.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v0.9.8
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
desktop-mode / assets / css / window-chrome.css

window-chrome.css in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 0.9.8, at assets/css/window-chrome.css

1,283 lines 47.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Desktop Mode — Window chrome.
3 *
4 * Everything that renders the base window frame: title bar, icon +
5 * title text, control buttons, focused / unfocused colour variants,
6 * the ⋯ actions menu panel, screen-meta buttons, the tab strip (for
7 * submenu + external tabs), the window body, the primary iframe, and
8 * the native-body variant. State-driven rules (dragging / resizing /
9 * maximized / fullscreen / overview / minimized / closing) live in
10 * sibling files loaded via `windows.css`.
11 *
12 * @since 6.9.0
13 */
14
15 /* Base window. */
16 .desktop-mode-window {
17 /*
18 * border-box sizing is essential for pixel-perfect maximize.
19 * `Window.maximize()` sets inline `width = parent.clientWidth`,
20 * which is the area's inner width. Under content-box sizing, the
21 * window's 1px border would add 2 extra pixels to the rendered
22 * element, pushing its right + bottom borders past the
23 * overflow-hidden boundary of `.desktop-mode-area` and making them
24 * look clipped/offscreen. With border-box, width/height INCLUDE
25 * the border, so `width = clientWidth` fits exactly.
26 *
27 * WP admin CSS does set `*, *::before, *::after { box-sizing:
28 * border-box }` globally, but setting it explicitly here
29 * insulates us from stylesheets or embeds that might reset the
30 * universal rule — the correctness of maximize geometry is too
31 * important to depend on ambient CSS.
32 */
33 box-sizing: border-box;
34 position: absolute;
35 display: flex;
36 flex-direction: column;
37 background: var(--desktop-mode-window-bg);
38 border: 1px solid var(--desktop-mode-window-border);
39 border-radius: var(--desktop-mode-window-radius);
40 box-shadow: var(--desktop-mode-window-shadow);
41 /*
42 * Desktop-theme window frame (WINDOW_FRAME texture slot). All
43 * four properties default to their CSS initial value via the
44 * `var( …, fallback )` second argument, so with no theme active
45 * this costs nothing and the 1px border above stays in charge.
46 * A theme that sets `--desktop-mode-window-border-image-source`
47 * takes the frame over entirely — border-image paints on top of
48 * (and visually replaces) the border. See docs/desktop-themes.md.
49 */
50 border-image-source: var(--desktop-mode-window-border-image-source, none);
51 border-image-slice: var(--desktop-mode-window-border-image-slice, 100%);
52 border-image-width: var(--desktop-mode-window-border-image-width, 1);
53 border-image-repeat: var(--desktop-mode-window-border-image-repeat, stretch);
54 overflow: hidden;
55 min-width: 320px;
56 min-height: 200px;
57 transform-origin: center center;
58 transition:
59 left 0.25s cubic-bezier(0.2, 0, 0.2, 1),
60 top 0.25s cubic-bezier(0.2, 0, 0.2, 1),
61 width 0.25s cubic-bezier(0.2, 0, 0.2, 1),
62 height 0.25s cubic-bezier(0.2, 0, 0.2, 1),
63 border-radius 0.25s ease,
64 transform 0.2s ease,
65 opacity 0.2s ease,
66 box-shadow 0.2s ease,
67 /* Unfocus effects (e.g. `--fx-darken` / `--fx-frost`) toggle
68 * `filter`; keep it in the shared transition list so the
69 * treatment fades both ways as focus moves between windows.
70 * Duration is a custom property (default in `effects.css`) so
71 * the blur/darken ramp can be tuned without editing this list. */
72 filter var( --desktop-mode-fx-transition-duration, 0.5s ) ease-in-out;
73 }
74
75 /*
76 * Suppress the left/top/width/height transition during drag or resize —
77 * otherwise every pointer move lerps and the window lags the cursor.
78 *
79 * Also during a viewport reflow (`--reflowing`) — when the browser
80 * window is being dragged smaller, the ResizeObserver fires many
81 * times per second and each style write would restart the 250 ms
82 * transition, leaving maximized / snapped windows ~250 ms behind the
83 * viewport the whole time. The class is added by
84 * `reflowStatefulWindows` and cleared ~140 ms after the last tick.
85 */
86 .desktop-mode-window--dragging,
87 .desktop-mode-window--resizing,
88 .desktop-mode-window--reflowing {
89 transition: none;
90 }
91
92 /*
93 * Snap-to-grid drag/resize: re-enable a SHORT transition so each
94 * cell-to-cell jump animates smoothly instead of teleporting. The
95 * `--snap-drag` class is added by the window class only when snap
96 * is on at drag/resize start; it composes with `--dragging` /
97 * `--resizing` and the source-order ensures it wins. Kept short
98 * (90 ms) so the window still feels glued to the cursor — anything
99 * longer reads as lag.
100 */
101 .desktop-mode-window--snap-drag {
102 transition:
103 left 0.09s ease-out,
104 top 0.09s ease-out,
105 width 0.09s ease-out,
106 height 0.09s ease-out;
107 }
108
109 /* Focused window gets elevated shadow and distinct title bar. */
110 .desktop-mode-window--focused {
111 box-shadow: var(--desktop-mode-window-shadow-focused);
112 /*
113 * WINDOW_FRAME_FOCUSED texture slot. Each property falls through
114 * the unfocused WINDOW_FRAME value before reaching its initial, so
115 * a theme shipping one frame gets it on both states and a theme
116 * shipping two gets a frame that lights up on focus — the same
117 * fallback chain TITLEBAR_FOCUSED uses.
118 */
119 border-image-source: var(
120 --desktop-mode-window-border-image-focused-source,
121 var(--desktop-mode-window-border-image-source, none)
122 );
123 border-image-slice: var(
124 --desktop-mode-window-border-image-focused-slice,
125 var(--desktop-mode-window-border-image-slice, 100%)
126 );
127 border-image-width: var(
128 --desktop-mode-window-border-image-focused-width,
129 var(--desktop-mode-window-border-image-width, 1)
130 );
131 border-image-repeat: var(
132 --desktop-mode-window-border-image-focused-repeat,
133 var(--desktop-mode-window-border-image-repeat, stretch)
134 );
135 }
136
137 /* Title bar. */
138 .desktop-mode-window__titlebar {
139 position: relative;
140 /* Promote the titlebar into its own stacking context above the
141 * body. Without this, the body (which comes later in source order
142 * and inherits z-auto) wins document-order overlap battles —
143 * notably any sticky-positioned content inside the body (e.g. the
144 * native Posts table's sticky header at z-index 20–40) would
145 * paint over the titlebar's ⋯ menu popover. */
146 z-index: 21;
147 display: flex;
148 align-items: center;
149 height: var(--desktop-mode-titlebar-height);
150 padding: 0 8px;
151 background-color: var(--desktop-mode-titlebar-bg);
152 /*
153 * Desktop-theme title-bar texture (TITLEBAR slot). `none` when
154 * unset, so the background-color above is the whole story for
155 * an unthemed shell. The `-repeat` / `-size` companions are
156 * declared unconditionally with their CSS initial values as
157 * fallbacks — cheaper than a second selector and it keeps the
158 * shorthand from resetting them.
159 */
160 background-image: var(--desktop-mode-titlebar-image, none);
161 background-repeat: var(--desktop-mode-titlebar-image-repeat, repeat);
162 background-size: var(--desktop-mode-titlebar-image-size, auto);
163 background-position: var(--desktop-mode-titlebar-image-position, center);
164 color: var(--desktop-mode-titlebar-color);
165 /*
166 * Title bars can carry their own face — a display typeface here
167 * with a text face in the body is the classic desktop split. Two
168 * levels of fallback: the title-bar token, then the shell-wide
169 * one, then `inherit`, which is what an undeclared font-family
170 * would have computed to anyway. Unthemed shells are unchanged.
171 */
172 font-family: var(
173 --desktop-mode-titlebar-font,
174 var(--desktop-mode-font, inherit)
175 );
176 cursor: default;
177 user-select: none;
178 flex-shrink: 0;
179 gap: 8px;
180 }
181
182 .desktop-mode-window--focused .desktop-mode-window__titlebar {
183 background-color: var(--desktop-mode-titlebar-bg-focused);
184 /*
185 * TITLEBAR_FOCUSED slot. Falls back through the unfocused
186 * TITLEBAR image before reaching `none`, so a theme that ships
187 * only one title-bar texture gets it on both states for free.
188 */
189 background-image: var(
190 --desktop-mode-titlebar-image-focused,
191 var(--desktop-mode-titlebar-image, none)
192 );
193 color: var(--desktop-mode-titlebar-color-focused);
194 }
195
196 /* Window icon in title bar. */
197 .desktop-mode-window__icon {
198 font-size: 18px;
199 width: 18px;
200 height: 18px;
201 flex-shrink: 0;
202 }
203
204 /* Letter-badge fallback (unrecognized icon values) — shrink the
205 * one-/two-letter monogram so it fits the 18px icon box. */
206 .desktop-mode-window__icon.desktop-mode-icon-letter {
207 font-size: 9px;
208 }
209
210 /* Activity indicator slot — sits between the icon and the title.
211 * Reserves a fixed width so the indicator's blink animation can't
212 * shift the title text horizontally. The inner `<wpd-save-status>`
213 * paints a 12px modem-style dot (always visible, accent-colored).
214 *
215 * `--wp-admin-theme-color` is forwarded as `color` on the host so
216 * the inner shadow-DOM stylesheet's `currentColor` references
217 * (used in the box-shadow glow) resolve to the live accent. */
218 .desktop-mode-window__activity {
219 display: inline-flex;
220 align-items: center;
221 justify-content: center;
222 width: 14px;
223 height: 14px;
224 flex-shrink: 0;
225 margin-inline-start: 6px;
226 margin-inline-end: 4px;
227 color: var(--wp-admin-theme-color, #2271b1);
228 }
229
230 /* Window title text. */
231 .desktop-mode-window__title {
232 flex: 1;
233 overflow: hidden;
234 text-overflow: ellipsis;
235 white-space: nowrap;
236 font-size: 14px;
237 font-weight: 500;
238 line-height: var(--desktop-mode-titlebar-height);
239 }
240
241 /*
242 * Window control buttons container.
243 *
244 * The cluster is TRANSPARENT by default, which is the whole point: a
245 * themed title-bar texture runs edge to edge underneath it, and the
246 * buttons float on the artwork rather than sitting on a plate. Every
247 * property below resolves to exactly that when unset.
248 *
249 * A theme that wants a plate instead sets `--desktop-mode-titlebar-
250 * controls-bg` (and optionally the TITLEBAR_CONTROLS texture slot,
251 * a radius, and some inline padding) — the classic "the controls live
252 * in their own well" look, without the framework picking it for
253 * everyone.
254 *
255 * `padding-block` stays 0 so the cluster never changes the title
256 * bar's height; only the inline padding is themable.
257 *
258 * @since 0.9.8
259 */
260 .desktop-mode-window__controls {
261 display: flex;
262 gap: var( --desktop-mode-titlebar-controls-gap, 4px );
263 align-items: center;
264 flex-shrink: 0;
265 padding-inline: var( --desktop-mode-titlebar-controls-padding, 0 );
266 border-radius: var( --desktop-mode-titlebar-controls-radius, 0 );
267 background-color: var( --desktop-mode-titlebar-controls-bg, transparent );
268 background-image: var( --desktop-mode-titlebar-controls-image, none );
269 background-repeat: var( --desktop-mode-titlebar-controls-image-repeat, repeat );
270 background-size: var( --desktop-mode-titlebar-controls-image-size, auto );
271 background-position: var( --desktop-mode-titlebar-controls-image-position, center );
272 }
273
274 /*
275 * Same treatment for the Screen Options / Help cluster, so a theme
276 * that plates one can plate the other and keep them consistent.
277 * Falls through to the controls tokens, so setting the pair above is
278 * enough for both.
279 */
280 .desktop-mode-window__screen-meta {
281 border-radius: var(
282 --desktop-mode-titlebar-meta-radius,
283 var( --desktop-mode-titlebar-controls-radius, 0 )
284 );
285 background-color: var( --desktop-mode-titlebar-meta-bg, transparent );
286 background-image: var( --desktop-mode-titlebar-meta-image, none );
287 background-repeat: var( --desktop-mode-titlebar-meta-image-repeat, repeat );
288 background-size: var( --desktop-mode-titlebar-meta-image-size, auto );
289 background-position: var( --desktop-mode-titlebar-meta-image-position, center );
290 }
291
292 /*
293 * Layer-3 slot hosts.
294 *
295 * Most in-titlebar slots wrap canonical chrome elements without
296 * contributing to the flex layout themselves — `display: contents`
297 * removes the wrapper's box so the inner element (icon span with
298 * `flex-shrink: 0`, before/after spacers, etc.) keeps its
299 * original layout role.
300 *
301 * The `title` slot is the exception: it's the flex-grow region of
302 * the title bar (`flex: 1`). When a plugin replaces the default
303 * title with custom HTML or a render callback, we want the new
304 * content to inherit the same "fill the remaining horizontal
305 * space" behaviour — so the slot itself owns `flex: 1` and the
306 * default title element's own `flex: 1` cooperates inside the
307 * nested flex.
308 *
309 * before/after-titlebar slots are flex-column children of
310 * `.desktop-mode-window` (siblings of the title bar). They're real
311 * boxes — plugins can paint backgrounds, padding, borders. Empty
312 * hosts collapse via `:empty` so they take no space until populated.
313 *
314 * @since 0.6.0
315 */
316 .desktop-mode-window__slot--before-icon,
317 .desktop-mode-window__slot--icon,
318 .desktop-mode-window__slot--after-title,
319 .desktop-mode-window__slot--before-controls,
320 .desktop-mode-window__slot--after-controls {
321 display: contents;
322 }
323
324 .desktop-mode-window__slot--title {
325 flex: 1;
326 min-width: 0;
327 display: flex;
328 align-items: center;
329 overflow: hidden;
330 }
331
332 .desktop-mode-window__slot--before-titlebar,
333 .desktop-mode-window__slot--after-titlebar {
334 display: block;
335 flex-shrink: 0;
336 }
337
338 .desktop-mode-window__slot--before-titlebar:empty,
339 .desktop-mode-window__slot--after-titlebar:empty {
340 display: none;
341 }
342
343 /* ---------------------------------------------------------------
344 * Window control buttons.
345 *
346 * Flat, icon-only buttons styled to feel at home in the WordPress
347 * admin: transparent by default, subtle tinted hover, focus ring in
348 * the active admin theme color, inline SVG icons inheriting
349 * currentColor so they adapt to focused / unfocused title bars.
350 *
351 * The close button gets a destructive red wash on hover only —
352 * never as a default state — so it reads as safe until interacted
353 * with, matching the WordPress admin's pattern of reserving color
354 * for semantic signal.
355 * --------------------------------------------------------------- */
356 /*
357 * Title-bar chrome buttons render as `<wpd-window-button>`.
358 * Shadow DOM can't reach across the window's focus class, so we
359 * drive the coloring via custom properties that inherit through
360 * the boundary. The component reads `--wpd-btn-color`,
361 * `--wpd-btn-bg-hover`, etc. and paints accordingly.
362 *
363 * Focused control glyphs are white at 70% — correct against the
364 * default focused title bar, which is the admin theme colour and so
365 * always a mid-to-dark fill, and invisible against a pale one. A
366 * theme could not reach them: these declarations land on the WINDOW
367 * element, so a `--wpd-btn-color` set at the shell root loses to
368 * them, and the same names also drive buttons outside the title bar
369 * (sticky notes, the desk chrome) that a theme usually does not want
370 * to move in the same stroke.
371 *
372 * `--desktop-mode-titlebar-btn-focused-*` mirrors the unfocused set
373 * below, name for name, so the two halves of the title bar are
374 * addressed the same way. Every one is UNDECLARED and falls back to
375 * the literal it replaced, so an unthemed shell paints exactly what
376 * it painted before.
377 *
378 * There is no `-danger-hover` twin: destructive red is semantic, not
379 * chrome, and both halves already resolve it through `--wpd-danger`.
380 */
381 .desktop-mode-window--focused {
382 --wpd-btn-color: var( --desktop-mode-titlebar-btn-focused-color, rgba( 255, 255, 255, 0.7 ) );
383 --wpd-btn-color-hover: var( --desktop-mode-titlebar-btn-focused-color-hover, #fff );
384 --wpd-btn-bg-hover: var( --desktop-mode-titlebar-btn-focused-bg-hover, rgba( 255, 255, 255, 0.18 ) );
385 --wpd-btn-bg-active: var( --desktop-mode-titlebar-btn-focused-bg-active, rgba( 255, 255, 255, 0.25 ) );
386 --wpd-btn-outline: var( --desktop-mode-titlebar-btn-focused-outline, rgba( 255, 255, 255, 0.65 ) );
387 --wpd-btn-danger-hover: var( --wpd-danger, #d63638 );
388 }
389
390 .desktop-mode-window:not( .desktop-mode-window--focused ) {
391 --wpd-btn-color: var( --desktop-mode-titlebar-btn-color, rgba( 0, 0, 0, 0.45 ) );
392 --wpd-btn-color-hover: var( --desktop-mode-titlebar-btn-color-hover, rgba( 0, 0, 0, 0.85 ) );
393 --wpd-btn-bg-hover: var( --desktop-mode-titlebar-btn-bg-hover, rgba( 0, 0, 0, 0.08 ) );
394 --wpd-btn-bg-active: var( --desktop-mode-titlebar-btn-bg-active, rgba( 0, 0, 0, 0.12 ) );
395 --wpd-btn-outline: var( --wp-admin-theme-color, #2271b1 );
396 --wpd-btn-danger-hover: var( --wpd-danger, #d63638 );
397 }
398
399 /*
400 * Unfocused control glyphs under an active desktop theme.
401 *
402 * The rule above paints them BLACK at 45% — correct against the
403 * default light unfocused title bar (`#f0f0f1`), invisible against a
404 * dark themed one. They can't simply follow the palette either:
405 * these are title-bar chrome, so the colour they owe allegiance to is
406 * the title bar's own text colour, not the window body's.
407 *
408 * Deriving it with `color-mix` means a theme gets legible unfocused
409 * controls for free from the `--desktop-mode-titlebar-color` it was
410 * already setting — no extra tokens to discover. The four
411 * `--desktop-mode-titlebar-btn-*` overrides above still win when an
412 * author wants exact control.
413 *
414 * Scoped to `[data-desktop-mode-desktop-theme]` on purpose: the
415 * default title-bar colour is `#50575e`, so applying this
416 * unconditionally would lighten the unfocused glyphs on every
417 * unthemed shell. No theme, no change.
418 */
419 .desktop-mode-shell[ data-desktop-mode-desktop-theme ]
420 .desktop-mode-window:not( .desktop-mode-window--focused ) {
421 --wpd-btn-color: var(
422 --desktop-mode-titlebar-btn-color,
423 color-mix( in srgb, var( --desktop-mode-titlebar-color, #50575e ) 72%, transparent )
424 );
425 --wpd-btn-color-hover: var(
426 --desktop-mode-titlebar-btn-color-hover,
427 var( --desktop-mode-titlebar-color, #50575e )
428 );
429 --wpd-btn-bg-hover: var(
430 --desktop-mode-titlebar-btn-bg-hover,
431 color-mix( in srgb, var( --desktop-mode-titlebar-color, #50575e ) 18%, transparent )
432 );
433 --wpd-btn-bg-active: var(
434 --desktop-mode-titlebar-btn-bg-active,
435 color-mix( in srgb, var( --desktop-mode-titlebar-color, #50575e ) 26%, transparent )
436 );
437 }
438
439 /* ---------------------------------------------------------------
440 * Title-bar actions menu (leading edge, before icon + title).
441 *
442 * Trigger is a standard `.desktop-mode-window__btn` — inherits focused /
443 * unfocused coloring from the existing control-button rules. The only
444 * bespoke rule is margin: it sits at the leading edge so a small trailing
445 * gap separates it from the icon without affecting overall title-bar gap.
446 *
447 * Panel is an absolute dropdown anchored to the title bar (which is now
448 * position: relative). Visibility is driven by the `hidden` attribute on
449 * the element itself — no separate `--open` class to keep in sync with
450 * aria-expanded.
451 * --------------------------------------------------------------- */
452 /*
453 * ⋯ button sits between the screen-meta cluster and the window
454 * controls, as the "last page-level chrome" item. When present it
455 * takes over the divider duty: screen-meta drops its own trailing
456 * divider, and the controls container gains a leading one — so a
457 * single clean line always sits between page chrome and window
458 * chrome regardless of which combination is visible.
459 */
460 .desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__screen-meta {
461 margin-inline-end: 0;
462 padding-inline-end: 0;
463 border-inline-end: none;
464 }
465
466 .desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__controls {
467 margin-inline-start: 8px;
468 padding-inline-start: 8px;
469 /* Tokenized so a theme can retint it — or set `transparent` and
470 * let its own title-bar artwork carry the separation. */
471 border-inline-start: 1px solid
472 var( --desktop-mode-titlebar-divider, rgba(255, 255, 255, 0.15) );
473 }
474
475 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__controls {
476 border-inline-start-color: var(
477 --desktop-mode-titlebar-divider-unfocused,
478 rgba(0, 0, 0, 0.1)
479 );
480 }
481
482 /*
483 * Menu popover lives inside the title bar's absolute-positioned
484 * coordinate system — positioning is caller-specific and stays
485 * outer. Background, border, items, checkbox styling all moved
486 * into `<wpd-menu>` / `<wpd-menu-item>`.
487 */
488 .desktop-mode-window__menu-panel {
489 position: absolute;
490 top: calc( var( --desktop-mode-titlebar-height ) + 2px );
491 inset-inline-end: 60px;
492 z-index: 2;
493 }
494
495 /*
496 * "Related" dropdown — opened by the related-entities title-bar
497 * button (`src/related-entities/`). The panel ALSO carries
498 * `.desktop-mode-window__menu-panel` (positioning + the drag-tracker
499 * exclusion come from there); this class only layers the related-menu
500 * extras: an internal scroll cap for media-heavy posts and a sane
501 * minimum width.
502 */
503 .desktop-mode-window__related-panel {
504 max-height: min( 60vh, 420px );
505 overflow-y: auto;
506 min-width: 180px;
507 }
508
509 /* Section header inside the Related dropdown ("Categories", "Media"). */
510 .desktop-mode-window__related-group {
511 padding: 6px 12px 2px;
512 font-size: 11px;
513 font-weight: 600;
514 text-transform: uppercase;
515 letter-spacing: 0.04em;
516 opacity: 0.6;
517 color: var( --wpd-fg );
518 }
519
520 .desktop-mode-window__related-group:not(:first-child) {
521 margin-block-start: 4px;
522 border-block-start: 1px solid rgba( 128, 128, 128, 0.25 );
523 padding-block-start: 8px;
524 }
525
526 /* ---------------------------------------------------------------
527 * Screen Meta buttons (Screen Options / Help) in the title bar.
528 * Positioned right after the title text, visually separated from
529 * the close / maximize / minimize cluster by a subtle divider.
530 * Sized to meet WCAG 2.2 target size (24x24 minimum).
531 * --------------------------------------------------------------- */
532 .desktop-mode-window__screen-meta {
533 display: flex;
534 gap: 4px;
535 align-items: center;
536 flex-shrink: 0;
537 margin-inline-end: 8px;
538 padding-inline-end: 8px;
539 border-inline-end: 1px solid
540 var( --desktop-mode-titlebar-divider, rgba(255, 255, 255, 0.15) );
541 }
542
543 /* Hide the divider when there are no screen-meta buttons. */
544 .desktop-mode-window__screen-meta:empty {
545 display: none;
546 }
547
548 .desktop-mode-window__meta-btn {
549 display: flex;
550 align-items: center;
551 justify-content: center;
552 width: 28px;
553 height: 28px;
554 border: none;
555 border-radius: 6px;
556 cursor: pointer;
557 padding: 0;
558 background: transparent;
559 transition: background-color 0.15s ease, color 0.15s ease;
560 }
561
562 .desktop-mode-window__meta-btn .dashicons {
563 font-size: 18px;
564 width: 18px;
565 height: 18px;
566 }
567 /* The flex-centring override for the dashicon glyph currently lives
568 * in `windows.css`. That placement was a cache workaround from when
569 * this file was an `@import` sub-sheet with no `?ver=` of its own;
570 * it is now a separately registered, filemtime-stamped handle, so
571 * the workaround no longer buys anything. Left where it is for now —
572 * relocating it is behaviour-neutral churn. */
573
574 /* Focused window: meta buttons visible.
575 *
576 * Screen-meta buttons are not `<wpd-window-button>` — they are plain
577 * buttons in the light DOM, so they paint themselves instead of
578 * reading the `--wpd-btn-*` bridge. They sit in the same title bar
579 * against the same fill, so they take the same
580 * `--desktop-mode-titlebar-btn-focused-*` tokens, each keeping its own
581 * literal as the fallback: unthemed they stay a touch dimmer at rest
582 * than the window controls (0.65 vs 0.7), themed they move together
583 * rather than one cluster going legible and the other staying white. */
584 .desktop-mode-window--focused .desktop-mode-window__meta-btn {
585 color: var( --desktop-mode-titlebar-btn-focused-color, rgba(255, 255, 255, 0.65) );
586 }
587 .desktop-mode-window--focused .desktop-mode-window__meta-btn:hover {
588 color: var( --desktop-mode-titlebar-btn-focused-color-hover, var( --wpd-fg-on-accent, #fff ) );
589 background: var( --desktop-mode-titlebar-btn-focused-bg-hover, rgba(255, 255, 255, 0.18) );
590 }
591 .desktop-mode-window--focused .desktop-mode-window__meta-btn:focus-visible {
592 color: var( --desktop-mode-titlebar-btn-focused-color-hover, var( --wpd-fg-on-accent, #fff ) );
593 background: var( --desktop-mode-titlebar-btn-focused-bg-hover, rgba(255, 255, 255, 0.18) );
594 outline: 2px solid var( --desktop-mode-titlebar-btn-focused-outline, rgba(255, 255, 255, 0.6) );
595 outline-offset: 1px;
596 }
597 .desktop-mode-window--focused .desktop-mode-window__meta-btn--active {
598 color: var( --desktop-mode-titlebar-btn-focused-color-hover, var( --wpd-fg-on-accent, #fff ) );
599 background: var( --desktop-mode-titlebar-btn-focused-bg-active, rgba(255, 255, 255, 0.25) );
600 }
601
602 /* Unfocused window: divider and buttons adapt to light title bar. */
603 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__screen-meta {
604 border-inline-end-color: var(
605 --desktop-mode-titlebar-divider-unfocused,
606 rgba(0, 0, 0, 0.1)
607 );
608 }
609 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn {
610 color: rgba(0, 0, 0, 0.3);
611 }
612 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn:hover {
613 color: rgba(0, 0, 0, 0.6);
614 background: var( --wpd-hover, rgba(0, 0, 0, 0.08) );
615 }
616 .desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn--active {
617 color: rgba(0, 0, 0, 0.6);
618 background: rgba(0, 0, 0, 0.12);
619 }
620
621 /* ---------------------------------------------------------------
622 * Tab strip — submenu navigation rendered in the parent shell, just
623 * below the title bar. Each tab swaps the iframe URL in place; no
624 * new window opens. Horizontally scrollable on narrow windows so the
625 * same component works on tablet and mobile shells unchanged.
626 * --------------------------------------------------------------- */
627 .desktop-mode-window__tabs {
628 display: flex;
629 flex-shrink: 0;
630 gap: 2px;
631 padding: 0 8px;
632 background-color: var( --desktop-mode-tabs-bg, var( --wpd-surface-elevated, #f6f7f7 ) );
633 /* TABBAR texture slot — layered over the strip's own colour. */
634 background-image: var( --desktop-mode-tabs-image, none );
635 background-repeat: var( --desktop-mode-tabs-image-repeat, repeat );
636 background-size: var( --desktop-mode-tabs-image-size, auto );
637 background-position: var( --desktop-mode-tabs-image-position, center );
638 border-bottom: 1px solid var(--desktop-mode-window-border);
639 overflow-x: auto;
640 overflow-y: hidden;
641 scrollbar-width: thin;
642 /*
643 * Soft edge fades so overflowing tabs tell the user "scroll for
644 * more." The mask is always applied — when the strip doesn't
645 * overflow, the faded ends are empty area beyond the last tab so
646 * the cosmetic is invisible. When it DOES overflow, the last-
647 * visible tab fades under the mask, creating the affordance.
648 * `overscroll-behavior-x: contain` keeps horizontal trackpad
649 * swipes inside the strip instead of triggering browser back.
650 */
651 mask-image: linear-gradient(
652 to right,
653 transparent 0,
654 #000 16px,
655 #000 calc(100% - 16px),
656 transparent 100%
657 );
658 -webkit-mask-image: linear-gradient(
659 to right,
660 transparent 0,
661 #000 16px,
662 #000 calc(100% - 16px),
663 transparent 100%
664 );
665 overscroll-behavior-x: contain;
666 }
667
668 /*
669 * When JS assigns the --has-overflow marker (future enhancement), the
670 * mask is stronger. For now the CSS-only mask is subtle enough to be
671 * invisible on non-overflowing strips — mask cut-offs land outside the
672 * flex content.
673 */
674
675 .desktop-mode-window__tab {
676 flex-shrink: 0;
677 display: inline-flex;
678 align-items: center;
679 height: 32px;
680 padding: 0 12px;
681 border: none;
682 background: transparent;
683 color: var( --desktop-mode-tabs-color, var( --wpd-fg-muted, #50575e ) );
684 font: inherit;
685 font-size: 12px;
686 line-height: 1;
687 cursor: pointer;
688 border-bottom: 2px solid transparent;
689 margin-bottom: -1px;
690 white-space: nowrap;
691 transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
692 }
693
694 .desktop-mode-window__tab:hover {
695 color: var(--wp-admin-theme-color, #2271b1);
696 background: var( --wpd-hover, rgba(0, 0, 0, 0.03) );
697 }
698
699 .desktop-mode-window__tab:focus-visible {
700 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
701 outline-offset: -2px;
702 }
703
704 .desktop-mode-window__tab--active {
705 color: var(--wp-admin-theme-color, #2271b1);
706 border-bottom-color: var(--wp-admin-theme-color, #2271b1);
707 font-weight: 600;
708 }
709
710 /*
711 * External sub-tab. Same shape as a submenu tab, plus two inline
712 * chips: detach (↗) and close (×). Chips are painted as spans
713 * (rather than nested buttons — nested interactive elements are a
714 * pain for a11y). The tab's click handler routes chip clicks via
715 * the `data-tab-action` dataset attribute.
716 */
717 .desktop-mode-window__tab--external {
718 /* Slightly wider gap between the label and the chip cluster, and
719 * more breathing room at the trailing edge so the chips aren't
720 * flush against the tab boundary. */
721 gap: 10px;
722 padding-inline-end: 4px;
723 }
724
725 .desktop-mode-window__tab-label {
726 max-width: 180px;
727 overflow: hidden;
728 text-overflow: ellipsis;
729 white-space: nowrap;
730 }
731
732 /* External-tab action chips moved to `<wpd-tab-chip>`. Spacing
733 * between chips handled by a single host-level rule since the
734 * component doesn't know about its siblings. */
735 wpd-tab-chip + wpd-tab-chip {
736 margin-inline-start: 4px;
737 }
738
739 /* Window body: contains the iframe. */
740 .desktop-mode-window__body {
741 flex: 1;
742 position: relative;
743 overflow: hidden;
744 /*
745 * WINDOW_BODY texture slot. Visible behind native window content
746 * and behind any iframe whose page has a transparent background;
747 * an opaque wp-admin page inside an iframe paints over it, which
748 * is why a theme wanting texture everywhere reaches for DESKTOP
749 * and the chrome slots instead.
750 */
751 background-image: var( --desktop-mode-window-body-image, none );
752 background-repeat: var( --desktop-mode-window-body-image-repeat, repeat );
753 background-size: var( --desktop-mode-window-body-image-size, auto );
754 background-position: var( --desktop-mode-window-body-image-position, center );
755 /*
756 * Body typeface. `--wpd-font` is the component kit's own token, so
757 * one declaration here reaches every `<wpd-*>` element inside the
758 * window: shadow DOM inherits `font-family` through the boundary.
759 */
760 font-family: var( --wpd-font, inherit );
761 }
762
763 /* Iframe fills the window body. */
764 .desktop-mode-window__iframe {
765 width: 100%;
766 height: 100%;
767 border: none;
768 display: block;
769 background: var(--desktop-mode-window-bg);
770 opacity: 1;
771 transition: opacity 0.25s ease;
772 }
773
774 /*
775 * Headings inside native window bodies.
776 *
777 * WordPress core's `wp-admin/css/common.css` styles headings with
778 * BARE ELEMENT selectors — `h1 { color: #1d2327 }` and
779 * `h2, h3 { color: #1d2327 }`. Native windows render in the parent
780 * shell rather than in an iframe, so those rules reach straight into
781 * our light-DOM window content: an `<h3>` in a `<wpd-card>` came out
782 * near-black on a dark theme while every sibling paragraph correctly
783 * followed the palette.
784 *
785 * The specificity here is doing real work. It has to sit BETWEEN two
786 * other rules:
787 *
788 * core `h3` (0,0,1) must lose
789 * THIS rule (0,0,1) wins on source order
790 * `.desktop-mode-my-wordpress__user-section h3` (0,1,1) must still win
791 *
792 * `:where()` contributes zero specificity, so the selector below
793 * weighs the same as core's bare `h3` and beats it only because our
794 * stylesheet prints later — while any of our own class-scoped heading
795 * rules continue to override it. Raising this to a plain
796 * `.desktop-mode-window__body h3` would tie with those and break them
797 * depending on file order.
798 *
799 * The `#1d2327` fallback is core's own value, so with no theme active
800 * the computed colour is unchanged.
801 */
802 :where( .desktop-mode-window__body ) :is( h1, h2, h3, h4, h5, h6 ) {
803 color: var( --wpd-fg, #1d2327 );
804 }
805
806 /*
807 * Same story, same fix, for the other bare-element rules core ships:
808 *
809 * a { color: #2271b1 }
810 * code { background: #f0f0f1 }
811 *
812 * `code` is the worse of the two — a light chip background with
813 * palette-coloured text on it inverts to unreadable the moment a
814 * theme goes dark. Both fallbacks are core's own values, so the
815 * default is unchanged.
816 *
817 * Form controls (`input`, `textarea`, `select`) are deliberately NOT
818 * included: core styles those through attribute selectors that carry
819 * real specificity, native windows overwhelmingly use the `<wpd-*>`
820 * components instead, and a half-applied override there would look
821 * worse than leaving them alone.
822 */
823 :where( .desktop-mode-window__body ) a {
824 color: var( --wpd-accent, #2271b1 );
825 }
826
827 :where( .desktop-mode-window__body ) code {
828 background: var( --wpd-surface-sunken, #f0f0f1 );
829 color: var( --wpd-fg, inherit );
830 }
831
832 /*
833 * Raw form controls in native window bodies.
834 *
835 * Core's `forms.css` styles these through ATTRIBUTE selectors —
836 * `input[type="search"], select, textarea { background-color: #fff;
837 * color: #1e1e1e; border: 1px solid #949494 }` — which weighs
838 * (0,1,1). That beats a plain class selector, so a control our own
839 * CSS had already tokenized (My WordPress's search box reads
840 * `background: var( --wpd-surface, #fff )` and has done all along)
841 * still came out white: core simply outranked it.
842 *
843 * Hence `.desktop-mode-window__body` + `:is( input[type], … )`, which
844 * weighs (0,2,1) and wins. `<wpd-*>` form components are untouched —
845 * they live in shadow DOM, where none of this reaches.
846 *
847 * Fallbacks are core's own values, so an unthemed shell is unchanged.
848 */
849 .desktop-mode-window__body
850 :is(
851 input[ type="text" ],
852 input[ type="search" ],
853 input[ type="email" ],
854 input[ type="url" ],
855 input[ type="tel" ],
856 input[ type="number" ],
857 input[ type="password" ],
858 input[ type="date" ],
859 input[ type="datetime-local" ],
860 input[ type="month" ],
861 input[ type="time" ],
862 input[ type="week" ],
863 select,
864 textarea
865 ) {
866 background-color: var( --wpd-surface, #fff );
867 color: var( --wpd-fg, #1e1e1e );
868 border-color: var( --wpd-border-strong, #949494 );
869 }
870
871 .desktop-mode-window__body :is( input, textarea )::placeholder {
872 color: var( --wpd-fg-muted, #646970 );
873 }
874
875 /* Cross-window drag drop-overlay rules currently live in windows.css.
876 * That was originally a cache workaround — this file was an `@import`
877 * sub-sheet with no `?ver=` of its own, so edits here could be served
878 * stale indefinitely. It is now a separately registered handle with
879 * its own filemtime stamp, so the workaround is obsolete and those
880 * rules could move back here. Left in place for now because moving
881 * them is a behaviour-neutral churn better done on its own. */
882
883 /*
884 * Loading-state overlay — painted by `src/window/dom.ts` into
885 * every window's body and removed once the body's content reports
886 * ready. The `<wpd-spinner>` inside is sized responsively against
887 * the window's width via `clamp(96px, 14vw, 192px)` so a tiny
888 * popover gets a small spinner and a maximized window gets a big
889 * one. Placed above the body content (iframe / native render
890 * output) and kept aria-hidden so the spinner's own SR-label is
891 * the only loading announcement.
892 *
893 * `transition-delay` on the overlay's fade-in means a render
894 * that lands within ~120ms never paints the spinner — the
895 * overlay only becomes visible when there's actually a wait
896 * worth surfacing. This keeps the affordance from flashing on
897 * fast local-dev loads while still covering the slow production
898 * iframe boots that motivated it.
899 */
900 .desktop-mode-window__loading {
901 position: absolute;
902 inset: 0;
903 display: flex;
904 align-items: center;
905 justify-content: center;
906 pointer-events: none;
907 background: var(--desktop-mode-window-bg);
908 opacity: 0;
909 /* Default (loaded → loading-removed) — fade out immediately. */
910 transition: opacity 0.25s ease;
911 /*
912 * Above both reveal layers (z-index 2 and 3), so the spinner stays
913 * readable for the whole load and the surface it is sitting on only
914 * becomes visible once the spinner has faded out.
915 */
916 z-index: 4;
917 }
918
919 /*
920 * Content hidden while loading. The reveal surface is excluded
921 * alongside the spinner overlay: it is chrome, not content, and fading
922 * it to transparent would show the very content it exists to cover.
923 */
924 .desktop-mode-window__body--loading > .desktop-mode-window__iframe,
925 .desktop-mode-window__body--loading
926 > :not(.desktop-mode-window__loading):not(.desktop-mode-window__reveal) {
927 opacity: 0;
928 transition: opacity 0.25s ease;
929 }
930
931 .desktop-mode-window__body--loading > .desktop-mode-window__loading {
932 opacity: 1;
933 /* Entry transition with a small delay — loads that finish in
934 * under ~120ms never paint the spinner, so fast local-dev /
935 * hot-cache fires don't flash. Slow production iframe boots
936 * still see the spinner. */
937 transition-delay: 0.12s;
938 }
939
940 @media ( prefers-reduced-motion: reduce ) {
941 .desktop-mode-window__iframe,
942 .desktop-mode-window__loading,
943 .desktop-mode-window__body--loading > .desktop-mode-window__iframe,
944 .desktop-mode-window__body--loading
945 > :not(.desktop-mode-window__loading):not(.desktop-mode-window__reveal) {
946 transition: none;
947 }
948 }
949
950 /*
951 * Window reveal — the opaque layers a window's content is uncovered
952 * from once it reports ready. Painted for every window (of either
953 * kind) from construction, animated away by `src/reveals/surface.ts`,
954 * then removed.
955 *
956 * They are SIBLINGS of the iframe, never wrappers. `clip-path` is
957 * animated on these elements alone, so the content below keeps its own
958 * compositing layer, its hit-testing, and its stacking context — a
959 * reveal cannot swallow a click or re-rasterize the page it is
960 * uncovering. Native window content gets the identical treatment for
961 * the same reason.
962 *
963 * `--desktop-mode-window-reveal-surface` is a theme token, white by
964 * default — the surface has to be opaque or there is nothing to reveal
965 * from. The EDGE token is `transparent` by default instead, since an
966 * edge is an accent rather than the effect itself. Either layer that
967 * computes to no paint is skipped rather than animated. A def may
968 * override the surface for itself with an inline `background` (see
969 * `WindowRevealDef.surfaceColor`), which is how `obturator` gets its
970 * near-black shutter blades regardless of the token.
971 *
972 * No `transition` here on purpose — the shape is driven entirely by
973 * the Web Animations API, which needs a matched `from` / `to` pair a
974 * CSS transition cannot express. Reduced motion is handled JS-side by
975 * removing the layers instead of animating them.
976 */
977 .desktop-mode-window__reveal {
978 position: absolute;
979 inset: 0;
980 pointer-events: none;
981 background: var( --desktop-mode-window-reveal-surface, #fff );
982 z-index: 3;
983 }
984
985 /*
986 * The reveal's leading edge. Same element shape, same keyframes, run
987 * over a slightly longer duration so it trails the surface — the sliver
988 * of it that is not yet covered by the surface IS the edge band. That
989 * is why this needs no shape of its own, and why every reveal (built-in
990 * or third-party) gets an edge that follows its geometry exactly.
991 *
992 * Behind the surface, still above the content.
993 *
994 * `--desktop-mode-window-reveal-edge` is `transparent` by default, and
995 * the shell drops the layer entirely while it computes that way — so
996 * the default costs no element and no animation. Give the token a
997 * colour (or a gradient) to turn the edge on across every reveal at
998 * once. `--desktop-mode-window-reveal-edge-thickness` tunes how wide
999 * the band is; `edgeLag: 0` on a def opts a single reveal out.
1000 */
1001 .desktop-mode-window__reveal--edge {
1002 background: var( --desktop-mode-window-reveal-edge, transparent );
1003 z-index: 2;
1004 }
1005
1006 /*
1007 * A reveal that renders its own DOM (`WindowRevealDef.render`) paints
1008 * itself — an `<svg>`, a canvas, whatever it built. The surface token
1009 * must NOT apply underneath it: an opaque rectangle behind the
1010 * renderer's output is what the effect would end up uncovering, so the
1011 * animation would play against a flat colour and the real content
1012 * would only appear when the layer is finally removed.
1013 */
1014 .desktop-mode-window__reveal--custom {
1015 background: none;
1016 }
1017
1018 /*
1019 * While a reveal plays, pin the content to full opacity with no
1020 * transition. Dropping `--loading` normally starts a 250 ms content
1021 * fade-in; a reveal running over that fade would show a
1022 * half-transparent strip along its leading edge. Beats the base
1023 * `.desktop-mode-window__iframe` rule on specificity, so no
1024 * `!important` is needed.
1025 */
1026 .desktop-mode-window__body--revealing
1027 > :not(.desktop-mode-window__loading):not(.desktop-mode-window__reveal) {
1028 opacity: 1;
1029 transition: none;
1030 }
1031
1032 /*
1033 * External sub-tab iframes stack in the same body as the primary
1034 * iframe. Only one is visible at a time (managed by `switchToTab`
1035 * via `style.display`). Absolute positioning so they don't push the
1036 * primary iframe around when added to the DOM; `display: none` on
1037 * inactive iframes is set inline by the JS.
1038 */
1039 .desktop-mode-window__iframe--external {
1040 position: absolute;
1041 inset: 0;
1042 }
1043
1044 /*
1045 * Double-buffer twin for `Window.swapReload()` — the silent refresh
1046 * the editor-preview companion uses. The twin loads UNDERNEATH the
1047 * visible frame at full opacity: a normal, fully-rasterized paint
1048 * target, completely covered by the (opaque) old frame while it
1049 * loads. Deliberately not `opacity: 0`-on-top or
1050 * `visibility: hidden` — browsers defer rasterizing invisible
1051 * iframes, and revealing such a frame paints its blank background
1052 * before its raster lands (a white blink).
1053 *
1054 * Stacking: positioned elements paint above static ones regardless
1055 * of DOM order, so the buffer (absolute) would land on top of the
1056 * static primary frame — `--swap-front` elevates the OLD frame for
1057 * the duration of the swap instead. The swap itself is instant and
1058 * animation-free: removing the old frame exposes the ready-painted
1059 * twin in the same compositor frame. At no point is unpainted
1060 * content the only thing on screen.
1061 */
1062 .desktop-mode-window__iframe--buffer {
1063 position: absolute;
1064 inset: 0;
1065 pointer-events: none;
1066 }
1067
1068 .desktop-mode-window__iframe--swap-front {
1069 position: relative;
1070 z-index: 1;
1071 }
1072
1073 /*
1074 * Native window body — fills the window but lets its contents scroll
1075 * independently. Iframe bodies use `overflow: hidden` so the iframe
1076 * controls its own scroll; native bodies render real document content
1077 * that needs the parent to manage overflow.
1078 */
1079 .desktop-mode-window__body--native {
1080 overflow: auto;
1081 color: var( --wpd-fg, #1d2327 );
1082 /* background-COLOR, not the shorthand. This is a modifier on the
1083 * SAME element as `.desktop-mode-window__body`, at the same
1084 * specificity and later in source order — the shorthand would
1085 * reset the WINDOW_BODY texture declared there, which is exactly
1086 * the surface native windows exist to show. */
1087 background-color: var(--desktop-mode-window-bg);
1088 }
1089
1090 /*
1091 * Highlight rings. Toggled from JS by `Window.setHighlight()` —
1092 * used by plugins that need to point at a window from outside it
1093 * (e.g. a "connect to" dropdown that previews candidate windows on
1094 * hover). `--wp-window-highlight-color` is overridable per-instance
1095 * via `setHighlight( mode, { color } )` or globally via the
1096 * variable.
1097 */
1098 .wp-window {
1099 --wp-window-highlight-color: var(
1100 --wp-admin-theme-color, #2271b1
1101 );
1102 }
1103 .wp-window--highlight-preview {
1104 box-shadow: 0 0 0 3px var( --wp-window-highlight-color ),
1105 0 0 24px 4px var( --wp-window-highlight-color );
1106 transition: box-shadow 0.12s ease;
1107 z-index: 9999;
1108 }
1109 .wp-window--highlight-persistent {
1110 box-shadow: 0 0 0 2px var( --wp-window-highlight-color );
1111 transition: box-shadow 0.12s ease;
1112 }
1113
1114 /*
1115 * Slots for plugin-registered title-bar buttons. Empty by default
1116 * — `display: contents` hides them entirely from layout when no
1117 * plugin has registered a button for the window. When buttons ARE
1118 * present, they sit inline next to the title (left slot) or just
1119 * before the window controls (right slot).
1120 */
1121 .desktop-mode-window__custom-buttons {
1122 display: contents;
1123 }
1124 .desktop-mode-window__btn--custom {
1125 margin: 0 2px;
1126 }
1127 /*
1128 * Plugin-supplied icons render in the host's light DOM so the
1129 * global Dashicons stylesheet reaches them (shadow DOM doesn't
1130 * inherit page CSS).
1131 *
1132 * Dashicons render at their native 20×20 — that's the size the
1133 * font is hinted for and the only one where glyph metrics put
1134 * the visual centre on the geometric centre. Earlier we tried
1135 * down-scaling them to 14×14 to match the built-in chrome icons,
1136 * but font-size: 14 on a Dashicon shifts the glyph 1–2 pixels
1137 * off centre because the font's ascent/descent ratio doesn't
1138 * scale linearly. The 30×30 button has 5px of padding around a
1139 * 20×20 glyph — comfortable, and visually consistent with the
1140 * 14×14 chrome icons because both are flex-centred in the same
1141 * box.
1142 *
1143 * For inline-SVG icons that plugin authors ship without explicit
1144 * width/height, we DO clamp to 18×18 — those are the cases where
1145 * a bare `<svg>` would otherwise size to its viewBox or default
1146 * to 300×150 (the spec default) and overflow the button.
1147 */
1148 .desktop-mode-window__btn--custom svg:not( [ width ] ):not( [ height ] ) {
1149 width: 18px;
1150 height: 18px;
1151 display: block;
1152 }
1153
1154 /*
1155 * Busy state for custom title-bar buttons — a gentle opacity pulse
1156 * while a round-trip is in flight (e.g. the editor-preview eye
1157 * waiting on the editor's autosave before opening the preview).
1158 * Driven by `aria-busy="true"` + this class, both set by the button's
1159 * render callback.
1160 */
1161 .desktop-mode-window__btn--busy {
1162 animation: desktop-mode-btn-busy-pulse 1s ease-in-out infinite;
1163 pointer-events: none;
1164 }
1165
1166 @keyframes desktop-mode-btn-busy-pulse {
1167 0%,
1168 100% {
1169 opacity: 1;
1170 }
1171
1172 50% {
1173 opacity: 0.4;
1174 }
1175 }
1176
1177 @media (prefers-reduced-motion: reduce) {
1178 .desktop-mode-window__btn--busy {
1179 animation: none;
1180 opacity: 0.6;
1181 }
1182 }
1183
1184 /*
1185 * Disabled state for custom title-bar buttons — visible but inert
1186 * (e.g. the editor-preview eye on an unsaved "Add New" screen, where
1187 * there is nothing to preview until the first save). The button keeps
1188 * pointer events so its explanatory tooltip/toast still works;
1189 * `aria-disabled` carries the semantics.
1190 */
1191 .desktop-mode-window__btn--disabled {
1192 opacity: 0.4;
1193 cursor: default;
1194 }
1195
1196 /* ----------------------------------------------------------------
1197 * Custom-chrome marker — hides every framework-shipped titlebar
1198 * child while a plugin's chrome is mounted.
1199 *
1200 * The window gets the `desktop-mode-window--custom-chrome` class the
1201 * moment `mountWindowChrome` succeeds (BEFORE the plugin's
1202 * `render()` runs). Default children carry the
1203 * `data-desktop-mode-default-chrome` attribute stamped at element-
1204 * creation time. The combination is a load-bearing guarantee:
1205 * even if the plugin's render() doesn't clear `titlebar.innerHTML`,
1206 * even if a plugin's destroy() leaks the standard chrome back into
1207 * place, even mid-fade during a window close — the default chrome
1208 * NEVER becomes visible while the marker class is active.
1209 *
1210 * The class is removed only when the chrome is swapped to standard
1211 * (in `Window.remountWindowChrome`); during a window close it
1212 * persists until `element.remove()` runs in `onDone()`, which is
1213 * after the fade has reached opacity 0 — so the user never sees
1214 * the swap.
1215 *
1216 * @since 0.18.0
1217 * ---------------------------------------------------------------- */
1218
1219 .desktop-mode-window--custom-chrome
1220 > .desktop-mode-window__titlebar
1221 > [ data-desktop-mode-default-chrome ] {
1222 display: none !important;
1223 }
1224
1225 /* ---------------------------------------------------------------
1226 * Reload button feedback.
1227 *
1228 * Click feedback is decoupled from the loading state:
1229 *
1230 * 1. On click, the icon performs a single 360° rotation with a
1231 * fast-start / slow-end ease curve — confirms the user's
1232 * gesture independently of how long the page takes to load.
1233 * The `--spinning` class is added by the click handler and
1234 * removed on `animationend` so a subsequent click restarts
1235 * the animation cleanly.
1236 *
1237 * 2. While the window body is in the `--loading` state (set by
1238 * `markContentLoading()` and cleared on the iframe's
1239 * `desktop-mode-ready` postMessage), the button is dimmed and
1240 * non-interactive so a second click can't desync the
1241 * chromeless bridge's loading handshake.
1242 *
1243 * `:has()` is used instead of mirroring the `--loading` modifier
1244 * onto the window root because upstream's loading API only
1245 * decorates the body element. Browsers without :has() (very old)
1246 * fall back to a static dimmed button — feature, not bug.
1247 * --------------------------------------------------------------- */
1248
1249 .desktop-mode-window:has( .desktop-mode-window__body--loading )
1250 .desktop-mode-window__btn--reload {
1251 pointer-events: none;
1252 opacity: 0.55;
1253 }
1254
1255 /*
1256 * Click feedback animation. The custom easing — `cubic-bezier( 0.05,
1257 * 0.7, 0.1, 1 )` — is a steeper-than-default ease-out: ~70% of the
1258 * rotation lands in the first 30% of the duration, then it decelerates.
1259 * Reads as "snap, then settle" rather than the linear glide of a
1260 * loading-spinner, reinforcing that this is a one-shot acknowledgement
1261 * of the gesture rather than progress through a long task. The `0.6s`
1262 * duration is short enough to never block the user but long enough
1263 * that the deceleration is legible.
1264 */
1265 .desktop-mode-window__btn--reload.desktop-mode-window__btn--spinning {
1266 animation: wpd-reload-spin-once 0.6s cubic-bezier( 0.05, 0.7, 0.1, 1 );
1267 }
1268
1269 @keyframes wpd-reload-spin-once {
1270 from {
1271 transform: rotate( 0deg );
1272 }
1273 to {
1274 transform: rotate( 360deg );
1275 }
1276 }
1277
1278 @media ( prefers-reduced-motion: reduce ) {
1279 .desktop-mode-window__btn--reload.desktop-mode-window__btn--spinning {
1280 animation: none;
1281 }
1282 }
1283