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

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

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