PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | assets/css/variables.css +1902 -160 0.9.51.1.10 View file →
@@ -1,84 +1,1797 @@
1 1 /**
2 - * Desktop Mode — CSS Custom Properties.
2 + * OpenStation — CSS Custom Properties.
3 3 *
4 - * All design tokens for the desktop shell. Per-scheme blocks below
5 - * retune the accent + titlebar colors to match the user's WordPress
6 - * admin color scheme — the shell root gets a `data-desktop-mode-scheme`
7 - * attribute in PHP so the right block wins before first paint.
4 + * All design tokens for the desktop shell, carrying the OpenStation
5 + * brand: https://nuriapenya.github.io/open-station-brand/
8 6 *
9 - * Palette source: wp-admin/css/colors/<scheme>/colors.scss in Core.
10 - * We read the `base` color as the focused title bar background (same
11 - * visual weight as the admin sidebar the user is used to) and the
12 - * `highlight` color as `--wp-admin-theme-color`, which Core publishes
13 - * as the accent — buttons, tabs, focus rings.
7 + * ## The palette
14 8 *
15 - * @since 6.9.0
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.
16 50 */
17 51
18 -:root {
19 - /* Desktop area */
20 - --desktop-mode-bg: linear-gradient(135deg, #1d2327 0%, #2c3338 50%, #1d2327 100%);
21 - --desktop-mode-area-inset: 0;
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 +}
22 76
23 - /* Window chrome */
24 - --desktop-mode-window-bg: #fff;
25 - --desktop-mode-window-border: rgba(0, 0, 0, 0.12);
26 - --desktop-mode-window-radius: 8px;
27 - --desktop-mode-window-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 2px 8px rgba(0, 0, 0, 0.15);
28 - --desktop-mode-window-shadow-focused: 0 12px 48px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.2);
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 +}
29 84
30 - /* Title bar — focused bg/color resolve via the scheme overrides below. */
31 - --desktop-mode-titlebar-bg: #f0f0f1;
32 - --desktop-mode-titlebar-bg-focused: var(--wp-admin-theme-color, #2271b1);
33 - --desktop-mode-titlebar-color: #50575e;
34 - --desktop-mode-titlebar-color-focused: #fff;
35 - --desktop-mode-titlebar-height: 40px;
85 +/*
86 + * ---- Registered, so "undeclared" never means transparent ---------
87 + *
88 + * The scope note below keeps this palette out of iframe documents on
89 + * purpose: an admin page inside a window renders with the values its
90 + * own stylesheets were written with. That promise has one hole CSS
91 + * digs by itself — a stylesheet that consumes
92 + * `var(--wp-admin-theme-color)` with no fallback, in a document where
93 + * nothing declares it, does not fall back to an earlier declaration
94 + * in the cascade. An unresolved `var()` is invalid at computed-value
95 + * time, so the property computes to `initial` — and an unregistered
96 + * custom property's initial value is the guaranteed-invalid value,
97 + * which for a `background` means transparent. Core's own scheme files
98 + * declare the property, but a third-party admin scheme that consumes
99 + * it without declaring it renders `.button-primary` white-on-white
100 + * inside every chromeless window (#702).
101 + *
102 + * Registering the property gives "undeclared" a real answer:
103 + * WordPress's default admin colour, the same value Core's fallbacks
104 + * reach for. Declarations are untouched — a scheme, the palette
105 + * below, or the accent picker's inline style all keep winning exactly
106 + * as before. Registration only decides what the absence of all of
107 + * them computes to.
108 + */
109 +@property --wp-admin-theme-color {
110 + syntax: "<color>";
111 + inherits: true;
112 + initial-value: #3858e9;
113 +}
36 114
37 - /* Admin accent. Each scheme block below retunes this; the fallback
38 - * matches the legacy hardcoded value so nothing regresses if the
39 - * root has no data-desktop-mode-scheme attribute (e.g. tests). */
40 - --wp-admin-theme-color: #2271b1;
115 +/*
116 + * ---- Scope: the shell document, and nothing else -----------------
117 + *
118 + * `body.os-active` rather than `:root`, and that is
119 + * load-bearing rather than tidy.
120 + *
121 + * This stylesheet is a dependency of `chromeless.css`, so it also
122 + * loads INSIDE every iframe window — a real `wp-admin` document. A
123 + * palette on `:root` would reach that document and repaint WordPress's
124 + * own UI: `--wp-admin-theme-color` alone would turn Core's primary
125 + * buttons, links and focus rings across every admin screen, with
126 + * white button text landing on a colour it was never contrast-tested
127 + * against.
128 + *
129 + * Iframe documents carry `body.os-chromeless` instead, so
130 + * they match nothing here and every rule in the tree falls back to the
131 + * literal it was always written with — the WordPress-admin values.
132 + * **An admin page inside a window looks exactly as it does outside
133 + * one.** That is deliberate: the station is the chrome around the
134 + * page, not a reskin of the page.
135 + *
136 + * Specificity: (0,1,1). A desktop theme compiles to
137 + * `body.os-desktop-theme-<slug>`, also (0,1,1) on the same
138 + * element, and its stylesheet declares `os-variables` as a
139 + * dependency — so it always prints after this file and wins the tie.
140 + * Do not remove that dependency; see the note in
141 + * `openstation_enqueue_desktop_theme_style()`.
142 + */
143 +body.os-active {
144 + /* MIO's conversation follows desktop themes through these public tokens. */
145 + --os-mio-chat-bg: var(--os-ui-modal-bg, #15151e);
146 + --os-mio-chat-fg: var(--os-ui-modal-text, #f4f4fb);
147 + --os-mio-chat-muted: var(--os-ui-modal-text-muted, #b4b4c6);
148 + --os-mio-chat-hover-bg: color-mix(in srgb, var(--os-mio-chat-bg, #fff) 90%, var(--os-mio-chat-fg, #1d2327));
149 + --os-mio-chat-border: color-mix(in srgb, var(--os-ui-accent, #a855f7) 40%, transparent);
150 + --os-mio-chat-user-bg: color-mix(in srgb, var(--os-ui-accent, #3f6dff) 15%, transparent);
151 + --os-mio-chat-glow: color-mix(in srgb, var(--os-ui-accent-dim, #ff4fd8) 10%, transparent);
152 + --os-mio-launcher-bg: color-mix(in srgb, var(--os-mio-chat-bg, #fff) 62%, transparent);
153 + --os-mio-callout-bg: var(--os-mio-chat-bg, #15151e);
154 + --os-mio-callout-fg: var(--os-mio-chat-fg, #f4f4fb);
155 + --os-mio-callout-border: color-mix(in srgb, var(--os-ui-accent-dim, #a855f7) 32%, transparent);
156 + --os-mio-chat-radius: max(18px, var(--os-window-radius, 22px));
41 157
158 + /*
159 + * ---- Desk --------------------------------------------------
160 + *
161 + * `Space`, the brand's quiet deep-space backdrop: a linear
162 + * gradient from near-black through to a faint violet horizon.
163 + *
164 + * Note this is the desk's CSS default, not usually what you see:
165 + * the wallpaper layer writes the ACTIVE wallpaper into this same
166 + * property as an inline style, and the shipped default is the
167 + * `Galaxy` artwork (`includes/wallpapers.php`). This value is what
168 + * paints before that runs, and for anyone whose wallpaper is
169 + * unavailable.
170 + */
171 + --os-bg: linear-gradient(160deg, #010101 0%, #111114 58%, #1e1d23 100%);
172 + /*
173 + * Flat colour painted on the shell itself, behind every other
174 + * layer including the wallpaper.
175 + *
176 + * The shell is `position: fixed` over the classic admin page, and
177 + * that page is WHITE. Anything that leaves a layer unpainted for a
178 + * single frame — a canvas wallpaper's context being disturbed, the
179 + * compositor re-rasterising after a large GPU layer goes away —
180 + * shows it. This is the floor that makes that impossible: the worst
181 + * a dropped frame can now look like is Void.
182 + *
183 + * Deliberately a solid rather than `--os-bg`: it is never
184 + * seen when things are working, so it should cost one flat fill,
185 + * not a second gradient rasterisation on every shell repaint.
186 + */
187 + --os-backstop: #0c0b0f;
188 + --os-area-inset: 0;
189 +
190 + /*
191 + * ── The icon grid ────────────────────────────────────────────
192 + *
193 + * ONE grid, everywhere placements are laid out: the wallpaper,
194 + * folder windows, and every canvas in the site folder. Before
195 + * these tokens each surface carried its own pitch and its own
196 + * tile width, and the two drifted apart — the desktop ended up
197 + * with an 88px tile in a 96px cell whose 8px of air the tile's
198 + * own padding ate, so icons touched edge to edge while the site
199 + * folder had 20px between them.
200 + *
201 + * The cell is derived, never declared: `cell = tile + gap`. A
202 + * gap you can see is the whole point, so it is the number that
203 + * gets tuned; the pitch follows.
204 + *
205 + * The TypeScript side mirrors these in `src/desktop-files/grid.ts`
206 + * (layout maths can't read CSS), and
207 + * `tests/vitest/grid-metrics.test.ts` parses this file to prove
208 + * the two agree. Change a number here and that test tells you
209 + * exactly which constant to move with it.
210 + */
211 + --os-tile-w: 88px;
212 + /*
213 + * A FIXED height, and it has to fit the tallest a tile can get:
214 + * 8px padding + 48px icon well + 6px gap + two clamped label
215 + * lines (2 × 12px × 1.2 = 28.8px) + 8px padding = 98.8px, so
216 + * 104px with a little slack for font metrics.
217 + *
218 + * Fixed rather than minimum because the selection ring is drawn
219 + * around the tile box: let the box follow its label and a row of
220 + * selected icons is a ragged run of different-height rectangles,
221 + * one per label that happened to wrap.
222 + */
223 + --os-tile-h: 104px;
224 + --os-grid-gap-x: 20px;
225 + --os-grid-gap-y: 16px;
226 + /* Gutter from the top / inline-start edge of any icon canvas. */
227 + --os-grid-padding: 16px;
228 + /*
229 + * Image-led sections opt into a bigger tile (`tileSize: 'large'`)
230 + * — a shop's products read as a catalogue, not a file list. Same
231 + * gaps, bigger tile.
232 + */
233 + --os-tile-w-large: 132px;
234 + --os-tile-h-large: 160px;
235 +
236 + /* Window chrome — Obsidian body inside a Starlight hairline. */
237 + --os-window-bg: #1a1721;
238 + --os-window-border: rgba(255, 251, 255, 0.12);
239 + /*
240 + * Window corner. Softer than the 8px the shell carried before, so
241 + * a window reads as an object on the desk rather than a panel
242 + * pinned to it.
243 + *
244 + * The tab strip's own radius is deliberately NOT tied to this one.
245 + * A tab is 30px tall, so a corner anywhere near this size eats the
246 + * whole straight edge and turns the tab into a capsule — it stops
247 + * reading as a folder tab attached to the page, which is the one
248 + * thing the tab has to say. `--os-tabs-radius` stays smaller and
249 + * independent on purpose.
250 + */
251 + --os-window-radius: 16px;
252 + /*
253 + * Shadows stay black and go deeper than they were: on a Void desk
254 + * a soft grey shadow is invisible, and depth is the only thing
255 + * separating a window from the sky behind it.
256 + */
257 + --os-window-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3);
258 + --os-window-shadow-focused: 0 12px 48px rgba(0, 0, 0, 0.6), 0 4px 12px rgba(0, 0, 0, 0.35);
259 + /*
260 + * Fill of the surface a window's content is revealed from once it
261 + * finishes loading (OS Settings → Effects → "Window reveal").
262 + *
263 + * Obsidian, so a window uncovers from its own colour rather than
264 + * flashing white on a dark desk. The surface has to be opaque or
265 + * there is nothing to reveal FROM — the content would simply be
266 + * visible the whole time and the clip animation would paint
267 + * nothing. `transparent` is a legitimate value meaning "no
268 + * covering surface"; the shell then skips the layer rather than
269 + * animating something invisible.
270 + *
271 + * A reveal may override this for itself when the paint IS the
272 + * reveal; `obturator` (Camera shutter) is the one built-in that
273 + * does, because near-black blades are what make it a shutter.
274 + */
275 + --os-window-reveal-surface: #1a1721;
276 + /*
277 + * Fill of the reveal's leading edge — the band that travels with
278 + * the clip boundary and draws each reveal's shape (six lines on
279 + * Blinds, an opening ring on Iris, a rotating spoke on Radar).
280 + *
281 + * `transparent` by default: the reveal reads as the page arriving,
282 + * and a hard graphic edge on top of that is a deliberate look
283 + * rather than the neutral one. A theme (or any stylesheet) opts in
284 + * by giving this a colour — no JS and no per-reveal configuration
285 + * needed, because the band follows whatever shape the active
286 + * reveal has. While it computes to fully transparent the shell
287 + * skips the layer altogether, so the default costs nothing.
288 + */
289 + --os-window-reveal-edge: transparent;
290 +
291 + /*
292 + * ---- Title bar ---------------------------------------------
293 + *
294 + * Obsidian when focused, Void when not: the focused window's
295 + * chrome sits one step up the Shade ramp while its unfocused
296 + * neighbours sink toward the desk they are lying on. That is the
297 + * entire focus signal, and it costs no colour — which matters,
298 + * because the accent is spoken for.
299 + *
300 + * The pair used to be Astro-over-Obsidian, a step higher. It came
301 + * down so a focused window's title bar and its tab strip are the
302 + * SAME Obsidian, and the only thing lifting out of that surface is
303 + * the tab itself. The step is the same size; the whole ramp moved.
304 + */
305 + --os-titlebar-bg: #0c0b0f;
306 + --os-titlebar-bg-focused: #1a1721;
307 + --os-titlebar-color: #99969c;
308 + --os-titlebar-color-focused: #fffbff;
309 + --os-titlebar-height: 40px;
310 + /* Hairline between page chrome and window chrome. */
311 + --os-titlebar-divider: rgba(255, 251, 255, 0.12);
312 + --os-titlebar-divider-unfocused: rgba(255, 251, 255, 0.06);
313 +
314 + /*
315 + * The status ring — leading mark of the title bar, where the app
316 + * icon used to be. Four states: the resting ring is Starlight,
317 + * work and success are both Pulse, failure is the danger colour.
318 + *
319 + * Pulse twice over is not an oversight. In flight and landed
320 + * differ in FILL — an open ring breathing, then a solid disc with
321 + * a check — and failure differs from both in fill AND glyph AND
322 + * hue. Shape carries the distinction that colour alone cannot for
323 + * a user who can't separate the two hues, and the accent stays the
324 + * shell's own voice for "this is your station working".
325 + *
326 + * Failure resolves through `--os-ui-danger` and is the one value
327 + * here that must not be quietened: a failure that whispers is a
328 + * failure the user misses.
329 + *
330 + * The resting ring is one value in both title-bar states, not two:
331 + * the ring reports a phase, and dimming it on an unfocused window
332 + * would make `idle` say something different depending on which
333 + * window you last clicked.
334 + */
335 + --os-titlebar-activity-idle-color: #fffbff;
336 + --os-titlebar-activity-color: var(--os-ui-accent, #2271b1);
337 + --os-titlebar-activity-saved-color: var(--os-ui-accent, #2271b1);
338 + --os-titlebar-activity-failed-color: var(--os-ui-danger, #d63638);
339 + --os-titlebar-activity-size: 16px;
340 +
341 + /*
342 + * Title-bar control glyphs — minimise / maximise / close, the ⋯
343 + * menu trigger, and the screen-meta cluster — in each of the two
344 + * title-bar states.
345 + *
346 + * Both halves are named explicitly. The unfocused half would
347 + * otherwise derive itself from `--os-titlebar-color` by
348 + * a `color-mix()` that only runs while a desktop theme is active,
349 + * and the focused half cannot derive at all: its glyphs sit on
350 + * `-bg-focused`, and CSS has no contrast-safe function of a
351 + * background colour.
352 + *
353 + * Close-button red is deliberately not in either set — it is
354 + * semantic signal, not chrome, and both states resolve it through
355 + * `--os-ui-danger`.
356 + */
357 + --os-titlebar-btn-color: rgba(255, 251, 255, 0.55);
358 + --os-titlebar-btn-color-hover: #fffbff;
359 + --os-titlebar-btn-bg-hover: rgba(255, 251, 255, 0.1);
360 + --os-titlebar-btn-bg-active: rgba(255, 251, 255, 0.16);
361 + --os-titlebar-btn-focused-color: rgba(255, 251, 255, 0.72);
362 + --os-titlebar-btn-focused-color-hover: #fffbff;
363 + --os-titlebar-btn-focused-bg-hover: rgba(255, 251, 255, 0.14);
364 + --os-titlebar-btn-focused-bg-active: rgba(255, 251, 255, 0.2);
365 + --os-titlebar-btn-focused-outline: #f252fc;
366 +
367 + /*
368 + * The control cluster and each button face stay TRANSPARENT, so a
369 + * themed title-bar texture runs edge to edge underneath them and
370 + * the glyphs float on the artwork. A plate behind the controls is
371 + * a choice a theme makes, not a default the framework imposes.
372 + * The knobs for building one:
373 + *
374 + * Cluster --os-titlebar-controls-bg
375 + * --os-titlebar-controls-image (+ -repeat,
376 + * -size, -position)
377 + * --os-titlebar-controls-radius
378 + * --os-titlebar-controls-padding (inline)
379 + * --os-titlebar-controls-gap
380 + * Screen-meta cluster
381 + * --os-titlebar-meta-bg / -image / -radius
382 + * (radius falls through to the controls radius)
383 + * Each face --os-ui-btn-bg / --os-ui-btn-bg-image / --os-ui-btn-radius
384 + */
385 +
386 + /*
387 + * Admin accent. Pulse, the brand's identity colour, and the value
388 + * behind the `pulse` swatch that OS Settings → Appearance ships as
389 + * its default (`includes/accents.php`). The accent picker writes
390 + * the user's choice into this same property as an inline style, so
391 + * this is the pre-JS value and the floor under every scheme block
392 + * below.
393 + */
394 + --wp-admin-theme-color: #f252fc;
395 +
42 396 /* Resize handle — larger hit area than visual glyph; corners
43 397 are transparent until hover so they don't compete with the
44 398 window's own drop shadow. */
45 - --desktop-mode-resize-size: 20px;
46 - --desktop-mode-resize-color: rgba(0, 0, 0, 0.15);
399 + --os-resize-size: 20px;
400 + --os-resize-color: rgba(255, 251, 255, 0.18);
47 401
48 - /* Dock */
49 - --desktop-mode-dock-width: 56px;
50 - --desktop-mode-dock-icon-size: 20px;
51 - --desktop-mode-dock-bg: rgba(0, 0, 0, 0.4);
52 - --desktop-mode-dock-border: rgba(255, 255, 255, 0.08);
402 + /*
403 + * ---- The `--os-ui-*` UI palette ------------------------------
404 + *
405 + * The single vocabulary for every surface, text tone, border, and
406 + * status colour inside window BODIES — the `<os-*>` component kit
407 + * AND the feature stylesheets both read these. Setting one value
408 + * retints everything that uses it, which is what makes a
409 + * whole-OS repaint eleven declarations rather than a thousand.
410 + *
411 + * It reaches further than window bodies: the shell's own
412 + * body-mounted overlays — toasts, confirm dialogs, context menus,
413 + * and the command palette — read it too.
414 + *
415 + * Roughly 190 component-local tokens (`--os-ui-card-bg`,
416 + * `--os-ui-table-header-bg`, …) fall through to these, e.g.
417 + *
418 + * background: var( --os-ui-card-bg, var( --os-ui-surface, #fff ) );
419 + *
420 + * so the ones named further down are only those whose own literal
421 + * does NOT chain through the palette and would otherwise stay a
422 + * light-mode value on a dark station.
423 + */
53 424
425 + /* Surfaces — Obsidian on Void, one step up the ramp for elevated. */
426 + --os-ui-surface: #1a1721;
427 + --os-ui-surface-elevated: #33303a;
428 + --os-ui-surface-sunken: #0c0b0f;
429 + --os-ui-surface-raised: rgba(255, 251, 255, 0.06);
430 + --os-ui-surface-subtle: rgba(255, 251, 255, 0.03);
431 + --os-ui-bg: #1a1721;
432 + --os-ui-bg-subtle: rgba(255, 251, 255, 0.03);
433 +
54 434 /*
55 - * File tiles — anywhere a `.desktop-mode-file-tile` (or sibling
56 - * canvas tile) renders. The defaults below match the wallpaper
57 - * (dark gradient → white text + soft white hover). Light-on-dark
58 - * surfaces override these in their own scope, e.g.:
435 + * Fields — the surface an input paints, for markup in the LIGHT
436 + * DOM.
59 437 *
438 + * The kit's own controls don't need these: `<os-text-field>` and
439 + * friends resolve `--os-window-bg` inside their shadow roots,
440 + * which the window chrome sets. A plugin writing a plain
441 + * `<input>` into a native window's body has neither, and the
442 + * palette had no general answer — no `surface` variant means
443 + * "input". What it had was `--os-ui-modal-field-bg`, which is a
444 + * MODAL token: right value, wrong scope, and the sort of thing a
445 + * plugin author finds by grepping for a colour that looks close.
446 + *
447 + * Consumers keep the pre-brand WordPress-admin literals as their
448 + * fallbacks — `#fff`, `#8c8f94`, `#2c3338` — so an unstyled page
449 + * lands on the classic admin's own field, not on nothing:
450 + *
451 + * background: var( --os-ui-field-bg, #fff );
452 + * border: 1px solid var( --os-ui-field-border, #8c8f94 );
453 + * color: var( --os-ui-field-fg, #2c3338 );
454 + */
455 + --os-ui-field-bg: #33303a;
456 + --os-ui-field-border: #4d4a52;
457 + --os-ui-field-fg: #fffbff;
458 +
459 + /*
460 + * Field sizing. The kit's own `<os-text-field>` and `<os-textarea>`
461 + * (and every kit control that hosts a typing input) read these for
462 + * the control's type size and corner. The phone layer raises the
463 + * size to 16px — below that, iOS zooms the whole page into a
464 + * focused control and never zooms back — and rounds its search;
465 + * a theme may do the same anywhere.
466 + */
467 + --os-ui-field-font-size: 13px;
468 + --os-ui-field-font-size-compact: 12px;
469 + --os-ui-field-radius: 6px;
470 +
471 + /* Text — Starlight down through the Shade ramp. */
472 + --os-ui-fg: #fffbff;
473 + --os-ui-fg-muted: #b3afb5;
474 + --os-ui-fg-faint: #66636b;
475 + --os-ui-text-muted: rgba(255, 251, 255, 0.6);
476 + /*
477 + * Text on a filled surface — and the name undersells it. Around
478 + * forty rules read this token, and only about half of them sit on
479 + * the accent: the rest are white-on-dark chips that never had a
480 + * name of their own — the toast, the drag hint, the widget picker,
481 + * the file-tile lock, the overview labels, every scrim-backed
482 + * caption in OS Settings.
483 + *
484 + * So it is Starlight. Void reads better ON Pulse (5.9:1 against
485 + * 2.9:1) and was the first choice here, but it turned all of those
486 + * chips into black text on a near-black wash. One token cannot be
487 + * both, and the dark-chip half is both larger and the half that
488 + * fails catastrophically rather than merely weakly.
489 + *
490 + * The surfaces that really are a bright fill — ribbons and step
491 + * chips — name their own dark text below, which is the escape
492 + * hatch this token leaves open.
493 + */
494 + --os-ui-fg-on-accent: #fffbff;
495 +
496 + /* Lines. */
497 + --os-ui-border: #33303a;
498 + --os-ui-border-strong: #4d4a52;
499 +
500 + /* Interaction — light washes, since the surface underneath is dark. */
501 + --os-ui-hover: rgba(255, 251, 255, 0.06);
502 + --os-ui-scrim: rgba(12, 11, 15, 0.72);
503 +
504 + /*
505 + * ---- Text selection ------------------------------------------
506 + *
507 + * Lagoon, the brand's violet-blue, at 60%. It is the one primary
508 + * colour in the guidelines the shell had no job for, and this is
509 + * the right job: violet-blue is what every OS has trained people
510 + * to read as "selected", so it needs no learning.
511 + *
512 + * The alpha is where the accessibility is, and it is squeezed from
513 + * both ends. A selection has to satisfy two constraints at once:
514 + *
515 + * 1. the selected TEXT stays readable on it — Starlight over
516 + * Lagoon@60% is 5.1:1 on Obsidian, 5.5:1 on Void; and
517 + * 2. the highlight is distinguishable from the unselected
518 + * background — 3.4:1 and 3.5:1 respectively.
519 + *
520 + * Raising the alpha helps (2) and hurts (1); lowering it does the
521 + * reverse. 60% is the middle of the band where both clear their
522 + * bar on both surfaces. An accent wash cannot do this at all: the
523 + * Pulse family only separates from the background above 75%, by
524 + * which point it is a fill rather than a highlight.
525 + *
526 + * The text colour is Starlight — the colour body text already is.
527 + * A selection that also RECOLOURS the text destroys every
528 + * distinction the text was carrying: syntax highlighting, a red
529 + * error, a muted timestamp. Highlighting is not restyling.
530 + */
531 + --os-ui-selection-bg: rgba(159, 152, 255, 0.6);
532 + --os-ui-selection-fg: #fffbff;
533 +
534 + /* Accent — the identity moment. */
535 + --os-ui-accent: #f252fc;
536 + /*
537 + * Ink on the accent FILL: the tick in a checked box, the dot in a
538 + * radio, the label of a primary button. Distinct from
539 + * `--os-ui-fg-on-accent`, which the shell reads as "light text on
540 + * a dark surface" in seventy-odd places that are not the accent.
541 + * Starlight here for Pulse; the accent picker rewrites it inline
542 + * to Void whenever the chosen accent is light (Starlight, Sirius,
543 + * Nebula, Lagoon), so the pair always agree.
544 + */
545 + --os-ui-accent-ink: #fffbff;
546 + --os-ui-accent-strong: #ec9bff;
547 + --os-ui-accent-soft: color-mix(in srgb, var(--os-ui-accent-dim) 14%, transparent);
548 + /*
549 + * Pulse, one step back — and the single knob for how loud the
550 + * station is.
551 + *
552 + * Pulse is not a contrast problem: #f252fc carries 6.2:1 against
553 + * Obsidian, better than most accents in a dark UI manage. What it
554 + * is, is SATURATED — HSL 296, 97, 65 — and the places that hurt
555 + * are the ones where it is spread rather than stated: a bloom
556 + * behind a focused control, a 18% wash under a selected row, a
557 + * fill wider than a chip. Ten of those on one panel and the whole
558 + * surface hums.
559 + *
560 + * So the fix is not to move Pulse. It is to spend it where it is
561 + * a statement and to spend THIS where it is atmosphere: the same
562 + * hue with saturation and lightness pulled down together (S 77,
563 + * L 53), which is what keeps it recognisably Pulse rather than a
564 + * different purple. 4.6:1 on Obsidian, and Starlight over it goes
565 + * from 2.8:1 to 3.8:1 — so the wash also stopped eating text.
566 + *
567 + * Everything ambient below resolves through it: the glows, the
568 + * soft wash, the focus bloom. Turning the station up or down is
569 + * one edit here.
570 + *
571 + * `--os-ui-accent` itself stays #f252fc. That one is the brand's,
572 + * not ours — it is what the guidelines name and what
573 + * `brand-palette.test.ts` pins, and moving it is a brand decision
574 + * rather than a UI one.
575 + */
576 + --os-ui-accent-dim: #d92ee3;
577 +
578 + /*
579 + * ---- The meshes ---------------------------------------------
580 + *
581 + * The brand ships five mesh gradients as SVG artwork. These are
582 + * those same meshes transcribed into CSS: the base linear plus
583 + * every radial glow, at the offsets, radii, colours and alphas
584 + * the vector files declare, converted from the 1440×960 artboard
585 + * into percentages so they scale to any box.
586 + *
587 + * Transcribed rather than referenced on purpose. A `url()` to an
588 + * SVG cannot be animated, cannot be tinted, costs a request, and
589 + * cannot resize its glows independently of the box — a mesh
590 + * painted on a 40 px switch track would be one flat corner of the
591 + * artwork. As gradient stacks they rasterise at whatever size the
592 + * control happens to be, and `background-position` can slide them,
593 + * which is the entire holographic effect: a foil that shifts as
594 + * you tilt it.
595 + *
596 + * Layer order is inverted from the SVG. In SVG the last `<rect>`
597 + * paints on top; in a CSS `background-image` list the FIRST layer
598 + * is on top. So each mesh below reads bottom-glow-first and ends
599 + * with its base linear.
600 + *
601 + * The brand's own rule — "meshes reserved for hero surfaces" — is
602 + * why none of these is a surface colour. They are what the shell
603 + * paints at an identity moment: a switch that is on, the primary
604 + * action in a dialog, the filled part of a progress bar. See the
605 + * `--os-ui-holo-*` block below for the tokens components actually
606 + * read.
607 + */
608 +
609 + /*
610 + * Holomesh. Lavender base under eight glows — white, cyan, pink,
611 + * warm, mint, white, pink, blue. The default holographic fill.
612 + */
613 + --os-mesh-holo:
614 + 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%),
615 + 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%),
616 + 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%),
617 + 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%),
618 + radial-gradient(ellipse 17.4% 35.7% at 23% 67.7%, rgba(248, 242, 182, 0.85) 0%, rgba(248, 242, 182, 0) 100%),
619 + 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%),
620 + 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%),
621 + 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%),
622 + linear-gradient(124deg, #afa2e8 0%, #b7abea 55%, #c3b8ef 100%);
623 +
624 + /*
625 + * Pulsemesh. The loud one — a blue-to-violet base torn open by
626 + * magenta. Reserved for the moments Holomesh would undersell.
627 + */
628 + --os-mesh-pulse:
629 + 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%),
630 + radial-gradient(ellipse 33.3% 50% at 61.1% 62.5%, rgba(251, 134, 236, 0.85) 0%, rgba(251, 134, 236, 0) 100%),
631 + radial-gradient(ellipse 36.1% 54.2% at 10.4% 39.6%, rgba(250, 61, 248, 1) 0%, rgba(250, 61, 248, 0) 100%),
632 + 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%),
633 + 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%),
634 + 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%),
635 + linear-gradient(236deg, #8d9bf3 0%, #c878f0 100%);
636 +
637 + /* Auromesh. Lavender through mint into cyan — the calm mesh. */
638 + --os-mesh-auro: linear-gradient(80deg, #d1c6f8 9.5%, #cefada 50%, #b8ffff 90.5%);
639 +
640 + /* Starmesh. A Starlight glow falling away into Cloud. */
641 + --os-mesh-star:
642 + radial-gradient(ellipse 72.7% 72.7% at 50% 12.5%, #fffbff 0%, #fffbff 67.8%, #ccc8ce 100%);
643 +
644 + /* Miomesh. The mascot's own sweep. Mio's, and used sparingly. */
645 + --os-mesh-mio: linear-gradient(90deg, #f252fc 7%, #aa67ff 48.3%, #a580ff 70.7%, #4b3eff 93%);
646 +
647 + /*
648 + * ---- Holographic controls -----------------------------------
649 + *
650 + * What a control reads when it wants to be holographic. Every
651 + * `<os-*>` resolves these through a private `--_holo-*` alias (see
652 + * `src/ui/holo.ts`), so a desktop theme can re-point any of them
653 + * and every control in the kit changes together.
654 + *
655 + * The holographic state is a MOMENT, not a skin. A control paints
656 + * `--os-ui-holo-fill` when it is on, selected, primary or filled —
657 + * the one instant it is speaking for the brand — and wears
658 + * ordinary Obsidian the rest of the time. That is the difference
659 + * between a station and a novelty; a panel where every control is
660 + * iridescent has no identity moments left to spend.
661 + */
662 +
663 + /* The fill itself. Holomesh, so the mesh is the default look. */
664 + --os-ui-holo-fill: var(--os-mesh-holo);
665 + /*
666 + * A hero FIELD, as distinct from a hero control.
667 + *
668 + * `--os-ui-holo-fill` is sized for a surface a control wears — a
669 + * button, a chip, a progress bar. Stretch that same value across
670 + * a few hundred square pixels of panel and a theme's answer stops
671 + * working: Legacy's `#f0f6fc -> #f6f7f7` reads as "on" on a 40px
672 + * chip and as nothing at all on a rail, where it lands within a
673 + * few units of the surface behind it.
674 + *
675 + * So a large decorative field reads THIS instead, and a theme
676 + * gets to answer the two cases separately — how a control looks
677 + * when it is on, and how much of itself the station shows in the
678 + * background. Legacy sets a flat pre-brand wash here while
679 + * leaving its control fill alone.
680 + *
681 + * The default is an aurora, and it is built differently from the
682 + * five meshes on purpose. Those are OPAQUE light surfaces — the
683 + * darkest stop in Holomesh still sits above 60% luminance — which
684 + * is right for a control that has to read as lit against whatever
685 + * is behind it. Spread one across a rail and the only way to calm
686 + * it down is `opacity`, which drags every hue toward the surface
687 + * underneath: on Void that is grey, and the mesh dies as a
688 + * grey-mauve smear.
689 + *
690 + * So the alpha lives in the COLOUR STOPS here, over transparent.
691 + * The desk shows through instead of being covered, the hues keep
692 + * their chroma at any strength, and the field stays dark the way
693 + * an aurora is dark — light added to a night sky, not a pale
694 + * sheet laid over one. Hues are the brand's: violet and Nebula
695 + * through the upper curtains, Sirius cyan and Holomesh's mint
696 + * below, one wide Pulse bloom tying them together.
697 + */
698 + --os-ui-hero-mesh:
699 + radial-gradient(ellipse 62% 38% at 34% 20%, rgba(170, 103, 255, 0.58) 0%, rgba(170, 103, 255, 0) 74%),
700 + radial-gradient(ellipse 54% 33% at 58% 36%, rgba(236, 155, 255, 0.45) 0%, rgba(236, 155, 255, 0) 76%),
701 + radial-gradient(ellipse 68% 30% at 26% 50%, rgba(125, 239, 245, 0.32) 0%, rgba(125, 239, 245, 0) 74%),
702 + radial-gradient(ellipse 58% 28% at 46% 66%, rgba(147, 240, 198, 0.26) 0%, rgba(147, 240, 198, 0) 76%),
703 + radial-gradient(ellipse 96% 62% at 40% 42%, rgba(242, 82, 252, 0.18) 0%, rgba(242, 82, 252, 0) 82%);
704 + /*
705 + * Glyphs and text ON that fill. Void, not Starlight — every mesh
706 + * in the brand is a LIGHT surface (its darkest stop, Holomesh's
707 + * #afa2e8, still sits at 62% luminance), so Starlight on it is a
708 + * white-on-lilac 1.4:1 that looks fine in a mock and is unreadable
709 + * on a screen. Void carries at 8.6:1 against the same stop.
710 + */
711 + --os-ui-holo-ink: #0c0b0f;
712 + /*
713 + * The hover film. A control that is NOT in its holographic state
714 + * still hints at one under the pointer: a low-alpha sweep of the
715 + * mesh's own hues, laid over whatever the surface already is.
716 + *
717 + * Alphas are deliberately in the 6–12% range. This lands on
718 + * Obsidian, where anything stronger stops reading as a sheen and
719 + * starts reading as a second, wrongly-coloured surface.
720 + */
721 + --os-ui-holo-sheen: linear-gradient(
722 + 124deg,
723 + rgba(159, 214, 255, 0.1) 0%,
724 + rgba(236, 155, 255, 0.12) 34%,
725 + rgba(242, 82, 252, 0.1) 58%,
726 + rgba(147, 240, 198, 0.09) 100%
727 + );
728 + /*
729 + * The iridescent hairline. Painted as a gradient on a padding-box
730 + * mask rather than a `border-color`, because a border cannot hold
731 + * a gradient and `border-image` loses the corner radius.
732 + */
733 + --os-ui-holo-edge: linear-gradient(
734 + 124deg,
735 + rgba(154, 242, 255, 0.7) 0%,
736 + rgba(236, 155, 255, 0.85) 38%,
737 + rgba(242, 82, 252, 0.7) 62%,
738 + rgba(159, 152, 255, 0.6) 100%
739 + );
740 + /* Resting edge — the same hairline, quieted, for an idle control. */
741 + --os-ui-holo-edge-quiet: linear-gradient(
742 + 124deg,
743 + rgba(154, 242, 255, 0.22) 0%,
744 + rgba(236, 155, 255, 0.28) 38%,
745 + rgba(242, 82, 252, 0.22) 62%,
746 + rgba(159, 152, 255, 0.2) 100%
747 + );
748 + /*
749 + * The bloom around a holographic surface. Pulse, because on a Void
750 + * desk the glow is what separates "lit" from "light-coloured".
751 + */
752 + --os-ui-holo-glow: 0 0 0 1px color-mix(in srgb, var(--os-ui-accent-dim) 20%, transparent),
753 + 0 2px 10px color-mix(in srgb, var(--os-ui-accent-dim) 15%, transparent);
754 + --os-ui-holo-glow-strong: 0 0 0 1px color-mix(in srgb, var(--os-ui-accent-dim) 30%, transparent),
755 + 0 4px 18px color-mix(in srgb, var(--os-ui-accent-dim) 26%, transparent),
756 + 0 1px 3px rgba(12, 11, 15, 0.6);
757 + /*
758 + * The unlit half — switch tracks, empty progress, the unchecked
759 + * checkbox.
760 + *
761 + * This was a sunken Void well, and it was a mistake that measured
762 + * badly: `rgba(12, 11, 15, 0.55)` composites to #121017 on an
763 + * Obsidian panel, which is **1.07:1** against that panel. An off
764 + * switch was, to a very good approximation, not drawn. WCAG 1.4.11
765 + * asks for 3:1 on the boundary of a control, and this was not in
766 + * the same postcode.
767 + *
768 + * So the well is LIFTED rather than sunk: a Starlight wash, which
769 + * also means it tracks whatever surface a theme puts behind it
770 + * instead of pinning a grey that only works on Obsidian. Every
771 + * dark OS does the same thing with its switches for the same
772 + * reason — on a dark UI the visible thing is the lighter one.
773 + *
774 + * The fill alone still only reaches ~1.6:1, which is a *look*
775 + * rather than a boundary. The 3:1 comes from the edge below, and
776 + * the two together are what make an off switch findable.
777 + */
778 + --os-ui-holo-track: rgba(255, 251, 255, 0.16);
779 + /*
780 + * The unlit half's boundary. Pewter, and not a rounder-looking
781 + * neighbour, because Pewter is the first step on the Shade ramp
782 + * that reaches 3.00:1 against Obsidian — Silver manages 2.03 and
783 + * Astro 1.37. This is the declaration that makes an off control
784 + * satisfy WCAG 1.4.11.
785 + */
786 + --os-ui-holo-track-edge: #66636b;
787 + /*
788 + * Focus. One ring for the whole kit — the accent, offset by a Void
789 + * gap, so it reads on Obsidian, on a mesh, and on the near-white
790 + * of a legacy theme alike.
791 + *
792 + * The RING is full accent and stays there: it is the only thing on
793 + * screen saying where the keyboard is, and a focus indicator is the
794 + * last place to trade legibility for calm. The bloom that used to
795 + * sit behind it is gone: it was the layer that made a focused
796 + * control look like it was on fire, and the Void gap already does
797 + * the work of separating the ring from whatever it lands on.
798 + */
799 + --os-ui-focus-ring: 0 0 0 2px rgba(12, 11, 15, 0.9),
800 + 0 0 0 4px var(--os-ui-accent, #f252fc);
801 + /*
802 + * …and one for FIELDS, which is a different problem. A text input
803 + * already has a border to thicken and it sits in a column of other
804 + * inputs, so the ring above — designed to survive landing on a
805 + * bright mesh — reads as an alarm on a settings form. This one
806 + * tightens the field's own edge to Pulse and puts a soft halo
807 + * outside it: unmistakable, and quiet enough to live in a stack of
808 + * twelve.
809 + */
810 + --os-ui-focus-ring-field: 0 0 0 1px var(--os-ui-accent, #f252fc),
811 + 0 0 0 4px color-mix(in srgb, var(--os-ui-accent-dim) 16%, transparent);
812 +
813 + /*
814 + * The selected row of a VERTICAL `<os-tabs>`: the sidebar in
815 + * OpenStation Preferences, and any native window that lays its
816 + * pages down the side rather than across the top.
817 + *
818 + * The one selection on the station that keeps the accent. The row
819 + * runs full-bleed and says "this one" with a 2px accent line at its
820 + * leading edge, the bloom that line throws back across the row, and
821 + * a wash fading out towards the label. Everywhere else a selection
822 + * is a surface (a grey key, an outline, a flat fill); the sidebar is
823 + * where you are, and the brand marks it.
824 + *
825 + * The three coloured layers are DERIVED from the accent: `-edge` is
826 + * the accent flat, `-wash` and `-bloom` resolve through
827 + * `--os-ui-accent-dim` like every other ambient use, so a picked
828 + * accent reaches the row on this palette and on Legacy alike (see
829 + * `desktopThemesLegacy.php`: a theme cannot answer a derived token,
830 + * so a derivation is shared). How much of each shows, and the row's
831 + * shape, are literals a theme answers on its own: edge width, wash
832 + * and bloom opacity, and a fill, radius and inset that together
833 + * would turn the row into a lifted pill. The wash's opacity scales
834 + * every stop of the gradient, because a background image has no
835 + * opacity of its own and both of the row's pseudo-elements are
836 + * already spent on the edge and the bloom.
837 + */
838 + --os-ui-tab-edge: linear-gradient(
839 + var(--os-ui-accent, #f252fc),
840 + var(--os-ui-accent, #f252fc)
841 + );
842 + --os-ui-tab-wash: linear-gradient(
843 + 90deg,
844 + color-mix(
845 + in srgb,
846 + var(--os-ui-accent-dim) calc(16% * var(--os-ui-tab-wash-opacity, 1)),
847 + transparent
848 + )
849 + 0%,
850 + rgba(255, 251, 255, calc(0.04 * var(--os-ui-tab-wash-opacity, 1))) 42%,
851 + transparent 100%
852 + );
853 + --os-ui-tab-bloom: linear-gradient(
854 + 90deg,
855 + color-mix(in srgb, var(--os-ui-accent-dim) 26%, transparent),
856 + transparent
857 + );
858 + --os-ui-tab-edge-width: 2px;
859 + --os-ui-tab-wash-opacity: 1;
860 + --os-ui-tab-bloom-opacity: 1;
861 + --os-ui-tab-fill: transparent;
862 + --os-ui-tab-radius: 0px;
863 + --os-ui-tab-inset: 0px;
864 +
865 + /*
866 + * `<os-segmented>`, same split again. The thumb and its label still
867 + * mix from the accent, so Legacy and any theme that answers 100%
868 + * keep a coloured pill that follows the picker. This palette answers
869 + * 0%: a Void track with the chosen option as a mid-grey key and a
870 + * Starlight label, whatever accent is picked.
871 + *
872 + * The key's grey is set by contrast, not by taste. The first pass
873 + * used the sidebar row's grey, and it sat at 1.37:1 against the
874 + * track and 1.15:1 against the card around it, so the key read as a
875 + * hole in the track rather than a thing pressed into it. This grey
876 + * clears 3:1 against Void, what a selected state needs against its
877 + * neighbours, and still carries its Starlight label at 5.7:1. A
878 + * small shadow under it and a hairline around the track do the
879 + * rest: without the hairline, a Void track on the page behind the
880 + * dock options measured 1.05:1 and all but disappeared.
881 + *
882 + * Under the pointer an unselected option takes a faint Starlight
883 + * lift instead of the holographic film.
884 + */
885 + --os-ui-segmented-bg: #0c0b0f;
886 + --os-ui-segmented-edge: inset 0 0 0 1px rgba(255, 251, 255, 0.1);
887 + --os-ui-segmented-selected-accent: 0%;
888 + --os-ui-segmented-selected-base: #67636e;
889 + --os-ui-segmented-selected-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
890 + --os-ui-segmented-hover-bg: rgba(255, 251, 255, 0.06);
891 + --os-ui-segmented-hover-sheen: none;
892 +
893 + /*
894 + * The chosen tile in a swatch grid (wallpapers, the theme and
895 + * layout cards in Preferences, and any grid a plugin builds from
896 + * `<os-swatch>`), same split. The ring is the accent and stays
897 + * derived; this palette sets its width to 0 and lifts the tile
898 + * instead: a solid Starlight stroke outlines it, a shadow sets it
899 + * above the grid, and a Starlight disc with a tick cut out of it
900 + * sits in the corner. The stroke is Starlight rather than the
901 + * accent, like the badge, so the chosen tile stands out the same
902 + * way whichever accent is picked; a translucent hairline was too
903 + * faint on the dark theme cards. Legacy answers 2px, no lift, no
904 + * badge. The accent chips in the colour row keep their ring
905 + * whatever these say: a colour is the whole content there, and a
906 + * lift on a 28px square is invisible.
907 + */
908 + --os-ui-swatch-ring-width: 0px;
909 + --os-ui-swatch-lift: 0 0 0 1px #fffbff,
910 + 0 12px 24px -12px rgba(0, 0, 0, 0.8);
911 + --os-ui-swatch-badge-bg: #fffbff;
912 +
913 + /*
914 + * ---- Motion --------------------------------------------------
915 + *
916 + * Four durations and three curves, and every animated thing in the
917 + * kit picks from them. Not for tidiness: a panel where the switch
918 + * settles in 220ms, the segmented thumb in 300 and the tab
919 + * underline in 150 does not read as three well-tuned controls, it
920 + * reads as one surface that cannot keep time.
921 + *
922 + * The scale is roughly logarithmic because perceived duration is:
923 + * 90 and 140 are distinguishable, 220 and 240 are not.
924 + */
925 + /* A state flip with no travel — a colour, an opacity, a tick. */
926 + --os-ui-motion-fast: 140ms;
927 + /*
928 + * How long the foil takes to tilt, and the kit's default. One
929 + * duration for every holographic transition, so a panel full of
930 + * controls moves as one surface rather than as thirty independent
931 + * ones.
932 + */
933 + --os-ui-holo-transition: 220ms;
934 + /* Something crossing a distance: a thumb, a drawer, a card. */
935 + --os-ui-motion-slow: 340ms;
936 + /* An ambient loop — a shimmer, a drift. Long enough to ignore. */
937 + --os-ui-motion-ambient: 12s;
938 +
939 + /*
940 + * The curves. `spring` overshoots ~9% and settles; it is what makes
941 + * a knob feel thrown rather than moved, and it is wrong for
942 + * anything that changes size (an overshooting width reads as a
943 + * glitch, not as weight).
944 + */
945 + --os-ui-ease-spring: cubic-bezier(0.32, 1.5, 0.55, 1);
946 + /* Decelerating. The default for anything arriving. */
947 + --os-ui-ease-out: cubic-bezier(0.22, 0.9, 0.28, 1);
948 + /*
949 + * Accelerating — the mirror of `out`, and the default for anything
950 + * LEAVING. A surface that eases out on the way in and eases in on
951 + * the way out is the difference between "dismissed" and "undone":
952 + * decelerating into nothing reads as the thing hesitating, which
953 + * is the wrong note when the user has already moved on.
954 + */
955 + --os-ui-ease-in: cubic-bezier(0.4, 0, 1, 1);
956 + /* Symmetric, for a loop that has to come back where it started. */
957 + --os-ui-ease-loop: cubic-bezier(0.45, 0, 0.55, 1);
958 +
959 + /*
960 + * Status. Derived, not quoted — see the header note. Info takes
961 + * Sirius and success takes Holomesh's mint because the brand
962 + * already has a colour that means those things; danger and warning
963 + * are the shell's own hues lifted until they carry on Obsidian.
964 + */
965 + --os-ui-danger: #ff5a5a;
966 + --os-ui-danger-hover: #ff8a8a;
967 + --os-ui-success-fg: #93f0c6;
968 + --os-ui-warning-fg: #f8f2b6;
969 + --os-ui-warning: #f8f2b6;
970 + --os-ui-warning-bg: rgba(248, 242, 182, 0.1);
971 + --os-ui-warning-border: rgba(248, 242, 182, 0.24);
972 + --os-ui-info-fg: #c2f1f1;
973 + --os-ui-info-bg: rgba(194, 241, 241, 0.1);
974 +
975 + /*
976 + * ---- Component tokens the palette cannot reach --------------
977 + *
978 + * Each of these is read with a literal of its own rather than a
979 + * `var( --os-ui-<palette> )` chain, so the palette above does not
980 + * carry to it. Everything else in the kit is left alone and
981 + * inherits.
982 + */
983 +
984 + /*
985 + * Dialogs, menus and flyouts.
986 + *
987 + * The `--os-ui-modal-*` names below are the escape hatch
988 + * `<os-modal>` needs. Its dialog surface is dark whatever the
989 + * admin colour scheme says, so it re-points `--os-ui-fg`,
990 + * `--os-ui-fg-muted`, `--os-ui-border`, `--os-window-bg` and
991 + * `--os-ui-button-bg-hover` on its own `:host` — and a declaration
992 + * on the host outranks anything the host inherits, which is how
993 + * the palette used to stop at the dialog's edge. It reads these
994 + * instead, so the station owns the dialog again and the literals
995 + * behind them stay as the no-stylesheet floor.
996 + *
997 + * The set has to cover a dark context COMPLETELY, and the four
998 + * added last are the ones that proved it. A dialog re-pointing
999 + * only its text colours is fine while the palette around it is
1000 + * dark too — every unre-pointed token happens to agree. Put a
1001 + * LIGHT palette outside it (any theme in the admin's own colours,
1002 + * Legacy included) and the halves come apart: `--os-ui-surface`
1003 + * stays white, so `<os-select>` inside the dialog painted a white
1004 + * trigger and the re-pointed `--os-ui-fg` wrote near-white text
1005 + * onto it. `--os-ui-hover` was a black wash over a dark row.
1006 + *
1007 + * So the rule for anything added here: if a token names a SURFACE
1008 + * or a wash on one, the dialog owns it. Leaving one out is not
1009 + * "inherit the theme", it is "disagree with the other five".
1010 + */
1011 + --os-ui-modal-bg: #1a1721;
1012 + --os-ui-modal-text: #fffbff;
1013 + --os-ui-modal-text-muted: #b3afb5;
1014 + --os-ui-modal-border: #4d4a52;
1015 + --os-ui-modal-field-bg: #33303a;
1016 + --os-ui-modal-button-bg-hover: rgba(255, 251, 255, 0.14);
1017 + --os-ui-modal-surface: #1a1721;
1018 + --os-ui-modal-surface-elevated: #33303a;
1019 + --os-ui-modal-border-strong: #4d4a52;
1020 + --os-ui-modal-hover: rgba(255, 251, 255, 0.06);
1021 + --os-ui-confirm-dialog-bg: #1a1721;
1022 + --os-ui-context-menu-bg: #1a1721;
1023 + --os-ui-context-menu-separator-color: rgba(255, 251, 255, 0.1);
1024 + --os-ui-flyout-shadow: 0 16px 48px rgba(0, 0, 0, 0.6);
1025 + --os-ui-card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.45);
1026 + --os-ui-ribbon-shadow: 0 2px 4px rgba(0, 0, 0, 0.45);
1027 +
1028 + /* Tables. */
1029 + --os-ui-table-bg: #1a1721;
1030 + --os-ui-table-header-bg: #33303a;
1031 + --os-ui-table-border: rgba(255, 251, 255, 0.1);
1032 + --os-ui-table-column-border: rgba(255, 251, 255, 0.16);
1033 + --os-ui-table-row-hover: rgba(255, 251, 255, 0.06);
1034 + --os-ui-table-stripe: rgba(255, 251, 255, 0.03);
1035 + --os-ui-table-sticky-edge: 2px solid rgba(255, 251, 255, 0.14);
1036 +
1037 + /* Notices and badges — tinted washes of their own status colour. */
1038 + --os-ui-notice-error-bg: rgba(255, 90, 90, 0.12);
1039 + --os-ui-notice-error-border: rgba(255, 90, 90, 0.28);
1040 + --os-ui-notice-success-bg: rgba(147, 240, 198, 0.12);
1041 + --os-ui-notice-success-border: rgba(147, 240, 198, 0.28);
1042 + --os-ui-notice-warning-bg: rgba(248, 242, 182, 0.12);
1043 + --os-ui-notice-warning-border: rgba(248, 242, 182, 0.28);
1044 + --os-ui-notice-info-bg: rgba(194, 241, 241, 0.12);
1045 + --os-ui-notice-info-border: rgba(194, 241, 241, 0.28);
1046 + --os-ui-notice-neutral-bg: rgba(255, 251, 255, 0.06);
1047 + --os-ui-notice-neutral-border: rgba(255, 251, 255, 0.14);
1048 + /*
1049 + * A link inside a notice reads Nebula, the same value `--os-link`
1050 + * gives every other shell link — NOT the accent.
1051 + *
1052 + * Undeclared, the base fell through to `--wp-admin-theme-color`,
1053 + * which OpenStation Preferences → Appearance writes inline on `<html>`
1054 + * from the accent picker. That made the one piece of text a notice
1055 + * most needs the user to read take whichever hue they chose for
1056 + * focus rings. An accent is an identity colour; legibility on a dark
1057 + * surface is not something it can promise, so the link stops asking
1058 + * it to. Hover lifts to Starlight, and both ends stay in the palette.
1059 + *
1060 + * These are consumed from a DOCUMENT-tree rule in `desktop.css`, not
1061 + * only from the component's own `::slotted( a )`. See the comment
1062 + * there: a slotted link is a document-tree element, so wp-admin's
1063 + * bare `a` rule outranks anything the shadow root says about it.
1064 + */
1065 + --os-ui-notice-link: #fffbff;
1066 + --os-ui-notice-link-hover: #ccc8ce;
1067 + --os-ui-badge-danger-bg: rgba(255, 90, 90, 0.16);
1068 + --os-ui-badge-success-bg: rgba(147, 240, 198, 0.16);
1069 + --os-ui-badge-warning-bg: rgba(248, 242, 182, 0.16);
1070 + --os-ui-badge-info-bg: rgba(194, 241, 241, 0.16);
1071 + --os-ui-badge-neutral-bg: rgba(255, 251, 255, 0.08);
1072 +
1073 + /*
1074 + * Fills that must stay fills. Each of these paints a solid shape —
1075 + * a ribbon, a chip, a focused input's edge — and each was reading
1076 + * a token the palette turned into a 10%-alpha wash or into the
1077 + * body colour itself. A ribbon the same colour as the card behind
1078 + * it is not a subtle ribbon, it is a missing one.
1079 + */
1080 + /*
1081 + * Text on the bright status fills. These are the surfaces the
1082 + * `--os-ui-fg-on-accent` note points at: a ribbon filled with Pulse,
1083 + * Sirius or Holomesh mint is a LIGHT surface, and Starlight on it
1084 + * disappears.
1085 + */
1086 + --os-ui-ribbon-fg: #0c0b0f;
1087 + --os-ui-step-chip-fg: #0c0b0f;
1088 + /*
1089 + * The step chip is the right size and shape for the mesh: a small
1090 + * round fill, one per row, already carrying dark text. Its
1091 + * `--os-ui-step-chip-fg` above is Void, which is what the mesh
1092 + * needs anyway — the two decisions were made independently and
1093 + * agree, which is usually the sign a palette is holding together.
1094 + */
1095 + --os-ui-step-chip-bg: var(--os-mesh-holo);
1096 +
1097 + /*
1098 + * The default ribbon. One per tile, small, angled, and already
1099 + * paired with Void ink above — which is what a mesh needs anyway.
1100 + * The status ribbons below keep their flat colours: a ribbon that
1101 + * says "deprecated" should say it in warning yellow, not in brand.
1102 + */
1103 + --os-ui-ribbon-bg: var(--os-mesh-holo);
1104 +
1105 + --os-ui-ribbon-info: #c2f1f1;
1106 + --os-ui-ribbon-warning: #f8f2b6;
1107 + --os-ui-ribbon-neutral: #4d4a52;
1108 + --os-ui-crumb-bg: #33303a;
1109 + --os-ui-tag-input-input-border: #f252fc;
1110 +
1111 + /*
1112 + * Pairs that have to stay distinguishable. Hover and pressed, or
1113 + * base and shimmer, both resolve through `--os-ui-hover` — one wash
1114 + * for two states reads as a dead control, so the second step of
1115 + * each pair is named.
1116 + */
1117 + --os-ui-btn-bg-active: rgba(255, 251, 255, 0.12);
1118 + /*
1119 + * `<os-button>` ghost + secondary. Their resting wash and their
1120 + * hover wash BOTH fall through to `--os-ui-hover` — in two different
1121 + * rules, with two different literals behind them (6% and 10%), so
1122 + * declaring the palette collapsed them onto one value and the
1123 + * buttons stopped reacting to the pointer entirely. Primary is
1124 + * unaffected: it hovers with `filter: brightness()` over its
1125 + * accent fill rather than through this token.
1126 + */
1127 + --os-ui-button-bg-hover: rgba(255, 251, 255, 0.14);
1128 + --os-ui-key-bg-hover: rgba(255, 251, 255, 0.1);
1129 + --os-ui-key-bg-pressed: rgba(255, 251, 255, 0.16);
1130 + --os-ui-table-skeleton-highlight: rgba(255, 251, 255, 0.14);
1131 +
1132 + /* Code, logs, progress, ratings, avatars. */
1133 + --os-ui-code-border: 1px solid rgba(255, 251, 255, 0.1);
1134 + --os-ui-log-border: 1px solid rgba(255, 251, 255, 0.08);
1135 + --os-ui-log-row-border: 1px solid rgba(255, 251, 255, 0.06);
1136 + --os-ui-progress-track-bg: rgba(255, 251, 255, 0.1);
1137 + /*
1138 + * The filled part of a progress bar is the one surface in the kit
1139 + * that is *already* a hero: bounded, watched, and the only thing
1140 + * moving on screen. So it takes Holomesh, and the bar stretches
1141 + * the mesh as it grows — the hue travelling from lavender toward
1142 + * cyan IS the progress, which a flat accent could only report.
1143 + *
1144 + * Retinted here rather than in `<os-progress-bar>` because the
1145 + * component's own literal has to stay the pre-brand WordPress blue
1146 + * (it is the no-stylesheet floor, and it is what Legacy collected).
1147 + * The tone modifiers — success, warning, danger — still declare
1148 + * flat colours and still win: a failing job should read as red, not
1149 + * as brand.
1150 + */
1151 + --os-ui-progress-fill: var(--os-mesh-holo);
1152 + --os-ui-rating-star: #f8f2b6;
1153 + --os-ui-rating-fill: linear-gradient(90deg, #f8f2b6 0%, #fffbff 100%);
1154 + --os-ui-avatar-dot-ring: #1a1721;
1155 + /* The avatar's hover halo. It was a fixed indigo — a hue no
1156 + palette in the station names — so it lit itself in a colour
1157 + nothing around it used. Pulse. */
1158 + --os-ui-avatar-halo: #f252fc;
1159 + /* Search-match highlight inside a label. A WASH, not a fill: the
1160 + text keeps its own colour, so the wash has to stay light enough
1161 + for Starlight to carry over it. */
1162 + --os-ui-search-highlight-bg: rgba(248, 242, 182, 0.3);
1163 + /*
1164 + * The content graph's cluster hue is NOT retinted. Graph node
1165 + * colours are composed artwork that encodes meaning — the docs list
1166 + * them under "what stays fixed" — and moving this one to Nebula put
1167 + * white cluster labels on a pale lilac chip at 1.9:1. It keeps its
1168 + * own colour, and the label keeps carrying on it.
1169 + *
1170 + * The graph's toolbar is chrome, though, and it follows the same
1171 + * split as selection everywhere else: the accent stays derived, a
1172 + * literal says how much of it shows. An ON filter chip mixes its
1173 + * edge, wash and text from the accent by `-accent`, towards the
1174 + * neutrals below; this palette answers 0%, so an on chip reads like
1175 + * the selected theme card in Preferences, a Starlight outline around
1176 + * the dark surface. Legacy answers 100%. An OFF chip sits on the
1177 + * toolbar with the strong border as its edge, because the plain
1178 + * border is the toolbar's own colour and a chip drawn with it has no
1179 + * outline at all.
1180 + *
1181 + * The count pill is the strong border's grey on both, with Starlight
1182 + * numerals: muted numerals on that grey land at 4.0:1, under what
1183 + * 12px text needs.
1184 + */
1185 + --os-ui-cg-chip-accent: 0%;
1186 + --os-ui-cg-chip-border: #4d4a52;
1187 + --os-ui-cg-chip-border-active: #fffbff;
1188 + --os-ui-cg-chip-fill-active: #1a1721;
1189 + --os-ui-cg-chip-count-bg: #4d4a52;
1190 + --os-ui-cg-chip-count-fg: #fffbff;
1191 + --os-ui-cg-chip-count-bg-active: #4d4a52;
1192 + --os-ui-cg-chip-count-fg-active: #fffbff;
1193 +
1194 + /*
1195 + * ---- The widget card's public token contract ----------------
1196 + *
1197 + * Five names a third-party widget stylesheet is told to read.
1198 + * The list lives in `src/plugins/starter-widget/styles.css`,
1199 + * which is the file plugin authors copy — so it is a contract,
1200 + * not a convenience, and four of the five were documented
1201 + * without ever being declared. Every consumer fell through to
1202 + * the light-mode grey in its own `var()` fallback, on a card
1203 + * whose glass is dark: Post Stats' metadata measured 3.81:1
1204 + * where `.os-widgets__title` on the same surface measures 18.4.
1205 + *
1206 + * They are mostly literals rather than aliases of the text ramp,
1207 + * and that is the point. `.os-widgets__card` paints a fixed dark
1208 + * glass — `--os-ui-color-surface` below IS that colour, and the
1209 + * card reads it — under every desktop theme, so a name that
1210 + * flips with the theme is the wrong chain here: Legacy's
1211 + * `--os-ui-fg-muted` is `#50575e`, which lands at 2.54:1 on the
1212 + * glass and makes the bug WORSE for anyone wearing it. The two
1213 + * text names below do chain, but through the two names Legacy
1214 + * keeps light — the ones describing the same on-dark chrome
1215 + * this card is.
1216 + *
1217 + * A theme that wants to move a widget card declares these names
1218 + * itself, the way Legacy already declares the accent.
1219 + */
1220 + --os-ui-color-surface: rgba(20, 20, 22, 0.55);
1221 + /* What the card's own `color` already resolves to — so a widget
1222 + asking for "primary text" gets the text it inherited. */
1223 + --os-ui-color-text: var(--os-ui-fg-on-accent, #fffbff);
1224 + /* The shell's muted-on-dark. 9.2:1 on the glass, and Legacy
1225 + keeps it at `rgba( 255, 255, 255, 0.7 )`. */
1226 + --os-ui-color-text-subtle: var(--os-fg-muted, rgba(255, 251, 255, 0.7));
1227 + /* A hairline INSIDE the card, so a touch stronger than the
1228 + card's own 0.08 edge, which is drawn against the wallpaper.
1229 + Literal because no general on-dark hairline exists: the ones
1230 + Legacy keeps light are `--os-dock-border` and
1231 + `--os-cn-border`, and borrowing either is the right value at
1232 + the wrong scope. It reads quieter than what it replaces —
1233 + the widgets' own fallback was `#e5e7eb`, a light-theme
1234 + divider that on dark glass drew the loudest line in the
1235 + station. 0.12 is what the window and dock edges use. */
1236 + --os-ui-color-border: rgba(255, 251, 255, 0.12);
1237 + /* The one name that has to reach the accent picker. It was a
1238 + hard `#f252fc`, so a widget told to "use the accent for
1239 + buttons and links" stayed brand pink after the user chose
1240 + teal, while every control around it moved. */
1241 + --os-ui-color-accent: var(--os-ui-accent, #f252fc);
1242 +
1243 + /*
1244 + * ---- Typography --------------------------------------------
1245 + *
1246 + * Geist for everything human — headlines, UI copy, body text — and
1247 + * Geist Mono for the instrument voice: labels, metadata, statuses,
1248 + * code. Both faces are declared above; a stack without them still
1249 + * reads correctly, which is why every entry ends in a generic
1250 + * family.
1251 + *
1252 + * The chrome / body split is the one real decision here, and it is
1253 + * the one real desktop environments make. Here both sides run
1254 + * Geist: the station is one surface, and the title bar is not a
1255 + * different product from the window under it.
1256 + *
1257 + * Note that iframe windows render a real wp-admin document in a
1258 + * separate browsing context that this stylesheet does not reach —
1259 + * these tokens style the shell and native windows.
1260 + */
1261 + --os-font: 'Geist', system-ui, sans-serif;
1262 + --os-titlebar-font: 'Geist', system-ui, sans-serif;
1263 + --os-ui-font: 'Geist', system-ui, sans-serif;
1264 + --os-ui-font-mono: 'Geist Mono', ui-monospace, monospace;
1265 +
1266 + /*
1267 + * ---- Desktop-theme texture family -------------------------
1268 + *
1269 + * These are DELIBERATELY not declared. Every rule that consumes
1270 + * one reads it as `var( --name, <initial> )`, so an unset variable
1271 + * resolves to the CSS initial value and costs exactly nothing.
1272 + * Declaring them with `none` up here would be equivalent but would
1273 + * imply they are part of the default palette — they are not. They
1274 + * exist only while an uploaded or code-registered desktop theme is
1275 + * active, and only the compiled theme stylesheet ever sets them.
1276 + *
1277 + * TITLEBAR --os-titlebar-image
1278 + * --os-titlebar-image-repeat
1279 + * --os-titlebar-image-size
1280 + * TITLEBAR_FOCUSED --os-titlebar-image-focused
1281 + * (falls back to the unfocused image)
1282 + * WINDOW_FRAME --os-window-border-image-source
1283 + * --os-window-border-image-slice
1284 + * --os-window-border-image-width
1285 + * --os-window-border-image-repeat
1286 + * WINDOW_CORNER_* --os-window-corner-{ne,nw,se,sw}-image
1287 + * --os-window-corner-size (shared)
1288 + * --os-window-corner-inset (shared;
1289 + * how far inside the rounded corner the
1290 + * ornament sits. Defaults to the resize
1291 + * handle's 3px overhang plus a share of
1292 + * --os-window-radius, because the
1293 + * window clips its descendants)
1294 + * WINDOW_FRAME_FOCUSED
1295 + * --os-window-border-image-focused-*
1296 + * (same four sub-properties as WINDOW_FRAME)
1297 + * WINDOW_BODY --os-window-body-image
1298 + * TABBAR --os-tabs-image
1299 + * TITLEBAR_CONTROLS --os-titlebar-controls-image
1300 + * TITLEBAR_BUTTON --os-ui-btn-bg-image
1301 + * DOCK --os-dock-bg-image
1302 + * --os-dock-bg-image-repeat
1303 + * --os-dock-bg-image-size
1304 + * DOCK_ITEM --os-dock-item-image
1305 + * DESKTOP --os-desktop-image
1306 + * --os-desktop-image-repeat
1307 + * --os-desktop-image-size
1308 + * ICON_TILE --os-tile-image
1309 + * WIDGET --os-widget-image
1310 + * MENU --os-ui-menu-bg-image
1311 + * DIALOG --os-ui-dialog-bg-image
1312 + * SCRIM --os-ui-scrim-image
1313 + * PANEL --os-ui-panel-bg-image
1314 + * TOAST --os-ui-toast-bg-image
1315 + * TABLE_HEADER --os-ui-table-header-bg-image
1316 + * BUTTON --os-ui-button-bg-image
1317 + *
1318 + * Every `image` slot also gets `<prop>-repeat` and `<prop>-size`
1319 + * companions (the corners share one `-size`, and
1320 + * TITLEBAR_FOCUSED inherits TITLEBAR's).
1321 + *
1322 + * Consumers: window-chrome.css (titlebar, frame, body, tab bar),
1323 + * window-states.css (corners), dock.css, desktop.css (wallpaper,
1324 + * icons, widgets), and the `<os-*>` component stylesheets under
1325 + * `src/ui/components/` for the last seven.
1326 + * Authoring reference: docs/desktop-themes.md.
1327 + */
1328 +
1329 + /*
1330 + * ---- Badges ----------------------------------------------
1331 + *
1332 + * Sizes stay undeclared and DERIVED: every badge sizes itself off
1333 + * the icon it decorates — the dock badge off
1334 + * `--os-dock-icon-size`, the wallpaper badges off
1335 + * `--os-icon-image-size` — so a user who picks a large
1336 + * dock, or a theme that scales its icons, doesn't end up with a
1337 + * speck stuck to an oversized tile.
1338 + *
1339 + * Dock --os-dock-badge-size (overrides the
1340 + * whole derivation; font + padding follow it)
1341 + * --os-dock-badge-font-size / -padding
1342 + * --os-dock-badge-offset / -bg / -fg
1343 + * --os-dock-badge-font-weight
1344 + * Desktop icon --os-icon-badge-size / -font-size
1345 + * --os-icon-badge-padding / -bg / -fg
1346 + * Recycle bin --os-recycle-badge-size / -font-size
1347 + * / -bg / -fg. Falls through to the generic
1348 + * icon-badge token, so "badges are 22px" reaches
1349 + * the bin too.
1350 + *
1351 + * The count badges wear Pulse, and wear it as a LITERAL gradient:
1352 + * both stops used to resolve through `--os-ui-danger`, which the
1353 + * palette moved, flattening the badge to one flat fill and turning
1354 + * its numerals Void by accident. Naming the stops here keeps it a
1355 + * gradient on purpose. The bin's neutral badge is deliberately not
1356 + * part of this: a count of things in the trash is ambient state,
1357 + * not something asking for you.
1358 + *
1359 + * **The numerals are Void, not Starlight, and that is a
1360 + * requirement rather than a preference.** Starlight on Pulse is
1361 + * 2.9:1, which fails AA for text this small; Void on Pulse is
1362 + * 6.8:1. Any future retint of these has to keep the pair legible,
1363 + * so check the ratio before swapping the ink back to white.
1364 + */
1365 + --os-dock-badge-bg: linear-gradient(180deg, #f97dff 0%, #f252fc 100%);
1366 + --os-dock-badge-fg: #0c0b0f;
1367 + --os-icon-badge-bg: linear-gradient(180deg, #f97dff 0%, #f252fc 100%);
1368 + --os-icon-badge-fg: #0c0b0f;
1369 + --os-recycle-badge-bg: rgba(26, 23, 33, 0.85);
1370 + --os-recycle-badge-fg: rgba(255, 251, 255, 0.96);
1371 + --os-dock-recycle-badge-bg: rgba(26, 23, 33, 0.85);
1372 + --os-dock-recycle-badge-fg: rgba(255, 251, 255, 0.92);
1373 +
1374 + /*
1375 + * ---- Tooltips ---------------------------------------------
1376 + *
1377 + * Two tokens own every tooltip CHIP in the shell — the dock tile
1378 + * tooltip and the content-graph satellite tooltip.
1379 + *
1380 + * They are worth naming rather than leaving derived. Without them
1381 + * tooltips borrow their colours from unrelated families —
1382 + * `--os-ui-scrim` (an overlay BACKDROP) or `--os-ui-surface-elevated`
1383 + * for the surface, `--os-ui-fg-on-accent` for the text — and those
1384 + * pairings come apart the moment the palette moves. On this
1385 + * palette `--os-ui-fg-on-accent` is Void, which would have rendered
1386 + * near-black text on a near-black chip.
1387 + *
1388 + * A chip is deliberately the SAME dark lozenge on every desktop
1389 + * theme, which is why these two are flat values and not derived
1390 + * from anything. That is right for one line of text pinned to a
1391 + * control, and wrong for a rich card: the My WordPress entity
1392 + * hover card used to read them and showed up Obsidian over a white
1393 + * Legacy window. It has its own derived family now —
1394 + * `--os-my-wordpress-card-*`, further down — and it still names
1395 + * these two as its last fallback.
1396 + */
1397 + --os-tooltip-bg: #33303a;
1398 + --os-tooltip-fg: #fffbff;
1399 +
1400 + /*
1401 + * ---- Dock --------------------------------------------------
1402 + *
1403 + * Void glass over the desk, with a Starlight hairline. The tile
1404 + * hover wash and the focus ring are the dock's identity moment:
1405 + * Pulse, at the two alphas that read as "warm" and "here".
1406 + *
1407 + * `--os-dock-icon-color` is a COLOUR, not a fill, and
1408 + * that is load-bearing: every image glyph in the dock is painted
1409 + * as a mask filled with this colour — a theme's own iconset via
1410 + * `"iconColor": "currentColor"`, and plugin / CPT artwork via
1411 + * `currentColor` on the tile. One token drives dashicons, theme
1412 + * art, plugin art, and the hover transition between them.
1413 + *
1414 + * Consumers: dock.css, dock-peek.css (which re-states the hover
1415 + * rule and so must read the same two tokens).
1416 + */
1417 + --os-dock-width: 56px;
1418 + --os-dock-icon-size: 20px;
1419 + /*
1420 + * The indicator line a dynamic dock collapses into while parked
1421 + * (Preferences → Appearance → Desktop layout → Dock behavior):
1422 + * the iOS home indicator, painted in the accent at partial alpha
1423 + * so it reads as a mark on the wallpaper rather than a control.
1424 + * `-length` runs along the rail's edge, `-thickness` across it; a
1425 + * theme retunes any of the three.
1426 + */
1427 + --os-dock-indicator-length: 180px;
1428 + --os-dock-indicator-thickness: 5px;
1429 + --os-dock-indicator-bg: color-mix( in srgb, var( --os-ui-accent, #f252fc ) 72%, transparent );
1430 + --os-dock-bg: rgba(12, 11, 15, 0.72);
1431 + --os-dock-icon-color: rgba(255, 251, 255, 0.72);
1432 + --os-dock-icon-color-hover: #fffbff;
1433 + --os-dock-item-bg-hover: rgba(255, 251, 255, 0.08);
1434 + /*
1435 + * The tile whose window is in front sits on a lit plate: the same
1436 + * Starlight wash the hover uses, one step brighter, so "this is the
1437 + * app you are in" reads as depth rather than as a coloured mark.
1438 + * The running dot under every open app stays; a lit plate says
1439 + * which one is in front, the dots say which ones are open.
1440 + */
1441 + --os-dock-item-bg-focused: rgba(255, 251, 255, 0.1);
1442 + /*
1443 + * The status indicator and the focus ring, and both say "this
1444 + * one" about a tile the user chose — so they read the accent
1445 + * rather than a fixed colour. Starlight is what an untouched
1446 + * install shows, because that is the accent the picker starts on.
1447 + */
1448 + --os-dock-item-outline: var(--os-ui-accent, #f252fc);
1449 + /*
1450 + * Hairline between the dock and the desktop. Reaches the side
1451 + * placements (as a single inline-edge border) AND the floating
1452 + * bottom pill (as its full outline). Set `transparent` for a
1453 + * borderless dock.
1454 + *
1455 + * The bottom pill's own chrome is separately overridable, because
1456 + * it is deliberately a different piece of glass — a warmer tint, a
1457 + * brighter top edge, an inset highlight, a drop shadow. Each falls
1458 + * back to the shared value above it.
1459 + */
1460 + --os-dock-border: rgba(255, 251, 255, 0.1);
1461 +
1462 + /*
1463 + * The dividers INSIDE the rail, which are a different job from the
1464 + * outline around it: they separate tiles that belong to different
1465 + * groups — core menus, plugin apps, and OpenStation's own controls.
1466 + *
1467 + * One treatment for all of them. An earlier pass gave the
1468 + * WordPress-to-OpenStation boundary Pulse and dropped the
1469 + * core-to-plugin one to a hairline, on the theory that only one
1470 + * boundary was worth reading. Two weights in one short rail read as
1471 + * two unrelated ideas instead of one system, so both are now the
1472 + * same line: Pulse at the waist, falling away to nothing at both
1473 + * ends, with a soft glow to hold it against the dock tint.
1474 + *
1475 + * Resolved one step back from the accent through
1476 + * `--os-ui-accent-dim`, where every ambient use of Pulse resolves,
1477 + * so "tone the station down" stays one edit.
1478 + */
1479 + --os-dock-divider: color-mix(in srgb, var(--os-ui-accent-dim) 70%, transparent);
1480 + --os-dock-floating-bg: rgba(12, 11, 15, 0.62);
1481 + --os-dock-floating-border: rgba(255, 251, 255, 0.12);
1482 + --os-dock-floating-border-top: rgba(255, 251, 255, 0.18);
1483 + --os-dock-floating-highlight: rgba(255, 251, 255, 0.1);
1484 + --os-dock-floating-shadow: rgba(0, 0, 0, 0.55);
1485 +
1486 + /*
1487 + * The phone layer (`mobile.css`). Its surfaces are the dock's
1488 + * family — translucent glass over the wallpaper, light glyphs —
1489 + * and read the dock tokens above for fill, text and lines. What
1490 + * the dock has no word for is declared here: the glass a tile, a
1491 + * round control and a switcher card are cut from, its pressed
1492 + * state, the shadow under a tile, the scrim behind the switcher,
1493 + * and the label shadow that keeps text legible over any wallpaper.
1494 + * A desktop theme that reskins the dock reskins the phone with it;
1495 + * one that wants the phone alone has these.
1496 + */
1497 + --os-mobile-glass: rgba(255, 251, 255, 0.1);
1498 + --os-mobile-glass-strong: rgba(255, 251, 255, 0.16);
1499 + --os-mobile-glass-border: rgba(255, 251, 255, 0.14);
1500 + --os-mobile-tile-shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
1501 + --os-mobile-card-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
1502 + --os-mobile-sheet-shadow: 0 -12px 40px rgba(0, 0, 0, 0.45);
1503 + --os-mobile-window-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
1504 + --os-mobile-scrim: rgba(0, 0, 0, 0.55);
1505 + --os-mobile-top-scrim: rgba(0, 0, 0, 0.45);
1506 + --os-mobile-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
1507 +
1508 + /*
1509 + * ---- The OpenStation layout ---------------------------------
1510 + *
1511 + * The constellation: the flyout that fans a menu's submenu out of
1512 + * its tile on hover, on every rail in every layout. The rail's
1513 + * divider used to live here too; it is shared chrome now and reads
1514 + * through `--os-dock-divider`.
1515 + *
1516 + * The constellation's SURFACE is Obsidian, deliberately. The mesh
1517 + * is spent on the row under the pointer and on the head's icon
1518 + * halo — the two moments where the panel is answering the user —
1519 + * and nowhere else. A flyout that was iridescent edge to edge
1520 + * would have nothing left to say when you actually pointed at
1521 + * something in it.
1522 + */
1523 + --os-cn-surface: rgba(26, 23, 33, 0.82);
1524 + --os-cn-border: rgba(255, 251, 255, 0.14);
1525 + --os-cn-shadow: 0 24px 64px rgba(0, 0, 0, 0.62),
1526 + 0 2px 8px rgba(0, 0, 0, 0.4);
1527 + --os-cn-fg: #fffbff;
1528 + --os-cn-fg-muted: rgba(255, 251, 255, 0.62);
1529 + --os-cn-legend: rgba(255, 251, 255, 0.4);
1530 + --os-cn-divider: rgba(255, 251, 255, 0.1);
1531 + /* The row under the pointer — the panel's identity moment. */
1532 + --os-cn-row-fill: var(--os-ui-holo-fill);
1533 + --os-cn-row-ink: var(--os-ui-holo-ink);
1534 + /* Beam: the thread from the panel's underside down to the tile. */
1535 + --os-cn-beam: color-mix(in srgb, var(--os-ui-accent-dim) 80%, transparent);
1536 + --os-cn-radius: 14px;
1537 + /*
1538 + * Above every window and above the dock. Two panels coexist while
1539 + * the pointer moves along the rail — one dismissing, one arriving
1540 + * — and the retiring one is painted one step below so it can
1541 + * never fade out on top of the menu being read.
1542 + */
1543 + --os-cn-z: 2147483000;
1544 +
1545 + /*
1546 + * ---- Shell surfaces ----------------------------------------
1547 + *
1548 + * The panels, tab strips and washes that belong to the shell
1549 + * rather than to the component kit. Same Obsidian-on-Void
1550 + * composition; the selection wash is Pulse.
1551 + */
1552 + --os-accent: #f252fc;
1553 + --os-link: #ec9bff;
1554 + --os-danger: #ff5a5a;
1555 + --os-border: rgba(255, 251, 255, 0.1);
1556 + --os-hover: rgba(255, 251, 255, 0.06);
1557 + --os-selected: rgba(242, 82, 252, 0.18);
1558 + --os-fg-muted: rgba(255, 251, 255, 0.7);
1559 + /*
1560 + * Shell text. Undeclared before the brand and it had to be: it read
1561 + * light on the desk (tile labels over the wallpaper) and dark
1562 + * inside a window, and one value broke one of the two. The station
1563 + * settles the argument — every surface it owns is dark now, so
1564 + * Starlight is right in both places.
1565 + */
1566 + --os-fg: #fffbff;
1567 + /*
1568 + * "…on light" is a name from before the station, and it is now a
1569 + * misnomer worth keeping: every surface that reads it — the folder
1570 + * window, its preview pane, the upload HUD — was a light card and
1571 + * is Obsidian today. The token means "text on the surfaces this one
1572 + * names", and those surfaces are dark, so the text is Starlight.
1573 + * Renaming it is a public-API change for the sake of tidiness; the
1574 + * comment is cheaper and the behaviour is what matters.
1575 + */
1576 + --os-fg-on-light: #fffbff;
1577 + --os-panel-bg: #1a1721;
1578 + --os-panel-header-bg: rgba(255, 251, 255, 0.04);
1579 + --os-folder-window-bg: #1a1721;
1580 + --os-my-wordpress-bg: #1a1721;
1581 + --os-my-wordpress-fg: #fffbff;
1582 + --os-my-wordpress-surface: rgba(255, 251, 255, 0.06);
1583 + /*
1584 + * The entity hover card — the "thumb" that floats over a tile with
1585 + * a title, a featured image and an excerpt.
1586 + *
1587 + * It is NOT a dock chip, and it used to be painted as one.
1588 + * `--os-tooltip-bg` is a fixed dark lozenge on every theme by
1589 + * design — one line of text, always the same object, no reason to
1590 + * follow anything. The card borrowed it, so a light desktop theme
1591 + * summoned an Obsidian card over a white window, with a border in
1592 + * `--os-ui-border` and an excerpt in `--os-ui-fg-muted` that both
1593 + * followed the WINDOW instead. Three families, one surface.
1594 + *
1595 + * The card is window furniture: it belongs to the surface it was
1596 + * summoned from, so it follows the theme that paints that surface.
1597 + * Every value below is DERIVED rather than declared flat, which is
1598 + * what makes that true — a theme that names only
1599 + * `--os-my-wordpress-bg` (Legacy names exactly that, `#fff`) moves
1600 + * the card with the window for free, and a theme that wants the
1601 + * card alone names a `-card-` token.
1602 + *
1603 + * A card lands on the same colour as the window under it, so the
1604 + * border and the shadow are the whole reason it reads as a
1605 + * separate object. Neither is decoration here and neither may
1606 + * collapse into the background — which is exactly what the border
1607 + * did: `--os-ui-border` is #33303a on this palette, the SAME value
1608 + * `--os-tooltip-bg` had, so the card had no edge at all.
1609 + *
1610 + * The shadow is the one flat value in the family. A drop shadow is
1611 + * cast light, dark on a light theme and dark on a dark one, so
1612 + * there is no window token for it to follow; it is named so a
1613 + * theme can still reach it.
1614 + */
1615 + --os-my-wordpress-card-bg: var(--os-my-wordpress-bg, #1a1721);
1616 + --os-my-wordpress-card-fg: var(--os-my-wordpress-fg, #fffbff);
1617 + --os-my-wordpress-card-fg-muted: var(--os-ui-fg-muted, #b3afb5);
1618 + --os-my-wordpress-card-border: var(--os-ui-border, #33303a);
1619 + --os-my-wordpress-card-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
1620 + --os-my-wordpress-card-thumb-bg: var(
1621 + --os-media-tile-bg,
1622 + rgba(255, 251, 255, 0.05)
1623 + );
1624 + --os-my-wordpress-card-lock-bg: var(
1625 + --os-ui-badge-danger-bg,
1626 + rgba(255, 90, 90, 0.16)
1627 + );
1628 + --os-ai-panel-bg: rgba(26, 23, 33, 0.97);
1629 + /*
1630 + * The Site assistant, the same split as selection everywhere else:
1631 + * its header tint and mark, search icon, lit row and icon tiles still
1632 + * mix from the accent, and so does its send button; --os-ai-accent
1633 + * says how much. This palette
1634 + * answers 0%, so the chrome is neutral and a lit row is the flat 8%
1635 + * Starlight of the Preferences sidebar; Legacy answers 100%. The
1636 + * command icons keep the accent here too: they read it directly, so
1637 + * the colour stays in the pictures and out of the chrome.
1638 + */
1639 + --os-ai-accent: 0%;
1640 + --os-ai-row-fill: rgba(255, 251, 255, 0.08);
1641 + --os-ai-tile-fill: rgba(255, 251, 255, 0.06);
1642 + /*
1643 + * ---- Window tab strip ---------------------------------------
1644 + *
1645 + * A window with sub-pages reads as three surfaces stacked in
1646 + * depth, and the values below are that ramp: Astro on the focused
1647 + * title bar, VOID for the track the tabs sit in, then the page
1648 + * itself. The track is the darkest of the three on purpose — it
1649 + * has to stay a distinct band under BOTH title-bar states, and
1650 + * Obsidian would collapse into the unfocused bar (also Obsidian)
1651 + * exactly when a window has the least going on to distinguish it.
1652 + *
1653 + * The active tab is not a colour, it is the page arriving early.
1654 + * `chromeless.css` paints every admin page inside a window `#fff`
1655 + * whatever the admin colour scheme says, so the tab can name that
1656 + * white and the joint between the two is seamless. Retinting the
1657 + * tab means retinting the page it belongs to, and these two move
1658 + * together or the seam comes back.
1659 + *
1660 + * `-color-muted` is the second tone anything nested in an active
1661 + * tab needs — the external tab's detach and close chips. It is
1662 + * the only reason a "muted on light" value exists this far down a
1663 + * dark palette.
1664 + */
1665 + --os-tabs-bg: #1a1721;
1666 + /*
1667 + * The track on an UNFOCUSED window, which follows the title bar
1668 + * down to Void rather than holding Obsidian.
1669 + *
1670 + * A focused window already reads as two surfaces: title bar and
1671 + * track are the same Obsidian, and the tab lifts out of them. Let
1672 + * the track keep that Obsidian while the bar above it dims and
1673 + * the window reads as THREE, with the strip belonging to neither
1674 + * the chrome above nor the page below. Following the bar down
1675 + * keeps the count at two in both states.
1676 + *
1677 + * Both tracks read this: the shell's strip on an iframe window,
1678 + * and `<os-tabs variant="window">` inside a native one. The track
1679 + * is chrome in both cases, so it dims with the chrome; what keeps
1680 + * the tab attached to the content while that happens is the
1681 + * plate's fill, which wears the body's own colour rather than the
1682 + * track's.
1683 + *
1684 + * Note the asymmetry with the title bar's pair, which is
1685 + * deliberate: there, `--os-titlebar-bg` is the unfocused base and
1686 + * `-focused` is the modifier. Here the base is the LIT value,
1687 + * so a theme naming one track colour lands on the state where the
1688 + * track is doing the most work.
1689 + *
1690 + * A theme that sets only `--os-tabs-bg` (Legacy, and anything
1691 + * written before this token existed) resolves through it in both
1692 + * states and keeps its single strip colour.
1693 + */
1694 + --os-tabs-bg-unfocused: #0c0b0f;
1695 + --os-tabs-color: #b3afb5;
1696 + --os-tabs-active-bg: #fff;
1697 + --os-tabs-active-color: #0c0b0f;
1698 + --os-tabs-active-color-muted: rgba(12, 11, 15, 0.6);
1699 + /*
1700 + * The plate's own body: a frosted crown resolving to flat page
1701 + * white well before the joint.
1702 + *
1703 + * Every stop is OPAQUE, and that is a requirement rather than a
1704 + * preference. The face used to be translucent over a
1705 + * `backdrop-filter`, which promotes it to its own compositor layer
1706 + * and leaves a faint grey hairline where that layer is clipped —
1707 + * down the plate's sides and across its bottom, which is precisely
1708 + * where this design has a bright rail and needs everything else to
1709 + * be invisible.
1710 + *
1711 + * The last stop must also land above the joint (the bottom
1712 + * `--os-tabs-radius` of the plate), because the joint is painted
1713 + * in flat `--os-tabs-active-bg` and a body still tinted where the
1714 + * two meet draws a seam of its own. If you retune these stops,
1715 + * check the bottom edge first.
1716 + */
1717 + --os-tabs-active-frost: linear-gradient(
1718 + 180deg,
1719 + #f4eff9 0%,
1720 + #ffffff 58%
1721 + );
1722 + /*
1723 + * The crown. Holomesh over the top of the plate, masked away before
1724 + * the joint, on exactly ONE tab at a time. Off in this palette: the
1725 + * plain frosted plate is the tab, and the one identity moment on a
1726 + * window is left to the wallpaper behind it. A theme that wants the
1727 + * mesh back declares `var(--os-mesh-holo)` here, at around 0.3.
1728 + */
1729 + --os-tabs-active-crown: none;
1730 + --os-tabs-active-crown-opacity: 0.3;
1731 + /*
1732 + * The rail: one continuous line around the silhouette of
1733 + * page-plus-tab, painted as a background-image through a mask,
1734 + * which is why it is a gradient and not a colour. It is the accent
1735 + * the user picked, flat, and it stays derived so the pick reaches
1736 + * it on every theme.
1737 + *
1738 + * On this palette it is drawn at 0 opacity. The plate already
1739 + * reads as "this tab" by being the one white shape on an Obsidian
1740 + * strip that joins the page below it; a coloured line around that
1741 + * silhouette was saying the same thing a second time, on every
1742 + * window on the desk. The opacity is the literal a theme answers
1743 + * (Legacy: 1), and the width stays at 2px because the strip's
1744 + * geometry is built on it: the plate sits on the rail whether or
1745 + * not the rail is visible, so turning it back on costs no seam.
1746 + */
1747 + --os-tabs-rail: linear-gradient(
1748 + var(--os-ui-accent, #f252fc),
1749 + var(--os-ui-accent, #f252fc)
1750 + );
1751 + --os-tabs-rail-opacity: 0;
1752 + --os-tabs-rail-width: 2px;
1753 + --os-media-tile-bg: rgba(255, 251, 255, 0.05);
1754 + --os-media-visual-bg: rgba(255, 251, 255, 0.04);
1755 + --os-skeleton-low: rgba(255, 251, 255, 0.05);
1756 + --os-skeleton-high: rgba(255, 251, 255, 0.13);
1757 + --os-drop-preview-bg: rgba(255, 251, 255, 0.08);
1758 + --os-drop-preview-border: rgba(255, 251, 255, 0.45);
1759 +
1760 + /*
1761 + * File tiles — anywhere a `.os-file-tile` (or sibling
1762 + * canvas tile) renders. The defaults match the desk: Starlight
1763 + * labels over a dark sky, and a selected tile is a lifted
1764 + * Starlight plate, the same language as the selected row in the
1765 + * Preferences sidebar, one step brighter than the hover wash so
1766 + * the two states stay apart. The accent ring around it is a
1767 + * derived colour with a literal width; this palette sets the
1768 + * width to 0 (Legacy keeps 1px) so selection is depth, not colour.
1769 + *
1770 + * Surfaces that are light in their own right override these in
1771 + * their own scope, e.g.:
1772 + *
60 1773 * .desktop-mode-folder-window {
61 - * --desktop-mode-tile-fg: #1d2327;
62 - * --desktop-mode-tile-hover-bg: rgba(0, 0, 0, 0.06);
1774 + * --os-tile-fg: #1d2327;
1775 + * --os-tile-hover-bg: rgba(0, 0, 0, 0.06);
63 1776 * }
64 1777 *
65 1778 * Plugin authors can override any of these in their own theme
66 1779 * to retint tiles globally without touching the rules below.
67 - *
68 - * @since 0.8.0
69 1780 */
70 - --desktop-mode-tile-fg: var(--desktop-mode-fg, #fff);
71 - --desktop-mode-tile-fg-muted: rgba(255, 255, 255, 0.7);
72 - --desktop-mode-tile-hover-bg: rgba(255, 255, 255, 0.12);
73 - --desktop-mode-tile-focus-ring: var(--wp-admin-theme-color, #2271b1);
74 - --desktop-mode-tile-shortcut-bg: rgba(0, 0, 0, 0.65);
75 - --desktop-mode-tile-shortcut-fg: #fff;
76 - --desktop-mode-tile-shortcut-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
1781 + --os-tile-fg: var(--os-fg, #fffbff);
1782 + --os-tile-fg-muted: rgba(255, 251, 255, 0.7);
1783 + --os-tile-hover-bg: rgba(255, 251, 255, 0.12);
1784 + --os-tile-selected-bg: rgba(255, 251, 255, 0.16);
1785 + --os-tile-selected-ring-width: 0px;
1786 + --os-tile-focus-ring: var(--wp-admin-theme-color, #f252fc);
1787 + --os-tile-shortcut-bg: rgba(12, 11, 15, 0.7);
1788 + --os-tile-shortcut-fg: #fffbff;
1789 + --os-tile-shortcut-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
77 1790
78 1791 /*
79 - * Label rendering tokens. Defaults match the dark wallpaper:
80 - * a soft drop-shadow boosts legibility against the gradient,
1792 + * Label rendering tokens. The desk is dark at every wallpaper, so
1793 + * a soft drop-shadow boosts legibility against the artwork,
81 1794 * regular weight, default antialiasing.
82 1795 *
83 1796 * Light-context surfaces (folder windows, My WordPress, any
84 1797 * future window with a light body) rebind these in their own
@@ -83,23 +1796,64 @@
83 1796 * Light-context surfaces (folder windows, My WordPress, any
84 1797 * future window with a light body) rebind these in their own
85 1798 * scope so tile labels render with the same crispness as the
86 1799 * surrounding chrome — no per-surface duplicate rules.
1800 + */
1801 + --os-tile-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
1802 + --os-tile-label-weight: 400;
1803 + --os-tile-label-smoothing: auto;
1804 + --os-tile-label-color: var(--os-tile-fg);
1805 +
1806 + /*
1807 + * Admin bar — how many pixels of the bar stay on screen while it
1808 + * is parked off the top edge in `dynamic` mode. This is the
1809 + * VISIBLE seam only: small enough to read as "nothing is there".
1810 + * Themes may retune it; `0` makes the bar invisible until hovered.
1811 + */
1812 + --os-admin-bar-peek: 4px;
1813 +
1814 + /*
1815 + * How far BELOW the parked bar the invisible reveal zone reaches.
1816 + * Decoupled from the peek on purpose — the seam wants to be a
1817 + * hairline, the pointer target wants to be forgiving, and tying
1818 + * them together forces one of the two to be wrong. The band the
1819 + * user actually has to hit is `peek + this`.
87 1820 *
88 - * @since 0.8.0
1821 + * CEILING: keep `peek + reveal-zone` comfortably under the admin
1822 + * bar's own height (32px, 46px below 783px). Past that, leaving
1823 + * the revealed bar drops the pointer straight back into the zone,
1824 + * which re-reveals it — an oscillation loop, not a wider target.
89 1825 */
90 - --desktop-mode-tile-label-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
91 - --desktop-mode-tile-label-weight: 400;
92 - --desktop-mode-tile-label-smoothing: auto;
93 - --desktop-mode-tile-label-color: var(--desktop-mode-tile-fg);
1826 + --os-admin-bar-reveal-zone: 16px;
94 1827
95 1828 /* Z-index layers */
96 - --desktop-mode-z-base: 100;
97 - --desktop-mode-z-dock: 200;
98 - --desktop-mode-z-adminbar: 9991;
99 - --desktop-mode-z-fullscreen: 99999;
1829 + --os-z-base: 100;
1830 + --os-z-dock: 200;
1831 + --os-z-adminbar: 9991;
1832 + --os-z-fullscreen: 99999;
100 1833 /* Window-link layer — above widgets (z 1), behind windows (z 100+). */
101 - --desktop-mode-z-window-links: 50;
1834 + --os-z-window-links: 50;
1835 + /*
1836 + * Mio layer — above every window (windows run 100 + stack
1837 + * index) so the companion can perch on top of them, but below
1838 + * the dock (200) so it never covers navigation.
1839 + */
1840 + --os-z-mio: 190;
1841 + /*
1842 + * The desktop-name caption. Same reasoning as Mio, one step up:
1843 + * above every window so a maximized Dashboard cannot hide it, and
1844 + * above the companion, since a momentary message should not be
1845 + * the thing that gets perched on. Still under the dock (200).
1846 + */
1847 + --os-z-desktop-name: 195;
1848 + /*
1849 + * The notch — on the desk, under the windows (100 + stack index),
1850 + * above the window-link wires (50) so a tie never draws across the
1851 + * pill. It hangs over the strip a window's title bar occupies, and
1852 + * a pill floating above someone's window is the shell talking over
1853 + * the thing they are working in.
1854 + */
1855 + --os-z-notch: 60;
102 1856
103 1857 /*
104 1858 * Window-link ties (relation splines between windows). The accent
105 1859 * follows the admin color scheme. Solid colors only — the resting
@@ -109,121 +1863,109 @@
109 1863 * at computed-value time, which turns SVG strokes invisible and
110 1864 * marker fills black (the written rgba() fallback declaration is
111 1865 * already gone from the cascade by then). Overridable per scheme
112 1866 * or by plugins/themes without touching the renderer.
113 - *
114 - * @since 0.9.4
115 1867 */
116 - --desktop-mode-window-link-color: var(--wp-admin-theme-color, #2271b1);
117 - --desktop-mode-window-link-color-active: var(--wp-admin-theme-color, #2271b1);
118 - --desktop-mode-window-link-width: 1.5px;
119 - --desktop-mode-window-link-accent: var(--wp-admin-theme-color, #2271b1);
1868 + --os-window-link-color: var(--wp-admin-theme-color, #f252fc);
1869 + --os-window-link-color-active: var(--wp-admin-theme-color, #f252fc);
1870 + --os-window-link-width: 1.5px;
1871 + --os-window-link-accent: var(--wp-admin-theme-color, #f252fc);
120 1872 /*
121 - * Halo behind related windows (`.desktop-mode-window--linked`).
122 - * A literal rgba of the default admin blue rather than a
123 - * color-mix() of the accent — same no-color-mix rationale as
124 - * above: if this token failed to resolve, the whole box-shadow
125 - * declaration (elevation shadow included) would go invalid.
126 - * Schemes/plugins that retint the accent should override this too.
1873 + * Halo behind related windows (`.os-window--linked`).
1874 + * A literal rgba of Pulse rather than a color-mix() of the accent —
1875 + * same no-color-mix rationale as above: if this token failed to
1876 + * resolve, the whole box-shadow declaration (elevation shadow
1877 + * included) would go invalid. Schemes/plugins that retint the
1878 + * accent should override this too.
127 1879 */
128 - --desktop-mode-window-link-glow: rgba(34, 113, 177, 0.45);
1880 + --os-window-link-glow: rgba(242, 82, 252, 0.45);
129 1881 }
130 1882
131 1883 /*
132 - * Per-scheme overrides. Scoped to `.desktop-mode-shell[data-desktop-mode-scheme]`
133 - * so the variables only affect the shell subtree — we don't want to touch
134 - * Gutenberg or other nested contexts that also read `--wp-admin-theme-color`.
1884 + * ---- Iframe windows keep a light body -----------------------------
135 1885 *
136 - * Setting the vars on the shell root means any descendant (title bar,
137 - * dock chip, tab underlines, focus rings) that references them inherits
138 - * the right hue without further plumbing.
1886 + * A window hosting a real `wp-admin` page is not a station surface — it
1887 + * is a frame around someone else's document, and that document is
1888 + * light. The reveal surface is what covers such a window while it
1889 + * loads, so a dark one wiping back to a white page is a flash in the
1890 + * wrong direction.
139 1891 *
140 - * Title bar bg deliberately differs from the WP admin bar (which uses
141 - * each scheme's "base" color) — when the two sit edge-to-edge the
142 - * shared hue reads as one slab. We mix base with white (dark schemes)
143 - * or black (light schemes) ~15–20% so the titlebar feels related but
144 - * clearly distinct.
1892 + * The window's own background is deliberately NOT overridden here, and
1893 + * that was learned the hard way: `chromeless.css` paints the iframe's
1894 + * canvas, so nothing of the window shows through the page anyway —
1895 + * while the tab strip above it carries a 16px `mask-image` fade at
1896 + * each end (the "scroll for more tabs" affordance). A mask makes the
1897 + * element's own background transparent, so a white window behind a
1898 + * dark strip turned that invisible cosmetic into a white gradient
1899 + * bleeding into the chrome. The strip needs the station's colour
1900 + * behind it; the page needs its own; those are two different surfaces
1901 + * and only one of them belongs to the window element.
1902 + *
1903 + * Native windows — the ones the station draws itself — are excluded
1904 + * and keep the Obsidian reveal from the palette above.
145 1905 */
1906 +.os-window:not( .os-window--native ) {
1907 + --os-window-reveal-surface: #fff;
1908 +}
146 1909
147 1910 /*
148 - * The focused titlebar colors below are the pre-computed sRGB result
149 - * of each scheme's base color mixed with white (dark schemes) or
150 - * black (light schemes). We used to write these as
151 - * `color-mix(in srgb, …)` calls, but `color-mix()` only shipped in
152 - * Chrome 111 — on older Chromium-based browsers the whole declaration
153 - * is invalid-at-computed-value-time and the titlebar falls back to
154 - * the unfocused light surface, which hides the white control glyphs.
155 - * Since the mix inputs are all static there is no behavioral loss
156 - * from inlining the resulting hex value.
1911 + * ---- The WordPress admin colour scheme no longer tints the shell ----
1912 + *
1913 + * There used to be nine blocks here, one per admin colour scheme, each
1914 + * setting `--wp-admin-theme-color` on
1915 + * `.os-shell[data-os-scheme="…"]`. They are gone,
1916 + * and their removal is a bug fix as much as a rebrand.
1917 + *
1918 + * OS Settings → Appearance writes the user's chosen accent as an inline
1919 + * style on `<html>` — deliberately, so it also reaches the WordPress
1920 + * admin bar, which is a SIBLING of `#os-shell`. But those
1921 + * blocks matched the shell root, a descendant of `<html>`, and for
1922 + * custom properties the nearest ancestor with a value wins no matter
1923 + * what the specificity of the rule that set it was. So inside the shell
1924 + * the scheme always beat the picker: choosing Rose, or Pulse, changed
1925 + * the admin bar and nothing else. Every focus ring, tab underline, sort
1926 + * arrow and selection wash kept the colour of the admin scheme.
1927 + *
1928 + * With the accent picker defaulting to Pulse there is nothing left for
1929 + * these blocks to express: the station has one identity colour, and a
1930 + * user who wants their admin scheme's hue picks it in the same panel.
1931 + * The `data-os-scheme` attribute is still stamped on the
1932 + * shell root for plugins and themes that key off it.
157 1933 */
158 1934
159 -/* Fresh — default WP blue. */
160 -.desktop-mode-shell[data-desktop-mode-scheme="fresh"] {
161 - --wp-admin-theme-color: #2271b1;
162 - /* color-mix(in srgb, #1d2327 80%, #fff 20%) */
163 - --desktop-mode-titlebar-bg-focused: #4a4f52;
164 - --desktop-mode-titlebar-color-focused: #fff;
1935 +/* The chat is a MIO surface even when its owning window wears a light theme.
1936 + * Component colours remain reachable through the public MIO token family. */
1937 +body.os-active .os-mio-chat,
1938 +body.os-active .os-mio-chat-launcher {
1939 + --os-ui-fg: var(--os-mio-chat-fg, #1d2327);
1940 + --os-ui-fg-muted: var(--os-mio-chat-muted, #646970);
1941 + --os-window-bg: var(--os-mio-chat-bg, #fff);
1942 + --os-ui-border: var(--os-mio-chat-border, #c3c4c7);
165 1943 }
166 1944
167 -/* Light — pale sidebar, teal accent. */
168 -.desktop-mode-shell[data-desktop-mode-scheme="light"] {
169 - --wp-admin-theme-color: #04a4cc;
170 - /* color-mix(in srgb, #e5e5e5 85%, #000 15%) */
171 - --desktop-mode-titlebar-bg-focused: #c3c3c3;
172 - --desktop-mode-titlebar-color-focused: #333;
1945 +/* Ghost labels sit on the chat; the hero button owns a lighter mesh. */
1946 +body.os-active .os-mio-chat os-button[variant="ghost"],
1947 +body.os-active .os-mio-chat-launcher {
1948 + --os-ui-button-fg: var(--os-mio-chat-fg, #1d2327);
1949 + --os-ui-button-bg-hover: var(--os-mio-chat-hover-bg, #f0f0f1);
173 1950 }
174 1951
175 -/* Modern — vivid indigo accent, near-black sidebar. */
176 -.desktop-mode-shell[data-desktop-mode-scheme="modern"] {
177 - --wp-admin-theme-color: #3858e9;
178 - /* color-mix(in srgb, #1e1e1e 78%, #fff 22%) */
179 - --desktop-mode-titlebar-bg-focused: #505050;
180 - --desktop-mode-titlebar-color-focused: #f3f1f1;
1952 +body.os-active .os-mio-chat os-button[variant="primary"],
1953 +body.os-active .os-mio-chat os-button[variant="holo"] {
1954 + --os-ui-button-fg: var(--os-ui-holo-ink, #1d2327);
181 1955 }
182 1956
183 -/* Blue. */
184 -.desktop-mode-shell[data-desktop-mode-scheme="blue"] {
185 - --wp-admin-theme-color: #096484;
186 - /* color-mix(in srgb, #096484 82%, #fff 18%) */
187 - --desktop-mode-titlebar-bg-focused: #35809a;
188 - --desktop-mode-titlebar-color-focused: #e5f8ff;
1957 +/* The component owns the launcher's single border and rounded surface. */
1958 +body.os-active .os-mio-chat-launcher {
1959 + --os-ui-button-bg: var(--os-mio-launcher-bg, #ffffffa0);
1960 + --os-ui-button-border: 1px solid var(--os-mio-chat-border, #c3c4c7);
1961 + --os-ui-button-border-radius: var(--os-mio-chat-radius, 22px);
189 1962 }
190 1963
191 -/* Coffee. */
192 -.desktop-mode-shell[data-desktop-mode-scheme="coffee"] {
193 - --wp-admin-theme-color: #c7a589;
194 - /* color-mix(in srgb, #46403c 80%, #fff 20%) */
195 - --desktop-mode-titlebar-bg-focused: #6b6663;
196 - --desktop-mode-titlebar-color-focused: #ece6f6;
197 -}
198 -
199 -/* Ectoplasm. */
200 -.desktop-mode-shell[data-desktop-mode-scheme="ectoplasm"] {
201 - --wp-admin-theme-color: #a3b745;
202 - /* color-mix(in srgb, #413256 80%, #fff 20%) */
203 - --desktop-mode-titlebar-bg-focused: #675b78;
204 - --desktop-mode-titlebar-color-focused: #ece6f6;
205 -}
206 -
207 -/* Midnight. */
208 -.desktop-mode-shell[data-desktop-mode-scheme="midnight"] {
209 - --wp-admin-theme-color: #e14d43;
210 - /* color-mix(in srgb, #25282b 80%, #fff 20%) */
211 - --desktop-mode-titlebar-bg-focused: #515355;
212 - --desktop-mode-titlebar-color-focused: #f1f2f3;
213 -}
214 -
215 -/* Ocean. */
216 -.desktop-mode-shell[data-desktop-mode-scheme="ocean"] {
217 - --wp-admin-theme-color: #9ebaa0;
218 - /* color-mix(in srgb, #627c83 80%, #fff 20%) */
219 - --desktop-mode-titlebar-bg-focused: #81969c;
220 - --desktop-mode-titlebar-color-focused: #f2fcff;
221 -}
222 -
223 -/* Sunrise. */
224 -.desktop-mode-shell[data-desktop-mode-scheme="sunrise"] {
225 - --wp-admin-theme-color: #dd823b;
226 - /* color-mix(in srgb, #b43c38 80%, #fff 20%) */
227 - --desktop-mode-titlebar-bg-focused: #c36360;
228 - --desktop-mode-titlebar-color-focused: #f3f1f1;
1964 +body.os-active .os-mio-callout {
1965 + --os-ui-button-border: 0;
1966 + --os-ui-button-border-radius: 50%;
1967 + --os-ui-button-padding: 0;
1968 + --os-ui-button-bg: transparent;
1969 + --os-ui-button-fg: var(--os-mio-callout-fg, #382b38);
1970 + --os-ui-button-bg-hover: color-mix(in srgb, var(--os-mio-callout-fg, #382b38) 10%, transparent);
229 1971 }