PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.11
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.11
1.1.11 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 All 35 releases
desktop-mode / assets / css / chromeless.css

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

1,253 lines 40.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OpenStation — Chromeless Overrides.
3 *
4 * CSS adjustments for legacy admin pages rendered inside OpenStation
5 * iframes (chromeless mode). All rules are scoped to .os-chromeless
6 * so they never affect the classic admin or the desktop shell.
7 *
8 * Plugin and theme developers: to add your own chromeless overrides,
9 * enqueue a stylesheet on the 'openstation_chromeless_styles' action.
10 * Your CSS just needs to target .os-chromeless as the body class.
11 *
12 * Example:
13 * add_action( 'openstation_chromeless_styles', function() {
14 * wp_enqueue_style( 'my-plugin-chromeless', plugin_dir_url( __FILE__ ) . 'chromeless.css' );
15 * } );
16 *
17 * @since 0.1.0
18 */
19
20 /* ---------------------------------------------------------------
21 * Hide the classic admin chrome elements. These would otherwise
22 * reserve space (sidebar gutter, footer, admin bar) around the
23 * chromeless page content.
24 * --------------------------------------------------------------- */
25 .os-chromeless #adminmenuwrap,
26 .os-chromeless #adminmenuback,
27 .os-chromeless #wpfooter,
28 .os-chromeless #wpadminbar,
29 .os-chromeless .wp-responsive-toggle,
30 .os-chromeless #collapse-menu {
31 display: none !important;
32 }
33
34 /* ---------------------------------------------------------------
35 * The iframe paints its own canvas.
36 *
37 * An admin page leaves whole regions unpainted — the gutter around
38 * `#wpbody-content`, the strip behind `.subsubsub` and `.tablenav`,
39 * everything below short content. In a normal admin tab the browser
40 * canvas is white underneath, so nobody notices. Inside an iframe the
41 * canvas is TRANSPARENT, and what shows through is the window element
42 * behind it — `--os-window-bg`.
43 *
44 * That was invisible while the window body was white and became a
45 * black band the moment the station's palette made it Obsidian. It is
46 * a real coupling either way: the look of a wp-admin page must not
47 * depend on the colour of the frame around it.
48 *
49 * So the chromeless document paints itself, in the colour the browser
50 * canvas would have been. Deliberately a literal and deliberately NOT
51 * a token: nothing about the shell's palette — or any desktop theme —
52 * should reach a real admin page. **An admin page in a window renders
53 * exactly as it does outside one.**
54 * --------------------------------------------------------------- */
55 body.os-chromeless {
56 background: #fff;
57 }
58
59 /*
60 * _wp_admin_html_begin() adds the `wp-toolbar` class to <html> whenever
61 * is_admin_bar_showing() is true — which, in admin, is unconditional.
62 * The class carries a `padding-top: var(--wp-admin--admin-bar--height)`
63 * that would leave a 32px (or 46px) dead gap at the top of the iframe.
64 * Zero it out inside chromeless iframes.
65 *
66 * We also rebind `--wp-admin--admin-bar--height` (and the derived
67 * `--wp-admin--admin-bar--position-offset` from block-library) to 0px
68 * inside chromeless so plugins that position UI relative to the admin
69 * bar resolve their math against the iframe's actual chrome state.
70 *
71 * WooCommerce's activity-panel wrapper, the block editor's sticky
72 * header, and several others use `top: var(--wp-admin--admin-bar--height)`
73 * to clear the bar. Without this override they reserve a 32px (or 46px
74 * on small screens) gap that no longer exists, producing visible jumps
75 * on first paint and dead space at the top of the content area.
76 */
77 html.wp-toolbar:has( body.os-chromeless ) {
78 padding-top: 0 !important;
79 --wp-admin--admin-bar--height: 0px;
80 --wp-admin--admin-bar--position-offset: 0px;
81 }
82
83 /* Remove the sidebar gutter on #wpcontent and let the body fill the frame. */
84 .os-chromeless #wpcontent {
85 margin-inline-start: 0 !important;
86 padding-inline-start: 0 !important;
87 }
88
89 /*
90 * WooCommerce sidebar-reservation override.
91 *
92 * `.woocommerce-layout__header` is a `position: fixed` bar WC
93 * mounts on every wc-admin and wc-embedded page. Its width is
94 * compiled from the SCSS source (header/style.scss) as a literal:
95 *
96 * position: fixed;
97 * top: 32px; // $adminbar-height
98 * width: calc(100% - 160px); // reserves classic sidebar
99 * z-index: 1001;
100 *
101 * The 160px subtraction is the classic admin menu width — WC bakes
102 * it in at build time so the header doesn't overlap the sidebar
103 * in standard admin. Inside chromeless we hide the sidebar, but
104 * the header keeps the reservation — so the header ends 160px
105 * short of the iframe right edge, and `.woocommerce-layout__activity-panel-wrapper`
106 * (`position: absolute; right: 0; top: 100%; transform: translateX(100%)`,
107 * containing block = the fixed header) translates past the
108 * header's right edge, NOT past the iframe's right edge. The
109 * resulting visible strip is exactly 160px wide — the size of the
110 * sidebar gap WC reserved for nothing.
111 *
112 * Reclaim the reservation: pin the header to full iframe width
113 * inside chromeless. The activity panel then translates past the
114 * iframe edge as WC's design intended, the gray strip disappears,
115 * and the header's content uses the full window width. No
116 * transform / overflow / visibility tricks needed.
117 */
118 .os-chromeless .woocommerce-layout__header {
119 width: 100% !important;
120 }
121
122 /*
123 * MailPoet top-bar overlap fix.
124 *
125 * MailPoet mounts a 64px-tall brand bar on its screens as
126 * `.wrap .mailpoet-top-bar { position: absolute; top: 0 }` —
127 * anchored to `#wpbody` (core's `position: relative`), overlaying
128 * whatever the page's first 64px contain. In classic admin nothing
129 * is visibly covered only by accident of geometry: the in-flow
130 * space above `.wrap` — the `#screen-meta-links` row plus `.wrap`'s
131 * own top margin — happens to add up to more than the bar's height,
132 * so the content starts below it (measured: `.wrap` at y=114 vs
133 * bar bottom at y=96).
134 *
135 * Chromeless collapses exactly that space on purpose — screen-meta
136 * links are hidden (the window title bar owns those buttons) and
137 * `.wrap` margins are trimmed — which slides MailPoet's content up
138 * underneath the overlaid bar. Symptom: the page's heading sits
139 * half-hidden behind the white logo bar inside the window.
140 *
141 * Reserve the bar's height in flow instead. `:has()` scopes the
142 * rule to precisely the pages that render the in-wrap bar —
143 * whatever MailPoet screen shape it is, present or future — and to
144 * nothing else.
145 */
146 .os-chromeless .wrap:has( .mailpoet-top-bar ) {
147 padding-top: 64px;
148 }
149
150 /* ---------------------------------------------------------------
151 * Screen Meta (Screen Options / Help panels)
152 * The toggle buttons (#screen-meta-links) are hidden because
153 * the parent desktop shell adds its own buttons to the window
154 * title bar. The panels themselves stay visible and functional —
155 * they're toggled via postMessage from the parent.
156 *
157 * Collapse all margins on the hidden links and the panel container
158 * so they don't leave a gap at the top of the iframe content.
159 * --------------------------------------------------------------- */
160 .os-chromeless #screen-meta-links {
161 display: none;
162 margin: 0;
163 }
164
165 .os-chromeless #screen-meta {
166 margin: 0;
167 border: none;
168 }
169
170 /* ---------------------------------------------------------------
171 * Wrap container
172 * Remove the default left margin that accounts for the sidebar
173 * which doesn't exist in chromeless mode.
174 * --------------------------------------------------------------- */
175 .os-chromeless .wrap {
176 margin: 0;
177 }
178
179 /* ---------------------------------------------------------------
180 * Page title & header
181 * The window title bar already shows the page title, so the
182 * in-page <h1> + header separator (.wp-header-end) are redundant
183 * inside chromeless iframes. Hide them so the window content
184 * starts flush.
185 *
186 * `.page-title-action` (the "Add New" / "Add Order" button next to
187 * the H1) stays VISIBLE by default — it's the only entry point to
188 * the add-new flow on many plugin pages (WooCommerce Orders, custom
189 * CPTs, plugin settings pages, etc.). A blanket hide would break
190 * every third-party plugin page that has no submenu equivalent, so
191 * the button is only removed where the window's own tab strip
192 * demonstrably leads to the same place — see
193 * `includes/render/chromeless-title-actions.php`, which emits one
194 * `[href="…"]` rule per submenu tab URL of the current screen.
195 *
196 * Sites that want to hide it somewhere that rule deliberately
197 * doesn't reach can add their own via the
198 * `openstation_chromeless_styles` action — e.g. WooCommerce's "Add
199 * order", which we keep because it points at `&action=new` and the
200 * Orders tab doesn't:
201 *
202 * body.woocommerce_page_wc-orders .wrap > .page-title-action {
203 * display: none;
204 * }
205 *
206 * --------------------------------------------------------------- */
207 .os-chromeless .wrap > h1,
208 .os-chromeless .wrap > h1.wp-heading-inline,
209 .os-chromeless #wpbody-content > .wrap > h1,
210 .os-chromeless .wrap > .wp-header-end {
211 display: none;
212 }
213
214 /*
215 * The H1 above it is hidden, so the button would float at the very
216 * top of the iframe area without breathing room — give it a small
217 * margin so it lands cleanly.
218 *
219 * `block` + `fit-content` instead of Core's `inline-block`: with the
220 * H1 hidden, an inline button ends up on the same line as the
221 * floated `.subsubsub` filter row and reads as one more filter link
222 * ("All (6) | Published (4) | Trash (2) Add Post"). Its own row is
223 * also what the screen already looks like when an admin notice is
224 * showing. `clear` keeps it below anything floated before it.
225 *
226 * Themes.php's native "Add Theme" button lives in the submenu tab
227 * strip via `openstation_inject_appearance_tabs`
228 * (includes/themes-tabs.php), so its in-page page-title-action is
229 * redundant on that one screen. The generic href-matching hide in
230 * `includes/render/chromeless-title-actions.php` only matches exact
231 * URLs, and the injected tab points at
232 * `theme-install.php?browse=popular` while the button points at
233 * plain `theme-install.php`. Hence the per-page rule below.
234 */
235 .os-chromeless .wrap > .page-title-action {
236 display: block;
237 width: fit-content;
238 margin-top: 12px;
239 margin-bottom: 12px;
240 clear: both;
241 }
242
243 .os-chromeless.themes-php .wrap > .page-title-action {
244 display: none;
245 }
246
247 /*
248 * Core offsets every title action by `top: -3px` (common.css) so it
249 * sits on the H1's baseline. Chromeless hides the H1, and the first
250 * thing in the frame has nothing above it to overlap: the offset
251 * pulls the button's top border under the window's content edge and
252 * it renders cropped. The offset has no baseline left to meet here,
253 * so drop it.
254 *
255 * The margins are the same 12px the rule above gives a lone button,
256 * applied to whatever element a plugin grouped its buttons in. On a
257 * Jetpack site that is `div.wpcom-media-library-action-buttons`
258 * (external-media moves core's "Add Media File" into it so it can
259 * append "Import Media"; Big Sky adds "Generate Image" beside them),
260 * and a group is not a `.page-title-action`, so it picks up neither
261 * the margin nor the block layout and lands flush against the top.
262 * `:has()` reaches the group whatever a plugin decided to call it.
263 */
264 .os-chromeless .wrap .page-title-action {
265 top: 0;
266 }
267
268 .os-chromeless .wrap :has( > .page-title-action ) {
269 margin-top: 12px;
270 margin-bottom: 12px;
271 }
272
273 /* ---------------------------------------------------------------
274 * WooCommerce "embed page" header overlay — page-scoped.
275 *
276 * Background: WC renders TWO layouts simultaneously on "connected"
277 * pages like `wc-orders` — the PHP-rendered legacy `.wrap` (with
278 * the h1 + the "Add order" `.page-title-action`) AND a React-mounted
279 * `EmbedHeader` (`client/admin/client/header/embed.tsx`) that
280 * `position: fixed`-overlays the page from the top. References:
281 * - `src/Internal/Admin/Loader.php::embed_page_header`
282 * - `includes/react-admin/connect-existing-pages.php` (registers
283 * wc-orders as a connected page when HPOS is on)
284 * - `client/admin/client/header/shared.tsx::useUpdateBodyMargin`
285 * (the hook that pushes `#wpbody.style.marginTop` to make
286 * room for the fixed header)
287 *
288 * The conflict, narrowly:
289 * 1. The legacy `.wrap > .page-title-action` ("Add order") at
290 * the top of the document body — primary add-new affordance.
291 * 2. The React `EmbedHeader` overlays the top of the iframe with
292 * a redundant `<h1>Orders</h1>` and an Activity Panel toggle.
293 *
294 * The fixed React header OBSCURES the legacy `.page-title-action`
295 * button when we reset `#wpbody`'s margin-top.
296 *
297 * Scope: ONLY the pages where the dual rendering is genuinely a
298 * problem — `wc-orders` (Orders list, the user-reported failure
299 * mode) and `wc-orders--shop_order` (the trash view variant). Other
300 * WC-admin pages (Analytics, Marketing, Customers, Coupons,
301 * Products, Reports, …) DON'T render a competing legacy `.wrap`
302 * with a `.page-title-action` button; their primary content IS the
303 * React app. On those pages the Activity Panel + EmbedHeader are
304 * the only header affordance the user has, so we keep them visible.
305 *
306 * If new HPOS-style connected pages emerge where the same dual
307 * layout creates the same conflict, add their per-page body class
308 * (`.woocommerce_page_<slug>`) to the selector — don't broaden the
309 * rule to `.woocommerce-admin-page` (that would silently strip the
310 * Activity Panel from every WC screen).
311 *
312 * @since 0.8.9
313 */
314 .os-chromeless.woocommerce_page_wc-orders .woocommerce-layout__header,
315 .os-chromeless.woocommerce_page_wc-orders--shop_order .woocommerce-layout__header {
316 display: none !important;
317 }
318 .os-chromeless.woocommerce_page_wc-orders #wpbody,
319 .os-chromeless.woocommerce_page_wc-orders--shop_order #wpbody {
320 margin-top: 0 !important;
321 }
322
323 /* ---------------------------------------------------------------
324 * Revisions screen — page-scoped.
325 *
326 * "← Go to editor" is a back button for a screen the user navigated
327 * into. Here they didn't: the editor is open in its own window
328 * behind this one, and closing this window is the way back.
329 *
330 * The revision tooltip is positioned upward from the bottom of
331 * `.revisions-control-frame` and clears the top of the viewport only
332 * thanks to the screen H1, which chromeless hides. An iframe can't
333 * overflow its box, so the space has to be given back: 48px covers
334 * both slider modes.
335 *
336 * The padding goes on `.revisions`, not on the frame — the frame is
337 * the positioned ancestor for the compare-mode checkbox and the
338 * tooltip, so padding it would leave both behind at the old top edge.
339 * --------------------------------------------------------------- */
340 .os-chromeless.revision-php .wrap > h1.long-header + a {
341 display: none;
342 }
343
344 .os-chromeless.revision-php .revisions {
345 padding-top: 48px;
346 }
347
348 /* ---------------------------------------------------------------
349 * Dashboard welcome panel
350 * The default 16px top margin pushes the panel down and creates a
351 * visible gap at the top of the iframe. Collapse it in chromeless
352 * mode so the panel sits flush with the top of the window body.
353 * --------------------------------------------------------------- */
354 .os-chromeless #welcome-panel,
355 .os-chromeless .welcome-panel {
356 margin-top: 0.5em;
357 }
358
359 /* ---------------------------------------------------------------
360 * Footer
361 * The classic footer is not rendered in chromeless mode,
362 * but some pages have bottom padding assuming it exists.
363 * --------------------------------------------------------------- */
364 .os-chromeless #wpbody-content {
365 margin: 0;
366 padding: 0 8px 8px;
367 float: none;
368 width: auto;
369 }
370
371 /* ---------------------------------------------------------------
372 * Metabox screens — the one-column breakpoint is 160px too early.
373 *
374 * Core collapses every `#poststuff` two-column screen (the classic
375 * post editor, any CPT editor, a WooCommerce order) to one column
376 * at `max-width: 850px`. That number is a VIEWPORT width, and it is
377 * sized for a viewport that still has the 160px admin menu in it:
378 * at 851px core leaves roughly 650px of usable content — less than
379 * the 763px the two columns actually need (`#post-body-content`'s
380 * 463px floor plus the 300px sidebar reservation), which is why
381 * core's own two-column layout scrolls sideways just above the
382 * breakpoint.
383 *
384 * A chromeless iframe hides the admin menu, so the same 851px gives
385 * 835px of content — comfortably two columns — and core stacks it
386 * anyway. The sidebar then lands BELOW the editor, and since the
387 * classic editor grows to fit its text, a long post pushes Publish,
388 * Categories, Tags and Featured image thousands of pixels down the
389 * page. The user's word for that is "vanished".
390 *
391 * Same shape as the WooCommerce header fix at the top of this file:
392 * the page reserved space for a sidebar we removed, so reclaim the
393 * reservation. Two columns stay until the content really stops
394 * fitting: 763px of content, plus the 16px `#wpbody-content` gutter
395 * above, plus up to 17px for a classic vertical scrollbar — which a
396 * media query does NOT subtract from the width it matches on, the
397 * same reason `100vw` overflows a scrolling page. That is 796px.
398 * Below it, core's one-column layout is correct and stays.
399 *
400 * The reservation is written flow-relative rather than as core's
401 * `margin-right` / `float: right`, because core ships the mirrored
402 * values in a separate `edit-rtl.css` and this file has no RTL
403 * build — one logical declaration lands on the correct side in
404 * both directions. `float: right` stays ahead of `float:
405 * inline-end` as the fallback.
406 *
407 * Attachments keep core's own, wider, 1200px breakpoint: the media
408 * editor's sidebar carries the whole attachment form and needs the
409 * room. `.post-type-attachment` is excluded rather than re-stacked
410 * so that rule keeps working untouched.
411 *
412 * Verifying: open a post with a few thousand words in a window
413 * ~800px wide. Publish/Categories/Tags sit beside the editor, not
414 * under it, and the page has no horizontal scrollbar.
415 * --------------------------------------------------------------- */
416 @media only screen and ( min-width: 796px ) and ( max-width: 850px ) {
417 .os-chromeless:not( .post-type-attachment ) #wpbody-content #poststuff #post-body.columns-2 {
418 margin-inline-end: 300px;
419 }
420
421 .os-chromeless:not( .post-type-attachment ) #wpbody-content #post-body.columns-2 #postbox-container-1 {
422 float: right;
423 float: inline-end;
424 margin-inline-end: -300px;
425 width: 280px;
426 }
427
428 .os-chromeless:not( .post-type-attachment ) #poststuff #post-body.columns-2 #side-sortables {
429 min-height: 250px;
430 width: 280px;
431 }
432
433 /*
434 * Core hides the Layout radios with the two-column layout. They
435 * are the way back to one column, so they come back with it.
436 *
437 * `edit.css` is part of the `wp-admin` bundle and loads on every
438 * screen, so that hide reaches the Dashboard's own 1-4 column
439 * radios too. Only un-hide them where this block actually gave a
440 * layout back.
441 */
442 .os-chromeless:not( .post-type-attachment ):has( #post-body.columns-2 ) .screen-layout,
443 .os-chromeless:not( .post-type-attachment ):has( #post-body.columns-2 ) .columns-prefs {
444 display: block;
445 }
446 }
447
448 /* ---------------------------------------------------------------
449 * Block Editor — hide Gutenberg chrome that would break the window.
450 *
451 * The fullscreen-mode close button (the "W" logo top-left of the editor)
452 * is an <a href="/wp-admin/edit.php"> that navigates the iframe to a
453 * non-chromeless URL — which re-renders the entire classic admin
454 * inside our desktop window. The link interceptor in the chromeless
455 * bridge catches it too, but hiding the button removes the visual
456 * affordance so users never try to click "back to dashboard" inside
457 * what looks like a self-contained window.
458 *
459 * The site editor's equivalent navigation affordances (site hub toggle,
460 * "back to dashboard" link) get the same treatment.
461 *
462 * The welcome guide is intentionally NOT hidden anymore. Earlier
463 * iterations both CSS-hid the dialog AND flipped `core/edit-post:
464 * welcomeGuide` to `false` at the data layer on every chromeless
465 * mount — but Gutenberg already persists the user's "Get started"
466 * dismissal to user meta the moment they close it, so the override
467 * was permanently stealing the one-time orientation tour from every
468 * user who never got to see it. Modal's focus trap is Tab-only and
469 * doesn't fight the shell. Let it run. (See git history for the
470 * removed `openstation_chromeless_editor_preferences` override.)
471 * --------------------------------------------------------------- */
472 .os-chromeless .edit-post-fullscreen-mode-close,
473 .os-chromeless .edit-post-fullscreen-mode-close__view-mode-toggle,
474 .os-chromeless .edit-site-navigation-link,
475 .os-chromeless .edit-site-site-hub,
476 .os-chromeless .edit-site-site-hub__toggle {
477 display: none !important;
478 }
479
480 /* ---------------------------------------------------------------
481 * Block Editor & Site Editor — full-bleed layouts.
482 * Gutenberg owns its entire viewport (its own header bar, side
483 * panels, etc.). Any padding around #wpbody-content crops the
484 * editor and breaks its layout, so reset to zero on those pages
485 * and let the editor render edge-to-edge.
486 * --------------------------------------------------------------- */
487 .os-chromeless.block-editor-page #wpbody-content,
488 .os-chromeless.site-editor-php #wpbody-content,
489 .os-chromeless.is-fullscreen-mode #wpbody-content {
490 padding: 0;
491 }
492
493 /* ---------------------------------------------------------------
494 * "Boot" SPA pages (Font Library, Options → Connectors).
495 *
496 * These screens mount a React app into `.boot-layout-container` and
497 * paint `#wpwrap` a dark `#1e1e1e` via inline <style>. Our 8px
498 * right/bottom padding on `#wpbody-content` lets that dark color
499 * bleed through as two black gaps on the right and bottom edges of
500 * the window. Zero the padding for any page hosting a boot layout
501 * — detection is purely structural (`:has()`), so a plugin that
502 * adopts the same container class inherits the fix automatically.
503 * --------------------------------------------------------------- */
504 .os-chromeless #wpbody-content:has( .boot-layout-container ) {
505 padding: 0;
506 }
507
508 .os-chromeless:has( .boot-layout-container ) #wpwrap {
509 /* Neutralize the inline dark background so any stray overflow
510 * matches the window body instead of flashing black. */
511 background: transparent;
512 }
513
514 /*
515 * The surfaces wrapper is the white card the React app draws its
516 * content onto. In classic admin it sits on the dark layout with
517 * rounded corners and a margin that makes it read as a "card." Inside
518 * a desktop window the surface IS the content, so that frame is just
519 * bleed at the right and bottom edges. Collapse it to a plain
520 * edge-to-edge surface.
521 *
522 * Core builds these class names with CSS modules, so only the
523 * `__<local-name>` suffix is stable; the hash in front of it changes
524 * with every Core build. Match on the suffix, scoped to the boot
525 * container. The bare `boot-layout__*` names are the pre-CSS-modules
526 * spelling, kept for Core builds that still ship it.
527 *
528 * The container is not a tight enough scope on its own. The page a
529 * boot screen routes to mounts INSIDE the layout, and a plugin page
530 * built with CSS modules gives its own elements the same
531 * `<hash>__<name>` shape: Jetpack's Stats v2 dashboard lays its
532 * widgets out in a `…__layout` grid, and the `min-height: 100vh` meant
533 * for Core's layout element stretched that grid to the window and
534 * spread the dashboard's rows across it, with the widgets pushed
535 * out of sight. Everything a page renders lives inside admin-ui's
536 * navigable region, so exclude that subtree: these rules only ever
537 * touch Core's own frame around the page, never the page.
538 */
539 .os-chromeless .boot-layout-container [class*='__surfaces']:not( .admin-ui-navigable-region * ),
540 .os-chromeless .boot-layout-container [class*='__stage']:not( .admin-ui-navigable-region * ),
541 .os-chromeless .boot-layout-container [class*='__canvas']:not( .admin-ui-navigable-region * ),
542 .os-chromeless .boot-layout__surfaces,
543 .os-chromeless .boot-layout__stage,
544 .os-chromeless .boot-layout__canvas {
545 border-radius: 0 !important;
546 margin: 0 !important;
547 }
548
549 /*
550 * The layout element is absolutely positioned inside a zero-height
551 * `#wpbody`, so `top: 0; bottom: 0` resolve to nothing and its
552 * `min-height: calc( 100vh - <admin bar> )` is what it actually gets.
553 * A chromeless iframe has no admin bar, so that reservation shows up
554 * as a dark band along the bottom of the window. Take the full
555 * viewport back. Same navigable-region guard as above: the page's
556 * own `…__layout` elements keep their height.
557 */
558 .os-chromeless .boot-layout-container [class*='__layout']:not( .admin-ui-navigable-region * ),
559 .os-chromeless .boot-layout {
560 min-height: 100vh !important;
561 }
562
563 /*
564 * The boot screens render their own H1 (`.boot-navigation-screen__title`,
565 * e.g., "Fonts") at the top of the surface. The desktop window already
566 * shows the page title in its title bar, so the in-page title is
567 * redundant. Visually hide it but keep it in the DOM for screen readers
568 * and for the boot app's own focus management.
569 */
570 .os-chromeless .boot-navigation-screen__title,
571 .os-chromeless .boot-navigation-screen__title-icon {
572 position: absolute;
573 width: 1px;
574 height: 1px;
575 padding: 0;
576 margin: -1px;
577 overflow: hidden;
578 clip: rect( 0, 0, 0, 0 );
579 white-space: nowrap;
580 border: 0;
581 }
582
583 /* ---------------------------------------------------------------
584 * Snackbars
585 *
586 * `components-snackbar-list` (Gutenberg `@wordpress/components`) and
587 * `boot-notices__snackbar` (Font Library / Connectors) position
588 * themselves `fixed` at the viewport bottom. Because a chromeless
589 * iframe IS its own viewport, those anchors already land inside the
590 * window — but our 8px bottom padding on `#wpbody-content` (applied
591 * to non-boot pages) plus some plugin-level `bottom: 40px` offsets
592 * meant for the classic admin footer can push them out of sight.
593 *
594 * Pin them flush to the bottom of the iframe and make sure nothing
595 * in the window chrome can occlude them.
596 *
597 * Triggering a snackbar to verify (OpenStation enabled):
598 * - Open Posts → Add New, save/publish → "Post published" toast.
599 * - Open Media, upload a file → "Uploaded" toast.
600 * - Open Appearance → Fonts, install a Google Font → toast.
601 * --------------------------------------------------------------- */
602 .os-chromeless .components-snackbar-list,
603 .os-chromeless .boot-notices__snackbar {
604 bottom: 16px !important;
605 z-index: 100000;
606 }
607
608 /*
609 * Force the Gutenberg interface skeleton to fill the iframe regardless
610 * of fullscreen state. When fullscreenMode is off, Gutenberg hardcodes
611 * `top: 32px` (admin bar reservation) and `left: 160px` (sidebar
612 * reservation) on `.interface-interface-skeleton` — both chromes we've
613 * already hidden, so those offsets become dead gaps at the top and
614 * left of the window. Zero them out unconditionally.
615 *
616 * The default is fullscreen (inset: 0), but users who turned fullscreen
617 * off in classic admin carry that preference into chromeless via the
618 * shared persistence layer. This rule keeps chromeless rendering
619 * edge-to-edge either way.
620 */
621 .os-chromeless .interface-interface-skeleton {
622 top: 0 !important;
623 left: 0 !important;
624 right: 0 !important;
625 bottom: 0 !important;
626 }
627
628 /* ---------------------------------------------------------------
629 * Notices
630 * Give notices a bit of top margin since there's no admin bar above.
631 * --------------------------------------------------------------- */
632 .os-chromeless .notice:first-child {
633 margin-top: 4px;
634 }
635
636 /* ---------------------------------------------------------------
637 * Update nag
638 * Core's global update / maintenance nags are detached server-side
639 * inside chromeless iframes (see
640 * `openstation_chromeless_suppress_update_nags()`) and re-surfaced
641 * once by the shell as a single notice.
642 * --------------------------------------------------------------- */
643 .os-chromeless .update-nag {
644 display: none !important;
645 }
646
647 /* ---------------------------------------------------------------
648 * Suppress WordPress's native command palette inside chromeless
649 * iframes. The chromeless bridge intercepts Cmd+K and forwards to
650 * the desktop shell, but if an in-page component triggers the
651 * palette via another path (programmatic dispatch, a menu item)
652 * the dialog would still render. Hide it so the shell's palette
653 * stays the only surface users ever see.
654 * --------------------------------------------------------------- */
655 .os-chromeless .commands-command-menu__container,
656 .os-chromeless .commands-command-menu {
657 display: none !important;
658 }
659
660 /* ---------------------------------------------------------------
661 * A file drag over a native upload box.
662 *
663 * The chromeless bridge hands an OS-file drop to Core's own
664 * `<input type="file">` when it lands on one, or anywhere on the
665 * `form.wp-upload-form` box around it (Upload Plugin, Upload
666 * Theme), and stamps `data-os-file-drop-active` on that box while
667 * the drag is over it. Outside the shell the box gives no sign it
668 * will take a drop; inside, the shell used to take the file
669 * instead, so people learned it would not. Say so while it can.
670 *
671 * Core's own admin colour, read the way common.css reads it, not
672 * the station's: this is a wp-admin control on a wp-admin page.
673 * --------------------------------------------------------------- */
674 .os-chromeless [data-os-file-drop-active] {
675 outline: 2px dashed var( --wp-admin-theme-color, #3858e9 );
676 outline-offset: -2px;
677 }
678
679 /* ---------------------------------------------------------------
680 * Appearance → Themes workspace
681 *
682 * Core's one-theme state renders the Theme Details dialog inline and
683 * removes its header. Inside a short desktop window that leaves a large,
684 * unexplained panel whose actions fall below the fold. Treat it as an
685 * active-site identity card instead: screenshot first, useful context at
686 * the side, and actions kept in the card's visible edge.
687 *
688 * With multiple installed themes the native cards become a deliberate
689 * library grid and Core's details view remains a dialog. All selectors are
690 * scoped to the chromeless Themes screen; the classic wp-admin page keeps
691 * Core's own presentation and the shell's palette cannot leak in here.
692 * --------------------------------------------------------------- */
693 .os-chromeless.themes-php #wpbody-content {
694 padding: 10px 16px;
695 background: #f6f7f7;
696 }
697
698 .os-chromeless.themes-php .openstation-themes-intro {
699 display: flex;
700 align-items: center;
701 justify-content: space-between;
702 gap: 24px;
703 margin: 0 0 8px;
704 padding: 14px 18px;
705 background: #fff;
706 border: 1px solid #dcdcde;
707 border-radius: 12px;
708 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
709 box-sizing: border-box;
710 }
711
712 .os-chromeless.themes-php .openstation-themes-intro__copy {
713 max-width: 680px;
714 }
715
716 .os-chromeless.themes-php .openstation-themes-intro__eyebrow {
717 margin: 0 0 5px;
718 color: var(--wp-admin-theme-color, #2271b1);
719 font-size: 11px;
720 font-weight: 700;
721 letter-spacing: 0.08em;
722 line-height: 1.4;
723 text-transform: uppercase;
724 }
725
726 .os-chromeless.themes-php .openstation-themes-intro h1 {
727 margin: 0;
728 color: #1d2327;
729 font-size: clamp(22px, 2.8vw, 30px);
730 font-weight: 650;
731 letter-spacing: -0.025em;
732 line-height: 1.12;
733 }
734
735 .os-chromeless.themes-php .openstation-themes-intro__copy > p:last-child {
736 max-width: 650px;
737 margin: 6px 0 0;
738 color: #50575e;
739 font-size: 13px;
740 line-height: 1.45;
741 }
742
743 .os-chromeless.themes-php .openstation-themes-intro__count {
744 display: grid;
745 flex: 0 0 auto;
746 min-width: 104px;
747 margin: 0;
748 padding: 10px 14px;
749 background: #f6f7f7;
750 border: 1px solid #dcdcde;
751 border-radius: 9px;
752 box-sizing: border-box;
753 text-align: center;
754 }
755
756 .os-chromeless.themes-php .openstation-themes-intro__count strong {
757 color: #1d2327;
758 font-size: 22px;
759 font-weight: 650;
760 letter-spacing: -0.03em;
761 line-height: 1;
762 }
763
764 .os-chromeless.themes-php .openstation-themes-intro__count span {
765 margin-top: 5px;
766 color: #646970;
767 font-size: 10px;
768 font-weight: 600;
769 letter-spacing: 0.04em;
770 line-height: 1.25;
771 text-transform: uppercase;
772 }
773
774 .os-chromeless.themes-php .wrap {
775 margin: 0;
776 }
777
778 .os-chromeless.themes-php .search-form {
779 display: flex;
780 margin: 0 0 16px;
781 padding: 0;
782 }
783
784 .os-chromeless.themes-php .wp-filter-search {
785 width: min(320px, 100%);
786 min-height: 36px;
787 padding-inline: 12px;
788 background: #fff;
789 border-color: #8c8f94;
790 border-radius: 7px;
791 }
792
793 /* Searching a library of one is noise, so the field goes away in Core's
794 * single-theme state. The general sibling combinator rather than `+`:
795 * themes.php emits `wp_admin_notice()` output between the search form and
796 * `.theme-browser` on `?activated`, `?deleted`, `?broken` and
797 * `?delete-active-child` — exactly the loads that follow activating or
798 * deleting a theme — which breaks strict adjacency. */
799 .os-chromeless.themes-php
800 .search-form:has(~ .theme-browser .themes.single-theme) {
801 display: none;
802 }
803
804 /* Installed-theme library. The Add Theme destination already has its own
805 * persistent window tab, so Core's duplicate dashed card is unnecessary. */
806 .os-chromeless.themes-php .theme-browser .themes:not(.single-theme) {
807 display: grid;
808 grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
809 gap: 18px;
810 clear: both;
811 }
812
813 .os-chromeless.themes-php .theme-browser .themes:not(.single-theme) > .theme {
814 float: none;
815 width: auto;
816 margin: 0;
817 background: #fff;
818 border: 1px solid #c3c4c7;
819 border-radius: 10px;
820 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
821 overflow: hidden;
822 transition:
823 border-color 120ms ease,
824 box-shadow 120ms ease,
825 transform 120ms ease;
826 }
827
828 .os-chromeless.themes-php
829 .theme-browser
830 .themes:not(.single-theme)
831 > .theme:hover,
832 .os-chromeless.themes-php
833 .theme-browser
834 .themes:not(.single-theme)
835 > .theme.focus {
836 border-color: #8c8f94;
837 box-shadow: 0 7px 20px rgba(0, 0, 0, 0.1);
838 transform: translateY(-2px);
839 }
840
841 .os-chromeless.themes-php
842 .theme-browser
843 .themes:not(.single-theme)
844 > .theme.active {
845 border-color: var(--wp-admin-theme-color, #2271b1);
846 box-shadow:
847 0 0 0 1px var(--wp-admin-theme-color, #2271b1),
848 0 7px 20px rgba(0, 0, 0, 0.08);
849 }
850
851 .os-chromeless.themes-php
852 .theme-browser
853 .themes:not(.single-theme)
854 > .add-new-theme {
855 display: none;
856 }
857
858 .os-chromeless.themes-php
859 .theme-browser
860 .themes:not(.single-theme)
861 .theme-screenshot {
862 background: #f0f0f1;
863 }
864
865 .os-chromeless.themes-php
866 .theme-browser
867 .themes:not(.single-theme)
868 .theme-id-container {
869 display: flex;
870 align-items: center;
871 justify-content: space-between;
872 min-height: 58px;
873 background: #fff;
874 }
875
876 .os-chromeless.themes-php .theme-browser .themes:not(.single-theme) .theme-name,
877 .os-chromeless.themes-php
878 .theme-browser
879 .themes:not(.single-theme)
880 .theme.active
881 .theme-name {
882 flex: 1 1 auto;
883 height: auto;
884 min-width: 0;
885 margin: 0;
886 padding: 14px 12px 14px 14px;
887 background: #fff;
888 box-shadow: none;
889 color: #1d2327;
890 font-size: 14px;
891 font-weight: 600;
892 line-height: 1.3;
893 }
894
895 .os-chromeless.themes-php
896 .theme-browser
897 .themes:not(.single-theme)
898 .theme.active
899 .theme-name
900 span {
901 display: block;
902 margin-bottom: 2px;
903 color: var(--wp-admin-theme-color, #2271b1);
904 font-size: 10px;
905 font-weight: 700;
906 letter-spacing: 0.06em;
907 line-height: 1.2;
908 text-transform: uppercase;
909 }
910
911 .os-chromeless.themes-php
912 .theme-browser
913 .themes:not(.single-theme)
914 .theme-actions,
915 .os-chromeless.themes-php
916 .theme-browser
917 .themes:not(.single-theme)
918 .theme.active
919 .theme-actions {
920 display: flex;
921 align-items: center;
922 flex: 0 0 auto;
923 position: static;
924 opacity: 1;
925 padding: 10px 12px 10px 0;
926 background: #fff;
927 border: 0;
928 box-shadow: none;
929 transform: none;
930 }
931
932 /* Multiple-theme details remain a modal, but fit the iframe rather than
933 * reserving the classic admin sidebar's 160px gutter. */
934 .os-chromeless.themes-php .theme-overlay.active .theme-backdrop {
935 position: fixed;
936 left: 0;
937 background: rgba(29, 35, 39, 0.62);
938 backdrop-filter: blur(2px);
939 }
940
941 .os-chromeless.themes-php .theme-overlay.active .theme-wrap {
942 top: 18px;
943 right: 18px;
944 bottom: 18px;
945 left: 18px;
946 border: 1px solid #c3c4c7;
947 border-radius: 12px;
948 box-shadow: 0 20px 55px rgba(0, 0, 0, 0.28);
949 overflow: hidden;
950 }
951
952 .os-chromeless.themes-php .theme-overlay.active .theme-header {
953 background: #fff;
954 }
955
956 .os-chromeless.themes-php .theme-overlay.active .theme-about {
957 padding: 24px;
958 }
959
960 .os-chromeless.themes-php .theme-overlay.active .theme-actions {
961 justify-content: flex-end;
962 padding: 10px 16px 5px;
963 }
964
965 /* Single-theme mode is a workspace card, not a stranded modal. */
966 .os-chromeless.themes-php .themes.single-theme {
967 display: block;
968 margin: 0;
969 }
970
971 .os-chromeless.themes-php .themes.single-theme .theme-overlay.active {
972 display: block;
973 position: static;
974 }
975
976 .os-chromeless.themes-php
977 .themes.single-theme
978 .theme-overlay.active
979 .theme-backdrop,
980 .os-chromeless.themes-php
981 .themes.single-theme
982 .theme-overlay.active
983 .theme-header {
984 display: none;
985 }
986
987 .os-chromeless.themes-php
988 .themes.single-theme
989 .theme-overlay.active
990 .theme-wrap {
991 display: grid;
992 grid-template-columns: minmax(320px, 0.95fr) minmax(300px, 1.05fr);
993 grid-template-areas: "screenshot details";
994 column-gap: 24px;
995 align-items: start;
996 position: relative;
997 top: auto;
998 right: auto;
999 bottom: auto;
1000 left: auto;
1001 min-height: 0;
1002 padding: 12px 16px;
1003 background: #fff;
1004 border: 1px solid #c3c4c7;
1005 border-radius: 12px;
1006 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
1007 overflow: visible;
1008 z-index: 10;
1009 }
1010
1011 .os-chromeless.themes-php
1012 .themes.single-theme
1013 .theme-overlay.active
1014 .theme-about {
1015 display: contents;
1016 }
1017
1018 .os-chromeless.themes-php
1019 .themes.single-theme
1020 .theme-overlay.active
1021 .theme-screenshots {
1022 grid-area: screenshot;
1023 float: none;
1024 width: 100%;
1025 max-width: none;
1026 margin: 0;
1027 }
1028
1029 .os-chromeless.themes-php
1030 .themes.single-theme
1031 .theme-overlay.active
1032 .screenshot {
1033 background: #f0f0f1;
1034 border: 0;
1035 border-radius: 8px;
1036 box-shadow: 0 0 0 1px #dcdcde;
1037 overflow: hidden;
1038 }
1039
1040 .os-chromeless.themes-php
1041 .themes.single-theme
1042 .theme-overlay.active
1043 .theme-info {
1044 grid-area: details;
1045 align-self: start;
1046 float: none;
1047 width: auto;
1048 max-height: 240px;
1049 padding: 2px 10px 2px 0;
1050 overflow: auto;
1051 scrollbar-gutter: stable;
1052 }
1053
1054 .os-chromeless.themes-php
1055 .themes.single-theme
1056 .theme-overlay.active
1057 .current-label {
1058 margin: 0 0 10px;
1059 padding: 4px 9px;
1060 background: #e7f5ed;
1061 border: 1px solid #9ad7b5;
1062 border-radius: 999px;
1063 color: #0a5c36;
1064 font-size: 10px;
1065 font-weight: 700;
1066 letter-spacing: 0.05em;
1067 line-height: 1.4;
1068 text-transform: uppercase;
1069 }
1070
1071 .os-chromeless.themes-php
1072 .themes.single-theme
1073 .theme-overlay.active
1074 .theme-name {
1075 margin: 0;
1076 color: #1d2327;
1077 font-size: clamp(25px, 3vw, 34px);
1078 font-weight: 650;
1079 letter-spacing: -0.03em;
1080 line-height: 1.12;
1081 }
1082
1083 .os-chromeless.themes-php
1084 .themes.single-theme
1085 .theme-overlay.active
1086 .theme-version {
1087 margin-left: 8px;
1088 color: #646970;
1089 font-size: 11px;
1090 font-weight: 500;
1091 letter-spacing: 0;
1092 }
1093
1094 .os-chromeless.themes-php
1095 .themes.single-theme
1096 .theme-overlay.active
1097 .theme-author {
1098 margin: 8px 0 14px;
1099 color: #646970;
1100 font-size: 13px;
1101 }
1102
1103 .os-chromeless.themes-php
1104 .themes.single-theme
1105 .theme-overlay.active
1106 .theme-autoupdate {
1107 margin: 0 0 14px;
1108 padding: 9px 10px;
1109 background: #f6f7f7;
1110 border: 1px solid #dcdcde;
1111 border-radius: 6px;
1112 font-size: 12px;
1113 }
1114
1115 .os-chromeless.themes-php
1116 .themes.single-theme
1117 .theme-overlay.active
1118 .theme-description {
1119 margin: 0;
1120 color: #3c434a;
1121 font-size: 13px;
1122 line-height: 1.55;
1123 }
1124
1125 .os-chromeless.themes-php
1126 .themes.single-theme
1127 .theme-overlay.active
1128 .theme-tags,
1129 .os-chromeless.themes-php
1130 .themes.single-theme
1131 .theme-overlay.active
1132 .parent-theme {
1133 margin-top: 16px;
1134 padding-top: 12px;
1135 border-top-width: 1px;
1136 color: #646970;
1137 font-size: 11px;
1138 line-height: 1.5;
1139 }
1140
1141 .os-chromeless.themes-php
1142 .themes.single-theme
1143 .theme-overlay.active
1144 .theme-actions {
1145 display: flex;
1146 grid-area: details;
1147 align-items: center;
1148 align-self: end;
1149 justify-content: flex-start;
1150 position: static;
1151 padding: 14px 0 0;
1152 background: transparent;
1153 border-top: 1px solid #dcdcde;
1154 text-align: left;
1155 }
1156
1157 .os-chromeless.themes-php
1158 .themes.single-theme
1159 .theme-overlay.active
1160 .theme-actions
1161 .active-theme {
1162 display: flex;
1163 flex-wrap: wrap;
1164 gap: 7px;
1165 }
1166
1167 .os-chromeless.themes-php
1168 .themes.single-theme
1169 .theme-overlay.active
1170 .theme-actions
1171 .inactive-theme {
1172 display: none;
1173 }
1174
1175 .os-chromeless.themes-php
1176 .themes.single-theme
1177 .theme-overlay.active
1178 .theme-actions
1179 .button {
1180 margin: 0;
1181 }
1182
1183 @media (max-width: 760px) {
1184 .os-chromeless.themes-php #wpbody-content {
1185 padding: 12px;
1186 }
1187
1188 .os-chromeless.themes-php .openstation-themes-intro {
1189 align-items: flex-start;
1190 gap: 16px;
1191 padding: 16px;
1192 }
1193
1194 .os-chromeless.themes-php .openstation-themes-intro__copy > p:last-child {
1195 display: none;
1196 }
1197
1198 .os-chromeless.themes-php
1199 .themes.single-theme
1200 .theme-overlay.active
1201 .theme-wrap {
1202 grid-template-columns: minmax(0, 1fr);
1203 grid-template-areas:
1204 "screenshot"
1205 "info"
1206 "actions";
1207 padding: 16px;
1208 }
1209
1210 .os-chromeless.themes-php
1211 .themes.single-theme
1212 .theme-overlay.active
1213 .theme-info {
1214 grid-area: info;
1215 max-height: none;
1216 padding-right: 0;
1217 overflow: visible;
1218 }
1219
1220 .os-chromeless.themes-php
1221 .themes.single-theme
1222 .theme-overlay.active
1223 .theme-actions {
1224 grid-area: actions;
1225 }
1226
1227 .os-chromeless.themes-php .theme-overlay.active .theme-wrap {
1228 top: 10px;
1229 right: 10px;
1230 bottom: 10px;
1231 left: 10px;
1232 }
1233 }
1234
1235 @media (max-width: 480px) {
1236 .os-chromeless.themes-php .openstation-themes-intro__count {
1237 display: none;
1238 }
1239
1240 .os-chromeless.themes-php .openstation-themes-intro h1 {
1241 font-size: 21px;
1242 }
1243 }
1244
1245 @media (prefers-reduced-motion: reduce) {
1246 .os-chromeless.themes-php
1247 .theme-browser
1248 .themes:not(.single-theme)
1249 > .theme {
1250 transition: none;
1251 }
1252 }
1253