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

os-settings.css in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.0, at assets/css/os-settings.css

2,182 lines 61.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OpenStation — OS Settings native panel.
3 *
4 * The preferences panel rendered inside the OS Settings native
5 * window: wallpaper swatches + editor slot, accent colors, dock size,
6 * and the "Or use your own image" uploader + Media Library grid.
7 * Currently shipped alongside the window-chrome stylesheet because
8 * historically the rules lived there; marked for a future move to its
9 * own enqueue handle once the coupling to the shell CSS is removed.
10 *
11 * @since 0.5.0
12 */
13 /*
14 * Body Small, straight off the brand guide: Geist 14 / Regular / 1.5.
15 * The panel used to sit at 13, which is the guide's Caption tier and
16 * therefore the size everything SECONDARY should be, not the size the
17 * body is. Every rule below that names a smaller size was lifted one
18 * step with it, so the panel now bottoms out at Caption instead of
19 * running two tiers under the smallest size the guide defines.
20 */
21 .os-settings {
22 font-size: 14px;
23 line-height: 1.5;
24 }
25
26 .os-settings__intro {
27 margin: 0 0 16px;
28 color: var( --os-ui-fg-muted, #50575e );
29 }
30
31 /*
32 * The page header: the title again, at the size of a title, plus the
33 * one line the sidebar has no room for.
34 *
35 * Sits outside the section cards rather than inside the first one, so
36 * a section stays reusable on any page and has no opinion about being
37 * first on one. Heading 24 / Medium 500 with the guide's optical
38 * tightening; the sentence under it is Body Small on Ash, the same as
39 * every other piece of secondary text in the panel.
40 */
41 .os-settings__page-header {
42 margin: 0 0 34px;
43 }
44
45 .os-settings__page-title {
46 margin: 0 0 6px;
47 font-size: 24px;
48 font-weight: 500;
49 line-height: 1.25;
50 letter-spacing: -0.012em;
51 color: var( --os-ui-fg, #1d2327 );
52 }
53
54 .os-settings__page-description {
55 margin: 0;
56 max-width: 74ch;
57 line-height: 1.55;
58 color: var( --os-ui-fg-muted, #50575e );
59 }
60
61 /*
62 * Section chrome. The heading and its sentence sit on the page, and
63 * the BOX is the section's body alone: a bounded surface under a
64 * heading, not a card with a title locked inside it. That is the
65 * separator system of the whole panel: a box is the only divider,
66 * so there is no horizontal rule anywhere.
67 *
68 * `<os-section>`'s shadow DOM handles the heading + description and
69 * exposes the body as `part="body"`; the box (background, border,
70 * radius, padding) lives here because it's a per-surface visual
71 * choice, not a property of the component itself.
72 *
73 * The box is the SUBTLE wash (3%) with the plain border on top of it,
74 * at the card radius. Its default padding is 18px, the mockup's
75 * inset for grids, which is what most section bodies hold.
76 */
77 .os-settings os-section {
78 display: block;
79 margin: 0 0 36px;
80 }
81
82 .os-settings os-section::part( body ) {
83 padding: 18px;
84 background: var( --os-ui-surface-subtle, rgba( 0, 0, 0, 0.03 ) );
85 border: 1px solid var( --os-ui-border, #dcdcde );
86 border-radius: 10px;
87 overflow: hidden;
88 }
89
90 /*
91 * Rhythm between stacked pickers inside one section box. The
92 * components carry no outer margin of their own (a component cannot
93 * know its context), so the surface says how far apart they sit:
94 * 16px, two steps on the space scale, enough that a control's label
95 * reads as a label and not as a caption of the control above it.
96 */
97 .os-settings os-section > os-select + os-select,
98 .os-settings os-section > os-select + os-segmented,
99 .os-settings os-section > os-segmented + os-select,
100 .os-settings os-section > os-segmented + os-segmented {
101 margin-block-start: 16px;
102 }
103
104 /*
105 * Generic grid. Auto-fill + minmax gives us a truly responsive
106 * layout — the panel reflows columns as OS Settings is resized,
107 * rather than locking to a fixed 3-across.
108 */
109 .os-settings__grid {
110 display: grid;
111 grid-template-columns: repeat( auto-fill, minmax( 140px, 1fr ) );
112 gap: 12px;
113 }
114
115 /*
116 * Wallpaper swatches — each is a `<os-swatch variant="wallpaper">`
117 * with its own shadow-DOM button + aspect ratio + selected state.
118 * The only outer knobs we need are: wider minmax for wallpapers
119 * (so each tile shows meaningful preview area), and the 16:9
120 * aspect ratio override. Everything else lives in the component.
121 */
122 .os-settings__grid--wallpapers {
123 grid-template-columns: repeat( auto-fill, minmax( 150px, 1fr ) );
124 }
125
126 /*
127 * Wallpaper preset label — sits above the gradient/image preview.
128 * Presets span the full tonal range (graphite dark, mono near-
129 * black, aurora blue, sunset hot pink) so neither pure white nor
130 * pure black text is universally readable. A Void chip at 75% behind
131 * the text gives the label its own local contrast on every preset.
132 * Flat rather than frosted, and at the small radius: a caption on
133 * the artwork, not a pill floating over it.
134 */
135 .os-settings__swatch-label {
136 display: inline-block;
137 /* Sit above the live-preview overlay (z-index 0) — the caption
138 * must stay readable over a moving canvas. */
139 position: relative;
140 z-index: 1;
141 padding: 3px 8px;
142 font-size: 13px;
143 font-weight: 500;
144 color: var( --os-ui-fg-on-accent, #fff );
145 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.45 ) );
146 border-radius: 6px;
147 pointer-events: none;
148 }
149
150 /*
151 * "Use your own image", the last tile in the wallpaper grid.
152 *
153 * Dashed rather than solid because it is the only tile in the row that
154 * cannot show you what you are choosing: every other one paints its
155 * own preview, and this one is a question. The same dashed border is
156 * what the upload dropzone below it wears, so the tile and the drawer
157 * it opens read as one control.
158 *
159 * Sized off the same 16:9 the wallpaper swatches use so it sits in the
160 * grid rather than beside it.
161 */
162 .os-settings__wallpaper-add {
163 display: flex;
164 flex-direction: column;
165 align-items: center;
166 justify-content: center;
167 gap: 6px;
168 aspect-ratio: 16 / 10;
169 padding: 8px;
170 background: transparent;
171 /*
172 * `-strong`, not `--os-ui-border`. The plain border token is
173 * #33303a, the same Astro the section box draws its own border in:
174 * a dashed edge in it against that frame reads as part of the box
175 * rather than as a tile. Every dashed dropzone in this file has
176 * the same problem and the same fix. One pixel like every other
177 * edge in the panel; dashes get their emphasis from the gaps.
178 */
179 border: 1px dashed var( --os-ui-border-strong, #c3c4c7 );
180 border-radius: 10px;
181 color: var( --os-ui-fg-muted, #50575e );
182 font: inherit;
183 font-size: 13px;
184 text-align: center;
185 cursor: pointer;
186 transition: border-color 0.15s ease, color 0.15s ease;
187 }
188
189 .os-settings__wallpaper-add:hover {
190 border-color: var( --os-ui-accent, #2271b1 );
191 color: var( --os-ui-fg, #1d2327 );
192 }
193
194 .os-settings__wallpaper-add:focus-visible {
195 outline: none;
196 box-shadow: var( --os-ui-focus-ring, 0 0 0 2px #2271b1 );
197 }
198
199 /* A bare plus, not a button-in-a-tile: the dashed edge already says
200 "action", and a filled disc inside it is a control inside a
201 control. */
202 .os-settings__wallpaper-add-plus {
203 font-size: 22px;
204 line-height: 1;
205 }
206
207 /*
208 * The drawer the tile opens. Same 0fr to 1fr collapse as the editor
209 * and description slots above it.
210 */
211 .os-settings__image-picker-slot {
212 display: grid;
213 grid-template-rows: 0fr;
214 margin-top: 0;
215 opacity: 0;
216 transition:
217 grid-template-rows 0.25s cubic-bezier(0.2, 0, 0.2, 1),
218 margin-top 0.25s cubic-bezier(0.2, 0, 0.2, 1),
219 opacity 0.2s ease;
220 }
221
222 .os-settings__image-picker-slot[data-expanded="true"] {
223 grid-template-rows: 1fr;
224 margin-top: 12px;
225 opacity: 1;
226 }
227
228 .os-settings__image-picker-slot-inner {
229 overflow: hidden;
230 min-height: 0;
231 }
232
233 /*
234 * Live wallpaper preview — an overlay div slotted into a wallpaper
235 * `<os-swatch>` by the preview manager (see
236 * settings/sections/wallpaper-previews.ts). Fills the tile button
237 * (which is position: relative in the component's shadow styles) and
238 * paints between the CSS `preview` background and the label chip.
239 * pointer-events: none so the tile button keeps receiving the click.
240 */
241 .os-settings__wallpaper-live-preview {
242 position: absolute;
243 inset: 0;
244 z-index: 0;
245 overflow: hidden;
246 pointer-events: none;
247 /* Match the swatch button's rounding so canvas corners don't
248 * poke out of the tile. */
249 border-radius: 10px;
250 }
251
252 .os-settings__wallpaper-live-preview canvas {
253 display: block;
254 width: 100%;
255 height: 100%;
256 }
257
258 /* Accent swatches now render as `<os-swatch>` via `<os-swatch-grid>`.
259 * The old `__swatch--accent` / `__swatch-label` / `__segmented` /
260 * `__segment` rules moved into those component shadow-DOM
261 * stylesheets; nothing at host level needs to override them. */
262
263 .os-settings__footer {
264 display: flex;
265 justify-content: flex-end;
266 margin-top: 12px;
267 }
268
269 /* Don't add a `display` rule for `os-tabpanel` here — the component's
270 * shadow-DOM `:host([hidden]) { display: none }` has lower specificity
271 * than any outer descendant selector, and overriding it from outside
272 * breaks the auto-swap (hidden panels stay visible). The component's
273 * own `:host { display: block }` already handles the shown state. */
274
275 /* `__reset` now renders as `<os-button variant="ghost">`. */
276
277 /* ---------------------------------------------------------------
278 * Custom gradient editor — color inputs + angle slider.
279 *
280 * Toggled by `data-expanded` on the outer wrapper. Uses the modern
281 * grid-template-rows 0fr ↔ 1fr trick so we can animate to natural
282 * content height without hard-coding a max-height. The inner wrapper
283 * owns the visible chrome (padding, border, background) and is
284 * clipped during the transition by overflow: hidden; the outer
285 * wrapper animates the row track + spacing + opacity.
286 *
287 * Supported in all evergreen browsers (Chrome 117+, Safari 17.2+,
288 * Firefox 128+). On older engines the transition is simply skipped —
289 * the editor still appears, just without the glide.
290 * --------------------------------------------------------------- */
291 /*
292 * Editor slot — the per-wallpaper panel that slides in below the
293 * swatch grid when a wallpaper with `renderEditor` is selected.
294 * Collapses via the grid-template-rows 0fr → 1fr trick; TS drives
295 * the `[data-expanded]` attribute on the slot wrapper.
296 */
297 .os-settings__editor-slot {
298 display: grid;
299 grid-template-rows: 0fr;
300 margin-top: 0;
301 opacity: 0;
302 transition:
303 grid-template-rows 0.25s cubic-bezier(0.2, 0, 0.2, 1),
304 margin-top 0.25s cubic-bezier(0.2, 0, 0.2, 1),
305 opacity 0.2s ease;
306 }
307
308 .os-settings__editor-slot[data-expanded="true"] {
309 grid-template-rows: 1fr;
310 margin-top: 12px;
311 opacity: 1;
312 }
313
314 .os-settings__editor-slot-inner {
315 overflow: hidden;
316 min-height: 0;
317 padding: 12px;
318 background: var( --os-ui-surface, #fff );
319 border: 1px solid var( --os-ui-border, #dcdcde );
320 border-radius: 6px;
321 }
322
323 /*
324 * Wallpaper config slot — hosts the "Wallpaper settings" button when
325 * the ACTIVE wallpaper ships a `renderConfig` dialog. Same 0fr → 1fr
326 * collapse as the slots above; TS drives `[data-expanded]` from
327 * `syncWallpaperConfigButton()`.
328 */
329 .os-settings__wallpaper-config-slot {
330 display: grid;
331 grid-template-rows: 0fr;
332 margin-top: 0;
333 opacity: 0;
334 transition:
335 grid-template-rows 0.25s cubic-bezier(0.2, 0, 0.2, 1),
336 margin-top 0.25s cubic-bezier(0.2, 0, 0.2, 1),
337 opacity 0.2s ease;
338 }
339
340 .os-settings__wallpaper-config-slot[data-expanded="true"] {
341 grid-template-rows: 1fr;
342 margin-top: 12px;
343 opacity: 1;
344 }
345
346 .os-settings__wallpaper-config-slot-inner {
347 overflow: hidden;
348 min-height: 0;
349 }
350
351 .os-settings__wallpaper-config os-icon {
352 margin-inline-end: 4px;
353 }
354
355 /*
356 * Wallpaper config dialog body — the container `renderConfig` renders
357 * into inside the `<os-modal>`. A simple vertical rhythm; each
358 * `<os-*>` field owns its own label / control styling. The critical
359 * column rules are ALSO inlined by the dialog opener (long-lived
360 * sessions can lazy-load the dialog with a stylesheet that predates
361 * this block); keep the two in sync.
362 */
363 .os-settings__wallpaper-config-form {
364 display: flex;
365 flex-direction: column;
366 gap: 14px;
367 min-width: 0;
368 }
369
370 /* Bare buttons in a config form (Reset to defaults and friends) are
371 * actions, not fields — don't stretch them to the column width. */
372 .os-settings__wallpaper-config-form > os-button {
373 align-self: flex-start;
374 }
375
376 /*
377 * Custom-gradient editor fills the inner slot. Kept as a separate
378 * class (vs. scoping to __editor-slot-inner) so a future plugin
379 * renderEditor can opt out of these per-control rules.
380 */
381 .os-settings__gradient-editor-inner {
382 display: flex;
383 flex-direction: column;
384 gap: 12px;
385 }
386
387 /*
388 * Gradient editor row — laid out as a flex of `<os-color-field>` +
389 * `<os-range-field>`. Each field owns its own label / control /
390 * readout styling internally; only spacing between fields lives here.
391 */
392 .os-settings__gradient-row {
393 display: flex;
394 gap: 16px;
395 }
396
397 .os-settings__gradient-row os-color-field {
398 flex: 1;
399 }
400
401 .os-settings__gradient-editor-inner os-range-field {
402 margin-top: 12px;
403 }
404
405 /* ---------------------------------------------------------------
406 * Image uploader tile.
407 *
408 * Empty state: dashed border, "+" glyph, drop-and-click zone.
409 * Filled state: thumbnail preview that doubles as a wallpaper swatch.
410 * --------------------------------------------------------------- */
411 .os-settings__uploader {
412 margin-top: 16px;
413 }
414
415 /* Sentence case, like every other label in the panel. The brand's
416 type scale has no uppercase tier, and a shouted 12px heading over a
417 single upload tile was the loudest thing on the page. */
418 .os-settings__uploader-heading {
419 margin: 0 0 6px;
420 font-size: 13px;
421 font-weight: 600;
422 color: var( --os-ui-fg-muted, #50575e );
423 }
424
425 .os-settings__file-input {
426 display: none;
427 }
428
429 .os-settings__upload-tile {
430 position: relative;
431 display: flex;
432 align-items: center;
433 justify-content: center;
434 min-height: 120px;
435 padding: 16px;
436 background: var( --os-ui-surface, #fff );
437 background-size: cover;
438 background-position: center;
439 /* See the note on `__wallpaper-add`: the plain border token is the
440 card's own colour and vanishes on it. */
441 border: 2px dashed var( --os-ui-border-strong, #c3c4c7 );
442 border-radius: 8px;
443 color: var( --os-ui-fg-muted, #50575e );
444 cursor: pointer;
445 text-align: center;
446 transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
447 }
448
449 .os-settings__upload-tile:hover {
450 border-color: var(--wp-admin-theme-color, #2271b1);
451 color: var(--wp-admin-theme-color, #2271b1);
452 }
453
454 .os-settings__upload-tile:focus-within,
455 .os-settings__upload-tile:focus-visible {
456 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
457 outline-offset: 2px;
458 }
459
460 .os-settings__upload-tile--dragover {
461 border-style: solid;
462 border-color: var(--wp-admin-theme-color, #2271b1);
463 background-color: rgba(34, 113, 177, 0.06);
464 transform: scale(1.01);
465 }
466
467 .os-settings__upload-tile--busy {
468 cursor: progress;
469 opacity: 0.8;
470 }
471
472 /* Filled state — thumbnail is the whole tile; selection uses the same
473 * aria-pressed pattern as the other swatches. */
474 .os-settings__upload-tile--filled {
475 border-style: solid;
476 border-color: transparent;
477 padding: 0;
478 min-height: 140px;
479 color: transparent;
480 }
481
482 .os-settings__upload-tile--filled:hover {
483 color: transparent;
484 }
485
486 .os-settings__upload-tile[aria-pressed="true"] {
487 border-color: var(--wp-admin-theme-color, #2271b1);
488 box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9) inset;
489 }
490
491 .os-settings__upload-inner {
492 display: flex;
493 flex-direction: column;
494 align-items: center;
495 gap: 6px;
496 pointer-events: none;
497 }
498
499 .os-settings__upload-plus {
500 display: inline-flex;
501 align-items: center;
502 justify-content: center;
503 width: 32px;
504 height: 32px;
505 border-radius: 50%;
506 background: var( --os-ui-hover, rgba(0, 0, 0, 0.05) );
507 font-size: 22px;
508 font-weight: 300;
509 line-height: 1;
510 color: inherit;
511 }
512
513 .os-settings__upload-tile:hover .os-settings__upload-plus {
514 background: rgba(34, 113, 177, 0.12);
515 }
516
517 .os-settings__upload-prompt {
518 font-size: 13px;
519 font-weight: 500;
520 }
521
522 .os-settings__upload-hint {
523 font-size: 12px;
524 color: var( --os-ui-fg-muted, #8c8f94 );
525 }
526
527 .os-settings__upload-status {
528 font-size: 13px;
529 font-weight: 500;
530 color: var( --os-ui-fg-muted, #50575e );
531 }
532
533 .os-settings__upload-error {
534 position: absolute;
535 inset-inline: 10px;
536 bottom: 10px;
537 padding: 6px 10px;
538 background: var( --os-ui-danger, #d63638 );
539 color: var( --os-ui-fg-on-accent, #fff );
540 border-radius: 4px;
541 font-size: 12px;
542 line-height: 1.3;
543 text-align: start;
544 box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
545 }
546
547 /*
548 * `__upload-remove` renders as `<os-button variant="danger">`
549 * with component-owned styling. Only the positioning on the
550 * filled tile is caller-specific — stays here.
551 */
552 .os-settings__upload-remove {
553 position: absolute;
554 top: 8px;
555 inset-inline-end: 8px;
556 }
557
558 /* Tab strip moved into `<os-tabs>` + `<os-tab>`
559 * (src/ui/components/os-tabs/). Only the surrounding pane keeps
560 * a host-level rule — it's an OS-Settings layout choice, not
561 * component-intrinsic. */
562 .os-settings__tab-pane {
563 min-height: 140px;
564 }
565
566 /*
567 * The invisible anchor for the Custom accent's colour wheel.
568 *
569 * There is no visible colour field: picking the Custom swatch opens
570 * the native picker directly, and the picker anchors to THIS input,
571 * which `accent.ts` parks just under the swatch (absolute against
572 * the section wrapper, measured at click time) before calling
573 * showPicker(). Absolute rather than fixed because a dragged window
574 * carries a transform, and a transform turns fixed positioning into
575 * a lie while getBoundingClientRect keeps telling viewport truth.
576 *
577 * It stays out of the tab order and off the accessibility tree: the
578 * Custom swatch is the control, and activating it (pointer or
579 * keyboard) is what opens the wheel.
580 */
581 .os-settings__accent-picker {
582 position: absolute;
583 width: 28px;
584 height: 1px;
585 margin: 0;
586 padding: 0;
587 border: 0;
588 opacity: 0;
589 pointer-events: none;
590 }
591
592 /* ---------------------------------------------------------------
593 * Desktop layout cards.
594 *
595 * A layout is a spatial choice, so each option draws the arrangement
596 * it is offering. The four names on their own ("One dock",
597 * "OpenStation", "Side bar", "Spatial") asked the user to guess what
598 * each would do to their screen and then find out by trying it.
599 *
600 * The previews are schematic: two or three rectangles and a rail, on
601 * Void, with the dock (or the current Space) in the accent so the eye
602 * lands on the thing that differs between the four. Geometry comes
603 * from `desktop-layout.ts` as percentages, so the cards stay legible
604 * as the grid reflows.
605 * --------------------------------------------------------------- */
606 /*
607 * Two layouts, half the pane each. Fixed tracks rather than
608 * auto-fill: there are exactly two of them and they are a pair to be
609 * compared, so a wide pane that fitted a third column would only
610 * shrink both cards away from each other for no reason.
611 *
612 * The cards stretch to the taller of the two (the grid's default
613 * `align-items`, left unset). They are a matched pair presented side
614 * by side, and two boxes of different heights read as two different
615 * KINDS of thing rather than as two answers to one question — which
616 * is the comparison the whole picker is for. What varies is the
617 * description: one line for Unified, two for Split, and neither is
618 * worth a ragged bottom edge.
619 *
620 * This used to be `align-items: start`, back when the Unified card
621 * also held Placement and Dock size and stretching Split to match
622 * left it mostly empty. Dock size has since moved out below both
623 * cards, so the gap it was guarding against is now one control deep.
624 */
625 .os-settings__layout-grid {
626 display: grid;
627 grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
628 gap: 12px;
629 }
630
631 /*
632 * The dock options, inside the One dock card. Stacked, because half
633 * a pane is not enough for two segmented controls side by side, and
634 * because reading them down the card follows the order the decision
635 * is made in: this layout, then where its dock sits, then how big.
636 */
637 .os-settings__dock-options {
638 display: flex;
639 flex-direction: column;
640 gap: 14px;
641 margin-top: 14px;
642 }
643
644 /*
645 * Dock size, under both cards rather than inside one: both layouts
646 * have a dock. Off the grid entirely, so it clears the taller card
647 * rather than sitting beside it, and on the section's own rhythm
648 * rather than the card's tighter one.
649 */
650 .os-settings__dock-options--page {
651 margin-top: 20px;
652 }
653
654 .os-settings__dock-option {
655 display: flex;
656 flex-direction: column;
657 align-items: flex-start;
658 gap: 8px;
659 }
660
661 .os-settings__dock-option-label {
662 font-size: 14px;
663 font-weight: 500;
664 color: var( --os-ui-fg, #1d2327 );
665 }
666
667 /*
668 * The card is the BOX, not the control. It holds the radio that
669 * picks the layout and, for One dock, that layout's own options —
670 * and a radio cannot contain a segmented control, so the two are
671 * siblings inside it. See the docblock in
672 * `src/settings/sections/desktop-layout.ts`.
673 *
674 * Which means the box wears the state of a child it does not own.
675 * Both `:has()` rules name the CHOICE rather than the card, so a
676 * pointer resting on a segmented control does not lift the card, and
677 * a segment taking focus does not light the selection ring.
678 */
679 .os-settings__layout-card {
680 display: flex;
681 flex-direction: column;
682 padding: 10px;
683 background: var( --os-ui-surface-raised, rgba( 0, 0, 0, 0.04 ) );
684 border-radius: 10px;
685 transition: transform 0.15s ease, box-shadow 0.15s ease;
686 }
687
688 .os-settings__layout-card:has( .os-settings__layout-choice:hover ) {
689 transform: translateY( -2px );
690 }
691
692 .os-settings__layout-card:has( [ aria-checked='true' ] ) {
693 box-shadow: 0 0 0 2px var( --os-ui-accent, #2271b1 );
694 }
695
696 /* The radio: everything in the card that means "this layout". */
697 .os-settings__layout-choice {
698 display: flex;
699 flex-direction: column;
700 width: 100%;
701 padding: 0;
702 background: none;
703 border: 0;
704 border-radius: 8px;
705 font: inherit;
706 color: inherit;
707 text-align: start;
708 cursor: pointer;
709 }
710
711 .os-settings__layout-choice:focus-visible {
712 outline: none;
713 box-shadow: var( --os-ui-focus-ring, 0 0 0 2px #2271b1 );
714 }
715
716 /*
717 * A screen, so it is shaped like one: 16/9, not whatever shape is left
718 * over once the card has been sized. A full-width box at a fixed 78px
719 * ran about 6.7:1, and every shape inside is a percentage of it — so
720 * the two windows came out five times wider than they were tall and
721 * read as stacked bars rather than as windows, which is the one thing
722 * these previews exist to show.
723 *
724 * Capped in width rather than stretched, because 16/9 across a whole
725 * half-pane card is ~290px of preview for a schematic with four
726 * rectangles in it. The cap sets the height (288 → 162px); the card
727 * keeps its own width and the preview sits centred in it, which is why
728 * `inline-size` is a percentage with a ceiling instead of a fixed
729 * width: below the cap — the stacked single-column pane — it goes back
730 * to filling the card, still at 16/9.
731 */
732 .os-settings__layout-preview {
733 position: relative;
734 display: block;
735 inline-size: 100%;
736 max-inline-size: 288px;
737 aspect-ratio: 16 / 9;
738 margin-block-end: 9px;
739 margin-inline: auto;
740 overflow: hidden;
741 background: var( --os-ui-surface-sunken, #f0f0f1 );
742 border-radius: 8px;
743 }
744
745 /* A window: a filled rectangle with a boundary, because at this size
746 a borderless one dissolves into the Void behind it. */
747 .os-settings__layout-win {
748 position: absolute;
749 background: var( --os-ui-surface-elevated, #f6f7f7 );
750 border: 1px solid var( --os-ui-border-strong, #c3c4c7 );
751 border-radius: 4px;
752 }
753
754 /* A rail: the wp-admin menu, or the side rail. No border, because a
755 rail reads as a solid mass and a window does not. */
756 .os-settings__layout-bar {
757 position: absolute;
758 background: var( --os-ui-border-strong, #c3c4c7 );
759 border-radius: 3px;
760 }
761
762 /* A rail the user gets to place: the dock, and in Side bar the admin
763 menu beside it. The accent is what separates the rails from the
764 windows they sit next to, so a card can carry more than one. */
765 .os-settings__layout-win.is-accent,
766 .os-settings__layout-bar.is-accent {
767 background: var( --os-ui-accent, #2271b1 );
768 border-color: transparent;
769 }
770
771 .os-settings__layout-name {
772 font-size: 14px;
773 font-weight: 500;
774 color: var( --os-ui-fg, #1d2327 );
775 }
776
777 .os-settings__layout-desc {
778 margin-top: 3px;
779 font-size: 13px;
780 line-height: 1.45;
781 color: var( --os-ui-fg-muted, #50575e );
782 }
783
784 @media ( prefers-reduced-motion: reduce ) {
785 .os-settings__layout-card {
786 transition-duration: 0.01ms;
787 }
788
789 .os-settings__layout-card:has( .os-settings__layout-choice:hover ) {
790 transform: none;
791 }
792 }
793
794 /* ---------------------------------------------------------------
795 * Media Library picker.
796 *
797 * Toolbar (search + HD toggle), responsive thumbnail grid, footer
798 * with count + Load more button.
799 * --------------------------------------------------------------- */
800 .os-settings__library-toolbar {
801 display: flex;
802 align-items: center;
803 gap: 12px;
804 margin-bottom: 10px;
805 flex-wrap: wrap;
806 }
807
808 .os-settings__library-search {
809 flex: 1;
810 min-width: 180px;
811 padding: 6px 10px;
812 border: 1px solid var( --os-ui-border, #dcdcde );
813 border-radius: 4px;
814 background: var( --os-ui-surface, #fff );
815 font: inherit;
816 font-size: 13px;
817 color: var( --os-ui-fg, #1d2327 );
818 }
819
820 .os-settings__library-search:focus {
821 border-color: var(--wp-admin-theme-color, #2271b1);
822 box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1);
823 outline: none;
824 }
825
826 /* `__library-hd` now renders as `<os-checkbox-label>`. */
827
828 .os-settings__library-grid {
829 display: grid;
830 grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
831 gap: 8px;
832 min-height: 140px;
833 }
834
835 .os-settings__library-tile {
836 position: relative;
837 aspect-ratio: 16 / 10;
838 padding: 0;
839 border: 2px solid transparent;
840 border-radius: 6px;
841 background-color: var( --os-ui-surface-sunken, #eef0f1 );
842 background-size: cover;
843 background-position: center;
844 background-repeat: no-repeat;
845 cursor: pointer;
846 overflow: hidden;
847 transition: border-color 0.15s ease, transform 0.15s ease;
848 }
849
850 .os-settings__library-tile:hover {
851 transform: translateY(-1px);
852 border-color: rgba(34, 113, 177, 0.4);
853 }
854
855 .os-settings__library-tile:focus-visible {
856 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
857 outline-offset: 2px;
858 }
859
860 .os-settings__library-tile--selected,
861 .os-settings__library-tile[aria-pressed="true"] {
862 border-color: var(--wp-admin-theme-color, #2271b1);
863 box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9) inset;
864 }
865
866 /* Dimension badge in the corner — readable over any thumbnail. */
867 .os-settings__library-tile-dims {
868 position: absolute;
869 bottom: 4px;
870 inset-inline-end: 4px;
871 padding: 2px 6px;
872 background: var( --os-ui-scrim, rgba(0, 0, 0, 0.6) );
873 color: var( --os-ui-fg-on-accent, #fff );
874 font-size: 10px;
875 font-weight: 500;
876 line-height: 1.3;
877 border-radius: 3px;
878 letter-spacing: 0.02em;
879 font-variant-numeric: tabular-nums;
880 pointer-events: none;
881 }
882
883 /* Skeleton tiles during the first-page fetch. Animated subtle pulse
884 * so the user perceives progress without us needing a spinner. */
885 .os-settings__library-tile--skeleton {
886 cursor: default;
887 background: linear-gradient(
888 90deg,
889 var( --os-ui-surface-sunken, #eef0f1 ) 0%,
890 #dcdcde 50%,
891 var( --os-ui-surface-sunken, #eef0f1 ) 100%
892 );
893 background-size: 200% 100%;
894 animation: os-library-shimmer 1.1s ease-in-out infinite;
895 }
896
897 .os-settings__library-tile--skeleton:hover {
898 transform: none;
899 border-color: transparent;
900 }
901
902 @keyframes os-library-shimmer {
903 from {
904 background-position: 200% 0;
905 }
906 to {
907 background-position: -200% 0;
908 }
909 }
910
911 .os-settings__library-empty,
912 .os-settings__library-error {
913 grid-column: 1 / -1;
914 margin: 0;
915 padding: 24px 16px;
916 text-align: center;
917 color: var( --os-ui-fg-muted, #646970 );
918 font-size: 13px;
919 }
920
921 .os-settings__library-error {
922 color: var( --os-ui-danger-hover, #b32d2e );
923 }
924
925 .os-settings__library-footer {
926 display: flex;
927 align-items: center;
928 justify-content: space-between;
929 margin-top: 10px;
930 gap: 10px;
931 min-height: 24px;
932 }
933
934 .os-settings__library-meta {
935 color: var( --os-ui-fg-muted, #8c8f94 );
936 font-size: 12px;
937 }
938
939 /* `__library-load-more` now renders as `<os-button variant="ghost">`. */
940
941 /* ---------------------------------------------------------------
942 * Reduced-motion: collapse every OS-Settings animation/transition to
943 * near-zero so motion-sensitive users still get the layout changes
944 * (hover feedback, expanded editor, tile highlights) but without the
945 * glide. Kept as a single scoped block so the intent is readable at
946 * a glance rather than spread across each rule.
947 * --------------------------------------------------------------- */
948 @media (prefers-reduced-motion: reduce) {
949 .os-settings__editor-slot,
950 .os-settings__image-picker-slot,
951 .os-settings__wallpaper-add,
952 .os-settings__wallpaper-config-slot,
953 .os-settings__upload-tile,
954 .os-settings__library-tile,
955 os-swatch {
956 transition-duration: 0.01ms !important;
957 }
958
959 .os-settings__library-tile--skeleton {
960 animation: none;
961 }
962 }
963
964 /* AI settings section — error and saving feedback */
965 .os-ai-settings__error {
966 margin: 6px 0 0;
967 font-size: 13px;
968 color: var( --os-ui-danger, #d63638 );
969 }
970
971 .os-ai-settings__saving {
972 margin: 6px 0 0;
973 font-size: 13px;
974 color: var( --os-ui-fg-muted, #646970 );
975 font-style: italic;
976 }
977
978 /* Extended options section — hint paragraph + error/saving states */
979 .os-ext__error {
980 margin: 6px 0 0;
981 font-size: 13px;
982 color: var( --os-ui-danger, #d63638 );
983 }
984
985 .os-ext__saving {
986 margin: 6px 0 0;
987 font-size: 13px;
988 color: var( --os-ui-fg-muted, #646970 );
989 font-style: italic;
990 }
991
992 /*
993 * Help tab — developer-facing component reference. Two-column layout
994 * (nav + detail) on desktop, stacks naturally on narrow windows via
995 * the same container the OS Settings panel lives in.
996 */
997
998 .os-settings__help-layout {
999 display: grid;
1000 grid-template-columns: 200px 1fr;
1001 gap: 20px;
1002 }
1003
1004 @container (max-width: 640px) {
1005 .os-settings__help-layout {
1006 grid-template-columns: 1fr;
1007 }
1008 }
1009
1010 /*
1011 * Search box + nav share one column so the field stays pinned while
1012 * the component list below it scrolls.
1013 */
1014 .os-settings__help-sidebar {
1015 display: flex;
1016 flex-direction: column;
1017 gap: 8px;
1018 min-height: 0;
1019 }
1020
1021 .os-settings__help-nav {
1022 display: flex;
1023 flex-direction: column;
1024 gap: 2px;
1025 align-self: stretch;
1026 min-height: 0;
1027 overflow-y: auto;
1028 padding: 4px;
1029 background: var( --os-ui-surface-elevated, #f6f7f7 );
1030 border: 1px solid var( --os-ui-border, #dcdcde );
1031 border-radius: 8px;
1032 }
1033
1034 .os-settings__help-nav-empty {
1035 margin: 0;
1036 padding: 8px 10px;
1037 font-size: 13px;
1038 color: var( --os-ui-fg-muted, #646970 );
1039 }
1040
1041 .os-settings__help-nav-item {
1042 display: flex;
1043 flex-direction: column;
1044 gap: 2px;
1045 align-items: flex-start;
1046 width: 100%;
1047 padding: 6px 10px;
1048 background: transparent;
1049 border: 1px solid transparent;
1050 border-radius: 6px;
1051 text-align: start;
1052 cursor: pointer;
1053 font: inherit;
1054 color: inherit;
1055 }
1056
1057 .os-settings__help-nav-item:hover {
1058 background: var( --os-ui-surface, #fff );
1059 border-color: var( --os-ui-border, #dcdcde );
1060 }
1061
1062 .os-settings__help-nav-item.is-active {
1063 background: var( --os-ui-surface, #fff );
1064 border-color: var( --wp-admin-theme-color, #2271b1 );
1065 }
1066
1067 .os-settings__help-nav-title {
1068 font-weight: 600;
1069 font-size: 13px;
1070 }
1071
1072 .os-settings__help-nav-tag {
1073 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1074 font-size: 12px;
1075 color: var( --os-ui-fg-muted, #646970 );
1076 }
1077
1078 .os-settings__help-detail {
1079 min-width: 0;
1080 padding: 16px 18px 18px;
1081 background: var( --os-ui-surface-elevated, #f6f7f7 );
1082 border: 1px solid var( --os-ui-border, #dcdcde );
1083 border-radius: 8px;
1084 }
1085
1086 .os-settings__help-head {
1087 display: flex;
1088 flex-wrap: wrap;
1089 align-items: baseline;
1090 gap: 8px;
1091 margin: 0 0 8px;
1092 }
1093
1094 .os-settings__help-title {
1095 margin: 0;
1096 font-size: 16px;
1097 font-weight: 600;
1098 }
1099
1100 .os-settings__help-code {
1101 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1102 font-size: 13px;
1103 color: var( --os-ui-fg-muted, #646970 );
1104 }
1105
1106 .os-settings__help-badge {
1107 padding: 1px 8px;
1108 border-radius: 999px;
1109 font-size: 13px;
1110 font-weight: 600;
1111 background: var( --os-ui-surface-sunken, #e4e7eb );
1112 color: var( --os-ui-fg, #1d2327 );
1113 }
1114
1115 .os-settings__help-badge.is-experimental {
1116 background: var( --os-ui-warning-bg, #fcf9e8 );
1117 color: var( --os-ui-warning-fg, #996800 );
1118 }
1119
1120 .os-settings__help-badge.is-planned {
1121 background: var( --os-ui-info-bg, #e1e8ff );
1122 color: var( --os-ui-info-fg, #123fb3 );
1123 }
1124
1125 .os-settings__help-summary {
1126 margin: 0 0 16px;
1127 color: var( --os-ui-fg, #1d2327 );
1128 }
1129
1130 .os-settings__help-group {
1131 margin: 0 0 16px;
1132 }
1133
1134 .os-settings__help-group h4 {
1135 margin: 0 0 8px;
1136 font-size: 13px;
1137 font-weight: 600;
1138 color: var( --os-ui-fg-muted, #646970 );
1139 }
1140
1141 .os-settings__help-example {
1142 padding: 16px;
1143 background: var( --os-ui-surface, #fff );
1144 border: 1px dashed var( --os-ui-border, #c3c4c7 );
1145 border-radius: 6px;
1146 }
1147
1148 .os-settings__help-table {
1149 width: 100%;
1150 border-collapse: collapse;
1151 font-size: 13px;
1152 }
1153
1154 /*
1155 * Rows are told apart by a wash rather than by a rule, which is the
1156 * one place in the panel that needed a replacement rather than just a
1157 * deletion: a props table with neither lines nor banding is a wall of
1158 * words. Zebra striping carries the row across without drawing
1159 * anything horizontal.
1160 */
1161 .os-settings__help-table th,
1162 .os-settings__help-table td {
1163 padding: 7px 8px;
1164 text-align: start;
1165 vertical-align: top;
1166 }
1167
1168 .os-settings__help-table tbody tr:nth-child( odd ) {
1169 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1170 }
1171
1172 .os-settings__help-table th {
1173 font-weight: 600;
1174 color: var( --os-ui-fg-muted, #646970 );
1175 background: var( --os-ui-surface, #fff );
1176 }
1177
1178 .os-settings__help-table code,
1179 .os-settings__help-list code {
1180 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1181 font-size: 12px;
1182 padding: 1px 4px;
1183 background: var( --os-ui-surface, #fff );
1184 border: 1px solid var( --os-ui-border, #dcdcde );
1185 border-radius: 3px;
1186 }
1187
1188 .os-settings__help-list {
1189 display: flex;
1190 flex-direction: column;
1191 gap: 4px;
1192 margin: 0;
1193 padding: 0;
1194 list-style: none;
1195 }
1196
1197 .os-settings__help-list li {
1198 padding: 6px 10px;
1199 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1200 border-radius: 6px;
1201 font-size: 13px;
1202 }
1203
1204 .os-settings__help-note {
1205 margin: 12px 0 0;
1206 padding: 10px 12px;
1207 background: var( --os-ui-warning-bg, #fcf9e8 );
1208 border: 1px solid var( --os-ui-warning-border, #f5e6a5 );
1209 border-radius: 6px;
1210 font-size: 13px;
1211 color: var( --os-ui-warning-fg, #996800 );
1212 }
1213
1214 /* ---------------------------------------------------------------
1215 * Window-fit scrolling.
1216 *
1217 * Without this block the OS Settings panel grows past the native
1218 * window's viewport and the whole body scrolls — fine for short tabs
1219 * but actively wrong for the Components tab, where a 200px nav rail
1220 * and a tall detail pane should each scroll inside their own column,
1221 * not in lockstep down a giant single-column page.
1222 *
1223 * Strategy:
1224 * 1. The native window body normally has overflow:auto. Override it
1225 * to `hidden` ONLY when the OS Settings root is mounted (via
1226 * :has()) so other native windows keep their default page-scroll.
1227 * 2. Make the OS Settings root a flex column that fills the body.
1228 * The tab strip + footer hug the top/bottom; the active
1229 * tabpanel takes the remaining height.
1230 * 3. Each tabpanel scrolls itself (overflow:auto) — replaces the
1231 * whole-body scroll, visually identical for the simple tabs
1232 * (Appearance, AI, Extended) but pins the reset footer.
1233 * 4. The Components tab opts OUT of overflow:auto so its inner
1234 * grid (nav + detail) can scroll independently per pane.
1235 *
1236 * `:has()` is supported in every evergreen engine (Chrome 105+,
1237 * Safari 15.4+, Firefox 121+); fine for an admin-context shell.
1238 * --------------------------------------------------------------- */
1239 /*
1240 * NOTE: the rule that used to sit here was
1241 * `.os-window__body--native:has( .os-settings )`, and it never once
1242 * matched. `renderOsSettingsPanel()` adds the `os-settings` class TO
1243 * the native window body rather than to a child of it, so the two
1244 * selectors name the same element, and `:has()` only ever looks at
1245 * descendants. The `overflow: hidden` it wanted now sits on
1246 * `.os-settings` itself, where it reaches the element it was written
1247 * for; its `display: flex` is dropped, because the grid this file
1248 * declares below superseded that column layout.
1249 *
1250 * The query context for every narrow-layout rule in this file goes on
1251 * the WINDOW, one level up. A container query styles a container's
1252 * descendants and never the container itself, so it cannot live on
1253 * the panel that has to respond to it. `:has()` does the right thing
1254 * from here: the panel really is a descendant of the window, so only
1255 * the OS Settings window becomes a container and every other native
1256 * window is left alone.
1257 *
1258 * It has to be a container and not the viewport. OS Settings is a
1259 * window inside the shell, so dragging that window narrow never
1260 * changes the viewport width; the `@media ( max-width: 640px )` rule
1261 * this replaced could only fire when the whole BROWSER went under the
1262 * breakpoint, which is both the wrong moment and, on a desktop,
1263 * never. The Components tab has wanted this since it was written: its
1264 * own `@container` rule further up this file had no container to
1265 * resolve against and so had never matched either.
1266 */
1267 .os-window--native:has( .os-settings ) {
1268 container-type: inline-size;
1269 }
1270
1271 /*
1272 * Two columns, two rows: the sidebar down the left, every pane sharing
1273 * the cell on the right (only the unhidden one takes up space), and
1274 * the footer spanning both.
1275 */
1276 /*
1277 * Two columns, four rows. The sidebar column stacks the search field,
1278 * the scrolling strip and the empty-state line; the pane spans all
1279 * three of those rows in column 2, and the reset bar spans both
1280 * columns underneath. Four rows rather than a wrapper element because
1281 * the strip and the panes have to stay siblings: see the note in
1282 * `panel.ts` where they are rendered.
1283 */
1284 .os-settings {
1285 flex: 1;
1286 min-height: 0;
1287 display: grid;
1288 grid-template-columns: var( --os-settings-nav-width, 224px ) minmax( 0, 1fr );
1289 grid-template-rows: auto minmax( 0, 1fr ) auto auto;
1290 /*
1291 * The native window body defaults to overflow: auto. Left alone,
1292 * a tall page scrolls the whole panel and takes the sidebar and
1293 * the reset bar with it; each tabpanel already scrolls itself, so
1294 * this pins the furniture and lets the content move inside it.
1295 * See the note above about where this declaration used to live.
1296 */
1297 overflow: hidden;
1298 }
1299
1300 /*
1301 * The sidebar column. Obsidian against the pane's Void, so the
1302 * column reads as part of the window furniture (it continues the
1303 * title bar) while the content sits on the deepest surface. No
1304 * dividing rule between them: two surfaces meeting IS the boundary,
1305 * and a line drawn on top of a tonal step only says the same thing
1306 * twice.
1307 *
1308 * The search field, the strip and the empty-state line are three
1309 * SIBLINGS assembled into one column by the grid rather than one
1310 * wrapped element, because `<os-tabs>` drives the panes by finding
1311 * `os-tabpanel` children of its own parent. A sidebar div around the
1312 * strip puts every pane out of its reach and the panel renders all
1313 * nine at once. The shared background is what makes the three read as
1314 * one surface.
1315 */
1316 .os-settings__search,
1317 .os-settings > os-tabs,
1318 .os-settings__search-empty {
1319 grid-column: 1;
1320 background: var( --os-ui-surface, #fff );
1321 }
1322
1323 /*
1324 * No inline padding on the strip, which is load-bearing rather than
1325 * tidy. The selected row's accent is a 2px edge at
1326 * `inset-inline-start: 0`, so any padding here would float it in the
1327 * middle of the gutter instead of sitting it on the column edge. The
1328 * rows carry their own inline padding to hold the labels off it.
1329 */
1330 .os-settings > os-tabs {
1331 grid-row: 2;
1332 min-height: 0;
1333 overflow-y: auto;
1334 }
1335
1336 /*
1337 * The only vertical space in the column, and the entire grouping
1338 * mechanism. `panel.ts` stamps this on the first row of each band;
1339 * see `navGroup()` there for why the bands come from tab order.
1340 */
1341 .os-settings > os-tabs > os-tab[ data-group-start='true' ] {
1342 margin-top: 20px;
1343 }
1344
1345 /*
1346 * Filtered out by the search field. A plain `display: none` rather
1347 * than anything cleverer, so a hidden row takes its group gap with it
1348 * and a filtered list closes up instead of keeping the shape of the
1349 * one it came from.
1350 */
1351 .os-settings > os-tabs > os-tab[ data-search-hidden ] {
1352 display: none;
1353 }
1354
1355 /*
1356 * Search. Sits above the nav rather than over the content, because
1357 * what it filters is the nav.
1358 *
1359 * The field wears the row's own geometry (same 14px, same Ash) so the
1360 * column reads as one list with a way in at the top, rather than as a
1361 * form control that happens to have been parked above some links.
1362 */
1363 /*
1364 * The cell holds the column's surface and the field's inset; the
1365 * field inside it holds the pill. Two elements because the surface has
1366 * to run edge to edge (it is the sidebar) while the field must not.
1367 */
1368 .os-settings__search {
1369 grid-row: 1;
1370 padding: 12px 12px 14px;
1371 }
1372
1373 /*
1374 * A sunken well in the Obsidian column: the field is a window onto
1375 * the pane's Void, with the same Astro border the section boxes
1376 * wear, at the inner radius (8px). The rows below it are
1377 * square-edged and full-bleed, so the inset alone already says "not
1378 * one of them".
1379 *
1380 * The field wears the row's own type: 14px Regular on Ash, a 15px
1381 * glyph at full strength. 8px top and bottom around a 21px line puts
1382 * the content box at 37px.
1383 */
1384 .os-settings__search-field {
1385 display: flex;
1386 align-items: center;
1387 gap: 8px;
1388 padding: 8px 10px;
1389 background: var( --os-ui-surface-sunken, #f0f0f1 );
1390 border: 1px solid var( --os-ui-border, #dcdcde );
1391 border-radius: 8px;
1392 color: var( --os-ui-fg-muted, #50575e );
1393 }
1394
1395 .os-settings__search svg {
1396 flex: 0 0 15px;
1397 width: 15px;
1398 height: 15px;
1399 }
1400
1401 /*
1402 * The class stack is the whole fix, not tidiness. The shell renders
1403 * inside a real wp-admin document, and TWO outrankers dress this
1404 * input as a second field inside the well: Core's forms.css styles
1405 * `input[type="search"]` at (0,1,1), and our own window-chrome rule
1406 * for raw controls in native window bodies weighs (0,2,1). Three
1407 * classes are (0,3,0) and beat both, and the input goes back to
1408 * being invisible: the FIELD is the control, the input is just
1409 * where the letters go.
1410 */
1411 .os-settings .os-settings__search .os-settings__search-input {
1412 flex: 1;
1413 min-width: 0;
1414 min-height: 0;
1415 padding: 0;
1416 border: 0;
1417 border-radius: 0;
1418 background: none;
1419 box-shadow: none;
1420 font: inherit;
1421 font-size: 14px;
1422 line-height: 21px;
1423 color: var( --os-ui-fg, #1d2327 );
1424 }
1425
1426 /* Chrome draws its own clear button on type=search; it lands on the
1427 pill's right edge and fights the rounding. */
1428 .os-settings__search-input::-webkit-search-cancel-button {
1429 appearance: none;
1430 }
1431
1432 .os-settings__search-input::placeholder {
1433 color: var( --os-ui-fg-muted, #50575e );
1434 opacity: 1;
1435 }
1436
1437 /* The field is the visible control, so the ring goes on it, not on
1438 the bare input inside. */
1439 .os-settings__search-field:focus-within {
1440 box-shadow: var( --os-ui-focus-ring-field, 0 0 0 1px #2271b1 );
1441 }
1442
1443 /* Same specificity story as above: Core's `input[type=search]:focus`
1444 is (0,2,1) and would draw its blue ring on the inner input. */
1445 .os-settings .os-settings__search .os-settings__search-input:focus,
1446 .os-settings .os-settings__search .os-settings__search-input:focus-visible {
1447 outline: none;
1448 border: 0;
1449 box-shadow: none;
1450 background: none;
1451 }
1452
1453 .os-settings__search-empty {
1454 grid-row: 3;
1455 margin: 0;
1456 padding: 4px 20px 12px;
1457 color: var( --os-ui-fg-muted, #50575e );
1458 font-size: 13px;
1459 }
1460
1461 /*
1462 * The column's surface has to survive the empty-state line being
1463 * hidden, which is most of the time: without this the sunken
1464 * background would stop at the bottom of the nav and leave the rest
1465 * of the column showing the pane's Obsidian.
1466 */
1467 .os-settings__search-empty[ hidden ] {
1468 display: block;
1469 padding: 0;
1470 font-size: 0;
1471 }
1472
1473 /*
1474 * Stand-in for a glyph a third-party tab cannot supply. Matches the
1475 * 17px the real icons are sized to in the component's shadow styles,
1476 * so the labels in a mixed column all start at the same x.
1477 */
1478 .os-settings__nav-glyph-blank {
1479 flex: 0 0 17px;
1480 width: 17px;
1481 }
1482
1483 /* Spans the three sidebar rows: the pane is one surface whatever the
1484 column beside it is doing. */
1485 .os-settings > os-tabpanel {
1486 grid-column: 2;
1487 grid-row: 1 / -1;
1488 min-height: 0;
1489 overflow: auto;
1490 padding: 28px 32px 44px;
1491 background: var( --os-ui-surface-sunken, #f0f0f1 );
1492 }
1493
1494 /*
1495 * Reset sits at the foot of the nav, and it resets everything.
1496 *
1497 * In the column rather than across the page because that is what it
1498 * addresses: the list names the pages, and the thing under the list
1499 * acts on all of them. Across the bottom of the pane it read as
1500 * belonging to whatever page happened to be open, which is the whole
1501 * reason it once had to grow a name and a sentence to disown that
1502 * reading.
1503 *
1504 * The column's own surface, so it continues the sidebar rather than
1505 * starting a third one. No rule above it, for the same reason the
1506 * sidebar has no divider against the pane: it is already the last
1507 * thing in a column that ends.
1508 */
1509 .os-settings > .os-settings__footer {
1510 grid-column: 1;
1511 grid-row: 4;
1512 display: flex;
1513 /*
1514 * Stated, not inherited. `<os-panel>` is a stack and sets
1515 * `flex-direction: column` on its own host, which would stretch
1516 * the button to the column's full width.
1517 */
1518 flex-direction: row;
1519 align-items: center;
1520 justify-content: flex-start;
1521 margin-top: 0;
1522 padding: 12px;
1523 background: var( --os-ui-surface, #fff );
1524 }
1525
1526 /*
1527 * The reset button at the mockup's measurements: a quiet 6% fill,
1528 * no border, inner radius, Caption type. All reachable through the
1529 * component's own custom-property knobs, so nothing here touches the
1530 * kit's defaults.
1531 */
1532 .os-settings > .os-settings__footer os-button {
1533 --os-ui-button-padding: 8px 15px;
1534 --os-ui-button-border-radius: 8px;
1535 --os-ui-button-border: 0;
1536 --os-ui-button-bg: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1537 font-size: 13px;
1538 }
1539
1540 /* Narrow windows: the sidebar would eat the pane, so fall back to the
1541 stacked layout the panel had before. Container-scoped, so it tracks
1542 the WINDOW being dragged narrow rather than the browser viewport. */
1543 @container ( max-width: 640px ) {
1544 .os-settings {
1545 grid-template-columns: minmax( 0, 1fr );
1546 grid-template-rows: auto minmax( 0, 1fr ) auto;
1547 }
1548 .os-settings > os-tabs {
1549 grid-column: 1;
1550 grid-row: 1;
1551 flex-direction: row;
1552 overflow-x: auto;
1553 overflow-y: hidden;
1554 }
1555 /*
1556 * Back to a strip, so the group gap becomes horizontal.
1557 *
1558 * The rows KEEP their vertical treatment, which this rule cannot
1559 * reach: it is styled inside the component's shadow root off
1560 * `data-orientation`, and a container query has no way in there.
1561 * They are full-bleed 40px rows laid in a horizontal line, which
1562 * reads as a strip of wide chips rather than as the compact tab
1563 * strip this width wants. Fixing it properly means the component
1564 * choosing its own layout at a width, not the panel overriding it
1565 * from outside; only the gap is restated here.
1566 */
1567 .os-settings > os-tabs > os-tab[ data-group-start='true' ] {
1568 margin-top: 0;
1569 margin-inline-start: 20px;
1570 }
1571 /*
1572 * The search field goes with the sidebar's width, which it no
1573 * longer has: a full-bleed input above a horizontal strip reads as
1574 * the window's search rather than the nav's. The strip is short
1575 * enough at this width to scan without it.
1576 */
1577 .os-settings__search,
1578 .os-settings__search-empty,
1579 .os-settings__search-empty[ hidden ] {
1580 display: none;
1581 }
1582 .os-settings > os-tabpanel {
1583 grid-column: 1;
1584 grid-row: 2;
1585 }
1586 /*
1587 * Row 3, not row 4. The wide layout has four rows and puts the
1588 * reset bar last; this one has three, so the wide declaration would
1589 * place the bar in a row that does not exist and the grid would
1590 * grow an implicit one below the fold.
1591 */
1592 .os-settings > .os-settings__footer {
1593 grid-row: 3;
1594 }
1595 /*
1596 * Half a pane this narrow is not a card any more, and the One dock
1597 * card has segmented controls inside it that would start clipping
1598 * their labels. The two stack instead.
1599 */
1600 .os-settings__layout-grid {
1601 grid-template-columns: minmax( 0, 1fr );
1602 }
1603 }
1604
1605 /* Components tab — split-pane layout. Don't scroll the whole panel;
1606 let the inner grid distribute height to nav + detail, each with
1607 their own overflow.
1608 *
1609 * The `:not([hidden])` guard is load-bearing: a bare
1610 * `os-tabpanel[for='help'] { display: flex }` rule has specificity
1611 * (0,0,2,1) which outranks the shadow-DOM `:host([hidden]) {
1612 * display: none }` rule (specificity 0,0,1,0), so the panel would
1613 * keep rendering when it should be hidden — bleeding the Components
1614 * UI into every other tab. Pinning the rule to the visible state
1615 * leaves the hidden state to the shadow-DOM default. */
1616 .os-settings > os-tabpanel[ for='help' ]:not( [ hidden ] ) {
1617 overflow: hidden;
1618 display: flex;
1619 flex-direction: column;
1620 /*
1621 * Square off the bottom inset. Every other page carries 44px there
1622 * so a scrolling column has somewhere to end; this one does not
1623 * scroll, so those extra 16px are just the panes stopping short of
1624 * the window. Matching the top puts the same frame on both ends.
1625 */
1626 padding-bottom: 28px;
1627 }
1628 .os-settings
1629 > os-tabpanel[ for='help' ]:not( [ hidden ] )
1630 > os-panel {
1631 flex: 1;
1632 min-height: 0;
1633 display: flex;
1634 flex-direction: column;
1635 }
1636
1637 .os-settings__help {
1638 flex: 1;
1639 min-height: 0;
1640 display: flex;
1641 flex-direction: column;
1642 gap: 12px;
1643 }
1644 /* The intro card (Component library description) shouldn't grow —
1645 it's content-sized so the grid below gets the remaining height. */
1646 .os-settings__help > os-section {
1647 flex: 0 0 auto;
1648 }
1649 .os-settings__help-layout {
1650 flex: 1;
1651 min-height: 0;
1652 }
1653
1654 /* Each split pane is its own scroll container. min-height:0 unlocks
1655 shrinking in a grid item (default is `auto` which equals content
1656 height — would defeat the overflow). max-height:100% pins the
1657 pane to the grid track. */
1658 .os-settings__help-nav,
1659 .os-settings__help-detail {
1660 min-height: 0;
1661 max-height: 100%;
1662 overflow-y: auto;
1663 }
1664
1665 /*
1666 * Features section — per-user opt-in toggles.
1667 *
1668 * Save status pill renders the live OS-settings save lifecycle
1669 * (`pending` → `saving` → `saved` / `failed`) so toggling a
1670 * per-user feature flag gets immediate, honest feedback instead
1671 * of an optimistic flicker.
1672 */
1673 .os-features__status-row {
1674 min-height: 22px;
1675 }
1676
1677 /*
1678 * One toggle + hint pair, in its own box.
1679 *
1680 * These used to be separated by a hairline above each item after the
1681 * first. A box does the same job without drawing a line: it groups the
1682 * label with its description and gives the eye a stopping point
1683 * between unrelated settings, and it says which text belongs to which
1684 * switch by enclosing it rather than by sitting between it and the
1685 * next one. No horizontal rule survives anywhere in this panel.
1686 */
1687 .os-features__item {
1688 display: flex;
1689 flex-direction: column;
1690 gap: 4px;
1691 padding: 12px 16px;
1692 background: var( --os-ui-surface, #fff );
1693 border-radius: 8px;
1694 }
1695
1696 .os-features__item + .os-features__item {
1697 margin-top: 12px;
1698 }
1699
1700 /* Feature title line: Body 14 / Medium 500. The outer-tree rule wins
1701 * over the component's own :host font-size, and the shadow label
1702 * inherits both properties from the host. */
1703 .os-features__item > os-checkbox-label {
1704 font-size: 14px;
1705 font-weight: 500;
1706 }
1707
1708 /* A functional notice (provider gating) gets a touch more air than
1709 * the 4px title-to-description rhythm: 4px gap + 4px margin = 8px. */
1710 .os-features__item > os-notice {
1711 margin-top: 4px;
1712 }
1713
1714 /* Sub-toggles (the window-links children) sit inside the parent box:
1715 * no box-in-box, indented 24px so their checkboxes align under the
1716 * parent's label text (16px box + 8px gap). Declared after the
1717 * sibling rule above so the 8px margin wins for nested siblings:
1718 * 4px parent gap + 8px margin = the same 12px block rhythm. */
1719 .os-features__item .os-features__item {
1720 margin-top: 8px;
1721 margin-left: 24px;
1722 padding: 0;
1723 background: none;
1724 border-radius: 0;
1725 }
1726
1727 /* "Reset what's-new dialogs" row: description first, button directly
1728 * under it on the shared 12px gap. `align-items: flex-start` keeps
1729 * the inline-flex `<os-button>` from stretching full-width inside
1730 * the column. */
1731 .os-features__row {
1732 display: flex;
1733 flex-direction: column;
1734 align-items: flex-start;
1735 gap: 12px;
1736 margin-top: 12px;
1737 padding: 12px 16px;
1738 background: var( --os-ui-surface, #fff );
1739 border-radius: 8px;
1740 }
1741
1742 .os-features__row > os-button {
1743 display: inline-flex;
1744 }
1745
1746 .os-features__row > .os-features__hint {
1747 margin: 0;
1748 }
1749
1750 /* "Delete folder sharing data": the one-line description and its
1751 * danger button read as a single row, button on the trailing edge.
1752 * 4px parent gap + 8px margin = the 12px block rhythm above it. */
1753 .os-features__danger-row {
1754 display: flex;
1755 align-items: center;
1756 justify-content: space-between;
1757 gap: 12px;
1758 margin-top: 8px;
1759 }
1760
1761 .os-features__danger-row > os-button {
1762 flex: 0 0 auto;
1763 }
1764
1765 /* Heartbeat rate: 8px label-to-control, description 8px below the
1766 * control (4px parent gap + 4px margin). */
1767 .os-features__select-label {
1768 display: flex;
1769 flex-direction: column;
1770 align-items: flex-start;
1771 gap: 8px;
1772 }
1773
1774 .os-features__select-title {
1775 font-size: 14px;
1776 font-weight: 500;
1777 }
1778
1779 .os-features__select-label + .os-features__hint {
1780 margin-top: 4px;
1781 }
1782
1783 .os-features__hint {
1784 margin: 0;
1785 font-size: 13px;
1786 color: var( --os-ui-fg-muted, #50575e );
1787 line-height: 1.5;
1788 }
1789
1790 .os-features__status {
1791 display: inline-flex;
1792 align-items: center;
1793 gap: 6px;
1794 font-size: 13px;
1795 font-weight: 500;
1796 padding: 2px 10px;
1797 border-radius: 999px;
1798 white-space: nowrap;
1799 transition: opacity 200ms ease;
1800 }
1801
1802 .os-features__status--saving {
1803 background: var( --os-ui-surface-elevated, #f0f0f1 );
1804 color: var( --os-ui-fg-muted, #50575e );
1805 }
1806
1807 .os-features__status--saved {
1808 background: rgba( 30, 132, 73, 0.12 );
1809 color: var( --os-ui-success-fg, #1d6f42 );
1810 }
1811
1812 .os-features__status--saved .dashicons {
1813 font-size: 14px;
1814 width: 14px;
1815 height: 14px;
1816 }
1817
1818 .os-features__status--failed {
1819 background: rgba( 214, 54, 56, 0.12 );
1820 color: var( --os-ui-danger-hover, #a02622 );
1821 }
1822
1823 .os-features__status--failed .dashicons {
1824 font-size: 14px;
1825 width: 14px;
1826 height: 14px;
1827 }
1828
1829 .os-features__status-dot {
1830 display: inline-block;
1831 width: 8px;
1832 height: 8px;
1833 border-radius: 50%;
1834 background: currentColor;
1835 animation: os-features-pulse 1.2s ease-in-out infinite;
1836 }
1837
1838 @keyframes os-features-pulse {
1839 0%, 100% { opacity: 0.35; transform: scale( 0.85 ); }
1840 50% { opacity: 1; transform: scale( 1 ); }
1841 }
1842
1843 /*
1844 * OS Settings panel header — hosts the global tabs strip and a single
1845 * `<os-save-status>` indicator that auto-listens to the save
1846 * lifecycle. The save status sits on the trailing edge of the header
1847 * so it's visible across every tab without competing with section
1848 * content.
1849 */
1850 .os-settings__header {
1851 display: flex;
1852 align-items: center;
1853 justify-content: space-between;
1854 gap: 16px;
1855 margin-bottom: 12px;
1856 }
1857
1858 .os-settings__header os-tabs {
1859 flex: 1 1 auto;
1860 min-width: 0;
1861 }
1862
1863 .os-settings__header os-save-status {
1864 flex: 0 0 auto;
1865 margin-inline-start: auto;
1866 }
1867
1868 /*
1869 * About tab — full-tabpanel PixiJS canvas. Every credit string and
1870 * the AUTOMATTIC logotype (formed by particles) render inside Pixi
1871 * itself; this stylesheet only sizes the host so the canvas fills
1872 * the available height + width without any HTML chrome around it.
1873 *
1874 * The chain is: tabpanel → os-panel → .about (flex column) →
1875 * .about-stage-host (the canvas mounts here). Each link sets
1876 * `flex: 1; min-height: 0;` so the canvas inherits the tabpanel's
1877 * full inner height instead of hugging an intrinsic minimum.
1878 *
1879 * @since 0.8.0
1880 */
1881 .os-settings
1882 > os-tabpanel[ for='about' ]:not( [ hidden ] ) {
1883 display: flex;
1884 flex-direction: column;
1885 overflow: hidden;
1886 }
1887
1888 .os-settings
1889 > os-tabpanel[ for='about' ]:not( [ hidden ] )
1890 > os-panel {
1891 flex: 1;
1892 min-height: 0;
1893 display: flex;
1894 flex-direction: column;
1895 }
1896
1897 .os-settings__about {
1898 flex: 1;
1899 min-height: 0;
1900 display: flex;
1901 flex-direction: column;
1902 margin: 0;
1903 /* Edge-to-edge — no border-radius / box-shadow framing — so the
1904 * canvas (and the logotype centred inside it) consume the entire
1905 * tabpanel surface. The gradient is just the colour-of-last-resort
1906 * underneath while Pixi initialises; once the canvas is rendering
1907 * it covers every pixel of this element anyway. */
1908 background: linear-gradient( 180deg, #060a1c 0%, #0c1733 100% );
1909 overflow: hidden;
1910 }
1911
1912 .os-settings__about-stage-host {
1913 flex: 1;
1914 min-height: 0;
1915 width: 100%;
1916 overflow: hidden;
1917 cursor: crosshair;
1918 }
1919
1920 .os-settings__about-stage-host canvas {
1921 display: block;
1922 width: 100%;
1923 height: 100%;
1924 }
1925
1926
1927 /* Apps & Icons section (since 0.25.0) — per-item placement chooser. */
1928 /*
1929 * The boxed list Núria pointed at when she asked for boxes instead of
1930 * rules. Each row encloses its own icon, title and placement select,
1931 * so a long list reads as a stack of things rather than as a table
1932 * that has lost its header.
1933 */
1934 .os-apps-icons__list {
1935 display: flex;
1936 flex-direction: column;
1937 gap: 6px;
1938 }
1939
1940 .os-apps-icons__row {
1941 display: flex;
1942 align-items: center;
1943 gap: 12px;
1944 padding: 8px 12px;
1945 background: var( --os-ui-surface, #fff );
1946 border-radius: 8px;
1947 }
1948
1949 .os-apps-icons__identity {
1950 display: flex;
1951 align-items: center;
1952 gap: 10px;
1953 flex: 1;
1954 min-width: 0;
1955 }
1956
1957 .os-apps-icons__icon {
1958 width: 24px;
1959 height: 24px;
1960 flex-shrink: 0;
1961 display: inline-flex;
1962 align-items: center;
1963 justify-content: center;
1964 }
1965
1966 .os-apps-icons__icon.dashicons {
1967 font-size: 20px;
1968 }
1969
1970 .os-apps-icons__title {
1971 overflow: hidden;
1972 text-overflow: ellipsis;
1973 white-space: nowrap;
1974 }
1975
1976 .os-apps-icons__row os-select {
1977 min-width: 180px;
1978 }
1979
1980 /* ------------------------------------------------------------------
1981 * OS Settings → Themes.
1982 *
1983 * Card grid of the site's desktop-theme library. Deliberately styled
1984 * with the SAME neutral palette as the rest of the panel rather than
1985 * with themed tokens: the picker has to stay legible while the user
1986 * is trying on themes, including ones with hostile contrast.
1987 * ------------------------------------------------------------------ */
1988
1989 .os-settings__theme-grid {
1990 display: grid;
1991 grid-template-columns: repeat( auto-fill, minmax( 160px, 1fr ) );
1992 gap: 12px;
1993 margin: 12px 0 16px;
1994 }
1995
1996 .os-settings__theme-card-wrap {
1997 position: relative;
1998 }
1999
2000 /*
2001 * Same geometry in every state: the border is 1px in both, and
2002 * selection is a box-shadow ring OUTSIDE the box, so an active card
2003 * occupies exactly the pixels an idle one does and the row stays
2004 * aligned. The selected ring is the flat accent, the same language
2005 * as the wallpaper tiles and layout cards beside it.
2006 */
2007 .os-settings__theme-card {
2008 display: flex;
2009 flex-direction: column;
2010 gap: 6px;
2011 width: 100%;
2012 height: 100%;
2013 padding: 10px;
2014 background: var( --os-ui-surface, #fff );
2015 border: 1px solid var( --os-ui-border, #dcdcde );
2016 border-radius: 10px;
2017 cursor: pointer;
2018 text-align: start;
2019 font: inherit;
2020 color: var( --os-ui-fg, #1d2327 );
2021 transition: border-color 0.15s ease, box-shadow 0.15s ease;
2022 }
2023
2024 .os-settings__theme-card:hover {
2025 border-color: var( --os-ui-border-strong, #8c8f94 );
2026 }
2027
2028 .os-settings__theme-card:focus-visible {
2029 outline: 2px solid var( --os-ui-accent, #2271b1 );
2030 outline-offset: 2px;
2031 }
2032
2033 .os-settings__theme-card[ aria-pressed="true" ] {
2034 border-color: transparent;
2035 box-shadow: 0 0 0 2px var( --os-ui-accent, #2271b1 );
2036 }
2037
2038 .os-settings__theme-preview {
2039 display: flex;
2040 align-items: center;
2041 justify-content: center;
2042 aspect-ratio: 16 / 10;
2043 overflow: hidden;
2044 border-radius: 6px;
2045 background: var( --os-ui-surface-elevated, #f0f0f1 );
2046 }
2047
2048 .os-settings__theme-preview img {
2049 width: 100%;
2050 height: 100%;
2051 object-fit: cover;
2052 display: block;
2053 }
2054
2055 /* Stand-in preview for "System default" — a miniature of the shipped
2056 * graphite desktop, so the card reads as a real option rather than an
2057 * absence. */
2058 .os-settings__theme-preview--system {
2059 background: linear-gradient( 135deg, #1d2327 0%, #2c3338 50%, #1d2327 100% );
2060 }
2061
2062 .os-settings__theme-initials {
2063 font-size: 24px;
2064 font-weight: 700;
2065 letter-spacing: 1px;
2066 color: var( --os-ui-fg-muted, #787c82 );
2067 }
2068
2069 .os-settings__theme-name {
2070 font-weight: 600;
2071 line-height: 1.3;
2072 overflow-wrap: anywhere;
2073 }
2074
2075 /* Wraps rather than clipping. This string is a theme's own
2076 description and a plugin can make it any length; a one-line box cut
2077 "The look OpenStation ships with" off mid-sentence. */
2078 .os-settings__theme-meta {
2079 font-size: 13px;
2080 color: var( --os-ui-fg-muted, #646970 );
2081 line-height: 1.45;
2082 min-height: 1.45em;
2083 overflow-wrap: anywhere;
2084 }
2085
2086 /* Delete overlay — outside the card button, because nesting a button
2087 * inside a button is invalid HTML and breaks keyboard traversal. */
2088 .os-settings__theme-delete {
2089 position: absolute;
2090 inset-block-start: 4px;
2091 inset-inline-end: 4px;
2092 display: flex;
2093 align-items: center;
2094 justify-content: center;
2095 width: 22px;
2096 height: 22px;
2097 padding: 0;
2098 border: none;
2099 border-radius: 50%;
2100 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
2101 color: var( --os-ui-fg-on-accent, #fff );
2102 font-size: 15px;
2103 line-height: 1;
2104 cursor: pointer;
2105 opacity: 0;
2106 transition: opacity 0.15s ease, background-color 0.15s ease;
2107 }
2108
2109 .os-settings__theme-card-wrap:hover
2110 .os-settings__theme-delete,
2111 .os-settings__theme-delete:focus-visible {
2112 opacity: 1;
2113 }
2114
2115 .os-settings__theme-delete:hover {
2116 background: var( --os-ui-danger, #d63638 );
2117 }
2118
2119 /*
2120 * "Apply <theme>'s recommended layout" — sits between the theme grid
2121 * and the upload tile, and only for a theme that recommends
2122 * something. Just the button: the dock resizing and the layout
2123 * moving is the feedback.
2124 */
2125 .os-settings__theme-recommendation {
2126 display: flex;
2127 margin-block: 16px;
2128 }
2129
2130 /*
2131 * The last cell of the theme grid, not a dropzone underneath it.
2132 * Uploading a theme is one of the ways to answer "which theme", so it
2133 * belongs in the row of answers; dashed because it is the only one
2134 * that cannot show you what you are picking until you have picked it.
2135 * Same reasoning, and the same treatment, as "Use your own image" in
2136 * the wallpaper grid.
2137 */
2138 .os-settings__theme-upload {
2139 display: block;
2140 /* Same reason as the other two dropzones above: the plain border
2141 token is the card's own colour and vanishes on it. */
2142 border: 2px dashed var( --os-ui-border-strong, #c3c4c7 );
2143 border-radius: 10px;
2144 background: transparent;
2145 transition: border-color 0.15s ease, background-color 0.15s ease;
2146 }
2147
2148 .os-settings__theme-upload-label {
2149 display: flex;
2150 flex-direction: column;
2151 align-items: center;
2152 justify-content: center;
2153 gap: 6px;
2154 /* Matches a theme card: 16:10 preview plus its name and meta. */
2155 min-height: 100%;
2156 padding: 16px;
2157 color: var( --os-ui-fg-muted, #50575e );
2158 cursor: pointer;
2159 text-align: center;
2160 }
2161
2162 .os-settings__theme-upload:hover,
2163 .os-settings__theme-upload--dragover {
2164 border-color: var( --wp-admin-theme-color, #2271b1 );
2165 background-color: rgba( 34, 113, 177, 0.06 );
2166 }
2167
2168 .os-settings__theme-upload--dragover {
2169 border-style: solid;
2170 }
2171
2172 .os-settings__theme-upload--busy
2173 .os-settings__theme-upload-label {
2174 cursor: progress;
2175 opacity: 0.7;
2176 }
2177
2178 .os-settings__theme-upload-plus {
2179 font-size: 22px;
2180 line-height: 1;
2181 }
2182