PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
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 0.8.6 All 33 releases
desktop-mode / apps / os-settings / os-settings.css

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

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