PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | assets/css/window-chrome.css +962 -293 0.9.81.1.10 View file →
@@ -1,6 +1,6 @@
1 1 /**
2 - * Desktop Mode — Window chrome.
2 + * OpenStation — Window chrome.
3 3 *
4 4 * Everything that renders the base window frame: title bar, icon +
5 5 * title text, control buttons, focused / unfocused colour variants,
6 6 * the ⋯ actions menu panel, screen-meta buttons, the tab strip (for
@@ -12,9 +12,9 @@
12 12 * @since 6.9.0
13 13 */
14 14
15 15 /* Base window. */
16 -.desktop-mode-window {
16 +.os-window {
17 17 /*
18 18 * border-box sizing is essential for pixel-perfect maximize.
19 19 * `Window.maximize()` sets inline `width = parent.clientWidth`,
20 20 * which is the area's inner width. Under content-box sizing, the
@@ -19,9 +19,9 @@
19 19 * `Window.maximize()` sets inline `width = parent.clientWidth`,
20 20 * which is the area's inner width. Under content-box sizing, the
21 21 * window's 1px border would add 2 extra pixels to the rendered
22 22 * element, pushing its right + bottom borders past the
23 - * overflow-hidden boundary of `.desktop-mode-area` and making them
23 + * overflow-hidden boundary of `.os-area` and making them
24 24 * look clipped/offscreen. With border-box, width/height INCLUDE
25 25 * the border, so `width = clientWidth` fits exactly.
26 26 *
27 27 * WP admin CSS does set `*, *::before, *::after { box-sizing:
@@ -33,25 +33,25 @@
33 33 box-sizing: border-box;
34 34 position: absolute;
35 35 display: flex;
36 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);
37 + background: var(--os-window-bg);
38 + border: 1px solid var(--os-window-border);
39 + border-radius: var(--os-window-radius);
40 + box-shadow: var(--os-window-shadow);
41 41 /*
42 42 * Desktop-theme window frame (WINDOW_FRAME texture slot). All
43 43 * four properties default to their CSS initial value via the
44 44 * `var( …, fallback )` second argument, so with no theme active
45 45 * this costs nothing and the 1px border above stays in charge.
46 - * A theme that sets `--desktop-mode-window-border-image-source`
46 + * A theme that sets `--os-window-border-image-source`
47 47 * takes the frame over entirely — border-image paints on top of
48 48 * (and visually replaces) the border. See docs/desktop-themes.md.
49 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);
50 + border-image-source: var(--os-window-border-image-source, none);
51 + border-image-slice: var(--os-window-border-image-slice, 100%);
52 + border-image-width: var(--os-window-border-image-width, 1);
53 + border-image-repeat: var(--os-window-border-image-repeat, stretch);
54 54 overflow: hidden;
55 55 min-width: 320px;
56 56 min-height: 200px;
57 57 transform-origin: center center;
@@ -68,9 +68,9 @@
68 68 * `filter`; keep it in the shared transition list so the
69 69 * treatment fades both ways as focus moves between windows.
70 70 * Duration is a custom property (default in `effects.css`) so
71 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;
72 + filter var( --os-fx-transition-duration, 0.5s ) ease-in-out;
73 73 }
74 74
75 75 /*
76 76 * Suppress the left/top/width/height transition during drag or resize —
@@ -82,11 +82,11 @@
82 82 * transition, leaving maximized / snapped windows ~250 ms behind the
83 83 * viewport the whole time. The class is added by
84 84 * `reflowStatefulWindows` and cleared ~140 ms after the last tick.
85 85 */
86 -.desktop-mode-window--dragging,
87 -.desktop-mode-window--resizing,
88 -.desktop-mode-window--reflowing {
86 +.os-window--dragging,
87 +.os-window--resizing,
88 +.os-window--reflowing {
89 89 transition: none;
90 90 }
91 91
92 92 /*
@@ -97,9 +97,9 @@
97 97 * `--resizing` and the source-order ensures it wins. Kept short
98 98 * (90 ms) so the window still feels glued to the cursor — anything
99 99 * longer reads as lag.
100 100 */
101 -.desktop-mode-window--snap-drag {
101 +.os-window--snap-drag {
102 102 transition:
103 103 left 0.09s ease-out,
104 104 top 0.09s ease-out,
105 105 width 0.09s ease-out,
@@ -106,10 +106,10 @@
106 106 height 0.09s ease-out;
107 107 }
108 108
109 109 /* Focused window gets elevated shadow and distinct title bar. */
110 -.desktop-mode-window--focused {
111 - box-shadow: var(--desktop-mode-window-shadow-focused);
110 +.os-window--focused {
111 + box-shadow: var(--os-window-shadow-focused);
112 112 /*
113 113 * WINDOW_FRAME_FOCUSED texture slot. Each property falls through
114 114 * the unfocused WINDOW_FRAME value before reaching its initial, so
115 115 * a theme shipping one frame gets it on both states and a theme
@@ -116,27 +116,27 @@
116 116 * shipping two gets a frame that lights up on focus — the same
117 117 * fallback chain TITLEBAR_FOCUSED uses.
118 118 */
119 119 border-image-source: var(
120 - --desktop-mode-window-border-image-focused-source,
121 - var(--desktop-mode-window-border-image-source, none)
120 + --os-window-border-image-focused-source,
121 + var(--os-window-border-image-source, none)
122 122 );
123 123 border-image-slice: var(
124 - --desktop-mode-window-border-image-focused-slice,
125 - var(--desktop-mode-window-border-image-slice, 100%)
124 + --os-window-border-image-focused-slice,
125 + var(--os-window-border-image-slice, 100%)
126 126 );
127 127 border-image-width: var(
128 - --desktop-mode-window-border-image-focused-width,
129 - var(--desktop-mode-window-border-image-width, 1)
128 + --os-window-border-image-focused-width,
129 + var(--os-window-border-image-width, 1)
130 130 );
131 131 border-image-repeat: var(
132 - --desktop-mode-window-border-image-focused-repeat,
133 - var(--desktop-mode-window-border-image-repeat, stretch)
132 + --os-window-border-image-focused-repeat,
133 + var(--os-window-border-image-repeat, stretch)
134 134 );
135 135 }
136 136
137 137 /* Title bar. */
138 -.desktop-mode-window__titlebar {
138 +.os-window__titlebar {
139 139 position: relative;
140 140 /* Promote the titlebar into its own stacking context above the
141 141 * body. Without this, the body (which comes later in source order
142 142 * and inherits z-auto) wins document-order overlap battles —
@@ -145,11 +145,11 @@
145 145 * paint over the titlebar's ⋯ menu popover. */
146 146 z-index: 21;
147 147 display: flex;
148 148 align-items: center;
149 - height: var(--desktop-mode-titlebar-height);
149 + height: var(--os-titlebar-height);
150 150 padding: 0 8px;
151 - background-color: var(--desktop-mode-titlebar-bg);
151 + background-color: var(--os-titlebar-bg);
152 152 /*
153 153 * Desktop-theme title-bar texture (TITLEBAR slot). `none` when
154 154 * unset, so the background-color above is the whole story for
155 155 * an unthemed shell. The `-repeat` / `-size` companions are
@@ -156,13 +156,13 @@
156 156 * declared unconditionally with their CSS initial values as
157 157 * fallbacks — cheaper than a second selector and it keeps the
158 158 * shorthand from resetting them.
159 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);
160 + background-image: var(--os-titlebar-image, none);
161 + background-repeat: var(--os-titlebar-image-repeat, repeat);
162 + background-size: var(--os-titlebar-image-size, auto);
163 + background-position: var(--os-titlebar-image-position, center);
164 + color: var(--os-titlebar-color);
165 165 /*
166 166 * Title bars can carry their own face — a display typeface here
167 167 * with a text face in the body is the classic desktop split. Two
168 168 * levels of fallback: the title-bar token, then the shell-wide
@@ -169,10 +169,10 @@
169 169 * one, then `inherit`, which is what an undeclared font-family
170 170 * would have computed to anyway. Unthemed shells are unchanged.
171 171 */
172 172 font-family: var(
173 - --desktop-mode-titlebar-font,
174 - var(--desktop-mode-font, inherit)
173 + --os-titlebar-font,
174 + var(--os-font, inherit)
175 175 );
176 176 cursor: default;
177 177 user-select: none;
178 178 flex-shrink: 0;
@@ -178,10 +178,10 @@
178 178 flex-shrink: 0;
179 179 gap: 8px;
180 180 }
181 181
182 -.desktop-mode-window--focused .desktop-mode-window__titlebar {
183 - background-color: var(--desktop-mode-titlebar-bg-focused);
182 +.os-window--focused .os-window__titlebar {
183 + background-color: var(--os-titlebar-bg-focused);
184 184 /*
185 185 * TITLEBAR_FOCUSED slot. Falls back through the unfocused
186 186 * TITLEBAR image before reaching `none`, so a theme that ships
187 187 * only one title-bar texture gets it on both states for free.
@@ -186,16 +186,16 @@
186 186 * TITLEBAR image before reaching `none`, so a theme that ships
187 187 * only one title-bar texture gets it on both states for free.
188 188 */
189 189 background-image: var(
190 - --desktop-mode-titlebar-image-focused,
191 - var(--desktop-mode-titlebar-image, none)
190 + --os-titlebar-image-focused,
191 + var(--os-titlebar-image, none)
192 192 );
193 - color: var(--desktop-mode-titlebar-color-focused);
193 + color: var(--os-titlebar-color-focused);
194 194 }
195 195
196 196 /* Window icon in title bar. */
197 -.desktop-mode-window__icon {
197 +.os-window__icon {
198 198 font-size: 18px;
199 199 width: 18px;
200 200 height: 18px;
201 201 flex-shrink: 0;
@@ -202,23 +202,103 @@
202 202 }
203 203
204 204 /* Letter-badge fallback (unrecognized icon values) — shrink the
205 205 * one-/two-letter monogram so it fits the 18px icon box. */
206 -.desktop-mode-window__icon.desktop-mode-icon-letter {
206 +.os-window__icon.os-icon-letter {
207 207 font-size: 9px;
208 208 }
209 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).
210 +/* ---------------------------------------------------------------
211 + * Window activity — the status ring.
214 212 *
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 {
213 + * The leading mark of the title bar, in the position the app icon
214 + * used to hold. That icon was a copy of the window's own dock tile a
215 + * few hundred pixels below it, and a title bar has room for one mark
216 + * of that size — better spent on something that changes.
217 + *
218 + * The ring is an `<os-save-status variant="ring" mode="icon">`, found
219 + * by `[data-os-activity-indicator]` — the same public attribute a
220 + * plugin uses to mount its own. The framework's ring is not a special
221 + * case; it is the first subscriber.
222 + *
223 + * Four states, and only one of them fills:
224 + *
225 + * idle white outline, no glyph
226 + * saving accent outline, breathing
227 + * saved accent fill, white check
228 + * failed open red outline, red bang
229 + *
230 + * Colour alone is not a distinction every user can make, which is why
231 + * the two outcomes differ in SHAPE — filled versus open, check versus
232 + * bang — and not only in hue.
233 + *
234 + * `Window._paintActivityIndicator()` also mirrors the phase onto the
235 + * title bar as `data-os-activity` (absent while idle) so a desktop
236 + * theme can react to window state without reaching into the
237 + * component's shadow root.
238 + */
239 +.os-window__status {
240 + --os-ui-save-status-size: var(--os-titlebar-activity-size, 16px);
241 + /*
242 + * At rest: a white ring. One value, in both title-bar states —
243 + * the phase is what the ring reports, and dimming it on an
244 + * unfocused window would make "idle" say two different things
245 + * depending on which window you last clicked.
246 + */
247 + --os-ui-save-status-idle-color: var(--os-titlebar-activity-idle-color, #fff);
248 + /*
249 + * The RING colour, not the dot's background. Setting
250 + * `--os-ui-save-status-bg` here instead is what once painted a
251 + * solid accent fill inside the resting outline: that token is the
252 + * dot's `background` on the component's base rule, and the ring
253 + * only borrows it as a border. The ring has its own name for
254 + * exactly this reason.
255 + */
256 + --os-ui-save-status-ring-color: var(--os-titlebar-activity-color, #2271b1);
257 + --os-ui-save-status-saved-bg: var(--os-titlebar-activity-saved-color, #2271b1);
258 + --os-ui-save-status-failed-bg: var(--os-titlebar-activity-failed-color, #d63638);
219 259 display: inline-flex;
220 260 align-items: center;
261 + flex-shrink: 0;
262 +}
263 +
264 +/*
265 + * The announcement. A glow is invisible to a screen reader, and
266 + * "did that save?" is precisely the question that can't be answered
267 + * by looking. `Window._paintActivityIndicator()` writes the outcome
268 + * here; absolute positioning keeps it out of the title bar's flex
269 + * flow so it contributes neither a box nor a `gap`.
270 + */
271 +.os-window__activity-status {
272 + position: absolute;
273 + width: 1px;
274 + height: 1px;
275 + margin: -1px;
276 + padding: 0;
277 + border: 0;
278 + overflow: hidden;
279 + clip-path: inset(50%);
280 + white-space: nowrap;
281 +}
282 +
283 +/*
284 + * Activity indicator slot — opt-in, and empty by default.
285 + *
286 + * The framework paints the glow above instead of putting an
287 + * `<os-save-status>` in the title bar of its own accord. The slot
288 + * survives for anything that DOES want a literal dot as well: give an
289 + * `<os-save-status>` the `data-os-activity-indicator` attribute, drop
290 + * it in a title-bar slot inside a `.os-window__activity` wrapper, and
291 + * `Window._paintActivityIndicator()` drives its phase for you.
292 + *
293 + * The fixed width is what keeps the blink from shifting the title
294 + * text sideways, and `--wp-admin-theme-color` is forwarded as `color`
295 + * so the component's shadow-DOM `currentColor` references (the glow's
296 + * box-shadow) resolve to the live accent.
297 + */
298 +.os-window__activity {
299 + display: inline-flex;
300 + align-items: center;
221 301 justify-content: center;
222 302 width: 14px;
223 303 height: 14px;
224 304 flex-shrink: 0;
@@ -227,9 +307,9 @@
227 307 color: var(--wp-admin-theme-color, #2271b1);
228 308 }
229 309
230 310 /* Window title text. */
231 -.desktop-mode-window__title {
311 +.os-window__title {
232 312 flex: 1;
233 313 overflow: hidden;
234 314 text-overflow: ellipsis;
235 315 white-space: nowrap;
@@ -234,9 +314,9 @@
234 314 text-overflow: ellipsis;
235 315 white-space: nowrap;
236 316 font-size: 14px;
237 317 font-weight: 500;
238 - line-height: var(--desktop-mode-titlebar-height);
318 + line-height: var(--os-titlebar-height);
239 319 }
240 320
241 321 /*
242 322 * Window control buttons container.
@@ -245,9 +325,9 @@
245 325 * themed title-bar texture runs edge to edge underneath it, and the
246 326 * buttons float on the artwork rather than sitting on a plate. Every
247 327 * property below resolves to exactly that when unset.
248 328 *
249 - * A theme that wants a plate instead sets `--desktop-mode-titlebar-
329 + * A theme that wants a plate instead sets `--os-titlebar-
250 330 * controls-bg` (and optionally the TITLEBAR_CONTROLS texture slot,
251 331 * a radius, and some inline padding) — the classic "the controls live
252 332 * in their own well" look, without the framework picking it for
253 333 * everyone.
@@ -256,20 +336,20 @@
256 336 * bar's height; only the inline padding is themable.
257 337 *
258 338 * @since 0.9.8
259 339 */
260 -.desktop-mode-window__controls {
340 +.os-window__controls {
261 341 display: flex;
262 - gap: var( --desktop-mode-titlebar-controls-gap, 4px );
342 + gap: var( --os-titlebar-controls-gap, 4px );
263 343 align-items: center;
264 344 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 );
345 + padding-inline: var( --os-titlebar-controls-padding, 0 );
346 + border-radius: var( --os-titlebar-controls-radius, 0 );
347 + background-color: var( --os-titlebar-controls-bg, transparent );
348 + background-image: var( --os-titlebar-controls-image, none );
349 + background-repeat: var( --os-titlebar-controls-image-repeat, repeat );
350 + background-size: var( --os-titlebar-controls-image-size, auto );
351 + background-position: var( --os-titlebar-controls-image-position, center );
272 352 }
273 353
274 354 /*
275 355 * Same treatment for the Screen Options / Help cluster, so a theme
@@ -276,18 +356,18 @@
276 356 * that plates one can plate the other and keep them consistent.
277 357 * Falls through to the controls tokens, so setting the pair above is
278 358 * enough for both.
279 359 */
280 -.desktop-mode-window__screen-meta {
360 +.os-window__screen-meta {
281 361 border-radius: var(
282 - --desktop-mode-titlebar-meta-radius,
283 - var( --desktop-mode-titlebar-controls-radius, 0 )
362 + --os-titlebar-meta-radius,
363 + var( --os-titlebar-controls-radius, 0 )
284 364 );
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 );
365 + background-color: var( --os-titlebar-meta-bg, transparent );
366 + background-image: var( --os-titlebar-meta-image, none );
367 + background-repeat: var( --os-titlebar-meta-image-repeat, repeat );
368 + background-size: var( --os-titlebar-meta-image-size, auto );
369 + background-position: var( --os-titlebar-meta-image-position, center );
290 370 }
291 371
292 372 /*
293 373 * Layer-3 slot hosts.
@@ -297,8 +377,13 @@
297 377 * removes the wrapper's box so the inner element (icon span with
298 378 * `flex-shrink: 0`, before/after spacers, etc.) keeps its
299 379 * original layout role.
300 380 *
381 + * The `icon` slot is empty by default now — the app icon it used to
382 + * carry duplicated the window's dock tile — so on most windows it
383 + * contributes nothing at all. It keeps `display: contents` for the
384 + * ones where a plugin or a desktop theme does render an icon into it.
385 + *
301 386 * The `title` slot is the exception: it's the flex-grow region of
302 387 * the title bar (`flex: 1`). When a plugin replaces the default
303 388 * title with custom HTML or a render callback, we want the new
304 389 * content to inherit the same "fill the remaining horizontal
@@ -306,23 +391,24 @@
306 391 * default title element's own `flex: 1` cooperates inside the
307 392 * nested flex.
308 393 *
309 394 * before/after-titlebar slots are flex-column children of
310 - * `.desktop-mode-window` (siblings of the title bar). They're real
395 + * `.os-window` (siblings of the title bar). They're real
311 396 * boxes — plugins can paint backgrounds, padding, borders. Empty
312 397 * hosts collapse via `:empty` so they take no space until populated.
313 398 *
314 399 * @since 0.6.0
315 400 */
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 {
401 +.os-window__slot--before-icon,
402 +.os-window__slot--icon,
403 +.os-window__slot--after-title,
404 +.os-window__slot--before-controls,
405 +.os-window__slot--after-controls {
321 406 display: contents;
322 407 }
323 408
324 -.desktop-mode-window__slot--title {
409 +
410 +.os-window__slot--title {
325 411 flex: 1;
326 412 min-width: 0;
327 413 display: flex;
328 414 align-items: center;
@@ -328,16 +414,16 @@
328 414 align-items: center;
329 415 overflow: hidden;
330 416 }
331 417
332 -.desktop-mode-window__slot--before-titlebar,
333 -.desktop-mode-window__slot--after-titlebar {
418 +.os-window__slot--before-titlebar,
419 +.os-window__slot--after-titlebar {
334 420 display: block;
335 421 flex-shrink: 0;
336 422 }
337 423
338 -.desktop-mode-window__slot--before-titlebar:empty,
339 -.desktop-mode-window__slot--after-titlebar:empty {
424 +.os-window__slot--before-titlebar:empty,
425 +.os-window__slot--after-titlebar:empty {
340 426 display: none;
341 427 }
342 428
343 429 /* ---------------------------------------------------------------
@@ -353,24 +439,24 @@
353 439 * with, matching the WordPress admin's pattern of reserving color
354 440 * for semantic signal.
355 441 * --------------------------------------------------------------- */
356 442 /*
357 - * Title-bar chrome buttons render as `<wpd-window-button>`.
443 + * Title-bar chrome buttons render as `<os-window-button>`.
358 444 * Shadow DOM can't reach across the window's focus class, so we
359 445 * 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.
446 + * the boundary. The component reads `--os-ui-btn-color`,
447 + * `--os-ui-btn-bg-hover`, etc. and paints accordingly.
362 448 *
363 449 * Focused control glyphs are white at 70% — correct against the
364 450 * default focused title bar, which is the admin theme colour and so
365 451 * always a mid-to-dark fill, and invisible against a pale one. A
366 452 * 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
453 + * element, so a `--os-ui-btn-color` set at the shell root loses to
368 454 * 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
455 + * (pinned notes, the desk chrome) that a theme usually does not want
370 456 * to move in the same stroke.
371 457 *
372 - * `--desktop-mode-titlebar-btn-focused-*` mirrors the unfocused set
458 + * `--os-titlebar-btn-focused-*` mirrors the unfocused set
373 459 * below, name for name, so the two halves of the title bar are
374 460 * addressed the same way. Every one is UNDECLARED and falls back to
375 461 * the literal it replaced, so an unthemed shell paints exactly what
376 462 * it painted before.
@@ -375,26 +461,26 @@
375 461 * the literal it replaced, so an unthemed shell paints exactly what
376 462 * it painted before.
377 463 *
378 464 * There is no `-danger-hover` twin: destructive red is semantic, not
379 - * chrome, and both halves already resolve it through `--wpd-danger`.
465 + * chrome, and both halves already resolve it through `--os-ui-danger`.
380 466 */
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 );
467 +.os-window--focused {
468 + --os-ui-btn-color: var( --os-titlebar-btn-focused-color, rgba( 255, 255, 255, 0.7 ) );
469 + --os-ui-btn-color-hover: var( --os-titlebar-btn-focused-color-hover, #fff );
470 + --os-ui-btn-bg-hover: var( --os-titlebar-btn-focused-bg-hover, rgba( 255, 255, 255, 0.18 ) );
471 + --os-ui-btn-bg-active: var( --os-titlebar-btn-focused-bg-active, rgba( 255, 255, 255, 0.25 ) );
472 + --os-ui-btn-outline: var( --os-titlebar-btn-focused-outline, rgba( 255, 255, 255, 0.65 ) );
473 + --os-ui-btn-danger-hover: var( --os-ui-danger, #d63638 );
388 474 }
389 475
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 );
476 +.os-window:not( .os-window--focused ) {
477 + --os-ui-btn-color: var( --os-titlebar-btn-color, rgba( 0, 0, 0, 0.45 ) );
478 + --os-ui-btn-color-hover: var( --os-titlebar-btn-color-hover, rgba( 0, 0, 0, 0.85 ) );
479 + --os-ui-btn-bg-hover: var( --os-titlebar-btn-bg-hover, rgba( 0, 0, 0, 0.08 ) );
480 + --os-ui-btn-bg-active: var( --os-titlebar-btn-bg-active, rgba( 0, 0, 0, 0.12 ) );
481 + --os-ui-btn-outline: var( --wp-admin-theme-color, #2271b1 );
482 + --os-ui-btn-danger-hover: var( --os-ui-danger, #d63638 );
397 483 }
398 484
399 485 /*
400 486 * Unfocused control glyphs under an active desktop theme.
@@ -405,35 +491,35 @@
405 491 * these are title-bar chrome, so the colour they owe allegiance to is
406 492 * the title bar's own text colour, not the window body's.
407 493 *
408 494 * Deriving it with `color-mix` means a theme gets legible unfocused
409 - * controls for free from the `--desktop-mode-titlebar-color` it was
495 + * controls for free from the `--os-titlebar-color` it was
410 496 * already setting — no extra tokens to discover. The four
411 - * `--desktop-mode-titlebar-btn-*` overrides above still win when an
497 + * `--os-titlebar-btn-*` overrides above still win when an
412 498 * author wants exact control.
413 499 *
414 - * Scoped to `[data-desktop-mode-desktop-theme]` on purpose: the
500 + * Scoped to `[data-os-desktop-theme]` on purpose: the
415 501 * default title-bar colour is `#50575e`, so applying this
416 502 * unconditionally would lighten the unfocused glyphs on every
417 503 * unthemed shell. No theme, no change.
418 504 */
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 )
505 +.os-shell[ data-os-desktop-theme ]
506 + .os-window:not( .os-window--focused ) {
507 + --os-ui-btn-color: var(
508 + --os-titlebar-btn-color,
509 + color-mix( in srgb, var( --os-titlebar-color, #50575e ) 72%, transparent )
424 510 );
425 - --wpd-btn-color-hover: var(
426 - --desktop-mode-titlebar-btn-color-hover,
427 - var( --desktop-mode-titlebar-color, #50575e )
511 + --os-ui-btn-color-hover: var(
512 + --os-titlebar-btn-color-hover,
513 + var( --os-titlebar-color, #50575e )
428 514 );
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 )
515 + --os-ui-btn-bg-hover: var(
516 + --os-titlebar-btn-bg-hover,
517 + color-mix( in srgb, var( --os-titlebar-color, #50575e ) 18%, transparent )
432 518 );
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 )
519 + --os-ui-btn-bg-active: var(
520 + --os-titlebar-btn-bg-active,
521 + color-mix( in srgb, var( --os-titlebar-color, #50575e ) 26%, transparent )
436 522 );
437 523 }
438 524
439 525 /* ---------------------------------------------------------------
@@ -438,9 +524,9 @@
438 524
439 525 /* ---------------------------------------------------------------
440 526 * Title-bar actions menu (leading edge, before icon + title).
441 527 *
442 - * Trigger is a standard `.desktop-mode-window__btn` — inherits focused /
528 + * Trigger is a standard `.os-window__btn` — inherits focused /
443 529 * unfocused coloring from the existing control-button rules. The only
444 530 * bespoke rule is margin: it sits at the leading edge so a small trailing
445 531 * gap separates it from the icon without affecting overall title-bar gap.
446 532 *
@@ -456,26 +542,26 @@
456 542 * divider, and the controls container gains a leading one — so a
457 543 * single clean line always sits between page chrome and window
458 544 * chrome regardless of which combination is visible.
459 545 */
460 -.desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__screen-meta {
546 +.os-window__titlebar:has(.os-window__menu-btn) .os-window__screen-meta {
461 547 margin-inline-end: 0;
462 548 padding-inline-end: 0;
463 549 border-inline-end: none;
464 550 }
465 551
466 -.desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__controls {
552 +.os-window__titlebar:has(.os-window__menu-btn) .os-window__controls {
467 553 margin-inline-start: 8px;
468 554 padding-inline-start: 8px;
469 555 /* Tokenized so a theme can retint it — or set `transparent` and
470 556 * let its own title-bar artwork carry the separation. */
471 557 border-inline-start: 1px solid
472 - var( --desktop-mode-titlebar-divider, rgba(255, 255, 255, 0.15) );
558 + var( --os-titlebar-divider, rgba(255, 255, 255, 0.15) );
473 559 }
474 560
475 -.desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__titlebar:has(.desktop-mode-window__menu-btn) .desktop-mode-window__controls {
561 +.os-window:not(.os-window--focused) .os-window__titlebar:has(.os-window__menu-btn) .os-window__controls {
476 562 border-inline-start-color: var(
477 - --desktop-mode-titlebar-divider-unfocused,
563 + --os-titlebar-divider-unfocused,
478 564 rgba(0, 0, 0, 0.1)
479 565 );
480 566 }
481 567
@@ -482,13 +568,13 @@
482 568 /*
483 569 * Menu popover lives inside the title bar's absolute-positioned
484 570 * coordinate system — positioning is caller-specific and stays
485 571 * outer. Background, border, items, checkbox styling all moved
486 - * into `<wpd-menu>` / `<wpd-menu-item>`.
572 + * into `<os-menu>` / `<os-menu-item>`.
487 573 */
488 -.desktop-mode-window__menu-panel {
574 +.os-window__menu-panel {
489 575 position: absolute;
490 - top: calc( var( --desktop-mode-titlebar-height ) + 2px );
576 + top: calc( var( --os-titlebar-height ) + 2px );
491 577 inset-inline-end: 60px;
492 578 z-index: 2;
493 579 }
494 580
@@ -494,14 +580,14 @@
494 580
495 581 /*
496 582 * "Related" dropdown — opened by the related-entities title-bar
497 583 * button (`src/related-entities/`). The panel ALSO carries
498 - * `.desktop-mode-window__menu-panel` (positioning + the drag-tracker
584 + * `.os-window__menu-panel` (positioning + the drag-tracker
499 585 * exclusion come from there); this class only layers the related-menu
500 586 * extras: an internal scroll cap for media-heavy posts and a sane
501 587 * minimum width.
502 588 */
503 -.desktop-mode-window__related-panel {
589 +.os-window__related-panel {
504 590 max-height: min( 60vh, 420px );
505 591 overflow-y: auto;
506 592 min-width: 180px;
507 593 }
@@ -506,9 +592,9 @@
506 592 min-width: 180px;
507 593 }
508 594
509 595 /* Section header inside the Related dropdown ("Categories", "Media"). */
510 -.desktop-mode-window__related-group {
596 +.os-window__related-group {
511 597 padding: 6px 12px 2px;
512 598 font-size: 11px;
513 599 font-weight: 600;
514 600 text-transform: uppercase;
@@ -513,12 +599,12 @@
513 599 font-weight: 600;
514 600 text-transform: uppercase;
515 601 letter-spacing: 0.04em;
516 602 opacity: 0.6;
517 - color: var( --wpd-fg );
603 + color: var( --os-ui-fg );
518 604 }
519 605
520 -.desktop-mode-window__related-group:not(:first-child) {
606 +.os-window__related-group:not(:first-child) {
521 607 margin-block-start: 4px;
522 608 border-block-start: 1px solid rgba( 128, 128, 128, 0.25 );
523 609 padding-block-start: 8px;
524 610 }
@@ -528,9 +614,9 @@
528 614 * Positioned right after the title text, visually separated from
529 615 * the close / maximize / minimize cluster by a subtle divider.
530 616 * Sized to meet WCAG 2.2 target size (24x24 minimum).
531 617 * --------------------------------------------------------------- */
532 -.desktop-mode-window__screen-meta {
618 +.os-window__screen-meta {
533 619 display: flex;
534 620 gap: 4px;
535 621 align-items: center;
536 622 flex-shrink: 0;
@@ -536,17 +622,17 @@
536 622 flex-shrink: 0;
537 623 margin-inline-end: 8px;
538 624 padding-inline-end: 8px;
539 625 border-inline-end: 1px solid
540 - var( --desktop-mode-titlebar-divider, rgba(255, 255, 255, 0.15) );
626 + var( --os-titlebar-divider, rgba(255, 255, 255, 0.15) );
541 627 }
542 628
543 629 /* Hide the divider when there are no screen-meta buttons. */
544 -.desktop-mode-window__screen-meta:empty {
630 +.os-window__screen-meta:empty {
545 631 display: none;
546 632 }
547 633
548 -.desktop-mode-window__meta-btn {
634 +.os-window__meta-btn {
549 635 display: flex;
550 636 align-items: center;
551 637 justify-content: center;
552 638 width: 28px;
@@ -558,9 +644,9 @@
558 644 background: transparent;
559 645 transition: background-color 0.15s ease, color 0.15s ease;
560 646 }
561 647
562 -.desktop-mode-window__meta-btn .dashicons {
648 +.os-window__meta-btn .dashicons {
563 649 font-size: 18px;
564 650 width: 18px;
565 651 height: 18px;
566 652 }
@@ -572,50 +658,57 @@
572 658 * relocating it is behaviour-neutral churn. */
573 659
574 660 /* Focused window: meta buttons visible.
575 661 *
576 - * Screen-meta buttons are not `<wpd-window-button>` — they are plain
662 + * Screen-meta buttons are not `<os-window-button>` — they are plain
577 663 * buttons in the light DOM, so they paint themselves instead of
578 - * reading the `--wpd-btn-*` bridge. They sit in the same title bar
664 + * reading the `--os-ui-btn-*` bridge. They sit in the same title bar
579 665 * against the same fill, so they take the same
580 - * `--desktop-mode-titlebar-btn-focused-*` tokens, each keeping its own
666 + * `--os-titlebar-btn-focused-*` tokens, each keeping its own
581 667 * literal as the fallback: unthemed they stay a touch dimmer at rest
582 668 * than the window controls (0.65 vs 0.7), themed they move together
583 669 * 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) );
670 +.os-window--focused .os-window__meta-btn {
671 + color: var( --os-titlebar-btn-focused-color, rgba(255, 255, 255, 0.65) );
586 672 }
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) );
673 +.os-window--focused .os-window__meta-btn:hover {
674 + color: var( --os-titlebar-btn-focused-color-hover, var( --os-ui-fg-on-accent, #fff ) );
675 + background: var( --os-titlebar-btn-focused-bg-hover, rgba(255, 255, 255, 0.18) );
590 676 }
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) );
677 +.os-window--focused .os-window__meta-btn:focus-visible {
678 + color: var( --os-titlebar-btn-focused-color-hover, var( --os-ui-fg-on-accent, #fff ) );
679 + background: var( --os-titlebar-btn-focused-bg-hover, rgba(255, 255, 255, 0.18) );
680 + outline: 2px solid var( --os-titlebar-btn-focused-outline, rgba(255, 255, 255, 0.6) );
595 681 outline-offset: 1px;
596 682 }
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) );
683 +.os-window--focused .os-window__meta-btn--active {
684 + color: var( --os-titlebar-btn-focused-color-hover, var( --os-ui-fg-on-accent, #fff ) );
685 + background: var( --os-titlebar-btn-focused-bg-active, rgba(255, 255, 255, 0.25) );
600 686 }
601 687
602 688 /* Unfocused window: divider and buttons adapt to light title bar. */
603 -.desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__screen-meta {
689 +.os-window:not(.os-window--focused) .os-window__screen-meta {
604 690 border-inline-end-color: var(
605 - --desktop-mode-titlebar-divider-unfocused,
691 + --os-titlebar-divider-unfocused,
606 692 rgba(0, 0, 0, 0.1)
607 693 );
608 694 }
609 -.desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn {
610 - color: rgba(0, 0, 0, 0.3);
695 +/*
696 + * Screen Options / Help on an UNFOCUSED title bar. These read the same
697 + * two tokens as the window controls beside them rather than their own
698 + * literals — a black glyph is invisible on a dark title bar, and these
699 + * buttons sit in the same strip as controls that were already themable.
700 + * The literals are unchanged, so an unthemed bar looks as it always did.
701 + */
702 +.os-window:not(.os-window--focused) .os-window__meta-btn {
703 + color: var( --os-titlebar-btn-color, rgba(0, 0, 0, 0.3) );
611 704 }
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) );
705 +.os-window:not(.os-window--focused) .os-window__meta-btn:hover {
706 + color: var( --os-titlebar-btn-color-hover, rgba(0, 0, 0, 0.6) );
707 + background: var( --os-ui-hover, rgba(0, 0, 0, 0.08) );
615 708 }
616 -.desktop-mode-window:not(.desktop-mode-window--focused) .desktop-mode-window__meta-btn--active {
617 - color: rgba(0, 0, 0, 0.6);
709 +.os-window:not(.os-window--focused) .os-window__meta-btn--active {
710 + color: var( --os-titlebar-btn-color-hover, rgba(0, 0, 0, 0.6) );
618 711 background: rgba(0, 0, 0, 0.12);
619 712 }
620 713
621 714 /* ---------------------------------------------------------------
@@ -622,35 +715,569 @@
622 715 * Tab strip — submenu navigation rendered in the parent shell, just
623 716 * below the title bar. Each tab swaps the iframe URL in place; no
624 717 * new window opens. Horizontally scrollable on narrow windows so the
625 718 * same component works on tablet and mobile shells unchanged.
719 + *
720 + * These are TABS in the physical sense: the strip is a recessed
721 + * track, and the active tab is a plate that rises out of it wearing
722 + * the page's own fill, filleted into the floor at both bottom
723 + * corners so tab and page read as one continuous surface. That joint
724 + * is the whole design. The previous treatment — flat text with an
725 + * accent underline, on a strip painted the same colour as the
726 + * focused title bar — gave the sub-pages no container to belong to
727 + * and no relationship to the page they navigate, which is why they
728 + * read as loose text floating in the chrome.
729 + *
730 + * The page under an iframe window is always `#fff`: `chromeless.css`
731 + * paints `body.os-chromeless` white whatever the admin colour scheme
732 + * says. So "the page's own fill" is a colour the shell can name, and
733 + * `--os-tabs-active-bg` names it.
626 734 * --------------------------------------------------------------- */
627 -.desktop-mode-window__tabs {
735 +.os-window__tabs {
736 + /*
737 + * Corner radius and fillet size are the same measurement — the
738 + * fillet is the *inverse* of the corner, so a mismatch reads as a
739 + * kink where the tab meets the floor. One alias, read by both.
740 + */
741 + --_tab-radius: var( --os-tabs-radius, 8px );
742 + /* Tab height, shared with the plate that has to sit exactly on it. */
743 + --_tab-h: 30px;
744 + /* Weight of the rail that traces the silhouette. */
745 + --_tab-stroke: var( --os-tabs-rail-width, 2px );
746 + /*
747 + * How far each straight run of the rail overlaps the arc it hands
748 + * over to. Purely an anti-seam allowance — see the mask sizes on
749 + * the ring. It has to stay well under the radius or the overlap
750 + * would reach past the arc it is covering for.
751 + */
752 + --_tab-seam: 1px;
753 + position: relative;
628 754 display: flex;
755 + /* Tabs sit ON the floor of the track, not centred in it. */
756 + align-items: flex-end;
629 757 flex-shrink: 0;
630 758 gap: 2px;
631 - padding: 0 8px;
632 - background-color: var( --desktop-mode-tabs-bg, var( --wpd-surface-elevated, #f6f7f7 ) );
759 + /*
760 + * Horizontal padding is a floor as well as a breathing space. The
761 + * ring reaches one radius plus one stroke (10px at the shipped
762 + * values) past the plate on each side, so a first or last tab
763 + * under a smaller padding would have its outer corner clipped by
764 + * the `overflow` rule below.
765 + */
766 + padding: 8px 12px 0;
767 + background-color: var( --os-tabs-bg, var( --os-ui-surface-elevated, #f6f7f7 ) );
633 768 /* 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);
769 + background-image: var( --os-tabs-image, none );
770 + background-repeat: var( --os-tabs-image-repeat, repeat );
771 + background-size: var( --os-tabs-image-size, auto );
772 + background-position: var( --os-tabs-image-position, center );
773 + /*
774 + * No bottom border. A hairline here would run straight through
775 + * the joint between the active tab and its page, which is the one
776 + * edge this design exists to erase.
777 + */
639 778 overflow-x: auto;
640 779 overflow-y: hidden;
641 780 scrollbar-width: thin;
642 781 /*
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 782 * `overscroll-behavior-x: contain` keeps horizontal trackpad
649 783 * swipes inside the strip instead of triggering browser back.
650 784 */
785 + overscroll-behavior-x: contain;
786 +}
787 +
788 +/*
789 + * Unfocused, the track follows the title bar down instead of holding
790 + * its lit colour.
791 + *
792 + * Focused, the bar and the track are the same Obsidian and the tab is
793 + * the only thing lifting out of them: two surfaces plus the tab. Dim
794 + * the bar alone and the strip becomes a third colour belonging to
795 + * neither the chrome above it nor the page below, which is the seam
796 + * this rule removes.
797 + *
798 + * The chain ends at `--os-tabs-bg`, so a theme that names only the one
799 + * strip colour keeps it in both states.
800 + */
801 +.os-window:not( .os-window--focused ) .os-window__tabs {
802 + background-color: var(
803 + --os-tabs-bg-unfocused,
804 + var( --os-tabs-bg, var( --os-ui-surface-elevated, #f6f7f7 ) )
805 + );
806 +}
807 +
808 +/*
809 + * The same strip on a NATIVE window, which is the same strip: one
810 + * stylesheet, whatever is behind the window. What changes is only
811 + * what the active tab is wearing, and it changes in tokens.
812 + *
813 + * The rule the whole design rests on is that the active tab wears the
814 + * page's own fill and is filleted into it, so tab and content read as
815 + * one surface. For an iframe window that fill is `#fff`, because
816 + * `chromeless.css` paints every admin page inside a window white
817 + * whatever the colour scheme says. A native window's page is its
818 + * body, and `--os-window-bg` is what paints it — so that is the fill
819 + * here, and the two move together by construction.
820 + *
821 + * The frost and the crown go with the white. They are what makes a
822 + * bright plate read as a surface lifting out of dark chrome; on a
823 + * native window the tab and the track are the same colour when the
824 + * window is focused, and a crown on a tab you cannot see the edges of
825 + * is decoration with nothing to decorate. The rail carries the
826 + * silhouette on its own, which is the point: a native window has no
827 + * value step to wear, so the line IS the tab.
828 + *
829 + * The label follows the fill. `--os-tabs-active-color` names the text
830 + * on a white plate and resolves to near-black, which would be
831 + * invisible here.
832 + */
833 +.os-window--native {
834 + --os-tabs-active-bg: var( --os-window-bg, #fff );
835 + --os-tabs-active-color: var( --os-ui-fg, #1d2327 );
836 + --os-tabs-active-color-muted: var( --os-ui-fg-muted, #50575e );
837 + --os-tabs-active-frost: none;
838 + --os-tabs-active-crown: none;
839 +}
840 +
841 +/*
842 + * Windows with no submenu still get a strip — `createWindowElement()`
843 + * appends it unconditionally so `addExternalTab()` has somewhere to
844 + * put a tab later. Empty, it must take no room at all: with vertical
845 + * padding on the track, an empty strip would otherwise paint a bare
846 + * band of track colour under every submenu-less window's title bar.
847 + *
848 + * `:has()` rather than `:empty`, because the strip is never empty any
849 + * more — it always carries the plate. What makes a strip vacant is
850 + * having no TABS in it.
851 + */
852 +.os-window__tabs:not( :has( .os-window__tab ) ) {
853 + display: none;
854 +}
855 +
856 +/* ---------------------------------------------------------------
857 + * The plate — the active tab's surface.
858 + *
859 + * It is one element that TRAVELS between tabs rather than a fill
860 + * that switches off on one tab and on at the next. That distinction
861 + * is the whole reason it exists: a fill that switches has to cross-
862 + * fade a dark tab into a light one, and every frame in between is a
863 + * muddy grey that belongs to neither. Nothing crossfades here. The
864 + * surface simply moves, and the labels change colour underneath it.
865 + *
866 + * `tabs.ts` publishes the target geometry as `--_tab-plate-x` and
867 + * `--_tab-plate-w` on this element; everything else is CSS.
868 + * --------------------------------------------------------------- */
869 +.os-window__tab-plate {
870 + position: absolute;
871 + bottom: 0;
872 + left: 0;
873 + height: var( --_tab-h );
874 + width: var( --_tab-plate-w, 0 );
875 + transform: translateX( var( --_tab-plate-x, 0 ) );
876 + pointer-events: none;
877 + transition:
878 + transform var( --os-tabs-slide, 340ms cubic-bezier( 0.22, 1, 0.28, 1 ) ),
879 + width var( --os-tabs-slide, 340ms cubic-bezier( 0.22, 1, 0.28, 1 ) ),
880 + opacity 0.15s ease;
881 +}
882 +
883 +/*
884 + * Until the first measurement lands, the plate has no business
885 + * animating — it would slide in from the strip's left edge every
886 + * time a window opens.
887 + */
888 +.os-window__tab-plate:not( [ data-placed ] ) {
889 + transition: none;
890 +}
891 +
892 +/*
893 + * The frosted face.
894 + *
895 + * The tint is TOP-WEIGHTED and reaches zero before the bottom of the
896 + * plate, which is the load-bearing part: the joint below is the
897 + * page's own colour, so anything still tinted down there draws a
898 + * seam across the one edge this design exists to erase. Keep the
899 + * gradient's last stop fully transparent and keep it above ~75%.
900 + *
901 + * The fallback is a flat `--os-tabs-active-bg`, so a stylesheet that
902 + * loses the palette gets the plain white tab rather than nothing.
903 + */
904 +.os-window__tab-plate-fill {
905 + position: absolute;
906 + /*
907 + * Overshoots the track's floor by one radius, and is then clipped
908 + * by the strip's own `overflow`. Ending the face exactly ON the
909 + * floor puts a paint boundary on the one edge that has to be
910 + * invisible; pushing it past and cutting it means there is no edge
911 + * there to resolve at all.
912 + */
913 + inset: 0 0 calc( var( --_tab-radius ) * -1 ) 0;
914 + overflow: hidden;
915 + border-radius: var( --_tab-radius ) var( --_tab-radius ) 0 0;
916 + /*
917 + * OPAQUE, and deliberately so — no `backdrop-filter`, no alpha.
918 + *
919 + * A translucent element with a backdrop filter is promoted to its
920 + * own compositor layer, and the edge of that layer is a clip the
921 + * compositor resolves against whatever is behind it. That leaves a
922 + * faint grey hairline down the plate's sides and across its
923 + * bottom. Everywhere else that would be a nuisance; here it draws
924 + * a second, dimmer line a few pixels inside the real one and flatly
925 + * contradicts what this design is claiming. The gradient below
926 + * reproduces the frosted crown by eye with no alpha at all.
927 + *
928 + * `background-size` pins the gradient to the tab's true height so
929 + * the overshoot underneath stays flat page-white.
930 + */
931 + background-color: var( --os-tabs-active-bg, #fff );
932 + background-image: var( --os-tabs-active-frost, none );
933 + background-size: 100% var( --_tab-h );
934 + background-repeat: no-repeat;
935 +}
936 +
937 +/*
938 + * The crown — Holomesh over the top of the plate, masked away before
939 + * the joint. Holomesh is nine stacked gradients, so it cannot be
940 + * faded by adding a colour stop; it needs its own layer and a mask.
941 + *
942 + * It is on exactly one tab at a time. That is what keeps the mesh an
943 + * identity moment rather than wallpaper, and it is the same rule the
944 + * rest of the kit follows (see `src/ui/holo.ts`).
945 + */
946 +.os-window__tab-plate-fill::before {
947 + content: '';
948 + position: absolute;
949 + inset: 0;
950 + background-image: var( --os-tabs-active-crown, none );
951 + background-size: 210% 210%;
952 + background-position: 20% 26%;
953 + opacity: var( --os-tabs-active-crown-opacity, 0.5 );
954 + -webkit-mask-image: linear-gradient(
955 + 180deg,
956 + #000 0%,
957 + rgba( 0, 0, 0, 0.55 ) 34%,
958 + transparent 66%
959 + );
651 960 mask-image: linear-gradient(
961 + 180deg,
962 + #000 0%,
963 + rgba( 0, 0, 0, 0.55 ) 34%,
964 + transparent 66%
965 + );
966 + animation: os-tab-crown-drift 16s ease-in-out infinite alternate;
967 +}
968 +
969 +@keyframes os-tab-crown-drift {
970 + from { background-position: 20% 26%; }
971 + to { background-position: 72% 62%; }
972 +}
973 +
974 +/*
975 + * The joint: two concave quarter-circles carrying the plate's fill
976 + * out to the floor of the track. Without them the plate is a rounded
977 + * rectangle NEAR the page; with them it is attached to it.
978 + *
979 + * Its own element rather than a pseudo on the face, so the face can
980 + * clip its crown (`overflow: hidden`) without clipping the curve
981 + * that does the attaching. Pure `--os-tabs-active-bg` — see the note
982 + * on the tint above.
983 + */
984 +.os-window__tab-plate-joint {
985 + position: absolute;
986 + bottom: 0;
987 + left: calc( var( --_tab-radius ) * -1 );
988 + right: calc( var( --_tab-radius ) * -1 );
989 + height: var( --_tab-radius );
990 + background:
991 + radial-gradient(
992 + circle at 0 0,
993 + transparent var( --_tab-radius ),
994 + var( --os-tabs-active-bg, #fff ) var( --_tab-radius )
995 + ) left bottom / var( --_tab-radius ) var( --_tab-radius ) no-repeat,
996 + radial-gradient(
997 + circle at 100% 0,
998 + transparent var( --_tab-radius ),
999 + var( --os-tabs-active-bg, #fff ) var( --_tab-radius )
1000 + ) right bottom / var( --_tab-radius ) var( --_tab-radius ) no-repeat;
1001 +}
1002 +
1003 +/* ---------------------------------------------------------------
1004 + * The rail — one continuous line around the whole silhouette.
1005 + *
1006 + * It runs the top edge of the page, curves up through the fillet,
1007 + * traces the tab and comes back down. What it outlines is therefore
1008 + * page-plus-tab as ONE shape, which is the same claim the joint makes
1009 + * by omission, said out loud.
1010 + *
1011 + * Two rules govern every number below.
1012 + *
1013 + * 1. **The line lives on the DARK side of the boundary, everywhere.**
1014 + * It hugs the white shape from outside and never paints on it. Get
1015 + * this wrong on any one segment and the line steps sideways by its
1016 + * own width at the tangent point where that segment meets the next
1017 + * — which is exactly what a stroke that does not follow the shape
1018 + * looks like. The convex tab corners are therefore annulus R→R+s
1019 + * (outside the plate) while the concave fillets are R−s→R (inside
1020 + * the fillet circle, whose white lies OUTSIDE it). Those look like
1021 + * opposite conventions and are the same one.
1022 + *
1023 + * 2. **Every piece samples one mesh laid across the whole strip.**
1024 + * `--_tab-strip-w` is why. Give the rail and the ring their own
1025 + * backgrounds and you get two unrelated gradients meeting at a
1026 + * visible join in the middle of the fillet.
1027 + *
1028 + * CSS cannot stroke a path, so the mesh is painted as a background and
1029 + * a mask cuts it to the outline. Seven layers, one per segment, all
1030 + * positioned off the box edges so the whole thing survives the plate
1031 + * changing width mid-slide with no JS.
1032 + * --------------------------------------------------------------- */
1033 +.os-window__tabs::before {
1034 + content: '';
1035 + position: absolute;
1036 + left: 0;
1037 + right: 0;
1038 + bottom: 0;
1039 + height: var( --_tab-stroke );
1040 + background-image: var( --os-tabs-rail, none );
1041 + /* The rail's colour is derived from the accent; how much of it
1042 + shows is a literal the palette answers. 1 is the earlier look. */
1043 + opacity: var( --os-tabs-rail-opacity, 1 );
1044 + background-size: var( --_tab-strip-w, 100% ) 100%;
1045 + background-repeat: no-repeat;
1046 + pointer-events: none;
1047 + /*
1048 + * Two segments, stopping where each fillet begins. Run the rail
1049 + * straight through and it draws a chord across the concave curve.
1050 + */
1051 + -webkit-mask-image: linear-gradient( #000 0 0 ), linear-gradient( #000 0 0 );
1052 + mask-image: linear-gradient( #000 0 0 ), linear-gradient( #000 0 0 );
1053 + /*
1054 + * Each segment runs one `--_tab-seam` PAST where its fillet begins,
1055 + * so the rail overlaps the arc rather than meeting it exactly —
1056 + * the same anti-seam allowance the ring uses, for the same reason.
1057 + */
1058 + -webkit-mask-size:
1059 + calc(
1060 + var( --_tab-plate-x, 0px ) - var( --_tab-radius ) + var( --_tab-seam )
1061 + ) 100%,
1062 + calc(
1063 + 100% - var( --_tab-plate-x, 0px ) - var( --_tab-plate-w, 0px ) -
1064 + var( --_tab-radius ) + var( --_tab-seam )
1065 + ) 100%;
1066 + mask-size:
1067 + calc(
1068 + var( --_tab-plate-x, 0px ) - var( --_tab-radius ) + var( --_tab-seam )
1069 + ) 100%,
1070 + calc(
1071 + 100% - var( --_tab-plate-x, 0px ) - var( --_tab-plate-w, 0px ) -
1072 + var( --_tab-radius ) + var( --_tab-seam )
1073 + ) 100%;
1074 + -webkit-mask-position: left top, right top;
1075 + mask-position: left top, right top;
1076 + -webkit-mask-repeat: no-repeat;
1077 + mask-repeat: no-repeat;
1078 + transition:
1079 + -webkit-mask-size var( --os-tabs-slide, 340ms cubic-bezier( 0.22, 1, 0.28, 1 ) ),
1080 + mask-size var( --os-tabs-slide, 340ms cubic-bezier( 0.22, 1, 0.28, 1 ) );
1081 +}
1082 +
1083 +/* With no active tab there is no gap to leave, so the rail is whole. */
1084 +.os-window__tabs[ data-tab-plate-empty ]::before {
1085 + -webkit-mask-image: none;
1086 + mask-image: none;
1087 +}
1088 +
1089 +/*
1090 + * The tab's half of the line. The box is the plate grown by one
1091 + * stroke upward and by radius-plus-stroke on each side, so the top
1092 + * edge, both corners and both fillet arcs all have room to sit
1093 + * OUTSIDE the white.
1094 + */
1095 +.os-window__tab-plate::after {
1096 + --_ring-arc-out: radial-gradient(
1097 + circle at 0 0,
1098 + transparent calc( var( --_tab-radius ) - var( --_tab-stroke ) ),
1099 + #000 calc( var( --_tab-radius ) - var( --_tab-stroke ) ),
1100 + #000 var( --_tab-radius ),
1101 + transparent var( --_tab-radius )
1102 + );
1103 + --_ring-arc-out-r: radial-gradient(
1104 + circle at 100% 0,
1105 + transparent calc( var( --_tab-radius ) - var( --_tab-stroke ) ),
1106 + #000 calc( var( --_tab-radius ) - var( --_tab-stroke ) ),
1107 + #000 var( --_tab-radius ),
1108 + transparent var( --_tab-radius )
1109 + );
1110 + --_ring-corner-l: radial-gradient(
1111 + circle at 100% 100%,
1112 + transparent var( --_tab-radius ),
1113 + #000 var( --_tab-radius ),
1114 + #000 calc( var( --_tab-radius ) + var( --_tab-stroke ) ),
1115 + transparent calc( var( --_tab-radius ) + var( --_tab-stroke ) )
1116 + );
1117 + --_ring-corner-r: radial-gradient(
1118 + circle at 0 100%,
1119 + transparent var( --_tab-radius ),
1120 + #000 var( --_tab-radius ),
1121 + #000 calc( var( --_tab-radius ) + var( --_tab-stroke ) ),
1122 + transparent calc( var( --_tab-radius ) + var( --_tab-stroke ) )
1123 + );
1124 + --_ring-bar: linear-gradient( #000 0 0 );
1125 +
1126 + content: '';
1127 + position: absolute;
1128 + inset:
1129 + calc( var( --_tab-stroke ) * -1 )
1130 + calc( ( var( --_tab-radius ) + var( --_tab-stroke ) ) * -1 )
1131 + 0;
1132 + background-image: var( --os-tabs-rail, none );
1133 + /* The rail's colour is derived from the accent; how much of it
1134 + shows is a literal the palette answers. 1 is the earlier look. */
1135 + opacity: var( --os-tabs-rail-opacity, 1 );
1136 + background-size: var( --_tab-strip-w, 100% ) 100%;
1137 + background-position:
1138 + calc(
1139 + var( --_tab-radius ) + var( --_tab-stroke ) -
1140 + var( --_tab-plate-x, 0px )
1141 + )
1142 + 0;
1143 + background-repeat: no-repeat;
1144 + pointer-events: none;
1145 +
1146 + -webkit-mask-image:
1147 + var( --_ring-bar ), var( --_ring-corner-l ), var( --_ring-corner-r ),
1148 + var( --_ring-bar ), var( --_ring-bar ),
1149 + var( --_ring-arc-out ), var( --_ring-arc-out-r );
1150 + mask-image:
1151 + var( --_ring-bar ), var( --_ring-corner-l ), var( --_ring-corner-r ),
1152 + var( --_ring-bar ), var( --_ring-bar ),
1153 + var( --_ring-arc-out ), var( --_ring-arc-out-r );
1154 + /*
1155 + * The three straight runs are each grown by `--_tab-seam` at BOTH
1156 + * ends, and shifted back by the same amount, so every one of them
1157 + * overlaps the arc it hands over to instead of meeting it exactly.
1158 + *
1159 + * Two layers that abut on a shared boundary each contribute a
1160 + * partial, antialiased alpha there, and the sum can fall short of
1161 + * 1 — which prints as a faint hairline across the stroke at all
1162 + * six tangent points. Overlapping cannot go wrong in the other
1163 + * direction: mask alpha is clamped, so doubling it is still opaque
1164 + * and the seam simply stops existing.
1165 + *
1166 + * The overlap stays on the dark side of the curve at every one of
1167 + * those points, so it never bleeds onto the white.
1168 + */
1169 + -webkit-mask-size:
1170 + calc( 100% - 4 * var( --_tab-radius ) - 2 * var( --_tab-stroke ) + 2 * var( --_tab-seam ) ) var( --_tab-stroke ),
1171 + calc( var( --_tab-radius ) + var( --_tab-stroke ) ) calc( var( --_tab-radius ) + var( --_tab-stroke ) ),
1172 + calc( var( --_tab-radius ) + var( --_tab-stroke ) ) calc( var( --_tab-radius ) + var( --_tab-stroke ) ),
1173 + var( --_tab-stroke ) calc( 100% - 2 * var( --_tab-radius ) - var( --_tab-stroke ) + 2 * var( --_tab-seam ) ),
1174 + var( --_tab-stroke ) calc( 100% - 2 * var( --_tab-radius ) - var( --_tab-stroke ) + 2 * var( --_tab-seam ) ),
1175 + var( --_tab-radius ) var( --_tab-radius ),
1176 + var( --_tab-radius ) var( --_tab-radius );
1177 + mask-size:
1178 + calc( 100% - 4 * var( --_tab-radius ) - 2 * var( --_tab-stroke ) + 2 * var( --_tab-seam ) ) var( --_tab-stroke ),
1179 + calc( var( --_tab-radius ) + var( --_tab-stroke ) ) calc( var( --_tab-radius ) + var( --_tab-stroke ) ),
1180 + calc( var( --_tab-radius ) + var( --_tab-stroke ) ) calc( var( --_tab-radius ) + var( --_tab-stroke ) ),
1181 + var( --_tab-stroke ) calc( 100% - 2 * var( --_tab-radius ) - var( --_tab-stroke ) + 2 * var( --_tab-seam ) ),
1182 + var( --_tab-stroke ) calc( 100% - 2 * var( --_tab-radius ) - var( --_tab-stroke ) + 2 * var( --_tab-seam ) ),
1183 + var( --_tab-radius ) var( --_tab-radius ),
1184 + var( --_tab-radius ) var( --_tab-radius );
1185 + /*
1186 + * Four-value syntax throughout, and it is load-bearing. A
1187 + * percentage in `mask-position` resolves against the container
1188 + * MINUS the layer's own size, so `calc(100% - 8px)` does not mean
1189 + * "8px from the right edge" — it means "right-aligned, then pushed
1190 + * left by 8px PLUS the layer's width". Every right-hand segment
1191 + * lands a radius too far left that way. `right <offset>` is
1192 + * measured from the edge and does not care how wide the layer is.
1193 + */
1194 + -webkit-mask-position:
1195 + left calc( 2 * var( --_tab-radius ) + var( --_tab-stroke ) - var( --_tab-seam ) ) top 0,
1196 + left var( --_tab-radius ) top 0,
1197 + right var( --_tab-radius ) top 0,
1198 + left var( --_tab-radius ) top calc( var( --_tab-radius ) + var( --_tab-stroke ) - var( --_tab-seam ) ),
1199 + right var( --_tab-radius ) top calc( var( --_tab-radius ) + var( --_tab-stroke ) - var( --_tab-seam ) ),
1200 + left var( --_tab-stroke ) bottom 0,
1201 + right var( --_tab-stroke ) bottom 0;
1202 + mask-position:
1203 + left calc( 2 * var( --_tab-radius ) + var( --_tab-stroke ) - var( --_tab-seam ) ) top 0,
1204 + left var( --_tab-radius ) top 0,
1205 + right var( --_tab-radius ) top 0,
1206 + left var( --_tab-radius ) top calc( var( --_tab-radius ) + var( --_tab-stroke ) - var( --_tab-seam ) ),
1207 + right var( --_tab-radius ) top calc( var( --_tab-radius ) + var( --_tab-stroke ) - var( --_tab-seam ) ),
1208 + left var( --_tab-stroke ) bottom 0,
1209 + right var( --_tab-stroke ) bottom 0;
1210 + -webkit-mask-repeat: no-repeat;
1211 + mask-repeat: no-repeat;
1212 +
1213 + /* Travels with the plate, so the mesh stays locked to the strip
1214 + * for the whole slide instead of snapping on the first frame. */
1215 + transition: background-position var( --os-tabs-slide, 340ms cubic-bezier( 0.22, 1, 0.28, 1 ) );
1216 +}
1217 +
1218 +/*
1219 + * No active tab to sit under — `syncActiveTab` can land on a URL that
1220 + * matches nothing, and an external sub-tab deactivates every submenu
1221 + * tab. The plate keeps its last geometry and fades, so re-activating
1222 + * a tab does not read as the plate flying in from nowhere.
1223 + */
1224 +.os-window__tab-plate[ data-empty ] {
1225 + opacity: 0;
1226 +}
1227 +
1228 +@media ( prefers-reduced-motion: reduce ) {
1229 + .os-window__tab-plate,
1230 + .os-window__tab-plate::after,
1231 + .os-window__tabs::before {
1232 + transition-duration: 1ms;
1233 + }
1234 + .os-window__tab-plate-fill::before {
1235 + animation: none;
1236 + }
1237 +}
1238 +
1239 +/*
1240 + * Soft edge fades so overflowing tabs tell the user "scroll for more."
1241 + *
1242 + * Each fade is painted ONLY on an edge that is actually hiding a tab.
1243 + * `observeTabOverflow()` in `src/window/tabs.ts` stamps `data-overflow`
1244 + * with the physical edges currently covered — `left`, `right`, `both`,
1245 + * or the attribute dropped when the strip fits — and re-measures on
1246 + * scroll, on resize, and when tabs are added or removed.
1247 + *
1248 + * This used to be one unconditional mask on both ends. The reasoning
1249 + * was that on a strip that fits, the faded ends land past the last tab
1250 + * and so fade nothing. That held on the pre-brand light strip, where
1251 + * the tab bar and the title bar above it were near enough the same
1252 + * near-white that a mask over empty area was invisible. It stopped
1253 + * holding on the station: the strip is Obsidian over a darker window
1254 + * edge, so masking its ends to transparent punches two grey smudges
1255 + * into every window's submenu, whether or not there is anything to
1256 + * scroll to. A permanent affordance for a state that is usually false
1257 + * is not an affordance — it is decoration that lies.
1258 + */
1259 +.os-window__tabs[ data-overflow='left' ] {
1260 + mask-image: linear-gradient( to right, transparent 0, #000 16px );
1261 + -webkit-mask-image: linear-gradient( to right, transparent 0, #000 16px );
1262 +}
1263 +
1264 +.os-window__tabs[ data-overflow='right' ] {
1265 + mask-image: linear-gradient(
652 1266 to right,
1267 + #000 calc(100% - 16px),
1268 + transparent 100%
1269 + );
1270 + -webkit-mask-image: linear-gradient(
1271 + to right,
1272 + #000 calc(100% - 16px),
1273 + transparent 100%
1274 + );
1275 +}
1276 +
1277 +.os-window__tabs[ data-overflow='both' ] {
1278 + mask-image: linear-gradient(
1279 + to right,
653 1280 transparent 0,
654 1281 #000 16px,
655 1282 #000 calc(100% - 16px),
656 1283 transparent 100%
@@ -661,53 +1288,74 @@
661 1288 #000 16px,
662 1289 #000 calc(100% - 16px),
663 1290 transparent 100%
664 1291 );
665 - overscroll-behavior-x: contain;
666 1292 }
667 1293
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 {
1294 +.os-window__tab {
1295 + position: relative;
676 1296 flex-shrink: 0;
677 1297 display: inline-flex;
678 1298 align-items: center;
679 - height: 32px;
680 - padding: 0 12px;
1299 + height: 30px;
1300 + padding: 0 14px;
681 1301 border: none;
1302 + border-radius: var( --_tab-radius ) var( --_tab-radius ) 0 0;
682 1303 background: transparent;
683 - color: var( --desktop-mode-tabs-color, var( --wpd-fg-muted, #50575e ) );
1304 + color: var( --os-tabs-color, var( --os-ui-fg-muted, #50575e ) );
684 1305 font: inherit;
685 1306 font-size: 12px;
686 1307 line-height: 1;
687 1308 cursor: pointer;
688 - border-bottom: 2px solid transparent;
689 - margin-bottom: -1px;
690 1309 white-space: nowrap;
691 - transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
1310 + transition: color 0.15s ease, background-color 0.15s ease;
692 1311 }
693 1312
694 -.desktop-mode-window__tab:hover {
1313 +.os-window__tab:hover {
695 1314 color: var(--wp-admin-theme-color, #2271b1);
696 - background: var( --wpd-hover, rgba(0, 0, 0, 0.03) );
1315 + background: var( --os-ui-hover, rgba(0, 0, 0, 0.03) );
697 1316 }
698 1317
699 -.desktop-mode-window__tab:focus-visible {
1318 +.os-window__tab:focus-visible {
700 1319 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
701 1320 outline-offset: -2px;
702 1321 }
703 1322
704 -.desktop-mode-window__tab--active {
705 - color: var(--wp-admin-theme-color, #2271b1);
706 - border-bottom-color: var(--wp-admin-theme-color, #2271b1);
1323 +/*
1324 + * The active tab. It paints NO surface of its own — the plate does
1325 + * that, and the plate is a sibling that slides. All this rule owns is
1326 + * the label.
1327 + *
1328 + * No accent anywhere on it either. The shape already says "this one",
1329 + * and the accent is spent: one Pulse divider in the dock, one focus
1330 + * ring. A tab that is both a distinct surface AND coloured is saying
1331 + * the same thing twice.
1332 + */
1333 +.os-window__tab--active {
1334 + color: var( --os-tabs-active-color, #1d2327 );
707 1335 font-weight: 600;
1336 + /*
1337 + * Anything nested in an active tab (the external tab's detach and
1338 + * close chips) is now sitting on a light fill inside dark chrome,
1339 + * so the two tones it reads have to flip with the tab. Both are
1340 + * re-pointed through names the palette owns rather than hardcoded
1341 + * here, so a desktop theme retints the chips with the tab.
1342 + */
1343 + --os-ui-fg-muted: var( --os-tabs-active-color-muted, rgba(29, 35, 39, 0.6) );
1344 + --os-ui-hover: color-mix(
1345 + in srgb,
1346 + var( --os-tabs-active-color, #1d2327 ) 8%,
1347 + transparent
1348 + );
708 1349 }
709 1350
1351 +/* An active tab has the plate under it; a hover wash on top of that
1352 + * would double-paint the surface. */
1353 +.os-window__tab--active:hover {
1354 + background: transparent;
1355 + color: var( --os-tabs-active-color, #1d2327 );
1356 +}
1357 +
710 1358 /*
711 1359 * External sub-tab. Same shape as a submenu tab, plus two inline
712 1360 * chips: detach (↗) and close (×). Chips are painted as spans
713 1361 * (rather than nested buttons — nested interactive elements are a
@@ -713,9 +1361,9 @@
713 1361 * (rather than nested buttons — nested interactive elements are a
714 1362 * pain for a11y). The tab's click handler routes chip clicks via
715 1363 * the `data-tab-action` dataset attribute.
716 1364 */
717 -.desktop-mode-window__tab--external {
1365 +.os-window__tab--external {
718 1366 /* Slightly wider gap between the label and the chip cluster, and
719 1367 * more breathing room at the trailing edge so the chips aren't
720 1368 * flush against the tab boundary. */
721 1369 gap: 10px;
@@ -721,9 +1369,9 @@
721 1369 gap: 10px;
722 1370 padding-inline-end: 4px;
723 1371 }
724 1372
725 -.desktop-mode-window__tab-label {
1373 +.os-window__tab-label {
726 1374 max-width: 180px;
727 1375 overflow: hidden;
728 1376 text-overflow: ellipsis;
729 1377 white-space: nowrap;
@@ -728,17 +1376,17 @@
728 1376 text-overflow: ellipsis;
729 1377 white-space: nowrap;
730 1378 }
731 1379
732 -/* External-tab action chips moved to `<wpd-tab-chip>`. Spacing
1380 +/* External-tab action chips moved to `<os-tab-chip>`. Spacing
733 1381 * between chips handled by a single host-level rule since the
734 1382 * component doesn't know about its siblings. */
735 -wpd-tab-chip + wpd-tab-chip {
1383 +os-tab-chip + os-tab-chip {
736 1384 margin-inline-start: 4px;
737 1385 }
738 1386
739 1387 /* Window body: contains the iframe. */
740 -.desktop-mode-window__body {
1388 +.os-window__body {
741 1389 flex: 1;
742 1390 position: relative;
743 1391 overflow: hidden;
744 1392 /*
@@ -747,27 +1395,27 @@
747 1395 * an opaque wp-admin page inside an iframe paints over it, which
748 1396 * is why a theme wanting texture everywhere reaches for DESKTOP
749 1397 * and the chrome slots instead.
750 1398 */
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 );
1399 + background-image: var( --os-window-body-image, none );
1400 + background-repeat: var( --os-window-body-image-repeat, repeat );
1401 + background-size: var( --os-window-body-image-size, auto );
1402 + background-position: var( --os-window-body-image-position, center );
755 1403 /*
756 - * Body typeface. `--wpd-font` is the component kit's own token, so
757 - * one declaration here reaches every `<wpd-*>` element inside the
1404 + * Body typeface. `--os-ui-font` is the component kit's own token, so
1405 + * one declaration here reaches every `<os-*>` element inside the
758 1406 * window: shadow DOM inherits `font-family` through the boundary.
759 1407 */
760 - font-family: var( --wpd-font, inherit );
1408 + font-family: var( --os-ui-font, inherit );
761 1409 }
762 1410
763 1411 /* Iframe fills the window body. */
764 -.desktop-mode-window__iframe {
1412 +.os-window__iframe {
765 1413 width: 100%;
766 1414 height: 100%;
767 1415 border: none;
768 1416 display: block;
769 - background: var(--desktop-mode-window-bg);
1417 + background: var(--os-window-bg);
770 1418 opacity: 1;
771 1419 transition: opacity 0.25s ease;
772 1420 }
773 1421
@@ -777,9 +1425,9 @@
777 1425 * WordPress core's `wp-admin/css/common.css` styles headings with
778 1426 * BARE ELEMENT selectors — `h1 { color: #1d2327 }` and
779 1427 * `h2, h3 { color: #1d2327 }`. Native windows render in the parent
780 1428 * 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
1429 + * our light-DOM window content: an `<h3>` in a `<os-card>` came out
782 1430 * near-black on a dark theme while every sibling paragraph correctly
783 1431 * followed the palette.
784 1432 *
785 1433 * The specificity here is doing real work. It has to sit BETWEEN two
@@ -786,22 +1434,22 @@
786 1434 * other rules:
787 1435 *
788 1436 * core `h3` (0,0,1) must lose
789 1437 * THIS rule (0,0,1) wins on source order
790 - * `.desktop-mode-my-wordpress__user-section h3` (0,1,1) must still win
1438 + * `.os-my-wordpress__user-section h3` (0,1,1) must still win
791 1439 *
792 1440 * `:where()` contributes zero specificity, so the selector below
793 1441 * weighs the same as core's bare `h3` and beats it only because our
794 1442 * stylesheet prints later — while any of our own class-scoped heading
795 1443 * rules continue to override it. Raising this to a plain
796 - * `.desktop-mode-window__body h3` would tie with those and break them
1444 + * `.os-window__body h3` would tie with those and break them
797 1445 * depending on file order.
798 1446 *
799 1447 * The `#1d2327` fallback is core's own value, so with no theme active
800 1448 * the computed colour is unchanged.
801 1449 */
802 -:where( .desktop-mode-window__body ) :is( h1, h2, h3, h4, h5, h6 ) {
803 - color: var( --wpd-fg, #1d2327 );
1450 +:where( .os-window__body ) :is( h1, h2, h3, h4, h5, h6 ) {
1451 + color: var( --os-ui-fg, #1d2327 );
804 1452 }
805 1453
806 1454 /*
807 1455 * Same story, same fix, for the other bare-element rules core ships:
@@ -815,19 +1463,19 @@
815 1463 * default is unchanged.
816 1464 *
817 1465 * Form controls (`input`, `textarea`, `select`) are deliberately NOT
818 1466 * included: core styles those through attribute selectors that carry
819 - * real specificity, native windows overwhelmingly use the `<wpd-*>`
1467 + * real specificity, native windows overwhelmingly use the `<os-*>`
820 1468 * components instead, and a half-applied override there would look
821 1469 * worse than leaving them alone.
822 1470 */
823 -:where( .desktop-mode-window__body ) a {
824 - color: var( --wpd-accent, #2271b1 );
1471 +:where( .os-window__body ) a {
1472 + color: var( --os-ui-accent, #2271b1 );
825 1473 }
826 1474
827 -:where( .desktop-mode-window__body ) code {
828 - background: var( --wpd-surface-sunken, #f0f0f1 );
829 - color: var( --wpd-fg, inherit );
1475 +:where( .os-window__body ) code {
1476 + background: var( --os-ui-surface-sunken, #f0f0f1 );
1477 + color: var( --os-ui-fg, inherit );
830 1478 }
831 1479
832 1480 /*
833 1481 * Raw form controls in native window bodies.
@@ -836,18 +1484,18 @@
836 1484 * `input[type="search"], select, textarea { background-color: #fff;
837 1485 * color: #1e1e1e; border: 1px solid #949494 }` — which weighs
838 1486 * (0,1,1). That beats a plain class selector, so a control our own
839 1487 * CSS had already tokenized (My WordPress's search box reads
840 - * `background: var( --wpd-surface, #fff )` and has done all along)
1488 + * `background: var( --os-ui-surface, #fff )` and has done all along)
841 1489 * still came out white: core simply outranked it.
842 1490 *
843 - * Hence `.desktop-mode-window__body` + `:is( input[type], … )`, which
844 - * weighs (0,2,1) and wins. `<wpd-*>` form components are untouched —
1491 + * Hence `.os-window__body` + `:is( input[type], … )`, which
1492 + * weighs (0,2,1) and wins. `<os-*>` form components are untouched —
845 1493 * they live in shadow DOM, where none of this reaches.
846 1494 *
847 1495 * Fallbacks are core's own values, so an unthemed shell is unchanged.
848 1496 */
849 -.desktop-mode-window__body
1497 +.os-window__body
850 1498 :is(
851 1499 input[ type="text" ],
852 1500 input[ type="search" ],
853 1501 input[ type="email" ],
@@ -862,15 +1510,15 @@
862 1510 input[ type="week" ],
863 1511 select,
864 1512 textarea
865 1513 ) {
866 - background-color: var( --wpd-surface, #fff );
867 - color: var( --wpd-fg, #1e1e1e );
868 - border-color: var( --wpd-border-strong, #949494 );
1514 + background-color: var( --os-ui-surface, #fff );
1515 + color: var( --os-ui-fg, #1e1e1e );
1516 + border-color: var( --os-ui-border-strong, #949494 );
869 1517 }
870 1518
871 -.desktop-mode-window__body :is( input, textarea )::placeholder {
872 - color: var( --wpd-fg-muted, #646970 );
1519 +.os-window__body :is( input, textarea )::placeholder {
1520 + color: var( --os-ui-fg-muted, #646970 );
873 1521 }
874 1522
875 1523 /* Cross-window drag drop-overlay rules currently live in windows.css.
876 1524 * That was originally a cache workaround — this file was an `@import`
@@ -882,9 +1530,9 @@
882 1530
883 1531 /*
884 1532 * Loading-state overlay — painted by `src/window/dom.ts` into
885 1533 * every window's body and removed once the body's content reports
886 - * ready. The `<wpd-spinner>` inside is sized responsively against
1534 + * ready. The `<os-spinner>` inside is sized responsively against
887 1535 * the window's width via `clamp(96px, 14vw, 192px)` so a tiny
888 1536 * popover gets a small spinner and a maximized window gets a big
889 1537 * one. Placed above the body content (iframe / native render
890 1538 * output) and kept aria-hidden so the spinner's own SR-label is
@@ -889,16 +1537,16 @@
889 1537 * one. Placed above the body content (iframe / native render
890 1538 * output) and kept aria-hidden so the spinner's own SR-label is
891 1539 * the only loading announcement.
892 1540 *
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.
1541 + * The `--visible` modifier turns the overlay on. JS adds it ~120ms
1542 + * into the load (`LOADING_OVERLAY_SHOW_DELAY_MS`), so a fast render
1543 + * never paints a spinner. The delay cannot be a `transition-delay`
1544 + * here: the overlay is appended into a body that already carries
1545 + * `--loading`, so its first computed style is the visible one and the
1546 + * transition never runs.
899 1547 */
900 -.desktop-mode-window__loading {
1548 +.os-window__loading {
901 1549 position: absolute;
902 1550 inset: 0;
903 1551 display: flex;
904 1552 align-items: center;
@@ -903,9 +1551,9 @@
903 1551 display: flex;
904 1552 align-items: center;
905 1553 justify-content: center;
906 1554 pointer-events: none;
907 - background: var(--desktop-mode-window-bg);
1555 + background: var(--os-window-bg);
908 1556 opacity: 0;
909 1557 /* Default (loaded → loading-removed) — fade out immediately. */
910 1558 transition: opacity 0.25s ease;
911 1559 /*
@@ -920,30 +1568,51 @@
920 1568 * Content hidden while loading. The reveal surface is excluded
921 1569 * alongside the spinner overlay: it is chrome, not content, and fading
922 1570 * it to transparent would show the very content it exists to cover.
923 1571 */
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) {
1572 +.os-window__body--loading > .os-window__iframe,
1573 +.os-window__body--loading
1574 + > :not(.os-window__loading):not(.os-window__reveal) {
927 1575 opacity: 0;
928 1576 transition: opacity 0.25s ease;
929 1577 }
930 1578
931 -.desktop-mode-window__body--loading > .desktop-mode-window__loading {
1579 +.os-window__body--loading > .os-window__loading--visible {
932 1580 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 1581 }
939 1582
1583 +/*
1584 + * Content hand-off. `--loading` drops as soon as the content is ready,
1585 + * but the overlay above it still needs 250ms to fade out. Without this
1586 + * both layers are on screen at once, which reads as a flash.
1587 + *
1588 + * This modifier holds the content transparent for the length of that
1589 + * fade, then fades it in. The delay in the shorthand does the holding.
1590 + *
1591 + * Only added when the spinner actually painted. A load that finishes
1592 + * inside the show delay never reached `--visible`, so the shell drops
1593 + * the overlay in the same tick instead.
1594 + *
1595 + * `--revealing` declares the same selector at the same specificity and
1596 + * comes later in the file, so a window playing a reveal keeps its
1597 + * content opaque under the reveal surface.
1598 + */
1599 +.os-window__body--loading-out > .os-window__iframe,
1600 +.os-window__body--loading-out
1601 + > :not(.os-window__loading):not(.os-window__reveal) {
1602 + opacity: 1;
1603 + transition: opacity 0.25s ease 0.25s;
1604 +}
1605 +
940 1606 @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) {
1607 + .os-window__iframe,
1608 + .os-window__loading,
1609 + .os-window__body--loading > .os-window__iframe,
1610 + .os-window__body--loading
1611 + > :not(.os-window__loading):not(.os-window__reveal),
1612 + .os-window__body--loading-out > .os-window__iframe,
1613 + .os-window__body--loading-out
1614 + > :not(.os-window__loading):not(.os-window__reveal) {
946 1615 transition: none;
947 1616 }
948 1617 }
949 1618
@@ -959,9 +1628,9 @@
959 1628 * reveal cannot swallow a click or re-rasterize the page it is
960 1629 * uncovering. Native window content gets the identical treatment for
961 1630 * the same reason.
962 1631 *
963 - * `--desktop-mode-window-reveal-surface` is a theme token, white by
1632 + * `--os-window-reveal-surface` is a theme token, white by
964 1633 * default — the surface has to be opaque or there is nothing to reveal
965 1634 * from. The EDGE token is `transparent` by default instead, since an
966 1635 * edge is an accent rather than the effect itself. Either layer that
967 1636 * computes to no paint is skipped rather than animated. A def may
@@ -973,13 +1642,13 @@
973 1642 * the Web Animations API, which needs a matched `from` / `to` pair a
974 1643 * CSS transition cannot express. Reduced motion is handled JS-side by
975 1644 * removing the layers instead of animating them.
976 1645 */
977 -.desktop-mode-window__reveal {
1646 +.os-window__reveal {
978 1647 position: absolute;
979 1648 inset: 0;
980 1649 pointer-events: none;
981 - background: var( --desktop-mode-window-reveal-surface, #fff );
1650 + background: var( --os-window-reveal-surface, #fff );
982 1651 z-index: 3;
983 1652 }
984 1653
985 1654 /*
@@ -990,17 +1659,17 @@
990 1659 * or third-party) gets an edge that follows its geometry exactly.
991 1660 *
992 1661 * Behind the surface, still above the content.
993 1662 *
994 - * `--desktop-mode-window-reveal-edge` is `transparent` by default, and
1663 + * `--os-window-reveal-edge` is `transparent` by default, and
995 1664 * the shell drops the layer entirely while it computes that way — so
996 1665 * the default costs no element and no animation. Give the token a
997 1666 * colour (or a gradient) to turn the edge on across every reveal at
998 - * once. `--desktop-mode-window-reveal-edge-thickness` tunes how wide
1667 + * once. `--os-window-reveal-edge-thickness` tunes how wide
999 1668 * the band is; `edgeLag: 0` on a def opts a single reveal out.
1000 1669 */
1001 -.desktop-mode-window__reveal--edge {
1002 - background: var( --desktop-mode-window-reveal-edge, transparent );
1670 +.os-window__reveal--edge {
1671 + background: var( --os-window-reveal-edge, transparent );
1003 1672 z-index: 2;
1004 1673 }
1005 1674
1006 1675 /*
@@ -1010,9 +1679,9 @@
1010 1679 * renderer's output is what the effect would end up uncovering, so the
1011 1680 * animation would play against a flat colour and the real content
1012 1681 * would only appear when the layer is finally removed.
1013 1682 */
1014 -.desktop-mode-window__reveal--custom {
1683 +.os-window__reveal--custom {
1015 1684 background: none;
1016 1685 }
1017 1686
1018 1687 /*
@@ -1019,13 +1688,13 @@
1019 1688 * While a reveal plays, pin the content to full opacity with no
1020 1689 * transition. Dropping `--loading` normally starts a 250 ms content
1021 1690 * fade-in; a reveal running over that fade would show a
1022 1691 * half-transparent strip along its leading edge. Beats the base
1023 - * `.desktop-mode-window__iframe` rule on specificity, so no
1692 + * `.os-window__iframe` rule on specificity, so no
1024 1693 * `!important` is needed.
1025 1694 */
1026 -.desktop-mode-window__body--revealing
1027 - > :not(.desktop-mode-window__loading):not(.desktop-mode-window__reveal) {
1695 +.os-window__body--revealing
1696 + > :not(.os-window__loading):not(.os-window__reveal) {
1028 1697 opacity: 1;
1029 1698 transition: none;
1030 1699 }
1031 1700
@@ -1035,9 +1704,9 @@
1035 1704 * via `style.display`). Absolute positioning so they don't push the
1036 1705 * primary iframe around when added to the DOM; `display: none` on
1037 1706 * inactive iframes is set inline by the JS.
1038 1707 */
1039 -.desktop-mode-window__iframe--external {
1708 +.os-window__iframe--external {
1040 1709 position: absolute;
1041 1710 inset: 0;
1042 1711 }
1043 1712
@@ -1058,15 +1727,15 @@
1058 1727 * animation-free: removing the old frame exposes the ready-painted
1059 1728 * twin in the same compositor frame. At no point is unpainted
1060 1729 * content the only thing on screen.
1061 1730 */
1062 -.desktop-mode-window__iframe--buffer {
1731 +.os-window__iframe--buffer {
1063 1732 position: absolute;
1064 1733 inset: 0;
1065 1734 pointer-events: none;
1066 1735 }
1067 1736
1068 -.desktop-mode-window__iframe--swap-front {
1737 +.os-window__iframe--swap-front {
1069 1738 position: relative;
1070 1739 z-index: 1;
1071 1740 }
1072 1741
@@ -1075,17 +1744,17 @@
1075 1744 * independently. Iframe bodies use `overflow: hidden` so the iframe
1076 1745 * controls its own scroll; native bodies render real document content
1077 1746 * that needs the parent to manage overflow.
1078 1747 */
1079 -.desktop-mode-window__body--native {
1748 +.os-window__body--native {
1080 1749 overflow: auto;
1081 - color: var( --wpd-fg, #1d2327 );
1750 + color: var( --os-ui-fg, #1d2327 );
1082 1751 /* background-COLOR, not the shorthand. This is a modifier on the
1083 - * SAME element as `.desktop-mode-window__body`, at the same
1752 + * SAME element as `.os-window__body`, at the same
1084 1753 * specificity and later in source order — the shorthand would
1085 1754 * reset the WINDOW_BODY texture declared there, which is exactly
1086 1755 * the surface native windows exist to show. */
1087 - background-color: var(--desktop-mode-window-bg);
1756 + background-color: var(--os-window-bg);
1088 1757 }
1089 1758
1090 1759 /*
1091 1760 * Highlight rings. Toggled from JS by `Window.setHighlight()` —
@@ -1117,12 +1786,12 @@
1117 1786 * plugin has registered a button for the window. When buttons ARE
1118 1787 * present, they sit inline next to the title (left slot) or just
1119 1788 * before the window controls (right slot).
1120 1789 */
1121 -.desktop-mode-window__custom-buttons {
1790 +.os-window__custom-buttons {
1122 1791 display: contents;
1123 1792 }
1124 -.desktop-mode-window__btn--custom {
1793 +.os-window__btn--custom {
1125 1794 margin: 0 2px;
1126 1795 }
1127 1796 /*
1128 1797 * Plugin-supplied icons render in the host's light DOM so the
@@ -1144,9 +1813,9 @@
1144 1813 * width/height, we DO clamp to 18×18 — those are the cases where
1145 1814 * a bare `<svg>` would otherwise size to its viewBox or default
1146 1815 * to 300×150 (the spec default) and overflow the button.
1147 1816 */
1148 -.desktop-mode-window__btn--custom svg:not( [ width ] ):not( [ height ] ) {
1817 +.os-window__btn--custom svg:not( [ width ] ):not( [ height ] ) {
1149 1818 width: 18px;
1150 1819 height: 18px;
1151 1820 display: block;
1152 1821 }
@@ -1157,14 +1826,14 @@
1157 1826 * waiting on the editor's autosave before opening the preview).
1158 1827 * Driven by `aria-busy="true"` + this class, both set by the button's
1159 1828 * render callback.
1160 1829 */
1161 -.desktop-mode-window__btn--busy {
1162 - animation: desktop-mode-btn-busy-pulse 1s ease-in-out infinite;
1830 +.os-window__btn--busy {
1831 + animation: os-btn-busy-pulse 1s ease-in-out infinite;
1163 1832 pointer-events: none;
1164 1833 }
1165 1834
1166 -@keyframes desktop-mode-btn-busy-pulse {
1835 +@keyframes os-btn-busy-pulse {
1167 1836 0%,
1168 1837 100% {
1169 1838 opacity: 1;
1170 1839 }
@@ -1174,9 +1843,9 @@
1174 1843 }
1175 1844 }
1176 1845
1177 1846 @media (prefers-reduced-motion: reduce) {
1178 - .desktop-mode-window__btn--busy {
1847 + .os-window__btn--busy {
1179 1848 animation: none;
1180 1849 opacity: 0.6;
1181 1850 }
1182 1851 }
@@ -1187,9 +1856,9 @@
1187 1856 * there is nothing to preview until the first save). The button keeps
1188 1857 * pointer events so its explanatory tooltip/toast still works;
1189 1858 * `aria-disabled` carries the semantics.
1190 1859 */
1191 -.desktop-mode-window__btn--disabled {
1860 +.os-window__btn--disabled {
1192 1861 opacity: 0.4;
1193 1862 cursor: default;
1194 1863 }
1195 1864
@@ -1196,12 +1865,12 @@
1196 1865 /* ----------------------------------------------------------------
1197 1866 * Custom-chrome marker — hides every framework-shipped titlebar
1198 1867 * child while a plugin's chrome is mounted.
1199 1868 *
1200 - * The window gets the `desktop-mode-window--custom-chrome` class the
1869 + * The window gets the `os-window--custom-chrome` class the
1201 1870 * moment `mountWindowChrome` succeeds (BEFORE the plugin's
1202 1871 * `render()` runs). Default children carry the
1203 - * `data-desktop-mode-default-chrome` attribute stamped at element-
1872 + * `data-os-default-chrome` attribute stamped at element-
1204 1873 * creation time. The combination is a load-bearing guarantee:
1205 1874 * even if the plugin's render() doesn't clear `titlebar.innerHTML`,
1206 1875 * even if a plugin's destroy() leaks the standard chrome back into
1207 1876 * place, even mid-fade during a window close — the default chrome
@@ -1215,11 +1884,11 @@
1215 1884 *
1216 1885 * @since 0.18.0
1217 1886 * ---------------------------------------------------------------- */
1218 1887
1219 -.desktop-mode-window--custom-chrome
1220 - > .desktop-mode-window__titlebar
1221 - > [ data-desktop-mode-default-chrome ] {
1888 +.os-window--custom-chrome
1889 + > .os-window__titlebar
1890 + > [ data-os-default-chrome ] {
1222 1891 display: none !important;
1223 1892 }
1224 1893
1225 1894 /* ---------------------------------------------------------------
@@ -1235,9 +1904,9 @@
1235 1904 * the animation cleanly.
1236 1905 *
1237 1906 * 2. While the window body is in the `--loading` state (set by
1238 1907 * `markContentLoading()` and cleared on the iframe's
1239 - * `desktop-mode-ready` postMessage), the button is dimmed and
1908 + * `os-ready` postMessage), the button is dimmed and
1240 1909 * non-interactive so a second click can't desync the
1241 1910 * chromeless bridge's loading handshake.
1242 1911 *
1243 1912 * `:has()` is used instead of mirroring the `--loading` modifier
@@ -1245,10 +1914,10 @@
1245 1914 * decorates the body element. Browsers without :has() (very old)
1246 1915 * fall back to a static dimmed button — feature, not bug.
1247 1916 * --------------------------------------------------------------- */
1248 1917
1249 -.desktop-mode-window:has( .desktop-mode-window__body--loading )
1250 - .desktop-mode-window__btn--reload {
1918 +.os-window:has( .os-window__body--loading )
1919 + .os-window__btn--reload {
1251 1920 pointer-events: none;
1252 1921 opacity: 0.55;
1253 1922 }
1254 1923
@@ -1261,13 +1930,13 @@
1261 1930 * of the gesture rather than progress through a long task. The `0.6s`
1262 1931 * duration is short enough to never block the user but long enough
1263 1932 * that the deceleration is legible.
1264 1933 */
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 );
1934 +.os-window__btn--reload.os-window__btn--spinning {
1935 + animation: os-reload-spin-once 0.6s cubic-bezier( 0.05, 0.7, 0.1, 1 );
1267 1936 }
1268 1937
1269 -@keyframes wpd-reload-spin-once {
1938 +@keyframes os-reload-spin-once {
1270 1939 from {
1271 1940 transform: rotate( 0deg );
1272 1941 }
1273 1942 to {
@@ -1275,8 +1944,8 @@
1275 1944 }
1276 1945 }
1277 1946
1278 1947 @media ( prefers-reduced-motion: reduce ) {
1279 - .desktop-mode-window__btn--reload.desktop-mode-window__btn--spinning {
1948 + .os-window__btn--reload.os-window__btn--spinning {
1280 1949 animation: none;
1281 1950 }
1282 1951 }