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
desktop-mode / assets / css / chromeless.css

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

1,219 lines 38.8 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 * `boot-layout__surfaces` is the white card the React app draws its
516 * content onto. In classic admin it sits on the dark #wpwrap with
517 * rounded corners and a margin that makes it read as a "card." Inside
518 * a desktop window the surface IS the content — rounded corners and
519 * side margins leave ugly bleed at the edges. Collapse it to a plain
520 * edge-to-edge surface.
521 */
522 .os-chromeless .boot-layout__surfaces,
523 .os-chromeless .boot-layout__stage,
524 .os-chromeless .boot-layout__canvas {
525 border-radius: 0 !important;
526 margin: 0 !important;
527 }
528
529 /*
530 * The boot screens render their own H1 (`.boot-navigation-screen__title`,
531 * e.g., "Fonts") at the top of the surface. The desktop window already
532 * shows the page title in its title bar, so the in-page title is
533 * redundant. Visually hide it but keep it in the DOM for screen readers
534 * and for the boot app's own focus management.
535 */
536 .os-chromeless .boot-navigation-screen__title,
537 .os-chromeless .boot-navigation-screen__title-icon {
538 position: absolute;
539 width: 1px;
540 height: 1px;
541 padding: 0;
542 margin: -1px;
543 overflow: hidden;
544 clip: rect( 0, 0, 0, 0 );
545 white-space: nowrap;
546 border: 0;
547 }
548
549 /* ---------------------------------------------------------------
550 * Snackbars
551 *
552 * `components-snackbar-list` (Gutenberg `@wordpress/components`) and
553 * `boot-notices__snackbar` (Font Library / Connectors) position
554 * themselves `fixed` at the viewport bottom. Because a chromeless
555 * iframe IS its own viewport, those anchors already land inside the
556 * window — but our 8px bottom padding on `#wpbody-content` (applied
557 * to non-boot pages) plus some plugin-level `bottom: 40px` offsets
558 * meant for the classic admin footer can push them out of sight.
559 *
560 * Pin them flush to the bottom of the iframe and make sure nothing
561 * in the window chrome can occlude them.
562 *
563 * Triggering a snackbar to verify (OpenStation enabled):
564 * - Open Posts → Add New, save/publish → "Post published" toast.
565 * - Open Media, upload a file → "Uploaded" toast.
566 * - Open Appearance → Fonts, install a Google Font → toast.
567 * --------------------------------------------------------------- */
568 .os-chromeless .components-snackbar-list,
569 .os-chromeless .boot-notices__snackbar {
570 bottom: 16px !important;
571 z-index: 100000;
572 }
573
574 /*
575 * Force the Gutenberg interface skeleton to fill the iframe regardless
576 * of fullscreen state. When fullscreenMode is off, Gutenberg hardcodes
577 * `top: 32px` (admin bar reservation) and `left: 160px` (sidebar
578 * reservation) on `.interface-interface-skeleton` — both chromes we've
579 * already hidden, so those offsets become dead gaps at the top and
580 * left of the window. Zero them out unconditionally.
581 *
582 * The default is fullscreen (inset: 0), but users who turned fullscreen
583 * off in classic admin carry that preference into chromeless via the
584 * shared persistence layer. This rule keeps chromeless rendering
585 * edge-to-edge either way.
586 */
587 .os-chromeless .interface-interface-skeleton {
588 top: 0 !important;
589 left: 0 !important;
590 right: 0 !important;
591 bottom: 0 !important;
592 }
593
594 /* ---------------------------------------------------------------
595 * Notices
596 * Give notices a bit of top margin since there's no admin bar above.
597 * --------------------------------------------------------------- */
598 .os-chromeless .notice:first-child {
599 margin-top: 4px;
600 }
601
602 /* ---------------------------------------------------------------
603 * Update nag
604 * Core's global update / maintenance nags are detached server-side
605 * inside chromeless iframes (see
606 * `openstation_chromeless_suppress_update_nags()`) and re-surfaced
607 * once by the shell as a single notice.
608 * --------------------------------------------------------------- */
609 .os-chromeless .update-nag {
610 display: none !important;
611 }
612
613 /* ---------------------------------------------------------------
614 * Suppress WordPress's native command palette inside chromeless
615 * iframes. The chromeless bridge intercepts Cmd+K and forwards to
616 * the desktop shell, but if an in-page component triggers the
617 * palette via another path (programmatic dispatch, a menu item)
618 * the dialog would still render. Hide it so the shell's palette
619 * stays the only surface users ever see.
620 * --------------------------------------------------------------- */
621 .os-chromeless .commands-command-menu__container,
622 .os-chromeless .commands-command-menu {
623 display: none !important;
624 }
625
626 /* ---------------------------------------------------------------
627 * A file drag over a native upload box.
628 *
629 * The chromeless bridge hands an OS-file drop to Core's own
630 * `<input type="file">` when it lands on one, or anywhere on the
631 * `form.wp-upload-form` box around it (Upload Plugin, Upload
632 * Theme), and stamps `data-os-file-drop-active` on that box while
633 * the drag is over it. Outside the shell the box gives no sign it
634 * will take a drop; inside, the shell used to take the file
635 * instead, so people learned it would not. Say so while it can.
636 *
637 * Core's own admin colour, read the way common.css reads it, not
638 * the station's: this is a wp-admin control on a wp-admin page.
639 * --------------------------------------------------------------- */
640 .os-chromeless [data-os-file-drop-active] {
641 outline: 2px dashed var( --wp-admin-theme-color, #3858e9 );
642 outline-offset: -2px;
643 }
644
645 /* ---------------------------------------------------------------
646 * Appearance → Themes workspace
647 *
648 * Core's one-theme state renders the Theme Details dialog inline and
649 * removes its header. Inside a short desktop window that leaves a large,
650 * unexplained panel whose actions fall below the fold. Treat it as an
651 * active-site identity card instead: screenshot first, useful context at
652 * the side, and actions kept in the card's visible edge.
653 *
654 * With multiple installed themes the native cards become a deliberate
655 * library grid and Core's details view remains a dialog. All selectors are
656 * scoped to the chromeless Themes screen; the classic wp-admin page keeps
657 * Core's own presentation and the shell's palette cannot leak in here.
658 * --------------------------------------------------------------- */
659 .os-chromeless.themes-php #wpbody-content {
660 padding: 10px 16px;
661 background: #f6f7f7;
662 }
663
664 .os-chromeless.themes-php .openstation-themes-intro {
665 display: flex;
666 align-items: center;
667 justify-content: space-between;
668 gap: 24px;
669 margin: 0 0 8px;
670 padding: 14px 18px;
671 background: #fff;
672 border: 1px solid #dcdcde;
673 border-radius: 12px;
674 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
675 box-sizing: border-box;
676 }
677
678 .os-chromeless.themes-php .openstation-themes-intro__copy {
679 max-width: 680px;
680 }
681
682 .os-chromeless.themes-php .openstation-themes-intro__eyebrow {
683 margin: 0 0 5px;
684 color: var(--wp-admin-theme-color, #2271b1);
685 font-size: 11px;
686 font-weight: 700;
687 letter-spacing: 0.08em;
688 line-height: 1.4;
689 text-transform: uppercase;
690 }
691
692 .os-chromeless.themes-php .openstation-themes-intro h1 {
693 margin: 0;
694 color: #1d2327;
695 font-size: clamp(22px, 2.8vw, 30px);
696 font-weight: 650;
697 letter-spacing: -0.025em;
698 line-height: 1.12;
699 }
700
701 .os-chromeless.themes-php .openstation-themes-intro__copy > p:last-child {
702 max-width: 650px;
703 margin: 6px 0 0;
704 color: #50575e;
705 font-size: 13px;
706 line-height: 1.45;
707 }
708
709 .os-chromeless.themes-php .openstation-themes-intro__count {
710 display: grid;
711 flex: 0 0 auto;
712 min-width: 104px;
713 margin: 0;
714 padding: 10px 14px;
715 background: #f6f7f7;
716 border: 1px solid #dcdcde;
717 border-radius: 9px;
718 box-sizing: border-box;
719 text-align: center;
720 }
721
722 .os-chromeless.themes-php .openstation-themes-intro__count strong {
723 color: #1d2327;
724 font-size: 22px;
725 font-weight: 650;
726 letter-spacing: -0.03em;
727 line-height: 1;
728 }
729
730 .os-chromeless.themes-php .openstation-themes-intro__count span {
731 margin-top: 5px;
732 color: #646970;
733 font-size: 10px;
734 font-weight: 600;
735 letter-spacing: 0.04em;
736 line-height: 1.25;
737 text-transform: uppercase;
738 }
739
740 .os-chromeless.themes-php .wrap {
741 margin: 0;
742 }
743
744 .os-chromeless.themes-php .search-form {
745 display: flex;
746 margin: 0 0 16px;
747 padding: 0;
748 }
749
750 .os-chromeless.themes-php .wp-filter-search {
751 width: min(320px, 100%);
752 min-height: 36px;
753 padding-inline: 12px;
754 background: #fff;
755 border-color: #8c8f94;
756 border-radius: 7px;
757 }
758
759 /* Searching a library of one is noise, so the field goes away in Core's
760 * single-theme state. The general sibling combinator rather than `+`:
761 * themes.php emits `wp_admin_notice()` output between the search form and
762 * `.theme-browser` on `?activated`, `?deleted`, `?broken` and
763 * `?delete-active-child` — exactly the loads that follow activating or
764 * deleting a theme — which breaks strict adjacency. */
765 .os-chromeless.themes-php
766 .search-form:has(~ .theme-browser .themes.single-theme) {
767 display: none;
768 }
769
770 /* Installed-theme library. The Add Theme destination already has its own
771 * persistent window tab, so Core's duplicate dashed card is unnecessary. */
772 .os-chromeless.themes-php .theme-browser .themes:not(.single-theme) {
773 display: grid;
774 grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
775 gap: 18px;
776 clear: both;
777 }
778
779 .os-chromeless.themes-php .theme-browser .themes:not(.single-theme) > .theme {
780 float: none;
781 width: auto;
782 margin: 0;
783 background: #fff;
784 border: 1px solid #c3c4c7;
785 border-radius: 10px;
786 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
787 overflow: hidden;
788 transition:
789 border-color 120ms ease,
790 box-shadow 120ms ease,
791 transform 120ms ease;
792 }
793
794 .os-chromeless.themes-php
795 .theme-browser
796 .themes:not(.single-theme)
797 > .theme:hover,
798 .os-chromeless.themes-php
799 .theme-browser
800 .themes:not(.single-theme)
801 > .theme.focus {
802 border-color: #8c8f94;
803 box-shadow: 0 7px 20px rgba(0, 0, 0, 0.1);
804 transform: translateY(-2px);
805 }
806
807 .os-chromeless.themes-php
808 .theme-browser
809 .themes:not(.single-theme)
810 > .theme.active {
811 border-color: var(--wp-admin-theme-color, #2271b1);
812 box-shadow:
813 0 0 0 1px var(--wp-admin-theme-color, #2271b1),
814 0 7px 20px rgba(0, 0, 0, 0.08);
815 }
816
817 .os-chromeless.themes-php
818 .theme-browser
819 .themes:not(.single-theme)
820 > .add-new-theme {
821 display: none;
822 }
823
824 .os-chromeless.themes-php
825 .theme-browser
826 .themes:not(.single-theme)
827 .theme-screenshot {
828 background: #f0f0f1;
829 }
830
831 .os-chromeless.themes-php
832 .theme-browser
833 .themes:not(.single-theme)
834 .theme-id-container {
835 display: flex;
836 align-items: center;
837 justify-content: space-between;
838 min-height: 58px;
839 background: #fff;
840 }
841
842 .os-chromeless.themes-php .theme-browser .themes:not(.single-theme) .theme-name,
843 .os-chromeless.themes-php
844 .theme-browser
845 .themes:not(.single-theme)
846 .theme.active
847 .theme-name {
848 flex: 1 1 auto;
849 height: auto;
850 min-width: 0;
851 margin: 0;
852 padding: 14px 12px 14px 14px;
853 background: #fff;
854 box-shadow: none;
855 color: #1d2327;
856 font-size: 14px;
857 font-weight: 600;
858 line-height: 1.3;
859 }
860
861 .os-chromeless.themes-php
862 .theme-browser
863 .themes:not(.single-theme)
864 .theme.active
865 .theme-name
866 span {
867 display: block;
868 margin-bottom: 2px;
869 color: var(--wp-admin-theme-color, #2271b1);
870 font-size: 10px;
871 font-weight: 700;
872 letter-spacing: 0.06em;
873 line-height: 1.2;
874 text-transform: uppercase;
875 }
876
877 .os-chromeless.themes-php
878 .theme-browser
879 .themes:not(.single-theme)
880 .theme-actions,
881 .os-chromeless.themes-php
882 .theme-browser
883 .themes:not(.single-theme)
884 .theme.active
885 .theme-actions {
886 display: flex;
887 align-items: center;
888 flex: 0 0 auto;
889 position: static;
890 opacity: 1;
891 padding: 10px 12px 10px 0;
892 background: #fff;
893 border: 0;
894 box-shadow: none;
895 transform: none;
896 }
897
898 /* Multiple-theme details remain a modal, but fit the iframe rather than
899 * reserving the classic admin sidebar's 160px gutter. */
900 .os-chromeless.themes-php .theme-overlay.active .theme-backdrop {
901 position: fixed;
902 left: 0;
903 background: rgba(29, 35, 39, 0.62);
904 backdrop-filter: blur(2px);
905 }
906
907 .os-chromeless.themes-php .theme-overlay.active .theme-wrap {
908 top: 18px;
909 right: 18px;
910 bottom: 18px;
911 left: 18px;
912 border: 1px solid #c3c4c7;
913 border-radius: 12px;
914 box-shadow: 0 20px 55px rgba(0, 0, 0, 0.28);
915 overflow: hidden;
916 }
917
918 .os-chromeless.themes-php .theme-overlay.active .theme-header {
919 background: #fff;
920 }
921
922 .os-chromeless.themes-php .theme-overlay.active .theme-about {
923 padding: 24px;
924 }
925
926 .os-chromeless.themes-php .theme-overlay.active .theme-actions {
927 justify-content: flex-end;
928 padding: 10px 16px 5px;
929 }
930
931 /* Single-theme mode is a workspace card, not a stranded modal. */
932 .os-chromeless.themes-php .themes.single-theme {
933 display: block;
934 margin: 0;
935 }
936
937 .os-chromeless.themes-php .themes.single-theme .theme-overlay.active {
938 display: block;
939 position: static;
940 }
941
942 .os-chromeless.themes-php
943 .themes.single-theme
944 .theme-overlay.active
945 .theme-backdrop,
946 .os-chromeless.themes-php
947 .themes.single-theme
948 .theme-overlay.active
949 .theme-header {
950 display: none;
951 }
952
953 .os-chromeless.themes-php
954 .themes.single-theme
955 .theme-overlay.active
956 .theme-wrap {
957 display: grid;
958 grid-template-columns: minmax(320px, 0.95fr) minmax(300px, 1.05fr);
959 grid-template-areas: "screenshot details";
960 column-gap: 24px;
961 align-items: start;
962 position: relative;
963 top: auto;
964 right: auto;
965 bottom: auto;
966 left: auto;
967 min-height: 0;
968 padding: 12px 16px;
969 background: #fff;
970 border: 1px solid #c3c4c7;
971 border-radius: 12px;
972 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
973 overflow: visible;
974 z-index: 10;
975 }
976
977 .os-chromeless.themes-php
978 .themes.single-theme
979 .theme-overlay.active
980 .theme-about {
981 display: contents;
982 }
983
984 .os-chromeless.themes-php
985 .themes.single-theme
986 .theme-overlay.active
987 .theme-screenshots {
988 grid-area: screenshot;
989 float: none;
990 width: 100%;
991 max-width: none;
992 margin: 0;
993 }
994
995 .os-chromeless.themes-php
996 .themes.single-theme
997 .theme-overlay.active
998 .screenshot {
999 background: #f0f0f1;
1000 border: 0;
1001 border-radius: 8px;
1002 box-shadow: 0 0 0 1px #dcdcde;
1003 overflow: hidden;
1004 }
1005
1006 .os-chromeless.themes-php
1007 .themes.single-theme
1008 .theme-overlay.active
1009 .theme-info {
1010 grid-area: details;
1011 align-self: start;
1012 float: none;
1013 width: auto;
1014 max-height: 240px;
1015 padding: 2px 10px 2px 0;
1016 overflow: auto;
1017 scrollbar-gutter: stable;
1018 }
1019
1020 .os-chromeless.themes-php
1021 .themes.single-theme
1022 .theme-overlay.active
1023 .current-label {
1024 margin: 0 0 10px;
1025 padding: 4px 9px;
1026 background: #e7f5ed;
1027 border: 1px solid #9ad7b5;
1028 border-radius: 999px;
1029 color: #0a5c36;
1030 font-size: 10px;
1031 font-weight: 700;
1032 letter-spacing: 0.05em;
1033 line-height: 1.4;
1034 text-transform: uppercase;
1035 }
1036
1037 .os-chromeless.themes-php
1038 .themes.single-theme
1039 .theme-overlay.active
1040 .theme-name {
1041 margin: 0;
1042 color: #1d2327;
1043 font-size: clamp(25px, 3vw, 34px);
1044 font-weight: 650;
1045 letter-spacing: -0.03em;
1046 line-height: 1.12;
1047 }
1048
1049 .os-chromeless.themes-php
1050 .themes.single-theme
1051 .theme-overlay.active
1052 .theme-version {
1053 margin-left: 8px;
1054 color: #646970;
1055 font-size: 11px;
1056 font-weight: 500;
1057 letter-spacing: 0;
1058 }
1059
1060 .os-chromeless.themes-php
1061 .themes.single-theme
1062 .theme-overlay.active
1063 .theme-author {
1064 margin: 8px 0 14px;
1065 color: #646970;
1066 font-size: 13px;
1067 }
1068
1069 .os-chromeless.themes-php
1070 .themes.single-theme
1071 .theme-overlay.active
1072 .theme-autoupdate {
1073 margin: 0 0 14px;
1074 padding: 9px 10px;
1075 background: #f6f7f7;
1076 border: 1px solid #dcdcde;
1077 border-radius: 6px;
1078 font-size: 12px;
1079 }
1080
1081 .os-chromeless.themes-php
1082 .themes.single-theme
1083 .theme-overlay.active
1084 .theme-description {
1085 margin: 0;
1086 color: #3c434a;
1087 font-size: 13px;
1088 line-height: 1.55;
1089 }
1090
1091 .os-chromeless.themes-php
1092 .themes.single-theme
1093 .theme-overlay.active
1094 .theme-tags,
1095 .os-chromeless.themes-php
1096 .themes.single-theme
1097 .theme-overlay.active
1098 .parent-theme {
1099 margin-top: 16px;
1100 padding-top: 12px;
1101 border-top-width: 1px;
1102 color: #646970;
1103 font-size: 11px;
1104 line-height: 1.5;
1105 }
1106
1107 .os-chromeless.themes-php
1108 .themes.single-theme
1109 .theme-overlay.active
1110 .theme-actions {
1111 display: flex;
1112 grid-area: details;
1113 align-items: center;
1114 align-self: end;
1115 justify-content: flex-start;
1116 position: static;
1117 padding: 14px 0 0;
1118 background: transparent;
1119 border-top: 1px solid #dcdcde;
1120 text-align: left;
1121 }
1122
1123 .os-chromeless.themes-php
1124 .themes.single-theme
1125 .theme-overlay.active
1126 .theme-actions
1127 .active-theme {
1128 display: flex;
1129 flex-wrap: wrap;
1130 gap: 7px;
1131 }
1132
1133 .os-chromeless.themes-php
1134 .themes.single-theme
1135 .theme-overlay.active
1136 .theme-actions
1137 .inactive-theme {
1138 display: none;
1139 }
1140
1141 .os-chromeless.themes-php
1142 .themes.single-theme
1143 .theme-overlay.active
1144 .theme-actions
1145 .button {
1146 margin: 0;
1147 }
1148
1149 @media (max-width: 760px) {
1150 .os-chromeless.themes-php #wpbody-content {
1151 padding: 12px;
1152 }
1153
1154 .os-chromeless.themes-php .openstation-themes-intro {
1155 align-items: flex-start;
1156 gap: 16px;
1157 padding: 16px;
1158 }
1159
1160 .os-chromeless.themes-php .openstation-themes-intro__copy > p:last-child {
1161 display: none;
1162 }
1163
1164 .os-chromeless.themes-php
1165 .themes.single-theme
1166 .theme-overlay.active
1167 .theme-wrap {
1168 grid-template-columns: minmax(0, 1fr);
1169 grid-template-areas:
1170 "screenshot"
1171 "info"
1172 "actions";
1173 padding: 16px;
1174 }
1175
1176 .os-chromeless.themes-php
1177 .themes.single-theme
1178 .theme-overlay.active
1179 .theme-info {
1180 grid-area: info;
1181 max-height: none;
1182 padding-right: 0;
1183 overflow: visible;
1184 }
1185
1186 .os-chromeless.themes-php
1187 .themes.single-theme
1188 .theme-overlay.active
1189 .theme-actions {
1190 grid-area: actions;
1191 }
1192
1193 .os-chromeless.themes-php .theme-overlay.active .theme-wrap {
1194 top: 10px;
1195 right: 10px;
1196 bottom: 10px;
1197 left: 10px;
1198 }
1199 }
1200
1201 @media (max-width: 480px) {
1202 .os-chromeless.themes-php .openstation-themes-intro__count {
1203 display: none;
1204 }
1205
1206 .os-chromeless.themes-php .openstation-themes-intro h1 {
1207 font-size: 21px;
1208 }
1209 }
1210
1211 @media (prefers-reduced-motion: reduce) {
1212 .os-chromeless.themes-php
1213 .theme-browser
1214 .themes:not(.single-theme)
1215 > .theme {
1216 transition: none;
1217 }
1218 }
1219