PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.1
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.1
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.1, at assets/css/os-settings.css

2,196 lines 61.8 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 * So the input is laid over the Custom swatch. The last cell of the
572 * accent row is its containing block and the input fills it exactly,
573 * so the wheel opens on the square it belongs to.
574 *
575 * Anchoring it in CSS rather than measuring it in JS is the whole
576 * fix: the browser places the popup from the box it has already laid
577 * out, so an input moved by an inline style in the same tick as
578 * showPicker() is placed from its PREVIOUS box, which put the wheel
579 * under the first swatch. A cell that owns the input has no such
580 * moment, and it survives resize and grid reflow for free.
581 *
582 * Absolute rather than fixed because a dragged window carries a
583 * transform, and a transform turns fixed positioning into a lie.
584 *
585 * It stays out of the tab order and off the accessibility tree: the
586 * Custom swatch is the control, and activating it (pointer or
587 * keyboard) is what opens the wheel.
588 */
589 .os-settings__accent-custom {
590 position: relative;
591 display: inline-flex;
592 }
593
594 .os-settings__accent-picker {
595 position: absolute;
596 inset: 0;
597 width: 100%;
598 height: 100%;
599 margin: 0;
600 padding: 0;
601 border: 0;
602 opacity: 0;
603 pointer-events: none;
604 }
605
606 /* ---------------------------------------------------------------
607 * Desktop layout cards.
608 *
609 * A layout is a spatial choice, so each option draws the arrangement
610 * it is offering. The four names on their own ("One dock",
611 * "OpenStation", "Side bar", "Spatial") asked the user to guess what
612 * each would do to their screen and then find out by trying it.
613 *
614 * The previews are schematic: two or three rectangles and a rail, on
615 * Void, with the dock (or the current Space) in the accent so the eye
616 * lands on the thing that differs between the four. Geometry comes
617 * from `desktop-layout.ts` as percentages, so the cards stay legible
618 * as the grid reflows.
619 * --------------------------------------------------------------- */
620 /*
621 * Two layouts, half the pane each. Fixed tracks rather than
622 * auto-fill: there are exactly two of them and they are a pair to be
623 * compared, so a wide pane that fitted a third column would only
624 * shrink both cards away from each other for no reason.
625 *
626 * The cards stretch to the taller of the two (the grid's default
627 * `align-items`, left unset). They are a matched pair presented side
628 * by side, and two boxes of different heights read as two different
629 * KINDS of thing rather than as two answers to one question — which
630 * is the comparison the whole picker is for. What varies is the
631 * description: one line for Unified, two for Split, and neither is
632 * worth a ragged bottom edge.
633 *
634 * This used to be `align-items: start`, back when the Unified card
635 * also held Placement and Dock size and stretching Split to match
636 * left it mostly empty. Dock size has since moved out below both
637 * cards, so the gap it was guarding against is now one control deep.
638 */
639 .os-settings__layout-grid {
640 display: grid;
641 grid-template-columns: repeat( 2, minmax( 0, 1fr ) );
642 gap: 12px;
643 }
644
645 /*
646 * The dock options, inside the One dock card. Stacked, because half
647 * a pane is not enough for two segmented controls side by side, and
648 * because reading them down the card follows the order the decision
649 * is made in: this layout, then where its dock sits, then how big.
650 */
651 .os-settings__dock-options {
652 display: flex;
653 flex-direction: column;
654 gap: 14px;
655 margin-top: 14px;
656 }
657
658 /*
659 * Dock size, under both cards rather than inside one: both layouts
660 * have a dock. Off the grid entirely, so it clears the taller card
661 * rather than sitting beside it, and on the section's own rhythm
662 * rather than the card's tighter one.
663 */
664 .os-settings__dock-options--page {
665 margin-top: 20px;
666 }
667
668 .os-settings__dock-option {
669 display: flex;
670 flex-direction: column;
671 align-items: flex-start;
672 gap: 8px;
673 }
674
675 .os-settings__dock-option-label {
676 font-size: 14px;
677 font-weight: 500;
678 color: var( --os-ui-fg, #1d2327 );
679 }
680
681 /*
682 * The card is the BOX, not the control. It holds the radio that
683 * picks the layout and, for One dock, that layout's own options —
684 * and a radio cannot contain a segmented control, so the two are
685 * siblings inside it. See the docblock in
686 * `src/settings/sections/desktop-layout.ts`.
687 *
688 * Which means the box wears the state of a child it does not own.
689 * Both `:has()` rules name the CHOICE rather than the card, so a
690 * pointer resting on a segmented control does not lift the card, and
691 * a segment taking focus does not light the selection ring.
692 */
693 .os-settings__layout-card {
694 display: flex;
695 flex-direction: column;
696 padding: 10px;
697 background: var( --os-ui-surface-raised, rgba( 0, 0, 0, 0.04 ) );
698 border-radius: 10px;
699 transition: transform 0.15s ease, box-shadow 0.15s ease;
700 }
701
702 .os-settings__layout-card:has( .os-settings__layout-choice:hover ) {
703 transform: translateY( -2px );
704 }
705
706 .os-settings__layout-card:has( [ aria-checked='true' ] ) {
707 box-shadow: 0 0 0 2px var( --os-ui-accent, #2271b1 );
708 }
709
710 /* The radio: everything in the card that means "this layout". */
711 .os-settings__layout-choice {
712 display: flex;
713 flex-direction: column;
714 width: 100%;
715 padding: 0;
716 background: none;
717 border: 0;
718 border-radius: 8px;
719 font: inherit;
720 color: inherit;
721 text-align: start;
722 cursor: pointer;
723 }
724
725 .os-settings__layout-choice:focus-visible {
726 outline: none;
727 box-shadow: var( --os-ui-focus-ring, 0 0 0 2px #2271b1 );
728 }
729
730 /*
731 * A screen, so it is shaped like one: 16/9, not whatever shape is left
732 * over once the card has been sized. A full-width box at a fixed 78px
733 * ran about 6.7:1, and every shape inside is a percentage of it — so
734 * the two windows came out five times wider than they were tall and
735 * read as stacked bars rather than as windows, which is the one thing
736 * these previews exist to show.
737 *
738 * Capped in width rather than stretched, because 16/9 across a whole
739 * half-pane card is ~290px of preview for a schematic with four
740 * rectangles in it. The cap sets the height (288 → 162px); the card
741 * keeps its own width and the preview sits centred in it, which is why
742 * `inline-size` is a percentage with a ceiling instead of a fixed
743 * width: below the cap — the stacked single-column pane — it goes back
744 * to filling the card, still at 16/9.
745 */
746 .os-settings__layout-preview {
747 position: relative;
748 display: block;
749 inline-size: 100%;
750 max-inline-size: 288px;
751 aspect-ratio: 16 / 9;
752 margin-block-end: 9px;
753 margin-inline: auto;
754 overflow: hidden;
755 background: var( --os-ui-surface-sunken, #f0f0f1 );
756 border-radius: 8px;
757 }
758
759 /* A window: a filled rectangle with a boundary, because at this size
760 a borderless one dissolves into the Void behind it. */
761 .os-settings__layout-win {
762 position: absolute;
763 background: var( --os-ui-surface-elevated, #f6f7f7 );
764 border: 1px solid var( --os-ui-border-strong, #c3c4c7 );
765 border-radius: 4px;
766 }
767
768 /* A rail: the wp-admin menu, or the side rail. No border, because a
769 rail reads as a solid mass and a window does not. */
770 .os-settings__layout-bar {
771 position: absolute;
772 background: var( --os-ui-border-strong, #c3c4c7 );
773 border-radius: 3px;
774 }
775
776 /* A rail the user gets to place: the dock, and in Side bar the admin
777 menu beside it. The accent is what separates the rails from the
778 windows they sit next to, so a card can carry more than one. */
779 .os-settings__layout-win.is-accent,
780 .os-settings__layout-bar.is-accent {
781 background: var( --os-ui-accent, #2271b1 );
782 border-color: transparent;
783 }
784
785 .os-settings__layout-name {
786 font-size: 14px;
787 font-weight: 500;
788 color: var( --os-ui-fg, #1d2327 );
789 }
790
791 .os-settings__layout-desc {
792 margin-top: 3px;
793 font-size: 13px;
794 line-height: 1.45;
795 color: var( --os-ui-fg-muted, #50575e );
796 }
797
798 @media ( prefers-reduced-motion: reduce ) {
799 .os-settings__layout-card {
800 transition-duration: 0.01ms;
801 }
802
803 .os-settings__layout-card:has( .os-settings__layout-choice:hover ) {
804 transform: none;
805 }
806 }
807
808 /* ---------------------------------------------------------------
809 * Media Library picker.
810 *
811 * Toolbar (search + HD toggle), responsive thumbnail grid, footer
812 * with count + Load more button.
813 * --------------------------------------------------------------- */
814 .os-settings__library-toolbar {
815 display: flex;
816 align-items: center;
817 gap: 12px;
818 margin-bottom: 10px;
819 flex-wrap: wrap;
820 }
821
822 .os-settings__library-search {
823 flex: 1;
824 min-width: 180px;
825 padding: 6px 10px;
826 border: 1px solid var( --os-ui-border, #dcdcde );
827 border-radius: 4px;
828 background: var( --os-ui-surface, #fff );
829 font: inherit;
830 font-size: 13px;
831 color: var( --os-ui-fg, #1d2327 );
832 }
833
834 .os-settings__library-search:focus {
835 border-color: var(--wp-admin-theme-color, #2271b1);
836 box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1);
837 outline: none;
838 }
839
840 /* `__library-hd` now renders as `<os-checkbox-label>`. */
841
842 .os-settings__library-grid {
843 display: grid;
844 grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
845 gap: 8px;
846 min-height: 140px;
847 }
848
849 .os-settings__library-tile {
850 position: relative;
851 aspect-ratio: 16 / 10;
852 padding: 0;
853 border: 2px solid transparent;
854 border-radius: 6px;
855 background-color: var( --os-ui-surface-sunken, #eef0f1 );
856 background-size: cover;
857 background-position: center;
858 background-repeat: no-repeat;
859 cursor: pointer;
860 overflow: hidden;
861 transition: border-color 0.15s ease, transform 0.15s ease;
862 }
863
864 .os-settings__library-tile:hover {
865 transform: translateY(-1px);
866 border-color: rgba(34, 113, 177, 0.4);
867 }
868
869 .os-settings__library-tile:focus-visible {
870 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
871 outline-offset: 2px;
872 }
873
874 .os-settings__library-tile--selected,
875 .os-settings__library-tile[aria-pressed="true"] {
876 border-color: var(--wp-admin-theme-color, #2271b1);
877 box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9) inset;
878 }
879
880 /* Dimension badge in the corner — readable over any thumbnail. */
881 .os-settings__library-tile-dims {
882 position: absolute;
883 bottom: 4px;
884 inset-inline-end: 4px;
885 padding: 2px 6px;
886 background: var( --os-ui-scrim, rgba(0, 0, 0, 0.6) );
887 color: var( --os-ui-fg-on-accent, #fff );
888 font-size: 10px;
889 font-weight: 500;
890 line-height: 1.3;
891 border-radius: 3px;
892 letter-spacing: 0.02em;
893 font-variant-numeric: tabular-nums;
894 pointer-events: none;
895 }
896
897 /* Skeleton tiles during the first-page fetch. Animated subtle pulse
898 * so the user perceives progress without us needing a spinner. */
899 .os-settings__library-tile--skeleton {
900 cursor: default;
901 background: linear-gradient(
902 90deg,
903 var( --os-ui-surface-sunken, #eef0f1 ) 0%,
904 #dcdcde 50%,
905 var( --os-ui-surface-sunken, #eef0f1 ) 100%
906 );
907 background-size: 200% 100%;
908 animation: os-library-shimmer 1.1s ease-in-out infinite;
909 }
910
911 .os-settings__library-tile--skeleton:hover {
912 transform: none;
913 border-color: transparent;
914 }
915
916 @keyframes os-library-shimmer {
917 from {
918 background-position: 200% 0;
919 }
920 to {
921 background-position: -200% 0;
922 }
923 }
924
925 .os-settings__library-empty,
926 .os-settings__library-error {
927 grid-column: 1 / -1;
928 margin: 0;
929 padding: 24px 16px;
930 text-align: center;
931 color: var( --os-ui-fg-muted, #646970 );
932 font-size: 13px;
933 }
934
935 .os-settings__library-error {
936 color: var( --os-ui-danger-hover, #b32d2e );
937 }
938
939 .os-settings__library-footer {
940 display: flex;
941 align-items: center;
942 justify-content: space-between;
943 margin-top: 10px;
944 gap: 10px;
945 min-height: 24px;
946 }
947
948 .os-settings__library-meta {
949 color: var( --os-ui-fg-muted, #8c8f94 );
950 font-size: 12px;
951 }
952
953 /* `__library-load-more` now renders as `<os-button variant="ghost">`. */
954
955 /* ---------------------------------------------------------------
956 * Reduced-motion: collapse every OS-Settings animation/transition to
957 * near-zero so motion-sensitive users still get the layout changes
958 * (hover feedback, expanded editor, tile highlights) but without the
959 * glide. Kept as a single scoped block so the intent is readable at
960 * a glance rather than spread across each rule.
961 * --------------------------------------------------------------- */
962 @media (prefers-reduced-motion: reduce) {
963 .os-settings__editor-slot,
964 .os-settings__image-picker-slot,
965 .os-settings__wallpaper-add,
966 .os-settings__wallpaper-config-slot,
967 .os-settings__upload-tile,
968 .os-settings__library-tile,
969 os-swatch {
970 transition-duration: 0.01ms !important;
971 }
972
973 .os-settings__library-tile--skeleton {
974 animation: none;
975 }
976 }
977
978 /* AI settings section — error and saving feedback */
979 .os-ai-settings__error {
980 margin: 6px 0 0;
981 font-size: 13px;
982 color: var( --os-ui-danger, #d63638 );
983 }
984
985 .os-ai-settings__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 /* Extended options section — hint paragraph + error/saving states */
993 .os-ext__error {
994 margin: 6px 0 0;
995 font-size: 13px;
996 color: var( --os-ui-danger, #d63638 );
997 }
998
999 .os-ext__saving {
1000 margin: 6px 0 0;
1001 font-size: 13px;
1002 color: var( --os-ui-fg-muted, #646970 );
1003 font-style: italic;
1004 }
1005
1006 /*
1007 * Help tab — developer-facing component reference. Two-column layout
1008 * (nav + detail) on desktop, stacks naturally on narrow windows via
1009 * the same container the OS Settings panel lives in.
1010 */
1011
1012 .os-settings__help-layout {
1013 display: grid;
1014 grid-template-columns: 200px 1fr;
1015 gap: 20px;
1016 }
1017
1018 @container (max-width: 640px) {
1019 .os-settings__help-layout {
1020 grid-template-columns: 1fr;
1021 }
1022 }
1023
1024 /*
1025 * Search box + nav share one column so the field stays pinned while
1026 * the component list below it scrolls.
1027 */
1028 .os-settings__help-sidebar {
1029 display: flex;
1030 flex-direction: column;
1031 gap: 8px;
1032 min-height: 0;
1033 }
1034
1035 .os-settings__help-nav {
1036 display: flex;
1037 flex-direction: column;
1038 gap: 2px;
1039 align-self: stretch;
1040 min-height: 0;
1041 overflow-y: auto;
1042 padding: 4px;
1043 background: var( --os-ui-surface-elevated, #f6f7f7 );
1044 border: 1px solid var( --os-ui-border, #dcdcde );
1045 border-radius: 8px;
1046 }
1047
1048 .os-settings__help-nav-empty {
1049 margin: 0;
1050 padding: 8px 10px;
1051 font-size: 13px;
1052 color: var( --os-ui-fg-muted, #646970 );
1053 }
1054
1055 .os-settings__help-nav-item {
1056 display: flex;
1057 flex-direction: column;
1058 gap: 2px;
1059 align-items: flex-start;
1060 width: 100%;
1061 padding: 6px 10px;
1062 background: transparent;
1063 border: 1px solid transparent;
1064 border-radius: 6px;
1065 text-align: start;
1066 cursor: pointer;
1067 font: inherit;
1068 color: inherit;
1069 }
1070
1071 .os-settings__help-nav-item:hover {
1072 background: var( --os-ui-surface, #fff );
1073 border-color: var( --os-ui-border, #dcdcde );
1074 }
1075
1076 .os-settings__help-nav-item.is-active {
1077 background: var( --os-ui-surface, #fff );
1078 border-color: var( --wp-admin-theme-color, #2271b1 );
1079 }
1080
1081 .os-settings__help-nav-title {
1082 font-weight: 600;
1083 font-size: 13px;
1084 }
1085
1086 .os-settings__help-nav-tag {
1087 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1088 font-size: 12px;
1089 color: var( --os-ui-fg-muted, #646970 );
1090 }
1091
1092 .os-settings__help-detail {
1093 min-width: 0;
1094 padding: 16px 18px 18px;
1095 background: var( --os-ui-surface-elevated, #f6f7f7 );
1096 border: 1px solid var( --os-ui-border, #dcdcde );
1097 border-radius: 8px;
1098 }
1099
1100 .os-settings__help-head {
1101 display: flex;
1102 flex-wrap: wrap;
1103 align-items: baseline;
1104 gap: 8px;
1105 margin: 0 0 8px;
1106 }
1107
1108 .os-settings__help-title {
1109 margin: 0;
1110 font-size: 16px;
1111 font-weight: 600;
1112 }
1113
1114 .os-settings__help-code {
1115 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1116 font-size: 13px;
1117 color: var( --os-ui-fg-muted, #646970 );
1118 }
1119
1120 .os-settings__help-badge {
1121 padding: 1px 8px;
1122 border-radius: 999px;
1123 font-size: 13px;
1124 font-weight: 600;
1125 background: var( --os-ui-surface-sunken, #e4e7eb );
1126 color: var( --os-ui-fg, #1d2327 );
1127 }
1128
1129 .os-settings__help-badge.is-experimental {
1130 background: var( --os-ui-warning-bg, #fcf9e8 );
1131 color: var( --os-ui-warning-fg, #996800 );
1132 }
1133
1134 .os-settings__help-badge.is-planned {
1135 background: var( --os-ui-info-bg, #e1e8ff );
1136 color: var( --os-ui-info-fg, #123fb3 );
1137 }
1138
1139 .os-settings__help-summary {
1140 margin: 0 0 16px;
1141 color: var( --os-ui-fg, #1d2327 );
1142 }
1143
1144 .os-settings__help-group {
1145 margin: 0 0 16px;
1146 }
1147
1148 .os-settings__help-group h4 {
1149 margin: 0 0 8px;
1150 font-size: 13px;
1151 font-weight: 600;
1152 color: var( --os-ui-fg-muted, #646970 );
1153 }
1154
1155 .os-settings__help-example {
1156 padding: 16px;
1157 background: var( --os-ui-surface, #fff );
1158 border: 1px dashed var( --os-ui-border, #c3c4c7 );
1159 border-radius: 6px;
1160 }
1161
1162 .os-settings__help-table {
1163 width: 100%;
1164 border-collapse: collapse;
1165 font-size: 13px;
1166 }
1167
1168 /*
1169 * Rows are told apart by a wash rather than by a rule, which is the
1170 * one place in the panel that needed a replacement rather than just a
1171 * deletion: a props table with neither lines nor banding is a wall of
1172 * words. Zebra striping carries the row across without drawing
1173 * anything horizontal.
1174 */
1175 .os-settings__help-table th,
1176 .os-settings__help-table td {
1177 padding: 7px 8px;
1178 text-align: start;
1179 vertical-align: top;
1180 }
1181
1182 .os-settings__help-table tbody tr:nth-child( odd ) {
1183 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1184 }
1185
1186 .os-settings__help-table th {
1187 font-weight: 600;
1188 color: var( --os-ui-fg-muted, #646970 );
1189 background: var( --os-ui-surface, #fff );
1190 }
1191
1192 .os-settings__help-table code,
1193 .os-settings__help-list code {
1194 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1195 font-size: 12px;
1196 padding: 1px 4px;
1197 background: var( --os-ui-surface, #fff );
1198 border: 1px solid var( --os-ui-border, #dcdcde );
1199 border-radius: 3px;
1200 }
1201
1202 .os-settings__help-list {
1203 display: flex;
1204 flex-direction: column;
1205 gap: 4px;
1206 margin: 0;
1207 padding: 0;
1208 list-style: none;
1209 }
1210
1211 .os-settings__help-list li {
1212 padding: 6px 10px;
1213 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1214 border-radius: 6px;
1215 font-size: 13px;
1216 }
1217
1218 .os-settings__help-note {
1219 margin: 12px 0 0;
1220 padding: 10px 12px;
1221 background: var( --os-ui-warning-bg, #fcf9e8 );
1222 border: 1px solid var( --os-ui-warning-border, #f5e6a5 );
1223 border-radius: 6px;
1224 font-size: 13px;
1225 color: var( --os-ui-warning-fg, #996800 );
1226 }
1227
1228 /* ---------------------------------------------------------------
1229 * Window-fit scrolling.
1230 *
1231 * Without this block the OS Settings panel grows past the native
1232 * window's viewport and the whole body scrolls — fine for short tabs
1233 * but actively wrong for the Components tab, where a 200px nav rail
1234 * and a tall detail pane should each scroll inside their own column,
1235 * not in lockstep down a giant single-column page.
1236 *
1237 * Strategy:
1238 * 1. The native window body normally has overflow:auto. Override it
1239 * to `hidden` ONLY when the OS Settings root is mounted (via
1240 * :has()) so other native windows keep their default page-scroll.
1241 * 2. Make the OS Settings root a flex column that fills the body.
1242 * The tab strip + footer hug the top/bottom; the active
1243 * tabpanel takes the remaining height.
1244 * 3. Each tabpanel scrolls itself (overflow:auto) — replaces the
1245 * whole-body scroll, visually identical for the simple tabs
1246 * (Appearance, AI, Extended) but pins the reset footer.
1247 * 4. The Components tab opts OUT of overflow:auto so its inner
1248 * grid (nav + detail) can scroll independently per pane.
1249 *
1250 * `:has()` is supported in every evergreen engine (Chrome 105+,
1251 * Safari 15.4+, Firefox 121+); fine for an admin-context shell.
1252 * --------------------------------------------------------------- */
1253 /*
1254 * NOTE: the rule that used to sit here was
1255 * `.os-window__body--native:has( .os-settings )`, and it never once
1256 * matched. `renderOsSettingsPanel()` adds the `os-settings` class TO
1257 * the native window body rather than to a child of it, so the two
1258 * selectors name the same element, and `:has()` only ever looks at
1259 * descendants. The `overflow: hidden` it wanted now sits on
1260 * `.os-settings` itself, where it reaches the element it was written
1261 * for; its `display: flex` is dropped, because the grid this file
1262 * declares below superseded that column layout.
1263 *
1264 * The query context for every narrow-layout rule in this file goes on
1265 * the WINDOW, one level up. A container query styles a container's
1266 * descendants and never the container itself, so it cannot live on
1267 * the panel that has to respond to it. `:has()` does the right thing
1268 * from here: the panel really is a descendant of the window, so only
1269 * the OS Settings window becomes a container and every other native
1270 * window is left alone.
1271 *
1272 * It has to be a container and not the viewport. OS Settings is a
1273 * window inside the shell, so dragging that window narrow never
1274 * changes the viewport width; the `@media ( max-width: 640px )` rule
1275 * this replaced could only fire when the whole BROWSER went under the
1276 * breakpoint, which is both the wrong moment and, on a desktop,
1277 * never. The Components tab has wanted this since it was written: its
1278 * own `@container` rule further up this file had no container to
1279 * resolve against and so had never matched either.
1280 */
1281 .os-window--native:has( .os-settings ) {
1282 container-type: inline-size;
1283 }
1284
1285 /*
1286 * Two columns, two rows: the sidebar down the left, every pane sharing
1287 * the cell on the right (only the unhidden one takes up space), and
1288 * the footer spanning both.
1289 */
1290 /*
1291 * Two columns, four rows. The sidebar column stacks the search field,
1292 * the scrolling strip and the empty-state line; the pane spans all
1293 * three of those rows in column 2, and the reset bar spans both
1294 * columns underneath. Four rows rather than a wrapper element because
1295 * the strip and the panes have to stay siblings: see the note in
1296 * `panel.ts` where they are rendered.
1297 */
1298 .os-settings {
1299 flex: 1;
1300 min-height: 0;
1301 display: grid;
1302 grid-template-columns: var( --os-settings-nav-width, 224px ) minmax( 0, 1fr );
1303 grid-template-rows: auto minmax( 0, 1fr ) auto auto;
1304 /*
1305 * The native window body defaults to overflow: auto. Left alone,
1306 * a tall page scrolls the whole panel and takes the sidebar and
1307 * the reset bar with it; each tabpanel already scrolls itself, so
1308 * this pins the furniture and lets the content move inside it.
1309 * See the note above about where this declaration used to live.
1310 */
1311 overflow: hidden;
1312 }
1313
1314 /*
1315 * The sidebar column. Obsidian against the pane's Void, so the
1316 * column reads as part of the window furniture (it continues the
1317 * title bar) while the content sits on the deepest surface. No
1318 * dividing rule between them: two surfaces meeting IS the boundary,
1319 * and a line drawn on top of a tonal step only says the same thing
1320 * twice.
1321 *
1322 * The search field, the strip and the empty-state line are three
1323 * SIBLINGS assembled into one column by the grid rather than one
1324 * wrapped element, because `<os-tabs>` drives the panes by finding
1325 * `os-tabpanel` children of its own parent. A sidebar div around the
1326 * strip puts every pane out of its reach and the panel renders all
1327 * nine at once. The shared background is what makes the three read as
1328 * one surface.
1329 */
1330 .os-settings__search,
1331 .os-settings > os-tabs,
1332 .os-settings__search-empty {
1333 grid-column: 1;
1334 background: var( --os-ui-surface, #fff );
1335 }
1336
1337 /*
1338 * No inline padding on the strip, which is load-bearing rather than
1339 * tidy. The selected row's accent is a 2px edge at
1340 * `inset-inline-start: 0`, so any padding here would float it in the
1341 * middle of the gutter instead of sitting it on the column edge. The
1342 * rows carry their own inline padding to hold the labels off it.
1343 */
1344 .os-settings > os-tabs {
1345 grid-row: 2;
1346 min-height: 0;
1347 overflow-y: auto;
1348 }
1349
1350 /*
1351 * The only vertical space in the column, and the entire grouping
1352 * mechanism. `panel.ts` stamps this on the first row of each band;
1353 * see `navGroup()` there for why the bands come from tab order.
1354 */
1355 .os-settings > os-tabs > os-tab[ data-group-start='true' ] {
1356 margin-top: 20px;
1357 }
1358
1359 /*
1360 * Filtered out by the search field. A plain `display: none` rather
1361 * than anything cleverer, so a hidden row takes its group gap with it
1362 * and a filtered list closes up instead of keeping the shape of the
1363 * one it came from.
1364 */
1365 .os-settings > os-tabs > os-tab[ data-search-hidden ] {
1366 display: none;
1367 }
1368
1369 /*
1370 * Search. Sits above the nav rather than over the content, because
1371 * what it filters is the nav.
1372 *
1373 * The field wears the row's own geometry (same 14px, same Ash) so the
1374 * column reads as one list with a way in at the top, rather than as a
1375 * form control that happens to have been parked above some links.
1376 */
1377 /*
1378 * The cell holds the column's surface and the field's inset; the
1379 * field inside it holds the pill. Two elements because the surface has
1380 * to run edge to edge (it is the sidebar) while the field must not.
1381 */
1382 .os-settings__search {
1383 grid-row: 1;
1384 padding: 12px 12px 14px;
1385 }
1386
1387 /*
1388 * A sunken well in the Obsidian column: the field is a window onto
1389 * the pane's Void, with the same Astro border the section boxes
1390 * wear, at the inner radius (8px). The rows below it are
1391 * square-edged and full-bleed, so the inset alone already says "not
1392 * one of them".
1393 *
1394 * The field wears the row's own type: 14px Regular on Ash, a 15px
1395 * glyph at full strength. 8px top and bottom around a 21px line puts
1396 * the content box at 37px.
1397 */
1398 .os-settings__search-field {
1399 display: flex;
1400 align-items: center;
1401 gap: 8px;
1402 padding: 8px 10px;
1403 background: var( --os-ui-surface-sunken, #f0f0f1 );
1404 border: 1px solid var( --os-ui-border, #dcdcde );
1405 border-radius: 8px;
1406 color: var( --os-ui-fg-muted, #50575e );
1407 }
1408
1409 .os-settings__search svg {
1410 flex: 0 0 15px;
1411 width: 15px;
1412 height: 15px;
1413 }
1414
1415 /*
1416 * The class stack is the whole fix, not tidiness. The shell renders
1417 * inside a real wp-admin document, and TWO outrankers dress this
1418 * input as a second field inside the well: Core's forms.css styles
1419 * `input[type="search"]` at (0,1,1), and our own window-chrome rule
1420 * for raw controls in native window bodies weighs (0,2,1). Three
1421 * classes are (0,3,0) and beat both, and the input goes back to
1422 * being invisible: the FIELD is the control, the input is just
1423 * where the letters go.
1424 */
1425 .os-settings .os-settings__search .os-settings__search-input {
1426 flex: 1;
1427 min-width: 0;
1428 min-height: 0;
1429 padding: 0;
1430 border: 0;
1431 border-radius: 0;
1432 background: none;
1433 box-shadow: none;
1434 font: inherit;
1435 font-size: 14px;
1436 line-height: 21px;
1437 color: var( --os-ui-fg, #1d2327 );
1438 }
1439
1440 /* Chrome draws its own clear button on type=search; it lands on the
1441 pill's right edge and fights the rounding. */
1442 .os-settings__search-input::-webkit-search-cancel-button {
1443 appearance: none;
1444 }
1445
1446 .os-settings__search-input::placeholder {
1447 color: var( --os-ui-fg-muted, #50575e );
1448 opacity: 1;
1449 }
1450
1451 /* The field is the visible control, so the ring goes on it, not on
1452 the bare input inside. */
1453 .os-settings__search-field:focus-within {
1454 box-shadow: var( --os-ui-focus-ring-field, 0 0 0 1px #2271b1 );
1455 }
1456
1457 /* Same specificity story as above: Core's `input[type=search]:focus`
1458 is (0,2,1) and would draw its blue ring on the inner input. */
1459 .os-settings .os-settings__search .os-settings__search-input:focus,
1460 .os-settings .os-settings__search .os-settings__search-input:focus-visible {
1461 outline: none;
1462 border: 0;
1463 box-shadow: none;
1464 background: none;
1465 }
1466
1467 .os-settings__search-empty {
1468 grid-row: 3;
1469 margin: 0;
1470 padding: 4px 20px 12px;
1471 color: var( --os-ui-fg-muted, #50575e );
1472 font-size: 13px;
1473 }
1474
1475 /*
1476 * The column's surface has to survive the empty-state line being
1477 * hidden, which is most of the time: without this the sunken
1478 * background would stop at the bottom of the nav and leave the rest
1479 * of the column showing the pane's Obsidian.
1480 */
1481 .os-settings__search-empty[ hidden ] {
1482 display: block;
1483 padding: 0;
1484 font-size: 0;
1485 }
1486
1487 /*
1488 * Stand-in for a glyph a third-party tab cannot supply. Matches the
1489 * 17px the real icons are sized to in the component's shadow styles,
1490 * so the labels in a mixed column all start at the same x.
1491 */
1492 .os-settings__nav-glyph-blank {
1493 flex: 0 0 17px;
1494 width: 17px;
1495 }
1496
1497 /* Spans the three sidebar rows: the pane is one surface whatever the
1498 column beside it is doing. */
1499 .os-settings > os-tabpanel {
1500 grid-column: 2;
1501 grid-row: 1 / -1;
1502 min-height: 0;
1503 overflow: auto;
1504 padding: 28px 32px 44px;
1505 background: var( --os-ui-surface-sunken, #f0f0f1 );
1506 }
1507
1508 /*
1509 * Reset sits at the foot of the nav, and it resets everything.
1510 *
1511 * In the column rather than across the page because that is what it
1512 * addresses: the list names the pages, and the thing under the list
1513 * acts on all of them. Across the bottom of the pane it read as
1514 * belonging to whatever page happened to be open, which is the whole
1515 * reason it once had to grow a name and a sentence to disown that
1516 * reading.
1517 *
1518 * The column's own surface, so it continues the sidebar rather than
1519 * starting a third one. No rule above it, for the same reason the
1520 * sidebar has no divider against the pane: it is already the last
1521 * thing in a column that ends.
1522 */
1523 .os-settings > .os-settings__footer {
1524 grid-column: 1;
1525 grid-row: 4;
1526 display: flex;
1527 /*
1528 * Stated, not inherited. `<os-panel>` is a stack and sets
1529 * `flex-direction: column` on its own host, which would stretch
1530 * the button to the column's full width.
1531 */
1532 flex-direction: row;
1533 align-items: center;
1534 justify-content: flex-start;
1535 margin-top: 0;
1536 padding: 12px;
1537 background: var( --os-ui-surface, #fff );
1538 }
1539
1540 /*
1541 * The reset button at the mockup's measurements: a quiet 6% fill,
1542 * no border, inner radius, Caption type. All reachable through the
1543 * component's own custom-property knobs, so nothing here touches the
1544 * kit's defaults.
1545 */
1546 .os-settings > .os-settings__footer os-button {
1547 --os-ui-button-padding: 8px 15px;
1548 --os-ui-button-border-radius: 8px;
1549 --os-ui-button-border: 0;
1550 --os-ui-button-bg: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1551 font-size: 13px;
1552 }
1553
1554 /* Narrow windows: the sidebar would eat the pane, so fall back to the
1555 stacked layout the panel had before. Container-scoped, so it tracks
1556 the WINDOW being dragged narrow rather than the browser viewport. */
1557 @container ( max-width: 640px ) {
1558 .os-settings {
1559 grid-template-columns: minmax( 0, 1fr );
1560 grid-template-rows: auto minmax( 0, 1fr ) auto;
1561 }
1562 .os-settings > os-tabs {
1563 grid-column: 1;
1564 grid-row: 1;
1565 flex-direction: row;
1566 overflow-x: auto;
1567 overflow-y: hidden;
1568 }
1569 /*
1570 * Back to a strip, so the group gap becomes horizontal.
1571 *
1572 * The rows KEEP their vertical treatment, which this rule cannot
1573 * reach: it is styled inside the component's shadow root off
1574 * `data-orientation`, and a container query has no way in there.
1575 * They are full-bleed 40px rows laid in a horizontal line, which
1576 * reads as a strip of wide chips rather than as the compact tab
1577 * strip this width wants. Fixing it properly means the component
1578 * choosing its own layout at a width, not the panel overriding it
1579 * from outside; only the gap is restated here.
1580 */
1581 .os-settings > os-tabs > os-tab[ data-group-start='true' ] {
1582 margin-top: 0;
1583 margin-inline-start: 20px;
1584 }
1585 /*
1586 * The search field goes with the sidebar's width, which it no
1587 * longer has: a full-bleed input above a horizontal strip reads as
1588 * the window's search rather than the nav's. The strip is short
1589 * enough at this width to scan without it.
1590 */
1591 .os-settings__search,
1592 .os-settings__search-empty,
1593 .os-settings__search-empty[ hidden ] {
1594 display: none;
1595 }
1596 .os-settings > os-tabpanel {
1597 grid-column: 1;
1598 grid-row: 2;
1599 }
1600 /*
1601 * Row 3, not row 4. The wide layout has four rows and puts the
1602 * reset bar last; this one has three, so the wide declaration would
1603 * place the bar in a row that does not exist and the grid would
1604 * grow an implicit one below the fold.
1605 */
1606 .os-settings > .os-settings__footer {
1607 grid-row: 3;
1608 }
1609 /*
1610 * Half a pane this narrow is not a card any more, and the One dock
1611 * card has segmented controls inside it that would start clipping
1612 * their labels. The two stack instead.
1613 */
1614 .os-settings__layout-grid {
1615 grid-template-columns: minmax( 0, 1fr );
1616 }
1617 }
1618
1619 /* Components tab — split-pane layout. Don't scroll the whole panel;
1620 let the inner grid distribute height to nav + detail, each with
1621 their own overflow.
1622 *
1623 * The `:not([hidden])` guard is load-bearing: a bare
1624 * `os-tabpanel[for='help'] { display: flex }` rule has specificity
1625 * (0,0,2,1) which outranks the shadow-DOM `:host([hidden]) {
1626 * display: none }` rule (specificity 0,0,1,0), so the panel would
1627 * keep rendering when it should be hidden — bleeding the Components
1628 * UI into every other tab. Pinning the rule to the visible state
1629 * leaves the hidden state to the shadow-DOM default. */
1630 .os-settings > os-tabpanel[ for='help' ]:not( [ hidden ] ) {
1631 overflow: hidden;
1632 display: flex;
1633 flex-direction: column;
1634 /*
1635 * Square off the bottom inset. Every other page carries 44px there
1636 * so a scrolling column has somewhere to end; this one does not
1637 * scroll, so those extra 16px are just the panes stopping short of
1638 * the window. Matching the top puts the same frame on both ends.
1639 */
1640 padding-bottom: 28px;
1641 }
1642 .os-settings
1643 > os-tabpanel[ for='help' ]:not( [ hidden ] )
1644 > os-panel {
1645 flex: 1;
1646 min-height: 0;
1647 display: flex;
1648 flex-direction: column;
1649 }
1650
1651 .os-settings__help {
1652 flex: 1;
1653 min-height: 0;
1654 display: flex;
1655 flex-direction: column;
1656 gap: 12px;
1657 }
1658 /* The intro card (Component library description) shouldn't grow —
1659 it's content-sized so the grid below gets the remaining height. */
1660 .os-settings__help > os-section {
1661 flex: 0 0 auto;
1662 }
1663 .os-settings__help-layout {
1664 flex: 1;
1665 min-height: 0;
1666 }
1667
1668 /* Each split pane is its own scroll container. min-height:0 unlocks
1669 shrinking in a grid item (default is `auto` which equals content
1670 height — would defeat the overflow). max-height:100% pins the
1671 pane to the grid track. */
1672 .os-settings__help-nav,
1673 .os-settings__help-detail {
1674 min-height: 0;
1675 max-height: 100%;
1676 overflow-y: auto;
1677 }
1678
1679 /*
1680 * Features section — per-user opt-in toggles.
1681 *
1682 * Save status pill renders the live OS-settings save lifecycle
1683 * (`pending` → `saving` → `saved` / `failed`) so toggling a
1684 * per-user feature flag gets immediate, honest feedback instead
1685 * of an optimistic flicker.
1686 */
1687 .os-features__status-row {
1688 min-height: 22px;
1689 }
1690
1691 /*
1692 * One toggle + hint pair, in its own box.
1693 *
1694 * These used to be separated by a hairline above each item after the
1695 * first. A box does the same job without drawing a line: it groups the
1696 * label with its description and gives the eye a stopping point
1697 * between unrelated settings, and it says which text belongs to which
1698 * switch by enclosing it rather than by sitting between it and the
1699 * next one. No horizontal rule survives anywhere in this panel.
1700 */
1701 .os-features__item {
1702 display: flex;
1703 flex-direction: column;
1704 gap: 4px;
1705 padding: 12px 16px;
1706 background: var( --os-ui-surface, #fff );
1707 border-radius: 8px;
1708 }
1709
1710 .os-features__item + .os-features__item {
1711 margin-top: 12px;
1712 }
1713
1714 /* Feature title line: Body 14 / Medium 500. The outer-tree rule wins
1715 * over the component's own :host font-size, and the shadow label
1716 * inherits both properties from the host. */
1717 .os-features__item > os-checkbox-label {
1718 font-size: 14px;
1719 font-weight: 500;
1720 }
1721
1722 /* A functional notice (provider gating) gets a touch more air than
1723 * the 4px title-to-description rhythm: 4px gap + 4px margin = 8px. */
1724 .os-features__item > os-notice {
1725 margin-top: 4px;
1726 }
1727
1728 /* Sub-toggles (the window-links children) sit inside the parent box:
1729 * no box-in-box, indented 24px so their checkboxes align under the
1730 * parent's label text (16px box + 8px gap). Declared after the
1731 * sibling rule above so the 8px margin wins for nested siblings:
1732 * 4px parent gap + 8px margin = the same 12px block rhythm. */
1733 .os-features__item .os-features__item {
1734 margin-top: 8px;
1735 margin-left: 24px;
1736 padding: 0;
1737 background: none;
1738 border-radius: 0;
1739 }
1740
1741 /* "Reset what's-new dialogs" row: description first, button directly
1742 * under it on the shared 12px gap. `align-items: flex-start` keeps
1743 * the inline-flex `<os-button>` from stretching full-width inside
1744 * the column. */
1745 .os-features__row {
1746 display: flex;
1747 flex-direction: column;
1748 align-items: flex-start;
1749 gap: 12px;
1750 margin-top: 12px;
1751 padding: 12px 16px;
1752 background: var( --os-ui-surface, #fff );
1753 border-radius: 8px;
1754 }
1755
1756 .os-features__row > os-button {
1757 display: inline-flex;
1758 }
1759
1760 .os-features__row > .os-features__hint {
1761 margin: 0;
1762 }
1763
1764 /* "Delete folder sharing data": the one-line description and its
1765 * danger button read as a single row, button on the trailing edge.
1766 * 4px parent gap + 8px margin = the 12px block rhythm above it. */
1767 .os-features__danger-row {
1768 display: flex;
1769 align-items: center;
1770 justify-content: space-between;
1771 gap: 12px;
1772 margin-top: 8px;
1773 }
1774
1775 .os-features__danger-row > os-button {
1776 flex: 0 0 auto;
1777 }
1778
1779 /* Heartbeat rate: 8px label-to-control, description 8px below the
1780 * control (4px parent gap + 4px margin). */
1781 .os-features__select-label {
1782 display: flex;
1783 flex-direction: column;
1784 align-items: flex-start;
1785 gap: 8px;
1786 }
1787
1788 .os-features__select-title {
1789 font-size: 14px;
1790 font-weight: 500;
1791 }
1792
1793 .os-features__select-label + .os-features__hint {
1794 margin-top: 4px;
1795 }
1796
1797 .os-features__hint {
1798 margin: 0;
1799 font-size: 13px;
1800 color: var( --os-ui-fg-muted, #50575e );
1801 line-height: 1.5;
1802 }
1803
1804 .os-features__status {
1805 display: inline-flex;
1806 align-items: center;
1807 gap: 6px;
1808 font-size: 13px;
1809 font-weight: 500;
1810 padding: 2px 10px;
1811 border-radius: 999px;
1812 white-space: nowrap;
1813 transition: opacity 200ms ease;
1814 }
1815
1816 .os-features__status--saving {
1817 background: var( --os-ui-surface-elevated, #f0f0f1 );
1818 color: var( --os-ui-fg-muted, #50575e );
1819 }
1820
1821 .os-features__status--saved {
1822 background: rgba( 30, 132, 73, 0.12 );
1823 color: var( --os-ui-success-fg, #1d6f42 );
1824 }
1825
1826 .os-features__status--saved .dashicons {
1827 font-size: 14px;
1828 width: 14px;
1829 height: 14px;
1830 }
1831
1832 .os-features__status--failed {
1833 background: rgba( 214, 54, 56, 0.12 );
1834 color: var( --os-ui-danger-hover, #a02622 );
1835 }
1836
1837 .os-features__status--failed .dashicons {
1838 font-size: 14px;
1839 width: 14px;
1840 height: 14px;
1841 }
1842
1843 .os-features__status-dot {
1844 display: inline-block;
1845 width: 8px;
1846 height: 8px;
1847 border-radius: 50%;
1848 background: currentColor;
1849 animation: os-features-pulse 1.2s ease-in-out infinite;
1850 }
1851
1852 @keyframes os-features-pulse {
1853 0%, 100% { opacity: 0.35; transform: scale( 0.85 ); }
1854 50% { opacity: 1; transform: scale( 1 ); }
1855 }
1856
1857 /*
1858 * OS Settings panel header — hosts the global tabs strip and a single
1859 * `<os-save-status>` indicator that auto-listens to the save
1860 * lifecycle. The save status sits on the trailing edge of the header
1861 * so it's visible across every tab without competing with section
1862 * content.
1863 */
1864 .os-settings__header {
1865 display: flex;
1866 align-items: center;
1867 justify-content: space-between;
1868 gap: 16px;
1869 margin-bottom: 12px;
1870 }
1871
1872 .os-settings__header os-tabs {
1873 flex: 1 1 auto;
1874 min-width: 0;
1875 }
1876
1877 .os-settings__header os-save-status {
1878 flex: 0 0 auto;
1879 margin-inline-start: auto;
1880 }
1881
1882 /*
1883 * About tab — full-tabpanel PixiJS canvas. Every credit string and
1884 * the AUTOMATTIC logotype (formed by particles) render inside Pixi
1885 * itself; this stylesheet only sizes the host so the canvas fills
1886 * the available height + width without any HTML chrome around it.
1887 *
1888 * The chain is: tabpanel → os-panel → .about (flex column) →
1889 * .about-stage-host (the canvas mounts here). Each link sets
1890 * `flex: 1; min-height: 0;` so the canvas inherits the tabpanel's
1891 * full inner height instead of hugging an intrinsic minimum.
1892 *
1893 * @since 0.8.0
1894 */
1895 .os-settings
1896 > os-tabpanel[ for='about' ]:not( [ hidden ] ) {
1897 display: flex;
1898 flex-direction: column;
1899 overflow: hidden;
1900 }
1901
1902 .os-settings
1903 > os-tabpanel[ for='about' ]:not( [ hidden ] )
1904 > os-panel {
1905 flex: 1;
1906 min-height: 0;
1907 display: flex;
1908 flex-direction: column;
1909 }
1910
1911 .os-settings__about {
1912 flex: 1;
1913 min-height: 0;
1914 display: flex;
1915 flex-direction: column;
1916 margin: 0;
1917 /* Edge-to-edge — no border-radius / box-shadow framing — so the
1918 * canvas (and the logotype centred inside it) consume the entire
1919 * tabpanel surface. The gradient is just the colour-of-last-resort
1920 * underneath while Pixi initialises; once the canvas is rendering
1921 * it covers every pixel of this element anyway. */
1922 background: linear-gradient( 180deg, #060a1c 0%, #0c1733 100% );
1923 overflow: hidden;
1924 }
1925
1926 .os-settings__about-stage-host {
1927 flex: 1;
1928 min-height: 0;
1929 width: 100%;
1930 overflow: hidden;
1931 cursor: crosshair;
1932 }
1933
1934 .os-settings__about-stage-host canvas {
1935 display: block;
1936 width: 100%;
1937 height: 100%;
1938 }
1939
1940
1941 /* Apps & Icons section (since 0.25.0) — per-item placement chooser. */
1942 /*
1943 * The boxed list Núria pointed at when she asked for boxes instead of
1944 * rules. Each row encloses its own icon, title and placement select,
1945 * so a long list reads as a stack of things rather than as a table
1946 * that has lost its header.
1947 */
1948 .os-apps-icons__list {
1949 display: flex;
1950 flex-direction: column;
1951 gap: 6px;
1952 }
1953
1954 .os-apps-icons__row {
1955 display: flex;
1956 align-items: center;
1957 gap: 12px;
1958 padding: 8px 12px;
1959 background: var( --os-ui-surface, #fff );
1960 border-radius: 8px;
1961 }
1962
1963 .os-apps-icons__identity {
1964 display: flex;
1965 align-items: center;
1966 gap: 10px;
1967 flex: 1;
1968 min-width: 0;
1969 }
1970
1971 .os-apps-icons__icon {
1972 width: 24px;
1973 height: 24px;
1974 flex-shrink: 0;
1975 display: inline-flex;
1976 align-items: center;
1977 justify-content: center;
1978 }
1979
1980 .os-apps-icons__icon.dashicons {
1981 font-size: 20px;
1982 }
1983
1984 .os-apps-icons__title {
1985 overflow: hidden;
1986 text-overflow: ellipsis;
1987 white-space: nowrap;
1988 }
1989
1990 .os-apps-icons__row os-select {
1991 min-width: 180px;
1992 }
1993
1994 /* ------------------------------------------------------------------
1995 * OS Settings → Themes.
1996 *
1997 * Card grid of the site's desktop-theme library. Deliberately styled
1998 * with the SAME neutral palette as the rest of the panel rather than
1999 * with themed tokens: the picker has to stay legible while the user
2000 * is trying on themes, including ones with hostile contrast.
2001 * ------------------------------------------------------------------ */
2002
2003 .os-settings__theme-grid {
2004 display: grid;
2005 grid-template-columns: repeat( auto-fill, minmax( 160px, 1fr ) );
2006 gap: 12px;
2007 margin: 12px 0 16px;
2008 }
2009
2010 .os-settings__theme-card-wrap {
2011 position: relative;
2012 }
2013
2014 /*
2015 * Same geometry in every state: the border is 1px in both, and
2016 * selection is a box-shadow ring OUTSIDE the box, so an active card
2017 * occupies exactly the pixels an idle one does and the row stays
2018 * aligned. The selected ring is the flat accent, the same language
2019 * as the wallpaper tiles and layout cards beside it.
2020 */
2021 .os-settings__theme-card {
2022 display: flex;
2023 flex-direction: column;
2024 gap: 6px;
2025 width: 100%;
2026 height: 100%;
2027 padding: 10px;
2028 background: var( --os-ui-surface, #fff );
2029 border: 1px solid var( --os-ui-border, #dcdcde );
2030 border-radius: 10px;
2031 cursor: pointer;
2032 text-align: start;
2033 font: inherit;
2034 color: var( --os-ui-fg, #1d2327 );
2035 transition: border-color 0.15s ease, box-shadow 0.15s ease;
2036 }
2037
2038 .os-settings__theme-card:hover {
2039 border-color: var( --os-ui-border-strong, #8c8f94 );
2040 }
2041
2042 .os-settings__theme-card:focus-visible {
2043 outline: 2px solid var( --os-ui-accent, #2271b1 );
2044 outline-offset: 2px;
2045 }
2046
2047 .os-settings__theme-card[ aria-pressed="true" ] {
2048 border-color: transparent;
2049 box-shadow: 0 0 0 2px var( --os-ui-accent, #2271b1 );
2050 }
2051
2052 .os-settings__theme-preview {
2053 display: flex;
2054 align-items: center;
2055 justify-content: center;
2056 aspect-ratio: 16 / 10;
2057 overflow: hidden;
2058 border-radius: 6px;
2059 background: var( --os-ui-surface-elevated, #f0f0f1 );
2060 }
2061
2062 .os-settings__theme-preview img {
2063 width: 100%;
2064 height: 100%;
2065 object-fit: cover;
2066 display: block;
2067 }
2068
2069 /* Stand-in preview for "System default" — a miniature of the shipped
2070 * graphite desktop, so the card reads as a real option rather than an
2071 * absence. */
2072 .os-settings__theme-preview--system {
2073 background: linear-gradient( 135deg, #1d2327 0%, #2c3338 50%, #1d2327 100% );
2074 }
2075
2076 .os-settings__theme-initials {
2077 font-size: 24px;
2078 font-weight: 700;
2079 letter-spacing: 1px;
2080 color: var( --os-ui-fg-muted, #787c82 );
2081 }
2082
2083 .os-settings__theme-name {
2084 font-weight: 600;
2085 line-height: 1.3;
2086 overflow-wrap: anywhere;
2087 }
2088
2089 /* Wraps rather than clipping. This string is a theme's own
2090 description and a plugin can make it any length; a one-line box cut
2091 "The look OpenStation ships with" off mid-sentence. */
2092 .os-settings__theme-meta {
2093 font-size: 13px;
2094 color: var( --os-ui-fg-muted, #646970 );
2095 line-height: 1.45;
2096 min-height: 1.45em;
2097 overflow-wrap: anywhere;
2098 }
2099
2100 /* Delete overlay — outside the card button, because nesting a button
2101 * inside a button is invalid HTML and breaks keyboard traversal. */
2102 .os-settings__theme-delete {
2103 position: absolute;
2104 inset-block-start: 4px;
2105 inset-inline-end: 4px;
2106 display: flex;
2107 align-items: center;
2108 justify-content: center;
2109 width: 22px;
2110 height: 22px;
2111 padding: 0;
2112 border: none;
2113 border-radius: 50%;
2114 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
2115 color: var( --os-ui-fg-on-accent, #fff );
2116 font-size: 15px;
2117 line-height: 1;
2118 cursor: pointer;
2119 opacity: 0;
2120 transition: opacity 0.15s ease, background-color 0.15s ease;
2121 }
2122
2123 .os-settings__theme-card-wrap:hover
2124 .os-settings__theme-delete,
2125 .os-settings__theme-delete:focus-visible {
2126 opacity: 1;
2127 }
2128
2129 .os-settings__theme-delete:hover {
2130 background: var( --os-ui-danger, #d63638 );
2131 }
2132
2133 /*
2134 * "Apply <theme>'s recommended layout" — sits between the theme grid
2135 * and the upload tile, and only for a theme that recommends
2136 * something. Just the button: the dock resizing and the layout
2137 * moving is the feedback.
2138 */
2139 .os-settings__theme-recommendation {
2140 display: flex;
2141 margin-block: 16px;
2142 }
2143
2144 /*
2145 * The last cell of the theme grid, not a dropzone underneath it.
2146 * Uploading a theme is one of the ways to answer "which theme", so it
2147 * belongs in the row of answers; dashed because it is the only one
2148 * that cannot show you what you are picking until you have picked it.
2149 * Same reasoning, and the same treatment, as "Use your own image" in
2150 * the wallpaper grid.
2151 */
2152 .os-settings__theme-upload {
2153 display: block;
2154 /* Same reason as the other two dropzones above: the plain border
2155 token is the card's own colour and vanishes on it. */
2156 border: 2px dashed var( --os-ui-border-strong, #c3c4c7 );
2157 border-radius: 10px;
2158 background: transparent;
2159 transition: border-color 0.15s ease, background-color 0.15s ease;
2160 }
2161
2162 .os-settings__theme-upload-label {
2163 display: flex;
2164 flex-direction: column;
2165 align-items: center;
2166 justify-content: center;
2167 gap: 6px;
2168 /* Matches a theme card: 16:10 preview plus its name and meta. */
2169 min-height: 100%;
2170 padding: 16px;
2171 color: var( --os-ui-fg-muted, #50575e );
2172 cursor: pointer;
2173 text-align: center;
2174 }
2175
2176 .os-settings__theme-upload:hover,
2177 .os-settings__theme-upload--dragover {
2178 border-color: var( --wp-admin-theme-color, #2271b1 );
2179 background-color: rgba( 34, 113, 177, 0.06 );
2180 }
2181
2182 .os-settings__theme-upload--dragover {
2183 border-style: solid;
2184 }
2185
2186 .os-settings__theme-upload--busy
2187 .os-settings__theme-upload-label {
2188 cursor: progress;
2189 opacity: 0.7;
2190 }
2191
2192 .os-settings__theme-upload-plus {
2193 font-size: 22px;
2194 line-height: 1;
2195 }
2196