PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.0.0
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.0.0
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 / variables.css

variables.css in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.0.0, at assets/css/variables.css

1,270 lines 55.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OpenStation — CSS Custom Properties.
3 *
4 * All design tokens for the desktop shell, carrying the OpenStation
5 * brand: https://nuriapenya.github.io/open-station-brand/
6 *
7 * ## The palette
8 *
9 * Pulse #f252fc neon identity accent
10 * Nebula #ec9bff soft identity accent
11 * Sirius #c2f1f1 aqua contrast accent
12 * Starlight #fffbff lightest — primary text on the station
13 * Void #0c0b0f darkest — the base everything sits on
14 * Obsidian #1a1721 surfaces: window bodies, cards, rows
15 *
16 * Shade ramp, Void to Starlight, for text hierarchy and lines:
17 * Astro #33303a · Silver #4d4a52 · Pewter #66636b ·
18 * Osmium #99969c · Ash #b3afb5 · Cloud #ccc8ce · Mist #e6e2e6
19 *
20 * The brand's own composition rule, followed here literally: "Void as
21 * the base, Obsidian for surfaces, Pulse and Nebula for identity
22 * moments, Sirius and Starlight for contrast, the Shade palette for
23 * text hierarchy and lines, meshes reserved for hero surfaces."
24 *
25 * ## Why these are declared at all
26 *
27 * Every rule in the tree reads its token as
28 * `var( --token, <literal> )`, and those literals are the look the
29 * shell had before the brand: WordPress-admin greys and blues. They
30 * are still there, still correct, and still what renders if this file
31 * fails to load. Declaring the tokens here is what puts the station on
32 * top of them — one declaration retints every surface that reads it.
33 *
34 * The way back is a theme, not an edit: **Desktop Mode (Legacy)**
35 * (`assets/desktop-themes/legacy/theme.json`) is a frozen snapshot of
36 * those pre-brand literals, and picking it in OS Settings → Themes
37 * restores the old look wholesale. A desktop theme's compiled selector
38 * weighs (0,2,0) against this file's `:root` (0,1,0) and loads after
39 * it, so any theme — Legacy or a third party's — outranks everything
40 * below.
41 *
42 * ## Semantic colours
43 *
44 * The brand defines no status palette, so danger / success / warning /
45 * info are derived rather than quoted: the hues the shell already used,
46 * lifted until they carry on a Void-to-Obsidian background, and where
47 * the brand does have a colour that means the right thing it is used
48 * instead — Sirius for info, and Holomesh's mint and warm for success
49 * and warning.
50 */
51
52 /*
53 * ---- Typefaces -------------------------------------------------
54 *
55 * Geist and Geist Mono, the brand faces, self-hosted from
56 * `assets/fonts/` under the SIL Open Font License 1.1 (the licence
57 * text ships beside them in `OFL.txt`). Both are VARIABLE fonts: one
58 * file covers 100–900, which is why a whole typographic scale costs
59 * ~70 kB per family rather than one file per weight.
60 *
61 * `font-display: swap` paints fallback text immediately and swaps when
62 * the face arrives — on admin chrome that is almost always right,
63 * where `block` would hide every dock label until the download lands.
64 *
65 * The `url()`s are relative to THIS stylesheet, which is what keeps
66 * them correct however the plugin directory is renamed or wherever it
67 * is symlinked from.
68 */
69 @font-face {
70 font-family: 'Geist';
71 src: url('../fonts/Geist-Variable.woff2') format('woff2');
72 font-weight: 100 900;
73 font-style: normal;
74 font-display: swap;
75 }
76
77 @font-face {
78 font-family: 'Geist Mono';
79 src: url('../fonts/GeistMono-Variable.woff2') format('woff2');
80 font-weight: 100 900;
81 font-style: normal;
82 font-display: swap;
83 }
84
85 /*
86 * ---- Scope: the shell document, and nothing else -----------------
87 *
88 * `body.os-active` rather than `:root`, and that is
89 * load-bearing rather than tidy.
90 *
91 * This stylesheet is a dependency of `chromeless.css`, so it also
92 * loads INSIDE every iframe window — a real `wp-admin` document. A
93 * palette on `:root` would reach that document and repaint WordPress's
94 * own UI: `--wp-admin-theme-color` alone would turn Core's primary
95 * buttons, links and focus rings across every admin screen, with
96 * white button text landing on a colour it was never contrast-tested
97 * against.
98 *
99 * Iframe documents carry `body.os-chromeless` instead, so
100 * they match nothing here and every rule in the tree falls back to the
101 * literal it was always written with — the WordPress-admin values.
102 * **An admin page inside a window looks exactly as it does outside
103 * one.** That is deliberate: the station is the chrome around the
104 * page, not a reskin of the page.
105 *
106 * Specificity: (0,1,1). A desktop theme compiles to
107 * `body.os-desktop-theme-<slug>`, also (0,1,1) on the same
108 * element, and its stylesheet declares `os-variables` as a
109 * dependency — so it always prints after this file and wins the tie.
110 * Do not remove that dependency; see the note in
111 * `openstation_enqueue_desktop_theme_style()`.
112 */
113 body.os-active {
114 /*
115 * ---- Desk --------------------------------------------------
116 *
117 * `Space`, the brand's quiet deep-space backdrop: a linear
118 * gradient from near-black through to a faint violet horizon.
119 *
120 * Note this is the desk's CSS default, not usually what you see:
121 * the wallpaper layer writes the ACTIVE wallpaper into this same
122 * property as an inline style, and the shipped default is the
123 * `Galaxy` artwork (`includes/wallpapers.php`). This value is what
124 * paints before that runs, and for anyone whose wallpaper is
125 * unavailable.
126 */
127 --os-bg: linear-gradient(160deg, #010101 0%, #111114 58%, #1e1d23 100%);
128 /*
129 * Flat colour painted on the shell itself, behind every other
130 * layer including the wallpaper.
131 *
132 * The shell is `position: fixed` over the classic admin page, and
133 * that page is WHITE. Anything that leaves a layer unpainted for a
134 * single frame — a canvas wallpaper's context being disturbed, the
135 * compositor re-rasterising after a large GPU layer goes away —
136 * shows it. This is the floor that makes that impossible: the worst
137 * a dropped frame can now look like is Void.
138 *
139 * Deliberately a solid rather than `--os-bg`: it is never
140 * seen when things are working, so it should cost one flat fill,
141 * not a second gradient rasterisation on every shell repaint.
142 */
143 --os-backstop: #0c0b0f;
144 --os-area-inset: 0;
145
146 /*
147 * ── The icon grid ────────────────────────────────────────────
148 *
149 * ONE grid, everywhere placements are laid out: the wallpaper,
150 * folder windows, and every canvas in the site folder. Before
151 * these tokens each surface carried its own pitch and its own
152 * tile width, and the two drifted apart — the desktop ended up
153 * with an 88px tile in a 96px cell whose 8px of air the tile's
154 * own padding ate, so icons touched edge to edge while the site
155 * folder had 20px between them.
156 *
157 * The cell is derived, never declared: `cell = tile + gap`. A
158 * gap you can see is the whole point, so it is the number that
159 * gets tuned; the pitch follows.
160 *
161 * The TypeScript side mirrors these in `src/desktop-files/grid.ts`
162 * (layout maths can't read CSS), and
163 * `tests/vitest/grid-metrics.test.ts` parses this file to prove
164 * the two agree. Change a number here and that test tells you
165 * exactly which constant to move with it.
166 */
167 --os-tile-w: 88px;
168 /*
169 * A FIXED height, and it has to fit the tallest a tile can get:
170 * 8px padding + 48px icon well + 6px gap + two clamped label
171 * lines (2 × 12px × 1.2 = 28.8px) + 8px padding = 98.8px, so
172 * 104px with a little slack for font metrics.
173 *
174 * Fixed rather than minimum because the selection ring is drawn
175 * around the tile box: let the box follow its label and a row of
176 * selected icons is a ragged run of different-height rectangles,
177 * one per label that happened to wrap.
178 */
179 --os-tile-h: 104px;
180 --os-grid-gap-x: 20px;
181 --os-grid-gap-y: 16px;
182 /* Gutter from the top / inline-start edge of any icon canvas. */
183 --os-grid-padding: 16px;
184 /*
185 * Image-led sections opt into a bigger tile (`tileSize: 'large'`)
186 * — a shop's products read as a catalogue, not a file list. Same
187 * gaps, bigger tile.
188 */
189 --os-tile-w-large: 132px;
190 --os-tile-h-large: 160px;
191
192 /* Window chrome — Obsidian body inside a Starlight hairline. */
193 --os-window-bg: #1a1721;
194 --os-window-border: rgba(255, 251, 255, 0.12);
195 --os-window-radius: 8px;
196 /*
197 * Shadows stay black and go deeper than they were: on a Void desk
198 * a soft grey shadow is invisible, and depth is the only thing
199 * separating a window from the sky behind it.
200 */
201 --os-window-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
202 --os-window-shadow-focused: 0 12px 48px rgba(0, 0, 0, 0.6), 0 4px 12px rgba(0, 0, 0, 0.35);
203 /*
204 * Fill of the surface a window's content is revealed from once it
205 * finishes loading (OS Settings → Effects → "Window reveal").
206 *
207 * Obsidian, so a window uncovers from its own colour rather than
208 * flashing white on a dark desk. The surface has to be opaque or
209 * there is nothing to reveal FROM — the content would simply be
210 * visible the whole time and the clip animation would paint
211 * nothing. `transparent` is a legitimate value meaning "no
212 * covering surface"; the shell then skips the layer rather than
213 * animating something invisible.
214 *
215 * A reveal may override this for itself when the paint IS the
216 * reveal; `obturator` (Camera shutter) is the one built-in that
217 * does, because near-black blades are what make it a shutter.
218 */
219 --os-window-reveal-surface: #1a1721;
220 /*
221 * Fill of the reveal's leading edge — the band that travels with
222 * the clip boundary and draws each reveal's shape (six lines on
223 * Blinds, an opening ring on Iris, a rotating spoke on Radar).
224 *
225 * `transparent` by default: the reveal reads as the page arriving,
226 * and a hard graphic edge on top of that is a deliberate look
227 * rather than the neutral one. A theme (or any stylesheet) opts in
228 * by giving this a colour — no JS and no per-reveal configuration
229 * needed, because the band follows whatever shape the active
230 * reveal has. While it computes to fully transparent the shell
231 * skips the layer altogether, so the default costs nothing.
232 */
233 --os-window-reveal-edge: transparent;
234
235 /*
236 * ---- Title bar ---------------------------------------------
237 *
238 * Astro when focused, Obsidian when not: the focused window's
239 * chrome lifts one step up the Shade ramp while its unfocused
240 * neighbours sit flush with their own bodies. That is the entire
241 * focus signal, and it costs no colour — which matters, because
242 * the accent is spoken for.
243 */
244 --os-titlebar-bg: #1a1721;
245 --os-titlebar-bg-focused: #33303a;
246 --os-titlebar-color: #99969c;
247 --os-titlebar-color-focused: #fffbff;
248 --os-titlebar-height: 40px;
249 /* Hairline between page chrome and window chrome. */
250 --os-titlebar-divider: rgba(255, 251, 255, 0.12);
251 --os-titlebar-divider-unfocused: rgba(255, 251, 255, 0.06);
252
253 /*
254 * Title-bar control glyphs — minimise / maximise / close, the ⋯
255 * menu trigger, and the screen-meta cluster — in each of the two
256 * title-bar states.
257 *
258 * Both halves are named explicitly. The unfocused half would
259 * otherwise derive itself from `--os-titlebar-color` by
260 * a `color-mix()` that only runs while a desktop theme is active,
261 * and the focused half cannot derive at all: its glyphs sit on
262 * `-bg-focused`, and CSS has no contrast-safe function of a
263 * background colour.
264 *
265 * Close-button red is deliberately not in either set — it is
266 * semantic signal, not chrome, and both states resolve it through
267 * `--os-ui-danger`.
268 */
269 --os-titlebar-btn-color: rgba(255, 251, 255, 0.55);
270 --os-titlebar-btn-color-hover: #fffbff;
271 --os-titlebar-btn-bg-hover: rgba(255, 251, 255, 0.1);
272 --os-titlebar-btn-bg-active: rgba(255, 251, 255, 0.16);
273 --os-titlebar-btn-focused-color: rgba(255, 251, 255, 0.72);
274 --os-titlebar-btn-focused-color-hover: #fffbff;
275 --os-titlebar-btn-focused-bg-hover: rgba(255, 251, 255, 0.14);
276 --os-titlebar-btn-focused-bg-active: rgba(255, 251, 255, 0.2);
277 --os-titlebar-btn-focused-outline: #f252fc;
278
279 /*
280 * The control cluster and each button face stay TRANSPARENT, so a
281 * themed title-bar texture runs edge to edge underneath them and
282 * the glyphs float on the artwork. A plate behind the controls is
283 * a choice a theme makes, not a default the framework imposes.
284 * The knobs for building one:
285 *
286 * Cluster --os-titlebar-controls-bg
287 * --os-titlebar-controls-image (+ -repeat,
288 * -size, -position)
289 * --os-titlebar-controls-radius
290 * --os-titlebar-controls-padding (inline)
291 * --os-titlebar-controls-gap
292 * Screen-meta cluster
293 * --os-titlebar-meta-bg / -image / -radius
294 * (radius falls through to the controls radius)
295 * Each face --os-ui-btn-bg / --os-ui-btn-bg-image / --os-ui-btn-radius
296 */
297
298 /*
299 * Admin accent. Pulse, the brand's identity colour, and the value
300 * behind the `pulse` swatch that OS Settings → Appearance ships as
301 * its default (`includes/accents.php`). The accent picker writes
302 * the user's choice into this same property as an inline style, so
303 * this is the pre-JS value and the floor under every scheme block
304 * below.
305 */
306 --wp-admin-theme-color: #f252fc;
307
308 /* Resize handle — larger hit area than visual glyph; corners
309 are transparent until hover so they don't compete with the
310 window's own drop shadow. */
311 --os-resize-size: 20px;
312 --os-resize-color: rgba(255, 251, 255, 0.18);
313
314 /*
315 * ---- The `--os-ui-*` UI palette ------------------------------
316 *
317 * The single vocabulary for every surface, text tone, border, and
318 * status colour inside window BODIES — the `<os-*>` component kit
319 * AND the feature stylesheets both read these. Setting one value
320 * retints everything that uses it, which is what makes a
321 * whole-OS repaint eleven declarations rather than a thousand.
322 *
323 * It reaches further than window bodies: the shell's own
324 * body-mounted overlays — toasts, confirm dialogs, context menus,
325 * and the command palette — read it too.
326 *
327 * Roughly 190 component-local tokens (`--os-ui-card-bg`,
328 * `--os-ui-table-header-bg`, …) fall through to these, e.g.
329 *
330 * background: var( --os-ui-card-bg, var( --os-ui-surface, #fff ) );
331 *
332 * so the ones named further down are only those whose own literal
333 * does NOT chain through the palette and would otherwise stay a
334 * light-mode value on a dark station.
335 */
336
337 /* Surfaces — Obsidian on Void, one step up the ramp for elevated. */
338 --os-ui-surface: #1a1721;
339 --os-ui-surface-elevated: #33303a;
340 --os-ui-surface-sunken: #0c0b0f;
341 --os-ui-surface-raised: rgba(255, 251, 255, 0.06);
342 --os-ui-surface-subtle: rgba(255, 251, 255, 0.03);
343 --os-ui-bg: #1a1721;
344 --os-ui-bg-subtle: rgba(255, 251, 255, 0.03);
345
346 /* Text — Starlight down through the Shade ramp. */
347 --os-ui-fg: #fffbff;
348 --os-ui-fg-muted: #b3afb5;
349 --os-ui-fg-faint: #66636b;
350 --os-ui-text-muted: rgba(255, 251, 255, 0.6);
351 /*
352 * Text on a filled surface — and the name undersells it. Around
353 * forty rules read this token, and only about half of them sit on
354 * the accent: the rest are white-on-dark chips that never had a
355 * name of their own — the toast, the drag hint, the widget picker,
356 * the file-tile lock, the overview labels, every scrim-backed
357 * caption in OS Settings.
358 *
359 * So it is Starlight. Void reads better ON Pulse (5.9:1 against
360 * 2.9:1) and was the first choice here, but it turned all of those
361 * chips into black text on a near-black wash. One token cannot be
362 * both, and the dark-chip half is both larger and the half that
363 * fails catastrophically rather than merely weakly.
364 *
365 * The surfaces that really are a bright fill — ribbons and step
366 * chips — name their own dark text below, which is the escape
367 * hatch this token leaves open.
368 */
369 --os-ui-fg-on-accent: #fffbff;
370
371 /* Lines. */
372 --os-ui-border: #33303a;
373 --os-ui-border-strong: #4d4a52;
374
375 /* Interaction — light washes, since the surface underneath is dark. */
376 --os-ui-hover: rgba(255, 251, 255, 0.06);
377 --os-ui-scrim: rgba(12, 11, 15, 0.72);
378
379 /*
380 * ---- Text selection ------------------------------------------
381 *
382 * Lagoon, the brand's violet-blue, at 60%. It is the one primary
383 * colour in the guidelines the shell had no job for, and this is
384 * the right job: violet-blue is what every OS has trained people
385 * to read as "selected", so it needs no learning.
386 *
387 * The alpha is where the accessibility is, and it is squeezed from
388 * both ends. A selection has to satisfy two constraints at once:
389 *
390 * 1. the selected TEXT stays readable on it — Starlight over
391 * Lagoon@60% is 5.1:1 on Obsidian, 5.5:1 on Void; and
392 * 2. the highlight is distinguishable from the unselected
393 * background — 3.4:1 and 3.5:1 respectively.
394 *
395 * Raising the alpha helps (2) and hurts (1); lowering it does the
396 * reverse. 60% is the middle of the band where both clear their
397 * bar on both surfaces. An accent wash cannot do this at all: the
398 * Pulse family only separates from the background above 75%, by
399 * which point it is a fill rather than a highlight.
400 *
401 * The text colour is Starlight — the colour body text already is.
402 * A selection that also RECOLOURS the text destroys every
403 * distinction the text was carrying: syntax highlighting, a red
404 * error, a muted timestamp. Highlighting is not restyling.
405 */
406 --os-ui-selection-bg: rgba(159, 152, 255, 0.6);
407 --os-ui-selection-fg: #fffbff;
408
409 /* Accent — the identity moment. */
410 --os-ui-accent: #f252fc;
411 --os-ui-accent-strong: #ec9bff;
412 --os-ui-accent-soft: color-mix(in srgb, var(--os-ui-accent-dim) 14%, transparent);
413 /*
414 * Pulse, one step back — and the single knob for how loud the
415 * station is.
416 *
417 * Pulse is not a contrast problem: #f252fc carries 6.2:1 against
418 * Obsidian, better than most accents in a dark UI manage. What it
419 * is, is SATURATED — HSL 296, 97, 65 — and the places that hurt
420 * are the ones where it is spread rather than stated: a bloom
421 * behind a focused control, a 18% wash under a selected row, a
422 * fill wider than a chip. Ten of those on one panel and the whole
423 * surface hums.
424 *
425 * So the fix is not to move Pulse. It is to spend it where it is
426 * a statement and to spend THIS where it is atmosphere: the same
427 * hue with saturation and lightness pulled down together (S 77,
428 * L 53), which is what keeps it recognisably Pulse rather than a
429 * different purple. 4.6:1 on Obsidian, and Starlight over it goes
430 * from 2.8:1 to 3.8:1 — so the wash also stopped eating text.
431 *
432 * Everything ambient below resolves through it: the glows, the
433 * soft wash, the focus bloom. Turning the station up or down is
434 * one edit here.
435 *
436 * `--os-ui-accent` itself stays #f252fc. That one is the brand's,
437 * not ours — it is what the guidelines name and what
438 * `brand-palette.test.ts` pins, and moving it is a brand decision
439 * rather than a UI one.
440 */
441 --os-ui-accent-dim: #d92ee3;
442
443 /*
444 * ---- The meshes ---------------------------------------------
445 *
446 * The brand ships five mesh gradients as SVG artwork. These are
447 * those same meshes transcribed into CSS: the base linear plus
448 * every radial glow, at the offsets, radii, colours and alphas
449 * the vector files declare, converted from the 1440×960 artboard
450 * into percentages so they scale to any box.
451 *
452 * Transcribed rather than referenced on purpose. A `url()` to an
453 * SVG cannot be animated, cannot be tinted, costs a request, and
454 * cannot resize its glows independently of the box — a mesh
455 * painted on a 40 px switch track would be one flat corner of the
456 * artwork. As gradient stacks they rasterise at whatever size the
457 * control happens to be, and `background-position` can slide them,
458 * which is the entire holographic effect: a foil that shifts as
459 * you tilt it.
460 *
461 * Layer order is inverted from the SVG. In SVG the last `<rect>`
462 * paints on top; in a CSS `background-image` list the FIRST layer
463 * is on top. So each mesh below reads bottom-glow-first and ends
464 * with its base linear.
465 *
466 * The brand's own rule — "meshes reserved for hero surfaces" — is
467 * why none of these is a surface colour. They are what the shell
468 * paints at an identity moment: a switch that is on, the primary
469 * action in a dialog, the filled part of a progress bar. See the
470 * `--os-ui-holo-*` block below for the tokens components actually
471 * read.
472 */
473
474 /*
475 * Holomesh. Lavender base under eight glows — white, cyan, pink,
476 * warm, mint, white, pink, blue. The default holographic fill.
477 */
478 --os-mesh-holo:
479 radial-gradient(ellipse 21.7% 39.6% at 96.5% 13.5%, rgba(159, 214, 255, 0.6) 0%, rgba(159, 214, 255, 0) 100%),
480 radial-gradient(ellipse 26.3% 57.2% at 83.3% 56.2%, rgba(243, 181, 236, 0.75) 0%, rgba(243, 181, 236, 0) 100%),
481 radial-gradient(ellipse 18.2% 47.3% at 73.6% 64.5%, rgba(255, 253, 255, 0.7) 0%, rgba(255, 253, 255, 0) 100%),
482 radial-gradient(ellipse 18.4% 34.1% at 29.8% 83.4%, rgba(147, 240, 198, 0.8) 0%, rgba(147, 240, 198, 0) 100%),
483 radial-gradient(ellipse 17.4% 35.7% at 23% 67.7%, rgba(248, 242, 182, 0.85) 0%, rgba(248, 242, 182, 0) 100%),
484 radial-gradient(ellipse 23.3% 45.9% at 45.2% 45.8%, rgba(245, 159, 232, 1) 0%, rgba(245, 168, 234, 0.55) 55%, rgba(245, 168, 234, 0) 100%),
485 radial-gradient(ellipse 29.9% 53.6% at 53.5% 7.3%, rgba(125, 239, 245, 0.9) 0%, rgba(142, 233, 247, 0.45) 60%, rgba(142, 233, 247, 0) 100%),
486 radial-gradient(ellipse 26.9% 70.7% at 29.8% 44.8%, rgba(255, 253, 255, 0.95) 0%, rgba(255, 253, 255, 0.5) 55%, rgba(255, 253, 255, 0) 100%),
487 linear-gradient(124deg, #afa2e8 0%, #b7abea 55%, #c3b8ef 100%);
488
489 /*
490 * Pulsemesh. The loud one — a blue-to-violet base torn open by
491 * magenta. Reserved for the moments Holomesh would undersell.
492 */
493 --os-mesh-pulse:
494 radial-gradient(ellipse 39.1% 48.6% at 92% 94.5%, rgba(255, 253, 255, 0.95) 0%, rgba(255, 240, 252, 0.55) 50%, rgba(255, 240, 252, 0) 100%),
495 radial-gradient(ellipse 33.3% 50% at 61.1% 62.5%, rgba(251, 134, 236, 0.85) 0%, rgba(251, 134, 236, 0) 100%),
496 radial-gradient(ellipse 36.1% 54.2% at 10.4% 39.6%, rgba(250, 61, 248, 1) 0%, rgba(250, 61, 248, 0) 100%),
497 radial-gradient(ellipse 87.3% 104.8% at 10.6% 100%, rgba(240, 75, 245, 1) 0%, rgba(240, 75, 245, 0.75) 43%, rgba(240, 75, 245, 0) 100%),
498 radial-gradient(ellipse 36.1% 45.8% at 92.4% 12.5%, rgba(125, 239, 245, 0.95) 0%, rgba(125, 239, 245, 0) 100%),
499 radial-gradient(ellipse 43.1% 54.2% at 81.3% 34.4%, rgba(108, 143, 245, 0.6) 0%, rgba(108, 143, 245, 0) 100%),
500 linear-gradient(236deg, #8d9bf3 0%, #c878f0 100%);
501
502 /* Auromesh. Lavender through mint into cyan — the calm mesh. */
503 --os-mesh-auro: linear-gradient(80deg, #d1c6f8 9.5%, #cefada 50%, #b8ffff 90.5%);
504
505 /* Starmesh. A Starlight glow falling away into Cloud. */
506 --os-mesh-star:
507 radial-gradient(ellipse 72.7% 72.7% at 50% 12.5%, #fffbff 0%, #fffbff 67.8%, #ccc8ce 100%);
508
509 /* Miomesh. The mascot's own sweep. Mio's, and used sparingly. */
510 --os-mesh-mio: linear-gradient(90deg, #f252fc 7%, #aa67ff 48.3%, #a580ff 70.7%, #4b3eff 93%);
511
512 /*
513 * ---- Holographic controls -----------------------------------
514 *
515 * What a control reads when it wants to be holographic. Every
516 * `<os-*>` resolves these through a private `--_holo-*` alias (see
517 * `src/ui/holo.ts`), so a desktop theme can re-point any of them
518 * and every control in the kit changes together.
519 *
520 * The holographic state is a MOMENT, not a skin. A control paints
521 * `--os-ui-holo-fill` when it is on, selected, primary or filled —
522 * the one instant it is speaking for the brand — and wears
523 * ordinary Obsidian the rest of the time. That is the difference
524 * between a station and a novelty; a panel where every control is
525 * iridescent has no identity moments left to spend.
526 */
527
528 /* The fill itself. Holomesh, so the mesh is the default look. */
529 --os-ui-holo-fill: var(--os-mesh-holo);
530 /*
531 * Glyphs and text ON that fill. Void, not Starlight — every mesh
532 * in the brand is a LIGHT surface (its darkest stop, Holomesh's
533 * #afa2e8, still sits at 62% luminance), so Starlight on it is a
534 * white-on-lilac 1.4:1 that looks fine in a mock and is unreadable
535 * on a screen. Void carries at 8.6:1 against the same stop.
536 */
537 --os-ui-holo-ink: #0c0b0f;
538 /*
539 * The hover film. A control that is NOT in its holographic state
540 * still hints at one under the pointer: a low-alpha sweep of the
541 * mesh's own hues, laid over whatever the surface already is.
542 *
543 * Alphas are deliberately in the 6–12% range. This lands on
544 * Obsidian, where anything stronger stops reading as a sheen and
545 * starts reading as a second, wrongly-coloured surface.
546 */
547 --os-ui-holo-sheen: linear-gradient(
548 124deg,
549 rgba(159, 214, 255, 0.1) 0%,
550 rgba(236, 155, 255, 0.12) 34%,
551 rgba(242, 82, 252, 0.1) 58%,
552 rgba(147, 240, 198, 0.09) 100%
553 );
554 /*
555 * The iridescent hairline. Painted as a gradient on a padding-box
556 * mask rather than a `border-color`, because a border cannot hold
557 * a gradient and `border-image` loses the corner radius.
558 */
559 --os-ui-holo-edge: linear-gradient(
560 124deg,
561 rgba(154, 242, 255, 0.7) 0%,
562 rgba(236, 155, 255, 0.85) 38%,
563 rgba(242, 82, 252, 0.7) 62%,
564 rgba(159, 152, 255, 0.6) 100%
565 );
566 /* Resting edge — the same hairline, quieted, for an idle control. */
567 --os-ui-holo-edge-quiet: linear-gradient(
568 124deg,
569 rgba(154, 242, 255, 0.22) 0%,
570 rgba(236, 155, 255, 0.28) 38%,
571 rgba(242, 82, 252, 0.22) 62%,
572 rgba(159, 152, 255, 0.2) 100%
573 );
574 /*
575 * The bloom around a holographic surface. Pulse, because on a Void
576 * desk the glow is what separates "lit" from "light-coloured".
577 */
578 --os-ui-holo-glow: 0 0 0 1px color-mix(in srgb, var(--os-ui-accent-dim) 20%, transparent),
579 0 2px 10px color-mix(in srgb, var(--os-ui-accent-dim) 15%, transparent);
580 --os-ui-holo-glow-strong: 0 0 0 1px color-mix(in srgb, var(--os-ui-accent-dim) 30%, transparent),
581 0 4px 18px color-mix(in srgb, var(--os-ui-accent-dim) 26%, transparent),
582 0 1px 3px rgba(12, 11, 15, 0.6);
583 /*
584 * The unlit half — switch tracks, empty progress, the unchecked
585 * checkbox.
586 *
587 * This was a sunken Void well, and it was a mistake that measured
588 * badly: `rgba(12, 11, 15, 0.55)` composites to #121017 on an
589 * Obsidian panel, which is **1.07:1** against that panel. An off
590 * switch was, to a very good approximation, not drawn. WCAG 1.4.11
591 * asks for 3:1 on the boundary of a control, and this was not in
592 * the same postcode.
593 *
594 * So the well is LIFTED rather than sunk: a Starlight wash, which
595 * also means it tracks whatever surface a theme puts behind it
596 * instead of pinning a grey that only works on Obsidian. Every
597 * dark OS does the same thing with its switches for the same
598 * reason — on a dark UI the visible thing is the lighter one.
599 *
600 * The fill alone still only reaches ~1.6:1, which is a *look*
601 * rather than a boundary. The 3:1 comes from the edge below, and
602 * the two together are what make an off switch findable.
603 */
604 --os-ui-holo-track: rgba(255, 251, 255, 0.16);
605 /*
606 * The unlit half's boundary. Pewter, and not a rounder-looking
607 * neighbour, because Pewter is the first step on the Shade ramp
608 * that reaches 3.00:1 against Obsidian — Silver manages 2.03 and
609 * Astro 1.37. This is the declaration that makes an off control
610 * satisfy WCAG 1.4.11.
611 */
612 --os-ui-holo-track-edge: #66636b;
613 /*
614 * Focus. One ring for the whole kit — Pulse, offset, over a soft
615 * halo, so it reads on Obsidian, on a mesh, and on the near-white
616 * of a legacy theme alike.
617 *
618 * Note which layer dims and which does not. The RING is full Pulse
619 * and stays there: it is the only thing on screen saying where the
620 * keyboard is, and a focus indicator is the last place to trade
621 * legibility for calm. Only the bloom behind it comes down, from
622 * 45% to 30%, and onto the dim — which is the layer that was
623 * making a focused control look like it was on fire.
624 */
625 --os-ui-focus-ring: 0 0 0 2px rgba(12, 11, 15, 0.9), 0 0 0 4px #f252fc,
626 0 0 12px 2px color-mix(in srgb, var(--os-ui-accent-dim) 30%, transparent);
627 /*
628 * …and one for FIELDS, which is a different problem. A text input
629 * already has a border to thicken and it sits in a column of other
630 * inputs, so the ring above — designed to survive landing on a
631 * bright mesh — reads as an alarm on a settings form. This one
632 * tightens the field's own edge to Pulse and puts a soft halo
633 * outside it: unmistakable, and quiet enough to live in a stack of
634 * twelve.
635 */
636 --os-ui-focus-ring-field: 0 0 0 1px #f252fc,
637 0 0 0 4px color-mix(in srgb, var(--os-ui-accent-dim) 16%, transparent);
638
639 /*
640 * ---- Motion --------------------------------------------------
641 *
642 * Four durations and three curves, and every animated thing in the
643 * kit picks from them. Not for tidiness: a panel where the switch
644 * settles in 220ms, the segmented thumb in 300 and the tab
645 * underline in 150 does not read as three well-tuned controls, it
646 * reads as one surface that cannot keep time.
647 *
648 * The scale is roughly logarithmic because perceived duration is:
649 * 90 and 140 are distinguishable, 220 and 240 are not.
650 */
651 /* A state flip with no travel — a colour, an opacity, a tick. */
652 --os-ui-motion-fast: 140ms;
653 /*
654 * How long the foil takes to tilt, and the kit's default. One
655 * duration for every holographic transition, so a panel full of
656 * controls moves as one surface rather than as thirty independent
657 * ones.
658 */
659 --os-ui-holo-transition: 220ms;
660 /* Something crossing a distance: a thumb, a drawer, a card. */
661 --os-ui-motion-slow: 340ms;
662 /* An ambient loop — a shimmer, a drift. Long enough to ignore. */
663 --os-ui-motion-ambient: 12s;
664
665 /*
666 * The curves. `spring` overshoots ~9% and settles; it is what makes
667 * a knob feel thrown rather than moved, and it is wrong for
668 * anything that changes size (an overshooting width reads as a
669 * glitch, not as weight).
670 */
671 --os-ui-ease-spring: cubic-bezier(0.32, 1.5, 0.55, 1);
672 /* Decelerating. The default for anything arriving. */
673 --os-ui-ease-out: cubic-bezier(0.22, 0.9, 0.28, 1);
674 /* Symmetric, for a loop that has to come back where it started. */
675 --os-ui-ease-loop: cubic-bezier(0.45, 0, 0.55, 1);
676
677 /*
678 * Status. Derived, not quoted — see the header note. Info takes
679 * Sirius and success takes Holomesh's mint because the brand
680 * already has a colour that means those things; danger and warning
681 * are the shell's own hues lifted until they carry on Obsidian.
682 */
683 --os-ui-danger: #ff5a5a;
684 --os-ui-danger-hover: #ff8a8a;
685 --os-ui-success-fg: #93f0c6;
686 --os-ui-warning-fg: #f8f2b6;
687 --os-ui-warning: #f8f2b6;
688 --os-ui-warning-bg: rgba(248, 242, 182, 0.1);
689 --os-ui-warning-border: rgba(248, 242, 182, 0.24);
690 --os-ui-info-fg: #c2f1f1;
691 --os-ui-info-bg: rgba(194, 241, 241, 0.1);
692
693 /*
694 * ---- Component tokens the palette cannot reach --------------
695 *
696 * Each of these is read with a literal of its own rather than a
697 * `var( --os-ui-<palette> )` chain, so the palette above does not
698 * carry to it. Everything else in the kit is left alone and
699 * inherits.
700 */
701
702 /*
703 * Dialogs, menus and flyouts.
704 *
705 * The five `--os-ui-modal-*` names below are the escape hatch
706 * `<os-modal>` needs. Its dialog surface is dark whatever the
707 * admin colour scheme says, so it re-points `--os-ui-fg`,
708 * `--os-ui-fg-muted`, `--os-ui-border`, `--os-window-bg` and
709 * `--os-ui-button-bg-hover` on its own `:host` — and a declaration
710 * on the host outranks anything the host inherits, which is how
711 * the palette used to stop at the dialog's edge. It reads these
712 * instead, so the station owns the dialog again and the literals
713 * behind them stay as the no-stylesheet floor.
714 */
715 --os-ui-modal-bg: #1a1721;
716 --os-ui-modal-text: #fffbff;
717 --os-ui-modal-text-muted: #b3afb5;
718 --os-ui-modal-border: #4d4a52;
719 --os-ui-modal-field-bg: #33303a;
720 --os-ui-modal-button-bg-hover: rgba(255, 251, 255, 0.14);
721 --os-ui-confirm-dialog-bg: #1a1721;
722 --os-ui-context-menu-bg: #1a1721;
723 --os-ui-context-menu-separator-color: rgba(255, 251, 255, 0.1);
724 --os-ui-flyout-shadow: 0 16px 48px rgba(0, 0, 0, 0.6);
725 --os-ui-card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.45);
726 --os-ui-ribbon-shadow: 0 2px 4px rgba(0, 0, 0, 0.45);
727
728 /* Tables. */
729 --os-ui-table-bg: #1a1721;
730 --os-ui-table-header-bg: #33303a;
731 --os-ui-table-border: rgba(255, 251, 255, 0.1);
732 --os-ui-table-column-border: rgba(255, 251, 255, 0.16);
733 --os-ui-table-row-hover: rgba(255, 251, 255, 0.06);
734 --os-ui-table-stripe: rgba(255, 251, 255, 0.03);
735 --os-ui-table-sticky-edge: 2px solid rgba(255, 251, 255, 0.14);
736
737 /* Notices and badges — tinted washes of their own status colour. */
738 --os-ui-notice-error-bg: rgba(255, 90, 90, 0.12);
739 --os-ui-notice-error-border: rgba(255, 90, 90, 0.28);
740 --os-ui-notice-success-bg: rgba(147, 240, 198, 0.12);
741 --os-ui-notice-success-border: rgba(147, 240, 198, 0.28);
742 --os-ui-notice-warning-bg: rgba(248, 242, 182, 0.12);
743 --os-ui-notice-warning-border: rgba(248, 242, 182, 0.28);
744 --os-ui-notice-info-bg: rgba(194, 241, 241, 0.12);
745 --os-ui-notice-info-border: rgba(194, 241, 241, 0.28);
746 --os-ui-notice-neutral-bg: rgba(255, 251, 255, 0.06);
747 --os-ui-notice-neutral-border: rgba(255, 251, 255, 0.14);
748 /*
749 * A link inside a notice reads Nebula, the same value `--os-link`
750 * gives every other shell link — NOT the accent.
751 *
752 * Undeclared, the base fell through to `--wp-admin-theme-color`,
753 * which OpenStation Preferences → Appearance writes inline on `<html>`
754 * from the accent picker. That made the one piece of text a notice
755 * most needs the user to read take whichever hue they chose for
756 * focus rings. An accent is an identity colour; legibility on a dark
757 * surface is not something it can promise, so the link stops asking
758 * it to. Hover lifts to Starlight, and both ends stay in the palette.
759 *
760 * These are consumed from a DOCUMENT-tree rule in `desktop.css`, not
761 * only from the component's own `::slotted( a )`. See the comment
762 * there: a slotted link is a document-tree element, so wp-admin's
763 * bare `a` rule outranks anything the shadow root says about it.
764 */
765 --os-ui-notice-link: #ec9bff;
766 --os-ui-notice-link-hover: #fffbff;
767 --os-ui-badge-danger-bg: rgba(255, 90, 90, 0.16);
768 --os-ui-badge-success-bg: rgba(147, 240, 198, 0.16);
769 --os-ui-badge-warning-bg: rgba(248, 242, 182, 0.16);
770 --os-ui-badge-info-bg: rgba(194, 241, 241, 0.16);
771 --os-ui-badge-neutral-bg: rgba(255, 251, 255, 0.08);
772
773 /*
774 * Fills that must stay fills. Each of these paints a solid shape —
775 * a ribbon, a chip, a focused input's edge — and each was reading
776 * a token the palette turned into a 10%-alpha wash or into the
777 * body colour itself. A ribbon the same colour as the card behind
778 * it is not a subtle ribbon, it is a missing one.
779 */
780 /*
781 * Text on the bright status fills. These are the surfaces the
782 * `--os-ui-fg-on-accent` note points at: a ribbon filled with Pulse,
783 * Sirius or Holomesh mint is a LIGHT surface, and Starlight on it
784 * disappears.
785 */
786 --os-ui-ribbon-fg: #0c0b0f;
787 --os-ui-step-chip-fg: #0c0b0f;
788 /*
789 * The step chip is the right size and shape for the mesh: a small
790 * round fill, one per row, already carrying dark text. Its
791 * `--os-ui-step-chip-fg` above is Void, which is what the mesh
792 * needs anyway — the two decisions were made independently and
793 * agree, which is usually the sign a palette is holding together.
794 */
795 --os-ui-step-chip-bg: var(--os-mesh-holo);
796
797 /*
798 * The default ribbon. One per tile, small, angled, and already
799 * paired with Void ink above — which is what a mesh needs anyway.
800 * The status ribbons below keep their flat colours: a ribbon that
801 * says "deprecated" should say it in warning yellow, not in brand.
802 */
803 --os-ui-ribbon-bg: var(--os-mesh-holo);
804
805 --os-ui-ribbon-info: #c2f1f1;
806 --os-ui-ribbon-warning: #f8f2b6;
807 --os-ui-ribbon-neutral: #4d4a52;
808 --os-ui-crumb-bg: #33303a;
809 --os-ui-tag-input-input-border: #f252fc;
810
811 /*
812 * Pairs that have to stay distinguishable. Hover and pressed, or
813 * base and shimmer, both resolve through `--os-ui-hover` — one wash
814 * for two states reads as a dead control, so the second step of
815 * each pair is named.
816 */
817 --os-ui-btn-bg-active: rgba(255, 251, 255, 0.12);
818 /*
819 * `<os-button>` ghost + secondary. Their resting wash and their
820 * hover wash BOTH fall through to `--os-ui-hover` — in two different
821 * rules, with two different literals behind them (6% and 10%), so
822 * declaring the palette collapsed them onto one value and the
823 * buttons stopped reacting to the pointer entirely. Primary is
824 * unaffected: it hovers with `filter: brightness()` over its
825 * accent fill rather than through this token.
826 */
827 --os-ui-button-bg-hover: rgba(255, 251, 255, 0.14);
828 --os-ui-key-bg-hover: rgba(255, 251, 255, 0.1);
829 --os-ui-key-bg-pressed: rgba(255, 251, 255, 0.16);
830 --os-ui-table-skeleton-highlight: rgba(255, 251, 255, 0.14);
831
832 /* Code, logs, progress, ratings, avatars. */
833 --os-ui-code-border: 1px solid rgba(255, 251, 255, 0.1);
834 --os-ui-log-border: 1px solid rgba(255, 251, 255, 0.08);
835 --os-ui-log-row-border: 1px solid rgba(255, 251, 255, 0.06);
836 --os-ui-progress-track-bg: rgba(255, 251, 255, 0.1);
837 /*
838 * The filled part of a progress bar is the one surface in the kit
839 * that is *already* a hero: bounded, watched, and the only thing
840 * moving on screen. So it takes Holomesh, and the bar stretches
841 * the mesh as it grows — the hue travelling from lavender toward
842 * cyan IS the progress, which a flat accent could only report.
843 *
844 * Retinted here rather than in `<os-progress-bar>` because the
845 * component's own literal has to stay the pre-brand WordPress blue
846 * (it is the no-stylesheet floor, and it is what Legacy collected).
847 * The tone modifiers — success, warning, danger — still declare
848 * flat colours and still win: a failing job should read as red, not
849 * as brand.
850 */
851 --os-ui-progress-fill: var(--os-mesh-holo);
852 --os-ui-rating-star: #f8f2b6;
853 --os-ui-rating-fill: linear-gradient(90deg, #f8f2b6 0%, #fffbff 100%);
854 --os-ui-avatar-dot-ring: #1a1721;
855 /* The avatar's hover halo. It was a fixed indigo — a hue no
856 palette in the station names — so it lit itself in a colour
857 nothing around it used. Pulse. */
858 --os-ui-avatar-halo: #f252fc;
859 /* Search-match highlight inside a label. A WASH, not a fill: the
860 text keeps its own colour, so the wash has to stay light enough
861 for Starlight to carry over it. */
862 --os-ui-search-highlight-bg: rgba(248, 242, 182, 0.3);
863 --os-ui-color-accent: #f252fc;
864 /*
865 * The content graph's cluster hue is NOT retinted. Graph node
866 * colours are composed artwork that encodes meaning — the docs list
867 * them under "what stays fixed" — and moving this one to Nebula put
868 * white cluster labels on a pale lilac chip at 1.9:1. It keeps its
869 * own colour, and the label keeps carrying on it.
870 *
871 * The graph's toolbar is chrome, though, and it does follow. The
872 * count pill on an ACTIVE filter chip is a 70%-white fill, and it
873 * was carrying `--os-ui-accent-strong` numerals — Nebula on white,
874 * ~1.7:1. It is one more bright fill naming its own dark text, the
875 * same escape hatch `--os-ui-ribbon-fg` and `--os-ui-step-chip-fg`
876 * take, and for the same reason.
877 */
878 --os-ui-cg-chip-count-fg-active: #0c0b0f;
879
880 /*
881 * ---- Typography --------------------------------------------
882 *
883 * Geist for everything human — headlines, UI copy, body text — and
884 * Geist Mono for the instrument voice: labels, metadata, statuses,
885 * code. Both faces are declared above; a stack without them still
886 * reads correctly, which is why every entry ends in a generic
887 * family.
888 *
889 * The chrome / body split is the one real decision here, and it is
890 * the one real desktop environments make. Here both sides run
891 * Geist: the station is one surface, and the title bar is not a
892 * different product from the window under it.
893 *
894 * Note that iframe windows render a real wp-admin document in a
895 * separate browsing context that this stylesheet does not reach —
896 * these tokens style the shell and native windows.
897 */
898 --os-font: 'Geist', system-ui, sans-serif;
899 --os-titlebar-font: 'Geist', system-ui, sans-serif;
900 --os-ui-font: 'Geist', system-ui, sans-serif;
901 --os-ui-font-mono: 'Geist Mono', ui-monospace, monospace;
902
903 /*
904 * ---- Desktop-theme texture family -------------------------
905 *
906 * These are DELIBERATELY not declared. Every rule that consumes
907 * one reads it as `var( --name, <initial> )`, so an unset variable
908 * resolves to the CSS initial value and costs exactly nothing.
909 * Declaring them with `none` up here would be equivalent but would
910 * imply they are part of the default palette — they are not. They
911 * exist only while an uploaded or code-registered desktop theme is
912 * active, and only the compiled theme stylesheet ever sets them.
913 *
914 * TITLEBAR --os-titlebar-image
915 * --os-titlebar-image-repeat
916 * --os-titlebar-image-size
917 * TITLEBAR_FOCUSED --os-titlebar-image-focused
918 * (falls back to the unfocused image)
919 * WINDOW_FRAME --os-window-border-image-source
920 * --os-window-border-image-slice
921 * --os-window-border-image-width
922 * --os-window-border-image-repeat
923 * WINDOW_CORNER_* --os-window-corner-{ne,nw,se,sw}-image
924 * --os-window-corner-size (shared)
925 * --os-window-corner-inset (shared;
926 * how far inside the rounded corner the
927 * ornament sits. Defaults to the resize
928 * handle's 3px overhang plus a share of
929 * --os-window-radius, because the
930 * window clips its descendants)
931 * WINDOW_FRAME_FOCUSED
932 * --os-window-border-image-focused-*
933 * (same four sub-properties as WINDOW_FRAME)
934 * WINDOW_BODY --os-window-body-image
935 * TABBAR --os-tabs-image
936 * TITLEBAR_CONTROLS --os-titlebar-controls-image
937 * TITLEBAR_BUTTON --os-ui-btn-bg-image
938 * DOCK --os-dock-bg-image
939 * --os-dock-bg-image-repeat
940 * --os-dock-bg-image-size
941 * DOCK_ITEM --os-dock-item-image
942 * DESKTOP --os-desktop-image
943 * --os-desktop-image-repeat
944 * --os-desktop-image-size
945 * ICON_TILE --os-tile-image
946 * WIDGET --os-widget-image
947 * MENU --os-ui-menu-bg-image
948 * DIALOG --os-ui-dialog-bg-image
949 * SCRIM --os-ui-scrim-image
950 * PANEL --os-ui-panel-bg-image
951 * TOAST --os-ui-toast-bg-image
952 * TABLE_HEADER --os-ui-table-header-bg-image
953 * BUTTON --os-ui-button-bg-image
954 *
955 * Every `image` slot also gets `<prop>-repeat` and `<prop>-size`
956 * companions (the corners share one `-size`, and
957 * TITLEBAR_FOCUSED inherits TITLEBAR's).
958 *
959 * Consumers: window-chrome.css (titlebar, frame, body, tab bar),
960 * window-states.css (corners), dock.css, desktop.css (wallpaper,
961 * icons, widgets), and the `<os-*>` component stylesheets under
962 * `src/ui/components/` for the last seven.
963 * Authoring reference: docs/desktop-themes.md.
964 */
965
966 /*
967 * ---- Badges ----------------------------------------------
968 *
969 * Sizes stay undeclared and DERIVED: every badge sizes itself off
970 * the icon it decorates — the dock badge off
971 * `--os-dock-icon-size`, the wallpaper badges off
972 * `--os-icon-image-size` — so a user who picks a large
973 * dock, or a theme that scales its icons, doesn't end up with a
974 * speck stuck to an oversized tile.
975 *
976 * Dock --os-dock-badge-size (overrides the
977 * whole derivation; font + padding follow it)
978 * --os-dock-badge-font-size / -padding
979 * --os-dock-badge-offset / -bg / -fg
980 * --os-dock-badge-font-weight
981 * Desktop icon --os-icon-badge-size / -font-size
982 * --os-icon-badge-padding / -bg / -fg
983 * Recycle bin --os-recycle-badge-size / -font-size
984 * / -bg / -fg. Falls through to the generic
985 * icon-badge token, so "badges are 22px" reaches
986 * the bin too.
987 *
988 * The count badges keep their red, and keep it as a LITERAL: a
989 * badge is a status, not an identity moment, and Pulse next to a
990 * Pulse-accented dock tile would read as decoration rather than as
991 * "three things need you". Naming the gradient here also keeps it
992 * a gradient — both stops used to resolve through `--os-ui-danger`,
993 * which the palette moved, flattening the badge to one flat red
994 * and turning its white numerals Void. The bin's neutral badge has
995 * no such constraint and follows the station.
996 */
997 --os-dock-badge-bg: linear-gradient(180deg, #ff5a5a 0%, #d63638 100%);
998 --os-dock-badge-fg: #fffbff;
999 --os-icon-badge-bg: linear-gradient(180deg, #ff5a5a 0%, #d63638 100%);
1000 --os-icon-badge-fg: #fffbff;
1001 --os-recycle-badge-bg: rgba(26, 23, 33, 0.85);
1002 --os-recycle-badge-fg: rgba(255, 251, 255, 0.96);
1003 --os-dock-recycle-badge-bg: rgba(26, 23, 33, 0.85);
1004 --os-dock-recycle-badge-fg: rgba(255, 251, 255, 0.92);
1005
1006 /*
1007 * ---- Tooltips ---------------------------------------------
1008 *
1009 * Two tokens own every tooltip in the shell — the dock tile
1010 * tooltip, the content-graph satellite tooltip, and the My
1011 * WordPress entity hover card.
1012 *
1013 * They are worth naming rather than leaving derived. Without them
1014 * tooltips borrow their colours from unrelated families —
1015 * `--os-ui-scrim` (an overlay BACKDROP) or `--os-ui-surface-elevated`
1016 * for the surface, `--os-ui-fg-on-accent` for the text — and those
1017 * pairings come apart the moment the palette moves. On this
1018 * palette `--os-ui-fg-on-accent` is Void, which would have rendered
1019 * near-black text on a near-black chip.
1020 *
1021 * Secondary text inside the richer tooltips — the hover card's
1022 * excerpt — still follows `--os-ui-fg-muted`.
1023 */
1024 --os-tooltip-bg: #33303a;
1025 --os-tooltip-fg: #fffbff;
1026
1027 /*
1028 * ---- Dock --------------------------------------------------
1029 *
1030 * Void glass over the desk, with a Starlight hairline. The tile
1031 * hover wash and the focus ring are the dock's identity moment:
1032 * Pulse, at the two alphas that read as "warm" and "here".
1033 *
1034 * `--os-dock-icon-color` is a COLOUR, not a fill, and
1035 * that is load-bearing: every image glyph in the dock is painted
1036 * as a mask filled with this colour — a theme's own iconset via
1037 * `"iconColor": "currentColor"`, and plugin / CPT artwork via
1038 * `currentColor` on the tile. One token drives dashicons, theme
1039 * art, plugin art, and the hover transition between them.
1040 *
1041 * Consumers: dock.css, dock-peek.css (which re-states the hover
1042 * rule and so must read the same two tokens).
1043 */
1044 --os-dock-width: 56px;
1045 --os-dock-icon-size: 20px;
1046 --os-dock-bg: rgba(12, 11, 15, 0.72);
1047 --os-dock-icon-color: rgba(255, 251, 255, 0.72);
1048 --os-dock-icon-color-hover: #fffbff;
1049 --os-dock-item-bg-hover: rgba(242, 82, 252, 0.18);
1050 --os-dock-item-outline: #f252fc;
1051 /*
1052 * Hairline between the dock and the desktop. Reaches the side
1053 * placements (as a single inline-edge border) AND the floating
1054 * bottom pill (as its full outline). Set `transparent` for a
1055 * borderless dock.
1056 *
1057 * The bottom pill's own chrome is separately overridable, because
1058 * it is deliberately a different piece of glass — a warmer tint, a
1059 * brighter top edge, an inset highlight, a drop shadow. Each falls
1060 * back to the shared value above it.
1061 */
1062 --os-dock-border: rgba(255, 251, 255, 0.1);
1063 --os-dock-floating-bg: rgba(12, 11, 15, 0.62);
1064 --os-dock-floating-border: rgba(255, 251, 255, 0.12);
1065 --os-dock-floating-border-top: rgba(255, 251, 255, 0.18);
1066 --os-dock-floating-highlight: rgba(255, 251, 255, 0.1);
1067 --os-dock-floating-shadow: rgba(0, 0, 0, 0.55);
1068
1069 /*
1070 * ---- Shell surfaces ----------------------------------------
1071 *
1072 * The panels, tab strips and washes that belong to the shell
1073 * rather than to the component kit. Same Obsidian-on-Void
1074 * composition; the selection wash is Pulse.
1075 */
1076 --os-accent: #f252fc;
1077 --os-link: #ec9bff;
1078 --os-danger: #ff5a5a;
1079 --os-border: rgba(255, 251, 255, 0.1);
1080 --os-hover: rgba(255, 251, 255, 0.06);
1081 --os-selected: rgba(242, 82, 252, 0.18);
1082 --os-fg-muted: rgba(255, 251, 255, 0.7);
1083 /*
1084 * Shell text. Undeclared before the brand and it had to be: it read
1085 * light on the desk (tile labels over the wallpaper) and dark
1086 * inside a window, and one value broke one of the two. The station
1087 * settles the argument — every surface it owns is dark now, so
1088 * Starlight is right in both places.
1089 */
1090 --os-fg: #fffbff;
1091 /*
1092 * "…on light" is a name from before the station, and it is now a
1093 * misnomer worth keeping: every surface that reads it — the folder
1094 * window, its preview pane, the upload HUD — was a light card and
1095 * is Obsidian today. The token means "text on the surfaces this one
1096 * names", and those surfaces are dark, so the text is Starlight.
1097 * Renaming it is a public-API change for the sake of tidiness; the
1098 * comment is cheaper and the behaviour is what matters.
1099 */
1100 --os-fg-on-light: #fffbff;
1101 --os-panel-bg: #1a1721;
1102 --os-panel-header-bg: rgba(255, 251, 255, 0.04);
1103 --os-folder-window-bg: #1a1721;
1104 --os-my-wordpress-bg: #1a1721;
1105 --os-my-wordpress-fg: #fffbff;
1106 --os-my-wordpress-surface: rgba(255, 251, 255, 0.06);
1107 --os-ai-panel-bg: rgba(26, 23, 33, 0.97);
1108 --os-tabs-bg: #33303a;
1109 --os-tabs-color: #b3afb5;
1110 --os-media-tile-bg: rgba(255, 251, 255, 0.05);
1111 --os-media-visual-bg: rgba(255, 251, 255, 0.04);
1112 --os-skeleton-low: rgba(255, 251, 255, 0.05);
1113 --os-skeleton-high: rgba(255, 251, 255, 0.13);
1114 --os-drop-preview-bg: rgba(242, 82, 252, 0.1);
1115 --os-drop-preview-border: rgba(242, 82, 252, 0.55);
1116
1117 /*
1118 * File tiles — anywhere a `.os-file-tile` (or sibling
1119 * canvas tile) renders. The defaults match the desk: Starlight
1120 * labels over a dark sky, with a Pulse selection wash.
1121 *
1122 * Surfaces that are light in their own right override these in
1123 * their own scope, e.g.:
1124 *
1125 * .desktop-mode-folder-window {
1126 * --os-tile-fg: #1d2327;
1127 * --os-tile-hover-bg: rgba(0, 0, 0, 0.06);
1128 * }
1129 *
1130 * Plugin authors can override any of these in their own theme
1131 * to retint tiles globally without touching the rules below.
1132 */
1133 --os-tile-fg: var(--os-fg, #fffbff);
1134 --os-tile-fg-muted: rgba(255, 251, 255, 0.7);
1135 --os-tile-hover-bg: rgba(255, 251, 255, 0.12);
1136 --os-tile-selected-bg: rgba(242, 82, 252, 0.24);
1137 --os-tile-focus-ring: var(--wp-admin-theme-color, #f252fc);
1138 --os-tile-shortcut-bg: rgba(12, 11, 15, 0.7);
1139 --os-tile-shortcut-fg: #fffbff;
1140 --os-tile-shortcut-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
1141
1142 /*
1143 * Label rendering tokens. The desk is dark at every wallpaper, so
1144 * a soft drop-shadow boosts legibility against the artwork,
1145 * regular weight, default antialiasing.
1146 *
1147 * Light-context surfaces (folder windows, My WordPress, any
1148 * future window with a light body) rebind these in their own
1149 * scope so tile labels render with the same crispness as the
1150 * surrounding chrome — no per-surface duplicate rules.
1151 */
1152 --os-tile-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
1153 --os-tile-label-weight: 400;
1154 --os-tile-label-smoothing: auto;
1155 --os-tile-label-color: var(--os-tile-fg);
1156
1157 /*
1158 * Admin bar — how many pixels of the bar stay on screen while it
1159 * is parked off the top edge in `dynamic` mode. This is the
1160 * VISIBLE seam only: small enough to read as "nothing is there".
1161 * Themes may retune it; `0` makes the bar invisible until hovered.
1162 */
1163 --os-admin-bar-peek: 4px;
1164
1165 /*
1166 * How far BELOW the parked bar the invisible reveal zone reaches.
1167 * Decoupled from the peek on purpose — the seam wants to be a
1168 * hairline, the pointer target wants to be forgiving, and tying
1169 * them together forces one of the two to be wrong. The band the
1170 * user actually has to hit is `peek + this`.
1171 *
1172 * CEILING: keep `peek + reveal-zone` comfortably under the admin
1173 * bar's own height (32px, 46px below 783px). Past that, leaving
1174 * the revealed bar drops the pointer straight back into the zone,
1175 * which re-reveals it — an oscillation loop, not a wider target.
1176 */
1177 --os-admin-bar-reveal-zone: 16px;
1178
1179 /* Z-index layers */
1180 --os-z-base: 100;
1181 --os-z-dock: 200;
1182 --os-z-adminbar: 9991;
1183 --os-z-fullscreen: 99999;
1184 /* Window-link layer — above widgets (z 1), behind windows (z 100+). */
1185 --os-z-window-links: 50;
1186 /*
1187 * Mio layer — above every window (windows run 100 + stack
1188 * index) so the companion can perch on top of them, but below
1189 * the dock (200) so it never covers navigation.
1190 */
1191 --os-z-mio: 190;
1192
1193 /*
1194 * Window-link ties (relation splines between windows). The accent
1195 * follows the admin color scheme. Solid colors only — the resting
1196 * dimming is done with stroke-opacity/fill-opacity in
1197 * window-links.css, NOT color-mix(): an unresolvable color-mix()
1198 * inside a custom property makes the consuming declaration invalid
1199 * at computed-value time, which turns SVG strokes invisible and
1200 * marker fills black (the written rgba() fallback declaration is
1201 * already gone from the cascade by then). Overridable per scheme
1202 * or by plugins/themes without touching the renderer.
1203 */
1204 --os-window-link-color: var(--wp-admin-theme-color, #f252fc);
1205 --os-window-link-color-active: var(--wp-admin-theme-color, #f252fc);
1206 --os-window-link-width: 1.5px;
1207 --os-window-link-accent: var(--wp-admin-theme-color, #f252fc);
1208 /*
1209 * Halo behind related windows (`.os-window--linked`).
1210 * A literal rgba of Pulse rather than a color-mix() of the accent —
1211 * same no-color-mix rationale as above: if this token failed to
1212 * resolve, the whole box-shadow declaration (elevation shadow
1213 * included) would go invalid. Schemes/plugins that retint the
1214 * accent should override this too.
1215 */
1216 --os-window-link-glow: rgba(242, 82, 252, 0.45);
1217 }
1218
1219 /*
1220 * ---- Iframe windows keep a light body -----------------------------
1221 *
1222 * A window hosting a real `wp-admin` page is not a station surface — it
1223 * is a frame around someone else's document, and that document is
1224 * light. The reveal surface is what covers such a window while it
1225 * loads, so a dark one wiping back to a white page is a flash in the
1226 * wrong direction.
1227 *
1228 * The window's own background is deliberately NOT overridden here, and
1229 * that was learned the hard way: `chromeless.css` paints the iframe's
1230 * canvas, so nothing of the window shows through the page anyway —
1231 * while the tab strip above it carries a 16px `mask-image` fade at
1232 * each end (the "scroll for more tabs" affordance). A mask makes the
1233 * element's own background transparent, so a white window behind a
1234 * dark strip turned that invisible cosmetic into a white gradient
1235 * bleeding into the chrome. The strip needs the station's colour
1236 * behind it; the page needs its own; those are two different surfaces
1237 * and only one of them belongs to the window element.
1238 *
1239 * Native windows — the ones the station draws itself — are excluded
1240 * and keep the Obsidian reveal from the palette above.
1241 */
1242 .os-window:not( .os-window--native ) {
1243 --os-window-reveal-surface: #fff;
1244 }
1245
1246 /*
1247 * ---- The WordPress admin colour scheme no longer tints the shell ----
1248 *
1249 * There used to be nine blocks here, one per admin colour scheme, each
1250 * setting `--wp-admin-theme-color` on
1251 * `.os-shell[data-os-scheme="…"]`. They are gone,
1252 * and their removal is a bug fix as much as a rebrand.
1253 *
1254 * OS Settings → Appearance writes the user's chosen accent as an inline
1255 * style on `<html>` — deliberately, so it also reaches the WordPress
1256 * admin bar, which is a SIBLING of `#os-shell`. But those
1257 * blocks matched the shell root, a descendant of `<html>`, and for
1258 * custom properties the nearest ancestor with a value wins no matter
1259 * what the specificity of the rule that set it was. So inside the shell
1260 * the scheme always beat the picker: choosing Rose, or Pulse, changed
1261 * the admin bar and nothing else. Every focus ring, tab underline, sort
1262 * arrow and selection wash kept the colour of the admin scheme.
1263 *
1264 * With the accent picker defaulting to Pulse there is nothing left for
1265 * these blocks to express: the station has one identity colour, and a
1266 * user who wants their admin scheme's hue picks it in the same panel.
1267 * The `data-os-scheme` attribute is still stamped on the
1268 * shell root for plugins and themes that key off it.
1269 */
1270