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

2,871 lines 78.4 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 * Inside a card, the control sits on the card's floor rather than
660 * under its description. The two cards share one height (grid rows
661 * stretch), but Split's description runs three lines to Unified's
662 * one, so a control that followed the text landed a couple of lines
663 * higher on one side. Pushed down, both controls sit on the same
664 * line and the cards read as the pair they are.
665 */
666 .os-settings__layout-card > .os-settings__dock-options {
667 margin-top: auto;
668 padding-top: 14px;
669 }
670
671 /*
672 * Dock size, under both cards rather than inside one: both layouts
673 * have a dock. Off the grid entirely, so it clears the taller card
674 * rather than sitting beside it, and on the section's own rhythm
675 * rather than the card's tighter one.
676 */
677 .os-settings__dock-options--page {
678 margin-top: 20px;
679 }
680
681 .os-settings__dock-option {
682 display: flex;
683 flex-direction: column;
684 align-items: flex-start;
685 gap: 8px;
686 }
687
688 .os-settings__dock-option-label {
689 font-size: 14px;
690 font-weight: 500;
691 color: var( --os-ui-fg, #1d2327 );
692 }
693
694 /*
695 * One line under a control saying what the picked value does. Dock
696 * behavior needs it where Dock size does not: "Compact" is
697 * self-explaining, "Dynamic" is a promise about the pointer.
698 */
699 .os-settings__dock-option-hint {
700 max-width: 56ch;
701 font-size: 12px;
702 line-height: 1.5;
703 color: var( --os-ui-fg-muted, #646970 );
704 }
705
706 /*
707 * The card is the BOX, not the control. It holds the radio that
708 * picks the layout and, for One dock, that layout's own options —
709 * and a radio cannot contain a segmented control, so the two are
710 * siblings inside it. See the docblock in
711 * `src/settings/sections/desktop-layout.ts`.
712 *
713 * Which means the box wears the state of a child it does not own.
714 * Both `:has()` rules name the CHOICE rather than the card, so a
715 * pointer resting on a segmented control does not lift the card, and
716 * a segment taking focus does not light the selection ring.
717 */
718 .os-settings__layout-card {
719 display: flex;
720 flex-direction: column;
721 padding: 10px;
722 background: var( --os-ui-surface-raised, rgba( 0, 0, 0, 0.04 ) );
723 border-radius: 10px;
724 transition: transform 0.15s ease, box-shadow 0.15s ease;
725 }
726
727 .os-settings__layout-card:has( .os-settings__layout-choice:hover ) {
728 transform: translateY( -2px );
729 }
730
731 /* The CHOICE's own checked state, not any checked thing in the card:
732 every card now holds a segmented control, and its picked segment
733 is `aria-checked` too. */
734 .os-settings__layout-card:has( .os-settings__layout-choice[ aria-checked='true' ] ) {
735 box-shadow: 0 0 0 2px var( --os-ui-accent, #2271b1 );
736 }
737
738 /* The radio: everything in the card that means "this layout". */
739 .os-settings__layout-choice {
740 display: flex;
741 flex-direction: column;
742 width: 100%;
743 padding: 0;
744 background: none;
745 border: 0;
746 border-radius: 8px;
747 font: inherit;
748 color: inherit;
749 text-align: start;
750 cursor: pointer;
751 }
752
753 .os-settings__layout-choice:focus-visible {
754 outline: none;
755 box-shadow: var( --os-ui-focus-ring, 0 0 0 2px #2271b1 );
756 }
757
758 /*
759 * A screen, so it is shaped like one: 16/9, not whatever shape is left
760 * over once the card has been sized. A full-width box at a fixed 78px
761 * ran about 6.7:1, and every shape inside is a percentage of it — so
762 * the two windows came out five times wider than they were tall and
763 * read as stacked bars rather than as windows, which is the one thing
764 * these previews exist to show.
765 *
766 * Capped in width rather than stretched, because 16/9 across a whole
767 * half-pane card is ~290px of preview for a schematic with four
768 * rectangles in it. The cap sets the height (288 → 162px); the card
769 * keeps its own width and the preview sits centred in it, which is why
770 * `inline-size` is a percentage with a ceiling instead of a fixed
771 * width: below the cap — the stacked single-column pane — it goes back
772 * to filling the card, still at 16/9.
773 */
774 .os-settings__layout-preview {
775 position: relative;
776 display: block;
777 inline-size: 100%;
778 max-inline-size: 288px;
779 aspect-ratio: 16 / 9;
780 margin-block-end: 9px;
781 margin-inline: auto;
782 overflow: hidden;
783 background: var( --os-ui-surface-sunken, #f0f0f1 );
784 border-radius: 8px;
785 }
786
787 /* A window: a filled rectangle with a boundary, because at this size
788 a borderless one dissolves into the Void behind it. */
789 .os-settings__layout-win {
790 position: absolute;
791 background: var( --os-ui-surface-elevated, #f6f7f7 );
792 border: 1px solid var( --os-ui-border-strong, #c3c4c7 );
793 border-radius: 4px;
794 }
795
796 /* A rail: the wp-admin menu, or the side rail. No border, because a
797 rail reads as a solid mass and a window does not. */
798 .os-settings__layout-bar {
799 position: absolute;
800 background: var( --os-ui-border-strong, #c3c4c7 );
801 border-radius: 3px;
802 }
803
804 /* A rail the user gets to place: the dock, and in Side bar the admin
805 menu beside it. The accent is what separates the rails from the
806 windows they sit next to, so a card can carry more than one. */
807 .os-settings__layout-win.is-accent,
808 .os-settings__layout-bar.is-accent {
809 background: var( --os-ui-accent, #2271b1 );
810 border-color: transparent;
811 transition: transform 0.35s cubic-bezier( 0.22, 1, 0.36, 1 ), opacity 0.35s ease;
812 }
813
814 /*
815 * A rail set to Dynamic is drawn as what the real one folds into: the
816 * thin indicator line at its edge (`dock.css`, the parked state). The
817 * card says which rails are folded — `is-dock-dynamic` for the bottom
818 * dock, `is-sidebar-dynamic` for Split's side rail — and the bar
819 * shrinks by TRANSFORM rather than by re-sizing, because its geometry
820 * is an inline style from the preview table in `desktop-layout.ts`
821 * and a transform leaves that table the single owner of where things
822 * sit. The dock keeps its feet on the floor (origin at the bottom
823 * edge, shrinking towards its centre), the sidebar stays on its wall
824 * (origin at the left edge, nudged in the way the real line sits 8px
825 * off the screen edge).
826 *
827 * The slow breathe is the one hint that this rail is not always
828 * there. It is opacity only, never scale, so the line's size stays
829 * an honest picture of the indicator; and it stops under reduced
830 * motion along with the fold itself.
831 */
832 .os-settings__layout-card.is-dock-dynamic .os-settings__layout-bar.is-dock {
833 transform: scale( 0.55, 0.4 );
834 transform-origin: 50% 100%;
835 animation: os-settings-layout-bar-breathe 2.6s ease-in-out infinite;
836 }
837
838 .os-settings__layout-card.is-sidebar-dynamic .os-settings__layout-bar.is-sidebar {
839 transform: translateX( 40% ) scale( 0.4, 0.45 );
840 transform-origin: 0 50%;
841 animation: os-settings-layout-bar-breathe 2.6s ease-in-out infinite;
842 }
843
844 @keyframes os-settings-layout-bar-breathe {
845 0%,
846 100% {
847 opacity: 1;
848 }
849
850 50% {
851 opacity: 0.55;
852 }
853 }
854
855 .os-settings__layout-name {
856 font-size: 14px;
857 font-weight: 500;
858 color: var( --os-ui-fg, #1d2327 );
859 }
860
861 .os-settings__layout-desc {
862 margin-top: 3px;
863 font-size: 13px;
864 line-height: 1.45;
865 color: var( --os-ui-fg-muted, #50575e );
866 }
867
868 @media ( prefers-reduced-motion: reduce ) {
869 .os-settings__layout-card {
870 transition-duration: 0.01ms;
871 }
872
873 .os-settings__layout-card:has( .os-settings__layout-choice:hover ) {
874 transform: none;
875 }
876
877 /* The fold lands at once and the line holds still. */
878 .os-settings__layout-bar.is-accent {
879 transition-duration: 0.01ms;
880 animation: none;
881 }
882 }
883
884 /* ---------------------------------------------------------------
885 * Media Library picker.
886 *
887 * Toolbar (search + HD toggle), responsive thumbnail grid, footer
888 * with count + Load more button.
889 * --------------------------------------------------------------- */
890 .os-settings__library-toolbar {
891 display: flex;
892 align-items: center;
893 gap: 12px;
894 margin-bottom: 10px;
895 flex-wrap: wrap;
896 }
897
898 .os-settings__library-search {
899 flex: 1;
900 min-width: 180px;
901 padding: 6px 10px;
902 border: 1px solid var( --os-ui-border, #dcdcde );
903 border-radius: 4px;
904 background: var( --os-ui-surface, #fff );
905 font: inherit;
906 font-size: 13px;
907 color: var( --os-ui-fg, #1d2327 );
908 }
909
910 .os-settings__library-search:focus {
911 border-color: var(--wp-admin-theme-color, #2271b1);
912 box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1);
913 outline: none;
914 }
915
916 /* `__library-hd` now renders as `<os-checkbox-label>`. */
917
918 .os-settings__library-grid {
919 display: grid;
920 grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
921 gap: 8px;
922 min-height: 140px;
923 }
924
925 .os-settings__library-tile {
926 position: relative;
927 aspect-ratio: 16 / 10;
928 padding: 0;
929 border: 2px solid transparent;
930 border-radius: 6px;
931 background-color: var( --os-ui-surface-sunken, #eef0f1 );
932 background-size: cover;
933 background-position: center;
934 background-repeat: no-repeat;
935 cursor: pointer;
936 overflow: hidden;
937 transition: border-color 0.15s ease, transform 0.15s ease;
938 }
939
940 .os-settings__library-tile:hover {
941 transform: translateY(-1px);
942 border-color: rgba(34, 113, 177, 0.4);
943 }
944
945 .os-settings__library-tile:focus-visible {
946 outline: 2px solid var(--wp-admin-theme-color, #2271b1);
947 outline-offset: 2px;
948 }
949
950 .os-settings__library-tile--selected,
951 .os-settings__library-tile[aria-pressed="true"] {
952 border-color: var(--wp-admin-theme-color, #2271b1);
953 box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9) inset;
954 }
955
956 /* Dimension badge in the corner — readable over any thumbnail. */
957 .os-settings__library-tile-dims {
958 position: absolute;
959 bottom: 4px;
960 inset-inline-end: 4px;
961 padding: 2px 6px;
962 background: var( --os-ui-scrim, rgba(0, 0, 0, 0.6) );
963 color: var( --os-ui-fg-on-accent, #fff );
964 font-size: 10px;
965 font-weight: 500;
966 line-height: 1.3;
967 border-radius: 3px;
968 letter-spacing: 0.02em;
969 font-variant-numeric: tabular-nums;
970 pointer-events: none;
971 }
972
973 /* Skeleton tiles during the first-page fetch. Animated subtle pulse
974 * so the user perceives progress without us needing a spinner. */
975 .os-settings__library-tile--skeleton {
976 cursor: default;
977 background: linear-gradient(
978 90deg,
979 var( --os-ui-surface-sunken, #eef0f1 ) 0%,
980 #dcdcde 50%,
981 var( --os-ui-surface-sunken, #eef0f1 ) 100%
982 );
983 background-size: 200% 100%;
984 animation: os-library-shimmer 1.1s ease-in-out infinite;
985 }
986
987 .os-settings__library-tile--skeleton:hover {
988 transform: none;
989 border-color: transparent;
990 }
991
992 @keyframes os-library-shimmer {
993 from {
994 background-position: 200% 0;
995 }
996 to {
997 background-position: -200% 0;
998 }
999 }
1000
1001 .os-settings__library-empty,
1002 .os-settings__library-error {
1003 grid-column: 1 / -1;
1004 margin: 0;
1005 padding: 24px 16px;
1006 text-align: center;
1007 color: var( --os-ui-fg-muted, #646970 );
1008 font-size: 13px;
1009 }
1010
1011 .os-settings__library-error {
1012 color: var( --os-ui-danger-hover, #b32d2e );
1013 }
1014
1015 .os-settings__library-footer {
1016 display: flex;
1017 align-items: center;
1018 justify-content: space-between;
1019 margin-top: 10px;
1020 gap: 10px;
1021 min-height: 24px;
1022 }
1023
1024 .os-settings__library-meta {
1025 color: var( --os-ui-fg-muted, #8c8f94 );
1026 font-size: 12px;
1027 }
1028
1029 /* `__library-load-more` now renders as `<os-button variant="ghost">`. */
1030
1031 /* ---------------------------------------------------------------
1032 * Reduced-motion: collapse every OS-Settings animation/transition to
1033 * near-zero so motion-sensitive users still get the layout changes
1034 * (hover feedback, expanded editor, tile highlights) but without the
1035 * glide. Kept as a single scoped block so the intent is readable at
1036 * a glance rather than spread across each rule.
1037 * --------------------------------------------------------------- */
1038 @media (prefers-reduced-motion: reduce) {
1039 .os-settings__editor-slot,
1040 .os-settings__image-picker-slot,
1041 .os-settings__wallpaper-add,
1042 .os-settings__wallpaper-config-slot,
1043 .os-settings__upload-tile,
1044 .os-settings__library-tile,
1045 os-swatch {
1046 transition-duration: 0.01ms !important;
1047 }
1048
1049 .os-settings__library-tile--skeleton {
1050 animation: none;
1051 }
1052 }
1053
1054 /* AI settings section — error and saving feedback */
1055 .os-ai-settings__error {
1056 margin: 6px 0 0;
1057 font-size: 13px;
1058 color: var( --os-ui-danger, #d63638 );
1059 }
1060
1061 .os-ai-settings__saving {
1062 margin: 6px 0 0;
1063 font-size: 13px;
1064 color: var( --os-ui-fg-muted, #646970 );
1065 font-style: italic;
1066 }
1067
1068 /* Extended options section — hint paragraph + error/saving states */
1069 .os-ext__error {
1070 margin: 6px 0 0;
1071 font-size: 13px;
1072 color: var( --os-ui-danger, #d63638 );
1073 }
1074
1075 .os-ext__saving {
1076 margin: 6px 0 0;
1077 font-size: 13px;
1078 color: var( --os-ui-fg-muted, #646970 );
1079 font-style: italic;
1080 }
1081
1082 /*
1083 * Help tab — developer-facing component reference. Two-column layout
1084 * (nav + detail) on desktop, stacks naturally on narrow windows via
1085 * the same container the OS Settings panel lives in.
1086 */
1087
1088 .os-settings__help-layout {
1089 display: grid;
1090 grid-template-columns: 200px 1fr;
1091 gap: 20px;
1092 }
1093
1094 @container (max-width: 640px) {
1095 .os-settings__help-layout {
1096 grid-template-columns: 1fr;
1097 }
1098 }
1099
1100 /*
1101 * Missing-import warner demo — the sentence and the snippet it talks
1102 * about, in the section box.
1103 *
1104 * The snippet is shown because the section's whole subject is three
1105 * tags and it used to show none of them: the live copies have to be
1106 * real elements for the warner to fire, and a real element is exactly
1107 * what cannot be read, so the box sat empty under a sentence pointing
1108 * at nothing.
1109 *
1110 * One column by default; two equal halves once the window is wide
1111 * enough for the prose to still have a paragraph's worth of room
1112 * beside the code.
1113 *
1114 * Both tracks are `1fr` — the split is the box in half, and neither
1115 * side carries a fixed width. A hard cap on the prose would leave a
1116 * gutter of nothing between two columns that both had more to say.
1117 * `minmax( 0, … )` on each so a long line of code shrinks the track to
1118 * its share and scrolls inside the `<pre>` rather than pushing the
1119 * grid wider than the section box.
1120 *
1121 * The container is the OS Settings WINDOW (`container-type` further
1122 * down this file), not the viewport: this panel is a window inside the
1123 * shell, so window width is the only width that moves here.
1124 */
1125 .os-settings__help-warner {
1126 display: grid;
1127 gap: 16px;
1128 align-items: start;
1129 }
1130
1131 @container (min-width: 1350px) {
1132 .os-settings__help-warner {
1133 grid-template-columns: minmax( 0, 1fr ) minmax( 0, 1fr );
1134 }
1135 }
1136
1137 .os-settings__help-warner-text {
1138 margin: 0;
1139 font-size: 14px;
1140 line-height: 1.55;
1141 color: var( --os-ui-fg-muted, #646970 );
1142 }
1143
1144 /*
1145 * `overflow-x: auto` rather than wrapping: the first tag is 56
1146 * characters of deliberately absurd component name, and breaking a tag
1147 * across lines turns the one thing the reader is here to recognise
1148 * into two things. The section box clips (`::part( body )` sets
1149 * `overflow: hidden`), so the scroll has to live on the `<pre>`.
1150 */
1151 .os-settings__help-warner-code {
1152 margin: 0;
1153 padding: 12px 14px;
1154 overflow-x: auto;
1155 background: var( --os-ui-surface, #fff );
1156 border: 1px solid var( --os-ui-border, #dcdcde );
1157 border-radius: 6px;
1158 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1159 font-size: 12px;
1160 line-height: 1.6;
1161 color: var( --os-ui-fg, #1d2327 );
1162 tab-size: 2;
1163 }
1164
1165 /*
1166 * The `<code>` inside is the whole block, not a run of inline code.
1167 *
1168 * Left inline it inherits wp-admin's own `code` chip — a wash and a
1169 * padding meant for a word mid-sentence — and an inline box wrapped
1170 * over nine lines paints that wash once per LINE BOX, so the two blank
1171 * lines in the snippet cut it into three chips and the one code block
1172 * read as three. `display: block` gives it a single box, and the
1173 * `<pre>` above is the only surface.
1174 */
1175 .os-settings__help-warner-code code {
1176 display: block;
1177 padding: 0;
1178 border: 0;
1179 background: none;
1180 font: inherit;
1181 color: inherit;
1182 }
1183
1184 /*
1185 * Search box + nav share one column so the field stays pinned while
1186 * the component list below it scrolls.
1187 */
1188 .os-settings__help-sidebar {
1189 display: flex;
1190 flex-direction: column;
1191 gap: 8px;
1192 min-height: 0;
1193 }
1194
1195 .os-settings__help-nav {
1196 display: flex;
1197 flex-direction: column;
1198 gap: 2px;
1199 align-self: stretch;
1200 min-height: 0;
1201 overflow-y: auto;
1202 padding: 4px;
1203 background: var( --os-ui-surface-elevated, #f6f7f7 );
1204 border: 1px solid var( --os-ui-border, #dcdcde );
1205 border-radius: 8px;
1206 }
1207
1208 .os-settings__help-nav-empty {
1209 margin: 0;
1210 padding: 8px 10px;
1211 font-size: 13px;
1212 color: var( --os-ui-fg-muted, #646970 );
1213 }
1214
1215 .os-settings__help-nav-item {
1216 display: flex;
1217 flex-direction: column;
1218 gap: 2px;
1219 align-items: flex-start;
1220 width: 100%;
1221 padding: 6px 10px;
1222 background: transparent;
1223 border: 1px solid transparent;
1224 border-radius: 6px;
1225 text-align: start;
1226 cursor: pointer;
1227 font: inherit;
1228 color: inherit;
1229 }
1230
1231 .os-settings__help-nav-item:hover {
1232 background: var( --os-ui-surface, #fff );
1233 border-color: var( --os-ui-border, #dcdcde );
1234 }
1235
1236 .os-settings__help-nav-item.is-active {
1237 background: var( --os-ui-surface, #fff );
1238 border-color: var( --wp-admin-theme-color, #2271b1 );
1239 }
1240
1241 .os-settings__help-nav-title {
1242 font-weight: 600;
1243 font-size: 13px;
1244 }
1245
1246 .os-settings__help-nav-tag {
1247 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1248 font-size: 12px;
1249 color: var( --os-ui-fg-muted, #646970 );
1250 }
1251
1252 .os-settings__help-detail {
1253 min-width: 0;
1254 padding: 16px 18px 18px;
1255 background: var( --os-ui-surface-elevated, #f6f7f7 );
1256 border: 1px solid var( --os-ui-border, #dcdcde );
1257 border-radius: 8px;
1258 }
1259
1260 .os-settings__help-head {
1261 display: flex;
1262 flex-wrap: wrap;
1263 align-items: baseline;
1264 gap: 8px;
1265 margin: 0 0 8px;
1266 }
1267
1268 .os-settings__help-title {
1269 margin: 0;
1270 font-size: 16px;
1271 font-weight: 600;
1272 }
1273
1274 .os-settings__help-code {
1275 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1276 font-size: 13px;
1277 color: var( --os-ui-fg-muted, #646970 );
1278 }
1279
1280 .os-settings__help-badge {
1281 padding: 1px 8px;
1282 border-radius: 999px;
1283 font-size: 13px;
1284 font-weight: 600;
1285 background: var( --os-ui-surface-sunken, #e4e7eb );
1286 color: var( --os-ui-fg, #1d2327 );
1287 }
1288
1289 .os-settings__help-badge.is-experimental {
1290 background: var( --os-ui-warning-bg, #fcf9e8 );
1291 color: var( --os-ui-warning-fg, #996800 );
1292 }
1293
1294 .os-settings__help-badge.is-planned {
1295 background: var( --os-ui-info-bg, #e1e8ff );
1296 color: var( --os-ui-info-fg, #123fb3 );
1297 }
1298
1299 .os-settings__help-summary {
1300 margin: 0 0 16px;
1301 color: var( --os-ui-fg, #1d2327 );
1302 }
1303
1304 .os-settings__help-group {
1305 margin: 0 0 16px;
1306 }
1307
1308 .os-settings__help-group h4 {
1309 margin: 0 0 8px;
1310 font-size: 13px;
1311 font-weight: 600;
1312 color: var( --os-ui-fg-muted, #646970 );
1313 }
1314
1315 .os-settings__help-example {
1316 padding: 16px;
1317 background: var( --os-ui-surface, #fff );
1318 border: 1px dashed var( --os-ui-border, #c3c4c7 );
1319 border-radius: 6px;
1320 }
1321
1322 .os-settings__help-table {
1323 width: 100%;
1324 border-collapse: collapse;
1325 font-size: 13px;
1326 }
1327
1328 /*
1329 * Rows are told apart by a wash rather than by a rule, which is the
1330 * one place in the panel that needed a replacement rather than just a
1331 * deletion: a props table with neither lines nor banding is a wall of
1332 * words. Zebra striping carries the row across without drawing
1333 * anything horizontal.
1334 */
1335 .os-settings__help-table th,
1336 .os-settings__help-table td {
1337 padding: 7px 8px;
1338 text-align: start;
1339 vertical-align: top;
1340 }
1341
1342 .os-settings__help-table tbody tr:nth-child( odd ) {
1343 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1344 }
1345
1346 .os-settings__help-table th {
1347 font-weight: 600;
1348 color: var( --os-ui-fg-muted, #646970 );
1349 background: var( --os-ui-surface, #fff );
1350 }
1351
1352 .os-settings__help-table code,
1353 .os-settings__help-list code {
1354 font-family: var( --os-ui-font-mono, Menlo, Consolas, monospace );
1355 font-size: 12px;
1356 padding: 1px 4px;
1357 background: var( --os-ui-surface, #fff );
1358 border: 1px solid var( --os-ui-border, #dcdcde );
1359 border-radius: 3px;
1360 }
1361
1362 .os-settings__help-list {
1363 display: flex;
1364 flex-direction: column;
1365 gap: 4px;
1366 margin: 0;
1367 padding: 0;
1368 list-style: none;
1369 }
1370
1371 .os-settings__help-list li {
1372 padding: 6px 10px;
1373 background: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1374 border-radius: 6px;
1375 font-size: 13px;
1376 }
1377
1378 .os-settings__help-note {
1379 margin: 12px 0 0;
1380 padding: 10px 12px;
1381 background: var( --os-ui-warning-bg, #fcf9e8 );
1382 border: 1px solid var( --os-ui-warning-border, #f5e6a5 );
1383 border-radius: 6px;
1384 font-size: 13px;
1385 color: var( --os-ui-warning-fg, #996800 );
1386 }
1387
1388 /* ---------------------------------------------------------------
1389 * Window-fit scrolling.
1390 *
1391 * Without this block the OS Settings panel grows past the native
1392 * window's viewport and the whole body scrolls — fine for short tabs
1393 * but actively wrong for the Components tab, where a 200px nav rail
1394 * and a tall detail pane should each scroll inside their own column,
1395 * not in lockstep down a giant single-column page.
1396 *
1397 * Strategy:
1398 * 1. The native window body normally has overflow:auto. Override it
1399 * to `hidden` ONLY when the OS Settings root is mounted (via
1400 * :has()) so other native windows keep their default page-scroll.
1401 * 2. Make the OS Settings root a flex column that fills the body.
1402 * The tab strip + footer hug the top/bottom; the active
1403 * tabpanel takes the remaining height.
1404 * 3. Each tabpanel scrolls itself (overflow:auto) — replaces the
1405 * whole-body scroll, visually identical for the simple tabs
1406 * (Appearance, AI, Extended) but pins the reset footer.
1407 * 4. The Components tab opts OUT of overflow:auto so its inner
1408 * grid (nav + detail) can scroll independently per pane.
1409 *
1410 * `:has()` is supported in every evergreen engine (Chrome 105+,
1411 * Safari 15.4+, Firefox 121+); fine for an admin-context shell.
1412 * --------------------------------------------------------------- */
1413 /*
1414 * NOTE: the rule that used to sit here was
1415 * `.os-window__body--native:has( .os-settings )`, and it never once
1416 * matched. `renderOsSettingsPanel()` adds the `os-settings` class TO
1417 * the native window body rather than to a child of it, so the two
1418 * selectors name the same element, and `:has()` only ever looks at
1419 * descendants. The `overflow: hidden` it wanted now sits on
1420 * `.os-settings` itself, where it reaches the element it was written
1421 * for; its `display: flex` is dropped, because the grid this file
1422 * declares below superseded that column layout.
1423 *
1424 * The query context for every narrow-layout rule in this file goes on
1425 * the WINDOW, one level up. A container query styles a container's
1426 * descendants and never the container itself, so it cannot live on
1427 * the panel that has to respond to it. `:has()` does the right thing
1428 * from here: the panel really is a descendant of the window, so only
1429 * the OS Settings window becomes a container and every other native
1430 * window is left alone.
1431 *
1432 * It has to be a container and not the viewport. OS Settings is a
1433 * window inside the shell, so dragging that window narrow never
1434 * changes the viewport width; the `@media ( max-width: 640px )` rule
1435 * this replaced could only fire when the whole BROWSER went under the
1436 * breakpoint, which is both the wrong moment and, on a desktop,
1437 * never. The Components tab has wanted this since it was written: its
1438 * own `@container` rule further up this file had no container to
1439 * resolve against and so had never matched either.
1440 */
1441 .os-window--native:has( .os-settings ) {
1442 container-type: inline-size;
1443 }
1444
1445 /*
1446 * Two columns, two rows: the sidebar down the left, every pane sharing
1447 * the cell on the right (only the unhidden one takes up space), and
1448 * the footer spanning both.
1449 */
1450 /*
1451 * Two columns, four rows. The sidebar column stacks the search field,
1452 * the scrolling strip and the empty-state line; the pane spans all
1453 * three of those rows in column 2, and the reset bar spans both
1454 * columns underneath. Four rows rather than a wrapper element because
1455 * the strip and the panes have to stay siblings: see the note in
1456 * `panel.ts` where they are rendered.
1457 */
1458 .os-settings {
1459 flex: 1;
1460 min-height: 0;
1461 display: grid;
1462 grid-template-columns: var( --os-settings-nav-width, 224px ) minmax( 0, 1fr );
1463 grid-template-rows: auto minmax( 0, 1fr ) auto auto;
1464 /*
1465 * The native window body defaults to overflow: auto. Left alone,
1466 * a tall page scrolls the whole panel and takes the sidebar and
1467 * the reset bar with it; each tabpanel already scrolls itself, so
1468 * this pins the furniture and lets the content move inside it.
1469 * See the note above about where this declaration used to live.
1470 */
1471 overflow: hidden;
1472 }
1473
1474 /*
1475 * The sidebar column. Obsidian against the pane's Void, so the
1476 * column reads as part of the window furniture (it continues the
1477 * title bar) while the content sits on the deepest surface. No
1478 * dividing rule between them: two surfaces meeting IS the boundary,
1479 * and a line drawn on top of a tonal step only says the same thing
1480 * twice.
1481 *
1482 * The search field, the strip and the empty-state line are three
1483 * SIBLINGS assembled into one column by the grid rather than one
1484 * wrapped element, because `<os-tabs>` drives the panes by finding
1485 * `os-tabpanel` children of its own parent. A sidebar div around the
1486 * strip puts every pane out of its reach and the panel renders all
1487 * nine at once. The shared background is what makes the three read as
1488 * one surface.
1489 */
1490 .os-settings__search,
1491 .os-settings > os-tabs,
1492 .os-settings__search-empty {
1493 grid-column: 1;
1494 background: var( --os-ui-surface, #fff );
1495 }
1496
1497 /*
1498 * No inline padding on the strip, which is load-bearing rather than
1499 * tidy. The selected row's accent is a 2px edge at
1500 * `inset-inline-start: 0`, so any padding here would float it in the
1501 * middle of the gutter instead of sitting it on the column edge. The
1502 * rows carry their own inline padding to hold the labels off it.
1503 */
1504 .os-settings > os-tabs {
1505 grid-row: 2;
1506 min-height: 0;
1507 overflow-y: auto;
1508 }
1509
1510 /*
1511 * The only vertical space in the column, and the entire grouping
1512 * mechanism. `panel.ts` stamps this on the first row of each band;
1513 * see `navGroup()` there for why the bands come from tab order.
1514 */
1515 .os-settings > os-tabs > os-tab[ data-group-start='true' ] {
1516 margin-top: 20px;
1517 }
1518
1519 /*
1520 * Filtered out by the search field. A plain `display: none` rather
1521 * than anything cleverer, so a hidden row takes its group gap with it
1522 * and a filtered list closes up instead of keeping the shape of the
1523 * one it came from.
1524 */
1525 .os-settings > os-tabs > os-tab[ data-search-hidden ] {
1526 display: none;
1527 }
1528
1529 /*
1530 * Search. Sits above the nav rather than over the content, because
1531 * what it filters is the nav.
1532 *
1533 * The field wears the row's own geometry (same 14px, same Ash) so the
1534 * column reads as one list with a way in at the top, rather than as a
1535 * form control that happens to have been parked above some links.
1536 */
1537 /*
1538 * The cell holds the column's surface and the field's inset; the
1539 * field inside it holds the pill. Two elements because the surface has
1540 * to run edge to edge (it is the sidebar) while the field must not.
1541 */
1542 .os-settings__search {
1543 grid-row: 1;
1544 padding: 12px 12px 14px;
1545 }
1546
1547 /*
1548 * A sunken well in the Obsidian column: the field is a window onto
1549 * the pane's Void, with the same Astro border the section boxes
1550 * wear, at the inner radius (8px). The rows below it are
1551 * square-edged and full-bleed, so the inset alone already says "not
1552 * one of them".
1553 *
1554 * The field wears the row's own type: 14px Regular on Ash, a 15px
1555 * glyph at full strength. 8px top and bottom around a 21px line puts
1556 * the content box at 37px.
1557 */
1558 .os-settings__search-field {
1559 display: flex;
1560 align-items: center;
1561 gap: 8px;
1562 padding: 8px 10px;
1563 background: var( --os-ui-surface-sunken, #f0f0f1 );
1564 border: 1px solid var( --os-ui-border, #dcdcde );
1565 border-radius: 8px;
1566 color: var( --os-ui-fg-muted, #50575e );
1567 }
1568
1569 .os-settings__search svg {
1570 flex: 0 0 18px;
1571 width: 18px;
1572 height: 18px;
1573 }
1574
1575 /*
1576 * The class stack is the whole fix, not tidiness. The shell renders
1577 * inside a real wp-admin document, and TWO outrankers dress this
1578 * input as a second field inside the well: Core's forms.css styles
1579 * `input[type="search"]` at (0,1,1), and our own window-chrome rule
1580 * for raw controls in native window bodies weighs (0,2,1). Three
1581 * classes are (0,3,0) and beat both, and the input goes back to
1582 * being invisible: the FIELD is the control, the input is just
1583 * where the letters go.
1584 */
1585 .os-settings .os-settings__search .os-settings__search-input {
1586 flex: 1;
1587 min-width: 0;
1588 min-height: 0;
1589 padding: 0;
1590 border: 0;
1591 border-radius: 0;
1592 background: none;
1593 box-shadow: none;
1594 font: inherit;
1595 font-size: 14px;
1596 line-height: 21px;
1597 color: var( --os-ui-fg, #1d2327 );
1598 }
1599
1600 /* Chrome draws its own clear button on type=search; it lands on the
1601 pill's right edge and fights the rounding. */
1602 .os-settings__search-input::-webkit-search-cancel-button {
1603 appearance: none;
1604 }
1605
1606 .os-settings__search-input::placeholder {
1607 color: var( --os-ui-fg-muted, #50575e );
1608 opacity: 1;
1609 }
1610
1611 /* The field is the visible control, so the ring goes on it, not on
1612 the bare input inside. */
1613 .os-settings__search-field:focus-within {
1614 box-shadow: var( --os-ui-focus-ring-field, 0 0 0 1px #2271b1 );
1615 }
1616
1617 /* Same specificity story as above: Core's `input[type=search]:focus`
1618 is (0,2,1) and would draw its blue ring on the inner input. */
1619 .os-settings .os-settings__search .os-settings__search-input:focus,
1620 .os-settings .os-settings__search .os-settings__search-input:focus-visible {
1621 outline: none;
1622 border: 0;
1623 box-shadow: none;
1624 background: none;
1625 }
1626
1627 .os-settings__search-empty {
1628 grid-row: 3;
1629 margin: 0;
1630 padding: 4px 20px 12px;
1631 color: var( --os-ui-fg-muted, #50575e );
1632 font-size: 13px;
1633 }
1634
1635 /*
1636 * The column's surface has to survive the empty-state line being
1637 * hidden, which is most of the time: without this the sunken
1638 * background would stop at the bottom of the nav and leave the rest
1639 * of the column showing the pane's Obsidian.
1640 */
1641 .os-settings__search-empty[ hidden ] {
1642 display: block;
1643 padding: 0;
1644 font-size: 0;
1645 }
1646
1647 /*
1648 * Stand-in for a glyph a third-party tab cannot supply. Matches the
1649 * 17px the real icons are sized to in the component's shadow styles,
1650 * so the labels in a mixed column all start at the same x.
1651 */
1652 .os-settings__nav-glyph-blank {
1653 flex: 0 0 17px;
1654 width: 17px;
1655 }
1656
1657 /* Spans the three sidebar rows: the pane is one surface whatever the
1658 column beside it is doing. */
1659 .os-settings > os-tabpanel {
1660 grid-column: 2;
1661 grid-row: 1 / -1;
1662 min-height: 0;
1663 overflow: auto;
1664 padding: 28px 32px 44px;
1665 background: var( --os-ui-surface-sunken, #f0f0f1 );
1666 }
1667
1668 /*
1669 * Reset sits at the foot of the nav, and it resets everything.
1670 *
1671 * In the column rather than across the page because that is what it
1672 * addresses: the list names the pages, and the thing under the list
1673 * acts on all of them. Across the bottom of the pane it read as
1674 * belonging to whatever page happened to be open, which is the whole
1675 * reason it once had to grow a name and a sentence to disown that
1676 * reading.
1677 *
1678 * The column's own surface, so it continues the sidebar rather than
1679 * starting a third one. No rule above it, for the same reason the
1680 * sidebar has no divider against the pane: it is already the last
1681 * thing in a column that ends.
1682 */
1683 .os-settings > .os-settings__footer {
1684 grid-column: 1;
1685 grid-row: 4;
1686 display: flex;
1687 /*
1688 * Stated, not inherited. `<os-panel>` is a stack and sets
1689 * `flex-direction: column` on its own host, which would stretch
1690 * the button to the column's full width.
1691 */
1692 flex-direction: row;
1693 align-items: center;
1694 justify-content: flex-start;
1695 margin-top: 0;
1696 padding: 12px;
1697 background: var( --os-ui-surface, #fff );
1698 }
1699
1700 /*
1701 * The reset button at the mockup's measurements: a quiet 6% fill,
1702 * no border, inner radius, Caption type. All reachable through the
1703 * component's own custom-property knobs, so nothing here touches the
1704 * kit's defaults.
1705 */
1706 .os-settings > .os-settings__footer os-button {
1707 --os-ui-button-padding: 8px 15px;
1708 --os-ui-button-border-radius: 8px;
1709 --os-ui-button-border: 0;
1710 --os-ui-button-bg: var( --os-ui-hover, rgba( 0, 0, 0, 0.05 ) );
1711 font-size: 13px;
1712 }
1713
1714 /* Narrow windows: the sidebar would eat the pane, so fall back to the
1715 stacked layout the panel had before. Container-scoped, so it tracks
1716 the WINDOW being dragged narrow rather than the browser viewport. */
1717 @container ( max-width: 640px ) {
1718 .os-settings {
1719 grid-template-columns: minmax( 0, 1fr );
1720 grid-template-rows: auto minmax( 0, 1fr ) auto;
1721 }
1722 .os-settings > os-tabs {
1723 grid-column: 1;
1724 grid-row: 1;
1725 flex-direction: row;
1726 overflow-x: auto;
1727 overflow-y: hidden;
1728 }
1729 /*
1730 * Back to a strip, so the group gap becomes horizontal.
1731 *
1732 * The rows KEEP their vertical treatment, which this rule cannot
1733 * reach: it is styled inside the component's shadow root off
1734 * `data-orientation`, and a container query has no way in there.
1735 * They are full-bleed 40px rows laid in a horizontal line, which
1736 * reads as a strip of wide chips rather than as the compact tab
1737 * strip this width wants. Fixing it properly means the component
1738 * choosing its own layout at a width, not the panel overriding it
1739 * from outside; only the gap is restated here.
1740 */
1741 .os-settings > os-tabs > os-tab[ data-group-start='true' ] {
1742 margin-top: 0;
1743 margin-inline-start: 20px;
1744 }
1745 /*
1746 * The search field goes with the sidebar's width, which it no
1747 * longer has: a full-bleed input above a horizontal strip reads as
1748 * the window's search rather than the nav's. The strip is short
1749 * enough at this width to scan without it.
1750 */
1751 .os-settings__search,
1752 .os-settings__search-empty,
1753 .os-settings__search-empty[ hidden ] {
1754 display: none;
1755 }
1756 .os-settings > os-tabpanel {
1757 grid-column: 1;
1758 grid-row: 2;
1759 }
1760 /*
1761 * Row 3, not row 4. The wide layout has four rows and puts the
1762 * reset bar last; this one has three, so the wide declaration would
1763 * place the bar in a row that does not exist and the grid would
1764 * grow an implicit one below the fold.
1765 */
1766 .os-settings > .os-settings__footer {
1767 grid-row: 3;
1768 }
1769 /*
1770 * Half a pane this narrow is not a card any more, and the One dock
1771 * card has segmented controls inside it that would start clipping
1772 * their labels. The two stack instead.
1773 */
1774 .os-settings__layout-grid {
1775 grid-template-columns: minmax( 0, 1fr );
1776 }
1777 }
1778
1779 /* Components tab — split-pane layout. Don't scroll the whole panel;
1780 let the inner grid distribute height to nav + detail, each with
1781 their own overflow.
1782 *
1783 * The `:not([hidden])` guard is load-bearing: a bare
1784 * `os-tabpanel[for='help'] { display: flex }` rule has specificity
1785 * (0,0,2,1) which outranks the shadow-DOM `:host([hidden]) {
1786 * display: none }` rule (specificity 0,0,1,0), so the panel would
1787 * keep rendering when it should be hidden — bleeding the Components
1788 * UI into every other tab. Pinning the rule to the visible state
1789 * leaves the hidden state to the shadow-DOM default. */
1790 .os-settings > os-tabpanel[ for='help' ]:not( [ hidden ] ) {
1791 overflow: hidden;
1792 display: flex;
1793 flex-direction: column;
1794 /*
1795 * Square off the bottom inset. Every other page carries 44px there
1796 * so a scrolling column has somewhere to end; this one does not
1797 * scroll, so those extra 16px are just the panes stopping short of
1798 * the window. Matching the top puts the same frame on both ends.
1799 */
1800 padding-bottom: 28px;
1801 }
1802 .os-settings
1803 > os-tabpanel[ for='help' ]:not( [ hidden ] )
1804 > os-panel {
1805 flex: 1;
1806 min-height: 0;
1807 display: flex;
1808 flex-direction: column;
1809 }
1810
1811 .os-settings__help {
1812 flex: 1;
1813 min-height: 0;
1814 display: flex;
1815 flex-direction: column;
1816 gap: 12px;
1817 }
1818 /* The intro card (Component library description) shouldn't grow —
1819 it's content-sized so the grid below gets the remaining height.
1820 The warner disclosure is the same: content-sized whether it is
1821 folded or open, so opening it takes room from the panes below
1822 rather than from the window. */
1823 .os-settings__help > os-section,
1824 .os-settings__help > os-disclosure {
1825 flex: 0 0 auto;
1826 }
1827
1828 /*
1829 * The warner demo reads as one of this tab's section boxes, but the
1830 * box has to start BELOW the summary row: a disclosure whose frame
1831 * wraps its own toggle looks like a card you cannot close, and when
1832 * folded it leaves an empty bordered strip with a title in it. So the
1833 * host stays transparent and the box is painted on the body part,
1834 * which is `hidden` while closed and therefore paints nothing at all.
1835 */
1836 .os-settings__help-warner-section::part( body ) {
1837 padding: 18px;
1838 background: var( --os-ui-surface-subtle, rgba( 0, 0, 0, 0.03 ) );
1839 border: 1px solid var( --os-ui-border, #dcdcde );
1840 border-radius: 10px;
1841 /*
1842 * `overflow: hidden` matches `os-section::part( body )` above, and
1843 * is load-bearing rather than cosmetic: the `<pre>` inside carries
1844 * `overflow-x: auto` precisely BECAUSE the box clips (see the
1845 * comment on `.os-settings__help-warner-code`). Drop it here and
1846 * the 56-character tag would push the box wide instead of
1847 * scrolling inside it.
1848 */
1849 overflow: hidden;
1850 }
1851 .os-settings__help-layout {
1852 flex: 1;
1853 min-height: 0;
1854 }
1855
1856 /* Each split pane is its own scroll container. min-height:0 unlocks
1857 shrinking in a grid item (default is `auto` which equals content
1858 height — would defeat the overflow). max-height:100% pins the
1859 pane to the grid track. */
1860 .os-settings__help-nav,
1861 .os-settings__help-detail {
1862 min-height: 0;
1863 max-height: 100%;
1864 overflow-y: auto;
1865 }
1866
1867 /*
1868 * Features section — per-user opt-in toggles.
1869 *
1870 * Save status pill renders the live OS-settings save lifecycle
1871 * (`pending` → `saving` → `saved` / `failed`) so toggling a
1872 * per-user feature flag gets immediate, honest feedback instead
1873 * of an optimistic flicker.
1874 */
1875 .os-features__status-row {
1876 min-height: 22px;
1877 }
1878
1879 /*
1880 * One toggle + hint pair, in its own box.
1881 *
1882 * These used to be separated by a hairline above each item after the
1883 * first. A box does the same job without drawing a line: it groups the
1884 * label with its description and gives the eye a stopping point
1885 * between unrelated settings, and it says which text belongs to which
1886 * switch by enclosing it rather than by sitting between it and the
1887 * next one. No horizontal rule survives anywhere in this panel.
1888 */
1889 .os-features__item {
1890 display: flex;
1891 flex-direction: column;
1892 gap: 4px;
1893 padding: 12px 16px;
1894 background: var( --os-ui-surface, #fff );
1895 border-radius: 8px;
1896 }
1897
1898 .os-features__item + .os-features__item {
1899 margin-top: 12px;
1900 }
1901
1902 /* Feature title line: Body 14 / Medium 500. The outer-tree rule wins
1903 * over the component's own :host font-size, and the shadow label
1904 * inherits both properties from the host. */
1905 .os-features__item > os-checkbox-label {
1906 font-size: 14px;
1907 font-weight: 500;
1908 }
1909
1910 /* A functional notice (provider gating) gets a touch more air than
1911 * the 4px title-to-description rhythm: 4px gap + 4px margin = 8px. */
1912 .os-features__item > os-notice {
1913 margin-top: 4px;
1914 }
1915
1916 /* Sub-toggles (the window-links children) sit inside the parent box:
1917 * no box-in-box, indented 24px so their checkboxes align under the
1918 * parent's label text (16px box + 8px gap). Declared after the
1919 * sibling rule above so the 8px margin wins for nested siblings:
1920 * 4px parent gap + 8px margin = the same 12px block rhythm. */
1921 .os-features__item .os-features__item {
1922 margin-top: 8px;
1923 margin-left: 24px;
1924 padding: 0;
1925 background: none;
1926 border-radius: 0;
1927 }
1928
1929 /* "Reset what's-new dialogs" row: description first, button directly
1930 * under it on the shared 12px gap. `align-items: flex-start` keeps
1931 * the inline-flex `<os-button>` from stretching full-width inside
1932 * the column. */
1933 .os-features__row {
1934 display: flex;
1935 flex-direction: column;
1936 align-items: flex-start;
1937 gap: 12px;
1938 margin-top: 12px;
1939 padding: 12px 16px;
1940 background: var( --os-ui-surface, #fff );
1941 border-radius: 8px;
1942 }
1943
1944 .os-features__row > os-button {
1945 display: inline-flex;
1946 }
1947
1948 .os-features__row > .os-features__hint {
1949 margin: 0;
1950 }
1951
1952 /* "Delete folder sharing data": the one-line description and its
1953 * danger button read as a single row, button on the trailing edge.
1954 * 4px parent gap + 8px margin = the 12px block rhythm above it. */
1955 .os-features__danger-row {
1956 display: flex;
1957 align-items: center;
1958 justify-content: space-between;
1959 gap: 12px;
1960 margin-top: 8px;
1961 }
1962
1963 .os-features__danger-row > os-button {
1964 flex: 0 0 auto;
1965 }
1966
1967 /* Heartbeat rate: 8px label-to-control, description 8px below the
1968 * control (4px parent gap + 4px margin). */
1969 .os-features__select-label {
1970 display: flex;
1971 flex-direction: column;
1972 align-items: flex-start;
1973 gap: 8px;
1974 }
1975
1976 .os-features__select-title {
1977 font-size: 14px;
1978 font-weight: 500;
1979 }
1980
1981 .os-features__select-label + .os-features__hint {
1982 margin-top: 4px;
1983 }
1984
1985 .os-features__hint {
1986 margin: 0;
1987 font-size: 13px;
1988 color: var( --os-ui-fg-muted, #50575e );
1989 line-height: 1.5;
1990 }
1991
1992 .os-features__status {
1993 display: inline-flex;
1994 align-items: center;
1995 gap: 6px;
1996 font-size: 13px;
1997 font-weight: 500;
1998 padding: 2px 10px;
1999 border-radius: 999px;
2000 white-space: nowrap;
2001 transition: opacity 200ms ease;
2002 }
2003
2004 .os-features__status--saving {
2005 background: var( --os-ui-surface-elevated, #f0f0f1 );
2006 color: var( --os-ui-fg-muted, #50575e );
2007 }
2008
2009 .os-features__status--saved {
2010 background: rgba( 30, 132, 73, 0.12 );
2011 color: var( --os-ui-success-fg, #1d6f42 );
2012 }
2013
2014 .os-features__status--saved .dashicons {
2015 font-size: 14px;
2016 width: 14px;
2017 height: 14px;
2018 }
2019
2020 .os-features__status--failed {
2021 background: rgba( 214, 54, 56, 0.12 );
2022 color: var( --os-ui-danger-hover, #a02622 );
2023 }
2024
2025 .os-features__status--failed .dashicons {
2026 font-size: 14px;
2027 width: 14px;
2028 height: 14px;
2029 }
2030
2031 .os-features__status-dot {
2032 display: inline-block;
2033 width: 8px;
2034 height: 8px;
2035 border-radius: 50%;
2036 background: currentColor;
2037 animation: os-features-pulse 1.2s ease-in-out infinite;
2038 }
2039
2040 @keyframes os-features-pulse {
2041 0%, 100% { opacity: 0.35; transform: scale( 0.85 ); }
2042 50% { opacity: 1; transform: scale( 1 ); }
2043 }
2044
2045 /*
2046 * OS Settings panel header — hosts the global tabs strip and a single
2047 * `<os-save-status>` indicator that auto-listens to the save
2048 * lifecycle. The save status sits on the trailing edge of the header
2049 * so it's visible across every tab without competing with section
2050 * content.
2051 */
2052 .os-settings__header {
2053 display: flex;
2054 align-items: center;
2055 justify-content: space-between;
2056 gap: 16px;
2057 margin-bottom: 12px;
2058 }
2059
2060 .os-settings__header os-tabs {
2061 flex: 1 1 auto;
2062 min-width: 0;
2063 }
2064
2065 .os-settings__header os-save-status {
2066 flex: 0 0 auto;
2067 margin-inline-start: auto;
2068 }
2069
2070 /*
2071 * About tab — product context first, then a compact editorial front page
2072 * for openstation.blog. The quiet overview establishes what OpenStation is;
2073 * the first story remains the one expressive hero on the page.
2074 */
2075 .os-settings > os-tabpanel[ for='about' ]:not( [ hidden ] ) {
2076 display: flex;
2077 flex-direction: column;
2078 overflow: auto;
2079 }
2080
2081 .os-settings > os-tabpanel[ for='about' ]:not( [ hidden ] ) > os-panel {
2082 flex: 1;
2083 min-height: 0;
2084 display: flex;
2085 flex-direction: column;
2086 }
2087
2088 .os-settings__about {
2089 flex: 1;
2090 min-height: 0;
2091 margin: 0;
2092 color: var( --os-ui-fg, #1d2327 );
2093 background:
2094 radial-gradient(
2095 circle at 92% 0%,
2096 color-mix(
2097 in srgb,
2098 var( --os-ui-accent-dim, #d92ee3 ) 10%,
2099 transparent
2100 ),
2101 transparent 30%
2102 ),
2103 var( --os-ui-surface-sunken, #f0f0f1 );
2104 container: os-about / inline-size;
2105 }
2106
2107 .os-settings__about-inner {
2108 width: min( 100%, 1120px );
2109 box-sizing: border-box;
2110 margin: 0 auto;
2111 padding: clamp( 24px, 4cqi, 48px );
2112 }
2113
2114 .os-settings__about-overview {
2115 display: grid;
2116 grid-template-columns: minmax( 0, 1.3fr ) minmax( 260px, 0.8fr );
2117 gap: clamp( 28px, 5cqi, 56px );
2118 padding-bottom: clamp( 26px, 4cqi, 40px );
2119 border-bottom: 1px solid var( --os-ui-border, #dcdcde );
2120 }
2121
2122 .os-settings__about-identity {
2123 display: flex;
2124 align-items: flex-start;
2125 gap: 18px;
2126 min-width: 0;
2127 }
2128
2129 .os-settings__about-identity img {
2130 display: block;
2131 width: 64px;
2132 height: 64px;
2133 flex: 0 0 64px;
2134 border: 1px solid
2135 color-mix(
2136 in srgb,
2137 var( --os-ui-border, #dcdcde ) 78%,
2138 var( --os-ui-surface, #fff )
2139 );
2140 border-radius: 16px;
2141 box-shadow: var( --os-ui-shadow-md, 0 12px 30px rgba( 0, 0, 0, 0.14 ) );
2142 }
2143
2144 .os-settings__about-eyebrow,
2145 .os-settings__about-kicker {
2146 display: block;
2147 margin: 0 0 4px;
2148 color: var( --os-ui-accent-strong, var( --os-ui-accent, #2271b1 ) );
2149 font-size: 10px;
2150 font-weight: 750;
2151 letter-spacing: 0.14em;
2152 line-height: 1.4;
2153 text-transform: uppercase;
2154 }
2155
2156 .os-settings__about-overview h2,
2157 .os-settings__about-journal-head h2 {
2158 margin: 0;
2159 font-size: clamp( 25px, 4.5cqi, 38px );
2160 font-weight: 670;
2161 letter-spacing: -0.045em;
2162 line-height: 1.06;
2163 }
2164
2165 .os-settings__about-overview p,
2166 .os-settings__about-journal-head p {
2167 max-width: 620px;
2168 margin: 8px 0 0;
2169 color: var( --os-ui-fg-muted, #50575e );
2170 font-size: 13px;
2171 line-height: 1.55;
2172 }
2173
2174 .os-settings__about-story {
2175 display: flex;
2176 min-width: 0;
2177 padding-inline-start: clamp( 20px, 3cqi, 34px );
2178 flex-direction: column;
2179 justify-content: space-between;
2180 gap: 20px;
2181 border-inline-start: 1px solid var( --os-ui-border, #dcdcde );
2182 }
2183
2184 .os-settings__about-story > p {
2185 margin: 0;
2186 font-size: 14px;
2187 line-height: 1.6;
2188 }
2189
2190 .os-settings__about-meta {
2191 display: flex;
2192 align-items: flex-end;
2193 justify-content: space-between;
2194 gap: 18px;
2195 }
2196
2197 .os-settings__about-meta > span {
2198 display: flex;
2199 min-width: 0;
2200 flex-direction: column;
2201 gap: 2px;
2202 }
2203
2204 .os-settings__about-meta strong {
2205 font-size: 12px;
2206 }
2207
2208 .os-settings__about-meta small {
2209 color: var( --os-ui-fg-faint, #646970 );
2210 font-size: 10px;
2211 }
2212
2213 .os-settings__about-meta a {
2214 display: inline-flex;
2215 align-items: center;
2216 gap: 4px;
2217 flex: 0 0 auto;
2218 color: var( --os-ui-accent-strong, var( --os-ui-accent, #2271b1 ) );
2219 font-size: 11px;
2220 font-weight: 650;
2221 text-decoration: none;
2222 }
2223
2224 .os-settings__about-meta a:hover {
2225 text-decoration: underline;
2226 }
2227
2228 .os-settings__about-journal-head {
2229 display: flex;
2230 align-items: flex-end;
2231 justify-content: space-between;
2232 gap: 28px;
2233 margin: clamp( 26px, 4cqi, 40px ) 0 18px;
2234 }
2235
2236 .os-settings__about-journal-head h2 {
2237 font-size: clamp( 22px, 3.7cqi, 31px );
2238 }
2239
2240 .os-settings__about-journal-link {
2241 display: inline-flex;
2242 align-items: center;
2243 justify-content: center;
2244 gap: 5px;
2245 min-height: 38px;
2246 box-sizing: border-box;
2247 padding: 7px 14px;
2248 flex: 0 0 auto;
2249 color: var( --os-ui-fg, #1d2327 );
2250 background: var( --os-ui-surface-elevated, #fff );
2251 border: 1px solid var( --os-ui-border-strong, #8c8f94 );
2252 border-radius: 999px;
2253 font-weight: 650;
2254 text-decoration: none;
2255 transition:
2256 border-color 0.16s ease,
2257 box-shadow 0.16s ease,
2258 transform 0.16s ease;
2259 }
2260
2261 .os-settings__about-journal-link:hover {
2262 color: var( --os-ui-fg, #1d2327 );
2263 border-color: var( --os-ui-accent, #2271b1 );
2264 box-shadow: var( --os-ui-holo-glow, 0 4px 14px rgba( 0, 0, 0, 0.12 ) );
2265 transform: translateY( -1px );
2266 }
2267
2268 .os-settings__about-journal-link:focus-visible,
2269 .os-settings__about-meta a:focus-visible,
2270 .os-settings__about-featured > a:focus-visible,
2271 .os-settings__about-card > a:focus-visible,
2272 .os-settings__about-footer a:focus-visible {
2273 outline: 2px solid var( --os-ui-accent, #2271b1 );
2274 outline-offset: 3px;
2275 }
2276
2277 .os-settings__about-feed {
2278 display: flex;
2279 flex-direction: column;
2280 gap: 18px;
2281 }
2282
2283 .os-settings__about-featured {
2284 position: relative;
2285 min-height: 270px;
2286 margin: 0;
2287 overflow: hidden;
2288 background:
2289 var(
2290 --os-ui-hero-mesh,
2291 linear-gradient(
2292 145deg,
2293 rgba( 34, 113, 177, 0.16 ) 0%,
2294 rgba( 34, 113, 177, 0.06 ) 55%,
2295 rgba( 34, 113, 177, 0 ) 100%
2296 )
2297 ),
2298 var( --os-ui-surface-sunken, #f0f0f1 );
2299 border: 1px solid
2300 color-mix(
2301 in srgb,
2302 var( --os-ui-accent-dim, #d92ee3 ) 34%,
2303 var( --os-ui-border, #dcdcde )
2304 );
2305 border-radius: 18px;
2306 box-shadow: var( --os-ui-shadow-md, 0 18px 44px rgba( 0, 0, 0, 0.2 ) );
2307 }
2308
2309 .os-settings__about-featured::after {
2310 position: absolute;
2311 inset: -34% -8% auto auto;
2312 width: 45%;
2313 aspect-ratio: 1;
2314 background: radial-gradient(
2315 circle,
2316 color-mix( in srgb, var( --os-ui-fg, #fff ) 24%, transparent ),
2317 transparent 66%
2318 );
2319 content: "";
2320 pointer-events: none;
2321 }
2322
2323 .os-settings__about-featured > a {
2324 position: relative;
2325 z-index: 1;
2326 display: flex;
2327 min-height: 270px;
2328 box-sizing: border-box;
2329 padding: clamp( 26px, 4.5cqi, 48px );
2330 flex-direction: column;
2331 justify-content: space-between;
2332 color: var( --os-ui-fg, #1d2327 );
2333 text-decoration: none;
2334 }
2335
2336 .os-settings__about-featured-copy {
2337 max-width: 760px;
2338 }
2339
2340 .os-settings__about-featured .os-settings__about-kicker {
2341 color: var( --os-ui-accent-strong, var( --os-ui-accent, #2271b1 ) );
2342 }
2343
2344 .os-settings__about-featured h3 {
2345 max-width: 780px;
2346 margin: 0;
2347 font-size: clamp( 26px, 5.2cqi, 46px );
2348 font-weight: 680;
2349 letter-spacing: -0.045em;
2350 line-height: 1.02;
2351 }
2352
2353 .os-settings__about-featured p {
2354 max-width: 700px;
2355 margin: 16px 0 0;
2356 color: var( --os-ui-fg-muted, #50575e );
2357 font-size: 14px;
2358 line-height: 1.55;
2359 }
2360
2361 .os-settings__about-featured footer {
2362 display: flex;
2363 align-items: flex-end;
2364 justify-content: space-between;
2365 gap: 24px;
2366 margin-top: 32px;
2367 color: var( --os-ui-fg-faint, #646970 );
2368 font-size: 11px;
2369 }
2370
2371 .os-settings__about-featured footer strong {
2372 display: inline-flex;
2373 align-items: center;
2374 gap: 4px;
2375 color: var( --os-ui-fg, #1d2327 );
2376 font-size: 12px;
2377 }
2378
2379 .os-settings__about-grid {
2380 display: grid;
2381 grid-template-columns: repeat( auto-fit, minmax( 200px, 1fr ) );
2382 gap: 14px;
2383 }
2384
2385 .os-settings__about-card {
2386 min-width: 0;
2387 margin: 0;
2388 background: var( --os-ui-surface-elevated, #fff );
2389 border: 1px solid var( --os-ui-border, #dcdcde );
2390 border-radius: 13px;
2391 box-shadow: var( --os-ui-shadow-sm, 0 3px 10px rgba( 0, 0, 0, 0.07 ) );
2392 transition:
2393 border-color 0.16s ease,
2394 box-shadow 0.16s ease,
2395 transform 0.16s ease;
2396 }
2397
2398 .os-settings__about-card:hover {
2399 border-color: var( --os-ui-border-strong, #8c8f94 );
2400 box-shadow: var( --os-ui-shadow-md, 0 10px 24px rgba( 0, 0, 0, 0.13 ) );
2401 transform: translateY( -2px );
2402 }
2403
2404 .os-settings__about-card > a {
2405 display: flex;
2406 height: 100%;
2407 box-sizing: border-box;
2408 padding: 18px;
2409 flex-direction: column;
2410 color: var( --os-ui-fg, #1d2327 );
2411 text-decoration: none;
2412 }
2413
2414 .os-settings__about-card-meta {
2415 min-height: 17px;
2416 color: var( --os-ui-fg-faint, #646970 );
2417 font-size: 10px;
2418 font-weight: 600;
2419 letter-spacing: 0.02em;
2420 }
2421
2422 .os-settings__about-card h3 {
2423 margin: 10px 0 0;
2424 font-size: 16px;
2425 font-weight: 660;
2426 letter-spacing: -0.018em;
2427 line-height: 1.24;
2428 }
2429
2430 .os-settings__about-card p {
2431 display: -webkit-box;
2432 margin: 10px 0 18px;
2433 overflow: hidden;
2434 color: var( --os-ui-fg-muted, #50575e );
2435 font-size: 12px;
2436 line-height: 1.5;
2437 -webkit-box-orient: vertical;
2438 -webkit-line-clamp: 4;
2439 }
2440
2441 .os-settings__about-card strong {
2442 display: inline-flex;
2443 align-items: center;
2444 gap: 4px;
2445 margin-top: auto;
2446 color: var( --os-ui-accent-strong, var( --os-ui-accent, #2271b1 ) );
2447 font-size: 11px;
2448 }
2449
2450 .os-settings__about-status {
2451 display: flex;
2452 min-height: 180px;
2453 box-sizing: border-box;
2454 align-items: center;
2455 justify-content: center;
2456 gap: 14px;
2457 padding: 28px;
2458 background: var( --os-ui-surface-elevated, #fff );
2459 border: 1px solid var( --os-ui-border, #dcdcde );
2460 border-radius: 14px;
2461 text-align: start;
2462 }
2463
2464 .os-settings__about-status > .dashicons,
2465 .os-settings__about-spinner {
2466 width: 28px;
2467 height: 28px;
2468 flex: 0 0 28px;
2469 }
2470
2471 .os-settings__about-status > .dashicons {
2472 color: var( --os-ui-accent, #2271b1 );
2473 font-size: 28px;
2474 }
2475
2476 .os-settings__about-spinner {
2477 box-sizing: border-box;
2478 border: 2px solid var( --os-ui-border-strong, #a7aaad );
2479 border-top-color: var( --os-ui-accent, #2271b1 );
2480 border-radius: 50%;
2481 animation: os-about-spin 0.75s linear infinite;
2482 }
2483
2484 .os-settings__about-status div {
2485 display: flex;
2486 flex-direction: column;
2487 gap: 2px;
2488 }
2489
2490 .os-settings__about-status strong {
2491 font-size: 14px;
2492 }
2493
2494 .os-settings__about-status div span {
2495 color: var( --os-ui-fg-muted, #50575e );
2496 }
2497
2498 .os-settings__about-stale {
2499 margin: 0 0 10px;
2500 color: var( --os-ui-fg-muted, #50575e );
2501 font-size: 11px;
2502 text-align: end;
2503 }
2504
2505 .os-settings__about-footer {
2506 display: flex;
2507 align-items: center;
2508 justify-content: space-between;
2509 gap: 20px;
2510 margin-top: 28px;
2511 padding-top: 18px;
2512 color: var( --os-ui-fg-muted, #50575e );
2513 border-top: 1px solid var( --os-ui-border, #dcdcde );
2514 }
2515
2516 .os-settings__about-footer p {
2517 display: flex;
2518 margin: 0;
2519 flex-direction: column;
2520 font-size: 11px;
2521 }
2522
2523 .os-settings__about-footer p strong {
2524 color: var( --os-ui-fg, #1d2327 );
2525 font-size: 12px;
2526 }
2527
2528 .os-settings__about-footer a {
2529 display: inline-flex;
2530 align-items: center;
2531 gap: 5px;
2532 color: var( --os-ui-fg-muted, #50575e );
2533 font-weight: 600;
2534 text-decoration: none;
2535 }
2536
2537 .os-settings__about-footer a:hover {
2538 color: var( --os-ui-accent-strong, var( --os-ui-accent, #2271b1 ) );
2539 }
2540
2541 .os-settings__about-footer .dashicons {
2542 width: 15px;
2543 height: 15px;
2544 font-size: 15px;
2545 }
2546
2547 @keyframes os-about-spin {
2548 to {
2549 transform: rotate( 360deg );
2550 }
2551 }
2552
2553 @container os-about ( max-width: 680px ) {
2554 .os-settings__about-overview {
2555 grid-template-columns: 1fr;
2556 gap: 22px;
2557 }
2558
2559 .os-settings__about-identity {
2560 align-items: flex-start;
2561 }
2562
2563 .os-settings__about-identity img {
2564 width: 52px;
2565 height: 52px;
2566 flex-basis: 52px;
2567 border-radius: 13px;
2568 }
2569
2570 .os-settings__about-story {
2571 padding: 20px 0 0;
2572 border-top: 1px solid var( --os-ui-border, #dcdcde );
2573 border-inline-start: 0;
2574 }
2575
2576 .os-settings__about-journal-head {
2577 align-items: flex-start;
2578 flex-direction: column;
2579 gap: 14px;
2580 }
2581
2582 .os-settings__about-featured footer,
2583 .os-settings__about-footer {
2584 align-items: flex-start;
2585 flex-direction: column;
2586 }
2587
2588 .os-settings__about-grid {
2589 grid-template-columns: 1fr;
2590 }
2591 }
2592
2593 @media ( prefers-reduced-motion: reduce ) {
2594 .os-settings__about-spinner {
2595 animation: none;
2596 }
2597
2598 .os-settings__about-journal-link,
2599 .os-settings__about-card {
2600 transition: none;
2601 }
2602 }
2603
2604 /* Navigation section — per-item placement chooser. */
2605 /*
2606 * A settings list, not a stack of cards: the section box is the only
2607 * container, rows sit flush inside it, and a hairline separates one
2608 * from the next. macOS Control Center is the reference — the row's
2609 * title on the leading edge, its answer on the trailing one, and no
2610 * chrome at all until the pointer arrives.
2611 *
2612 * The section's own 18px padding is dropped so the separators reach
2613 * both edges; each row pays its own inset instead. Without that the
2614 * hairlines would float with a gap at either end and read as
2615 * underlines rather than as divisions.
2616 */
2617 /*
2618 * The page's opening sentence, standing in for the one the header
2619 * would have carried. It moved here so it can follow a layout change
2620 * made in the tab next door, and it keeps the header's spacing.
2621 */
2622 .os-nav-settings__lead {
2623 margin: -28px 0 34px;
2624 }
2625
2626 .os-settings os-section.os-nav-settings::part( body ) {
2627 padding: 0;
2628 }
2629
2630 .os-nav-settings__row {
2631 display: flex;
2632 align-items: center;
2633 gap: 12px;
2634 padding: 6px 14px;
2635 min-height: 44px;
2636 }
2637
2638 .os-nav-settings__row + .os-nav-settings__row {
2639 border-block-start: 1px solid var( --os-ui-border, #dcdcde );
2640 }
2641
2642 .os-nav-settings__identity {
2643 display: flex;
2644 align-items: center;
2645 gap: 10px;
2646 flex: 1;
2647 min-width: 0;
2648 }
2649
2650 .os-nav-settings__icon {
2651 width: 24px;
2652 height: 24px;
2653 flex-shrink: 0;
2654 display: inline-flex;
2655 align-items: center;
2656 justify-content: center;
2657 }
2658
2659 .os-nav-settings__icon.dashicons {
2660 font-size: 20px;
2661 }
2662
2663 .os-nav-settings__title {
2664 overflow: hidden;
2665 text-overflow: ellipsis;
2666 white-space: nowrap;
2667 }
2668
2669 /* ------------------------------------------------------------------
2670 * OS Settings → Themes.
2671 *
2672 * Card grid of the site's desktop-theme library. Deliberately styled
2673 * with the SAME neutral palette as the rest of the panel rather than
2674 * with themed tokens: the picker has to stay legible while the user
2675 * is trying on themes, including ones with hostile contrast.
2676 * ------------------------------------------------------------------ */
2677
2678 .os-settings__theme-grid {
2679 display: grid;
2680 grid-template-columns: repeat( auto-fill, minmax( 160px, 1fr ) );
2681 gap: 12px;
2682 margin: 12px 0 16px;
2683 }
2684
2685 .os-settings__theme-card-wrap {
2686 position: relative;
2687 }
2688
2689 /*
2690 * Same geometry in every state: the border is 1px in both, and
2691 * selection is a box-shadow ring OUTSIDE the box, so an active card
2692 * occupies exactly the pixels an idle one does and the row stays
2693 * aligned. The selected ring is the flat accent, the same language
2694 * as the wallpaper tiles and layout cards beside it.
2695 */
2696 .os-settings__theme-card {
2697 display: flex;
2698 flex-direction: column;
2699 gap: 6px;
2700 width: 100%;
2701 height: 100%;
2702 padding: 10px;
2703 background: var( --os-ui-surface, #fff );
2704 border: 1px solid var( --os-ui-border, #dcdcde );
2705 border-radius: 10px;
2706 cursor: pointer;
2707 text-align: start;
2708 font: inherit;
2709 color: var( --os-ui-fg, #1d2327 );
2710 transition: border-color 0.15s ease, box-shadow 0.15s ease;
2711 }
2712
2713 .os-settings__theme-card:hover {
2714 border-color: var( --os-ui-border-strong, #8c8f94 );
2715 }
2716
2717 .os-settings__theme-card:focus-visible {
2718 outline: 2px solid var( --os-ui-accent, #2271b1 );
2719 outline-offset: 2px;
2720 }
2721
2722 .os-settings__theme-card[ aria-pressed="true" ] {
2723 border-color: transparent;
2724 box-shadow: 0 0 0 2px var( --os-ui-accent, #2271b1 );
2725 }
2726
2727 .os-settings__theme-preview {
2728 display: flex;
2729 align-items: center;
2730 justify-content: center;
2731 aspect-ratio: 16 / 10;
2732 overflow: hidden;
2733 border-radius: 6px;
2734 background: var( --os-ui-surface-elevated, #f0f0f1 );
2735 }
2736
2737 .os-settings__theme-preview img {
2738 width: 100%;
2739 height: 100%;
2740 object-fit: cover;
2741 display: block;
2742 }
2743
2744 /* Stand-in preview for "System default" — a miniature of the shipped
2745 * graphite desktop, so the card reads as a real option rather than an
2746 * absence. */
2747 .os-settings__theme-preview--system {
2748 background: linear-gradient( 135deg, #1d2327 0%, #2c3338 50%, #1d2327 100% );
2749 }
2750
2751 .os-settings__theme-initials {
2752 font-size: 24px;
2753 font-weight: 700;
2754 letter-spacing: 1px;
2755 color: var( --os-ui-fg-muted, #787c82 );
2756 }
2757
2758 .os-settings__theme-name {
2759 font-weight: 600;
2760 line-height: 1.3;
2761 overflow-wrap: anywhere;
2762 }
2763
2764 /* Wraps rather than clipping. This string is a theme's own
2765 description and a plugin can make it any length; a one-line box cut
2766 "The look OpenStation ships with" off mid-sentence. */
2767 .os-settings__theme-meta {
2768 font-size: 13px;
2769 color: var( --os-ui-fg-muted, #646970 );
2770 line-height: 1.45;
2771 min-height: 1.45em;
2772 overflow-wrap: anywhere;
2773 }
2774
2775 /* Delete overlay — outside the card button, because nesting a button
2776 * inside a button is invalid HTML and breaks keyboard traversal. */
2777 .os-settings__theme-delete {
2778 position: absolute;
2779 inset-block-start: 4px;
2780 inset-inline-end: 4px;
2781 display: flex;
2782 align-items: center;
2783 justify-content: center;
2784 width: 22px;
2785 height: 22px;
2786 padding: 0;
2787 border: none;
2788 border-radius: 50%;
2789 background: var( --os-ui-scrim, rgba( 0, 0, 0, 0.55 ) );
2790 color: var( --os-ui-fg-on-accent, #fff );
2791 font-size: 15px;
2792 line-height: 1;
2793 cursor: pointer;
2794 opacity: 0;
2795 transition: opacity 0.15s ease, background-color 0.15s ease;
2796 }
2797
2798 .os-settings__theme-card-wrap:hover
2799 .os-settings__theme-delete,
2800 .os-settings__theme-delete:focus-visible {
2801 opacity: 1;
2802 }
2803
2804 .os-settings__theme-delete:hover {
2805 background: var( --os-ui-danger, #d63638 );
2806 }
2807
2808 /*
2809 * "Apply <theme>'s recommended layout" — sits between the theme grid
2810 * and the upload tile, and only for a theme that recommends
2811 * something. Just the button: the dock resizing and the layout
2812 * moving is the feedback.
2813 */
2814 .os-settings__theme-recommendation {
2815 display: flex;
2816 margin-block: 16px;
2817 }
2818
2819 /*
2820 * The last cell of the theme grid, not a dropzone underneath it.
2821 * Uploading a theme is one of the ways to answer "which theme", so it
2822 * belongs in the row of answers; dashed because it is the only one
2823 * that cannot show you what you are picking until you have picked it.
2824 * Same reasoning, and the same treatment, as "Use your own image" in
2825 * the wallpaper grid.
2826 */
2827 .os-settings__theme-upload {
2828 display: block;
2829 /* Same reason as the other two dropzones above: the plain border
2830 token is the card's own colour and vanishes on it. */
2831 border: 2px dashed var( --os-ui-border-strong, #c3c4c7 );
2832 border-radius: 10px;
2833 background: transparent;
2834 transition: border-color 0.15s ease, background-color 0.15s ease;
2835 }
2836
2837 .os-settings__theme-upload-label {
2838 display: flex;
2839 flex-direction: column;
2840 align-items: center;
2841 justify-content: center;
2842 gap: 6px;
2843 /* Matches a theme card: 16:10 preview plus its name and meta. */
2844 min-height: 100%;
2845 padding: 16px;
2846 color: var( --os-ui-fg-muted, #50575e );
2847 cursor: pointer;
2848 text-align: center;
2849 }
2850
2851 .os-settings__theme-upload:hover,
2852 .os-settings__theme-upload--dragover {
2853 border-color: var( --wp-admin-theme-color, #2271b1 );
2854 background-color: rgba( 34, 113, 177, 0.06 );
2855 }
2856
2857 .os-settings__theme-upload--dragover {
2858 border-style: solid;
2859 }
2860
2861 .os-settings__theme-upload--busy
2862 .os-settings__theme-upload-label {
2863 cursor: progress;
2864 opacity: 0.7;
2865 }
2866
2867 .os-settings__theme-upload-plus {
2868 font-size: 22px;
2869 line-height: 1;
2870 }
2871