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

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

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