PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.12
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.12
1.1.12 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 All 36 releases
desktop-mode / assets / css / mobile.css

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

1,076 lines 31.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OpenStation — the phone layer.
3 *
4 * Every rule is scoped to `html[data-os-mode="mobile"]`, the
5 * attribute the PHP head stamp writes before the body parses and the
6 * mode controller keeps current. On a desktop this sheet matches
7 * nothing; on a phone the first paint is already the phone layer.
8 *
9 * Three jobs:
10 *
11 * 1. Retire the desktop chrome — dock, widgets, wallpaper icons,
12 * admin bar, window title bars and resize handles. The
13 * elements stay in the DOM (their listeners and registries keep
14 * working); they just stop painting.
15 * 2. Make every window the whole work area, between the top bar
16 * and the tab bar, under the safe areas.
17 * 3. Paint the four phone surfaces: top bar, home grid, tab bar,
18 * app switcher.
19 *
20 * Every colour resolves through a palette token with the pre-brand
21 * literal as its floor (see AGENTS.md, "The palette lives in
22 * variables.css"). Motion is opt-out under `prefers-reduced-motion`.
23 */
24
25 /* ------------------------------------------------------------------
26 * 1. The desktop chrome steps aside.
27 * ---------------------------------------------------------------- */
28
29 html[data-os-mode="mobile"] #wpadminbar,
30 html[data-os-mode="mobile"] #os-dock,
31 html[data-os-mode="mobile"] #os-side-dock,
32 html[data-os-mode="mobile"] #os-widgets,
33 html[data-os-mode="mobile"] .os-icons,
34 html[data-os-mode="mobile"] .os-files-layer,
35 html[data-os-mode="mobile"] .os-notes,
36 html[data-os-mode="mobile"] .os-mio,
37 html[data-os-mode="mobile"] #os-mio,
38 html[data-os-mode="mobile"] .os-window__titlebar,
39 html[data-os-mode="mobile"] .os-window__resize-handle {
40 display: none !important;
41 }
42
43 /* The 32px WordPress reserves for the admin bar. */
44 html[data-os-mode="mobile"].wp-toolbar {
45 padding-top: 0 !important;
46 }
47
48 /*
49 * The shell starts at the top edge, and the admin-bar height token
50 * says so too: anything that still reads it (a plugin's own chrome)
51 * measures against a bar that is not there.
52 */
53 html[data-os-mode="mobile"] .os-shell {
54 inset-block-start: 0;
55 --wp-admin--admin-bar--height: 0px;
56 }
57
58 /*
59 * No page zoom. `pan-x pan-y` keeps every scroll and hands nothing
60 * else to the browser: no pinch, no double-tap. The viewport meta
61 * says the same (`maximum-scale=1`, `openstation_mode_viewport_meta()`)
62 * and `src/mode/zoom-guard.ts` cancels the gesture where a browser
63 * ignores both. `overscroll-behavior: none` is the rubber band at the
64 * document's edges — the shell is fixed and has no document to pull.
65 */
66 html[data-os-mode="mobile"],
67 html[data-os-mode="mobile"] body {
68 touch-action: pan-x pan-y;
69 overscroll-behavior: none;
70 }
71
72 /* The work area is the whole area: no dock band to keep clear of. */
73 html[data-os-mode="mobile"] .os-area {
74 padding: 0 !important;
75 }
76
77 /* ------------------------------------------------------------------
78 * Tokens for this layer alone. Private aliases read the palette so a
79 * desktop theme that repaints the surfaces repaints the phone too.
80 * ---------------------------------------------------------------- */
81
82 /*
83 * The phone chrome is the DOCK's family, not the window's. The window
84 * tokens describe a document surface (white body, dark text under most
85 * admin colour schemes) and they are wrong for anything that sits on
86 * the wallpaper: a white tile grid with grey labels on a dark desk.
87 * The dock tokens describe exactly this situation — translucent dark
88 * glass over the wallpaper, light glyphs — so every phone surface
89 * reads them, and a desktop theme that reskins the dock reskins the
90 * phone with it. Bars and sheets sit on the solid backstop, the same
91 * floor the shell paints under everything. What the dock has no word
92 * for — the glass a tile is cut from, the shadows, the scrims — is
93 * the `--os-mobile-*` family in `variables.css`.
94 */
95 html[data-os-mode="mobile"] .os-shell {
96 --_m-surface: var( --os-backstop, #0c0b0f );
97 --_m-surface-raised: var( --os-dock-bg, rgba( 12, 11, 15, 0.72 ) );
98 --_m-glass: var( --os-mobile-glass, rgba( 255, 255, 255, 0.1 ) );
99 --_m-glass-strong: var( --os-mobile-glass-strong, rgba( 255, 255, 255, 0.16 ) );
100 --_m-glass-border: var( --os-mobile-glass-border, rgba( 255, 255, 255, 0.14 ) );
101 --_m-fg: var( --os-dock-icon-color-hover, #fffbff );
102 --_m-fg-muted: var( --os-dock-icon-color, rgba( 255, 251, 255, 0.72 ) );
103 --_m-border: var( --os-dock-border, rgba( 255, 251, 255, 0.1 ) );
104 --_m-accent: var( --os-ui-accent, #f252fc );
105 --_m-accent-dim: var( --os-ui-accent-dim, rgba( 242, 82, 252, 0.35 ) );
106 --_m-danger: var( --os-ui-danger, #d63638 );
107 --_m-tile-shadow: var( --os-mobile-tile-shadow, 0 8px 20px rgba( 0, 0, 0, 0.35 ) );
108 --_m-card-shadow: var( --os-mobile-card-shadow, 0 -8px 24px rgba( 0, 0, 0, 0.4 ) );
109 --_m-sheet-shadow: var( --os-mobile-sheet-shadow, 0 -12px 40px rgba( 0, 0, 0, 0.45 ) );
110 --_m-window-shadow: var( --os-mobile-window-shadow, 0 12px 32px rgba( 0, 0, 0, 0.45 ) );
111 --_m-scrim: var( --os-mobile-scrim, rgba( 0, 0, 0, 0.55 ) );
112 --_m-top-scrim: var( --os-mobile-top-scrim, rgba( 0, 0, 0, 0.45 ) );
113 --_m-label-shadow: var( --os-mobile-label-shadow, 0 1px 2px rgba( 0, 0, 0, 0.6 ) );
114 --_m-radius: 14px;
115 --_m-gap: 8px;
116 --_m-top-h: 48px;
117 --_m-tabs-h: 56px;
118 --_m-safe-top: env( safe-area-inset-top, 0px );
119 --_m-safe-bottom: env( safe-area-inset-bottom, 0px );
120 --_m-safe-left: env( safe-area-inset-left, 0px );
121 --_m-safe-right: env( safe-area-inset-right, 0px );
122 /* The tab bar's whole footprint: its items, and the home indicator under them. */
123 --_m-tabs-total: calc( var( --_m-tabs-h ) + 8px + var( --_m-safe-bottom ) );
124 --_m-ease: cubic-bezier( 0.2, 0.8, 0.2, 1 );
125 /*
126 * Every kit field on the phone is 16px: below that, iOS zooms the
127 * page into a focused control and never zooms back out.
128 */
129 --os-ui-field-font-size: 16px;
130 --os-ui-field-font-size-compact: 16px;
131 }
132
133 /*
134 * The phone surfaces are DARK surfaces, and the kit components they
135 * host (`<os-text-field>` in the home search, `<os-button>` in the
136 * switcher footer) read the public `--os-ui-*` tokens, which under a
137 * light admin scheme describe the window's white body and near-black
138 * text. Re-pointing those public names on the surface itself is the
139 * sanctioned move (a document-tree rule the component's `var()`
140 * resolves through — see AGENTS.md, "Never declare a themeable token
141 * on a component's :host"): every kit control inside reads the phone
142 * palette, and a theme that recolours the dock recolours them too.
143 * `--_m-*` are the private aliases declared on `.os-shell` above.
144 */
145 html[data-os-mode="mobile"] .os-mobile-home,
146 html[data-os-mode="mobile"] .os-mobile-top,
147 html[data-os-mode="mobile"] .os-mobile-tabs,
148 html[data-os-mode="mobile"] .os-mobile-switcher__sheet {
149 --os-ui-fg: var( --_m-fg );
150 --os-ui-fg-muted: var( --_m-fg-muted );
151 --os-ui-border: var( --_m-border );
152 --os-ui-surface: var( --_m-surface );
153 --os-window-bg: var( --_m-surface );
154 --os-ui-hover: var( --_m-glass );
155 --os-ui-button-fg: var( --_m-fg );
156 --os-ui-button-bg: var( --_m-glass );
157 --os-ui-button-bg-hover: var( --_m-glass-strong );
158 --os-ui-button-border: 1px solid var( --_m-border );
159 }
160
161 /* ------------------------------------------------------------------
162 * 2. Windows: one at a time, a card on the desk.
163 *
164 * The window is inset from the edges by `--_m-gap`, rounded and
165 * shadowed, so the desk shows around it and the app reads as a card
166 * sitting on something (the switcher draws it the same way). `inset`
167 * with `!important` beats the inline left/top/width/height that
168 * `maximize()` writes; the inline values stay what they were, which
169 * is what the session hands back to the desktop.
170 * ---------------------------------------------------------------- */
171
172 html[data-os-mode="mobile"] .os-window {
173 border-radius: var( --_m-radius ) !important;
174 border: 1px solid var( --_m-border ) !important;
175 box-shadow: var( --_m-window-shadow ) !important;
176 overflow: hidden;
177 }
178
179 html[data-os-mode="mobile"] .os-window--maximized {
180 inset: var( --_m-gap ) calc( var( --_m-gap ) + var( --_m-safe-right ) ) var( --_m-gap )
181 calc( var( --_m-gap ) + var( --_m-safe-left ) ) !important;
182 width: auto !important;
183 height: auto !important;
184 }
185
186 /*
187 * Only the topmost window paints. The rest are still open (their
188 * iframes keep their state) but skip layout and paint entirely, so a
189 * phone with six apps open composites one.
190 */
191 html[data-os-mode="mobile"] .os-window:not( .os-window--focused ) {
192 content-visibility: hidden;
193 }
194
195 /*
196 * The tab strip (All Posts / Add New / Categories) stays: on a phone
197 * it is the in-app navigation. It scrolls sideways instead of
198 * wrapping.
199 */
200 html[data-os-mode="mobile"] .os-window__tabs {
201 overflow-x: auto;
202 overflow-y: hidden;
203 -webkit-overflow-scrolling: touch;
204 scrollbar-width: none;
205 padding-inline: var( --_m-safe-left ) var( --_m-safe-right );
206 }
207
208 html[data-os-mode="mobile"] .os-window__tabs::-webkit-scrollbar {
209 display: none;
210 }
211
212 /* A new app slides up from the tab bar. */
213 @keyframes os-mobile-app-in {
214 from {
215 opacity: 0;
216 transform: translateY( 24px ) scale( 0.98 );
217 }
218 to {
219 opacity: 1;
220 transform: none;
221 }
222 }
223
224 html[data-os-mode="mobile"] .os-window.os-mobile-enter {
225 animation: os-mobile-app-in 220ms var( --_m-ease ) both;
226 }
227
228 /* On the home screen nothing but the wallpaper and the grid paint. */
229 html[data-os-mode="mobile"] .os-shell[data-os-mobile-state="home"] .os-window {
230 visibility: hidden;
231 }
232
233 /* ------------------------------------------------------------------
234 * The edge zones: thin strips the shell owns over both edges of the
235 * area, so a pointer that starts there never belongs to the iframe.
236 * The start edge is the Back gesture; both cancel the browser's
237 * history swipe (`bindHistorySwipeGuard` in `gestures.ts`), so they
238 * stay up on the home screen too — a swipe that navigated away would
239 * navigate away from home as well. Above the windows and the home
240 * grid, below the switcher sheet.
241 * ---------------------------------------------------------------- */
242
243 html[data-os-mode="mobile"] .os-mobile-edge {
244 position: absolute;
245 inset-block: 0;
246 width: 22px;
247 z-index: 2000;
248 touch-action: none;
249 background: transparent;
250 }
251
252 html[data-os-mode="mobile"] .os-mobile-edge--start {
253 inset-inline-start: 0;
254 }
255
256 html[data-os-mode="mobile"] .os-mobile-edge--end {
257 inset-inline-end: 0;
258 }
259
260 /* ------------------------------------------------------------------
261 * 3a. The top bar.
262 * ---------------------------------------------------------------- */
263
264 /*
265 * The bar sits on the desk, not on the window: no fill and no rule
266 * under it, only a scrim so the title reads over any wallpaper, and
267 * the window card starts `--_m-gap` below it. Icon and name above a
268 * card is the picture the switcher draws too.
269 */
270 html[data-os-mode="mobile"] .os-mobile-top {
271 display: flex;
272 align-items: center;
273 gap: 6px;
274 flex: 0 0 auto;
275 min-height: var( --_m-top-h );
276 padding-block-start: var( --_m-safe-top );
277 padding-inline: calc( 4px + var( --_m-safe-left ) ) calc( 4px + var( --_m-safe-right ) );
278 background: linear-gradient( to bottom, var( --_m-top-scrim ), transparent );
279 color: var( --_m-fg );
280 position: relative;
281 z-index: 3000;
282 }
283
284 html[data-os-mode="mobile"] .os-mobile-top[hidden] {
285 display: none;
286 }
287
288 /* Icon + title, the app's identity. Follows an in-flight back gesture. */
289 html[data-os-mode="mobile"] .os-mobile-top__identity {
290 flex: 1 1 auto;
291 min-width: 0;
292 display: flex;
293 align-items: center;
294 gap: 10px;
295 padding-inline-start: 10px;
296 transition: transform 80ms linear;
297 }
298
299 html[data-os-mode="mobile"] .os-mobile-top--peeking .os-mobile-top__identity {
300 transform: translateX( calc( var( --os-mobile-back-progress, 0 ) * 14px ) );
301 }
302
303 /*
304 * One control, a round glass ×: the app closes and morphs back into
305 * its tile. The bar also answers a flick down by sending the app
306 * home, window kept.
307 */
308 html[data-os-mode="mobile"] .os-mobile-top__controls {
309 display: inline-flex;
310 align-items: center;
311 gap: 8px;
312 margin-inline-end: 6px;
313 flex: 0 0 auto;
314 }
315
316 html[data-os-mode="mobile"] .os-mobile-top__button {
317 appearance: none;
318 border: 1px solid var( --_m-border );
319 background: var( --_m-glass );
320 color: inherit;
321 width: 34px;
322 height: 34px;
323 display: inline-flex;
324 align-items: center;
325 justify-content: center;
326 border-radius: 999px;
327 flex: 0 0 auto;
328 -webkit-tap-highlight-color: transparent;
329 }
330
331 html[data-os-mode="mobile"] .os-mobile-top__button:active {
332 background: var( --_m-glass-strong );
333 }
334
335 html[data-os-mode="mobile"] .os-mobile-top__button:focus-visible {
336 outline: 2px solid var( --_m-accent );
337 outline-offset: 2px;
338 }
339
340 html[data-os-mode="mobile"] .os-mobile-top__icon {
341 display: inline-flex;
342 width: 24px;
343 height: 24px;
344 align-items: center;
345 justify-content: center;
346 flex: 0 0 auto;
347 color: var( --_m-fg-muted );
348 }
349
350 html[data-os-mode="mobile"] .os-mobile-top__icon > * {
351 width: 22px;
352 height: 22px;
353 font-size: 22px;
354 }
355
356 html[data-os-mode="mobile"] .os-mobile-top__title {
357 flex: 1 1 auto;
358 min-width: 0;
359 margin: 0;
360 font-size: 17px;
361 font-weight: 600;
362 line-height: 1.2;
363 color: inherit;
364 white-space: nowrap;
365 overflow: hidden;
366 text-overflow: ellipsis;
367 text-shadow: var( --_m-label-shadow );
368 }
369
370 /* ------------------------------------------------------------------
371 * 3b. Home.
372 * ---------------------------------------------------------------- */
373
374 html[data-os-mode="mobile"] .os-mobile-home {
375 position: absolute;
376 inset: 0;
377 z-index: 1500;
378 display: flex;
379 flex-direction: column;
380 color: var( --_m-fg );
381 padding-block-start: calc( 8px + var( --_m-safe-top ) );
382 padding-inline: calc( 12px + var( --_m-safe-left ) ) calc( 12px + var( --_m-safe-right ) );
383 animation: os-mobile-fade-in 160ms var( --_m-ease ) both;
384 }
385
386 html[data-os-mode="mobile"] .os-mobile-home[hidden] {
387 display: none;
388 }
389
390 @keyframes os-mobile-fade-in {
391 from {
392 opacity: 0;
393 }
394 to {
395 opacity: 1;
396 }
397 }
398
399 /*
400 * The search is a phone's search: a rounded glass field, 16px type
401 * (the size under which iOS zooms the page into it), no hairline
402 * box. The kit field reads the public tokens re-pointed here.
403 */
404 html[data-os-mode="mobile"] .os-mobile-home__search {
405 flex: 0 0 auto;
406 padding-block: 4px 8px;
407 --os-window-bg: var( --_m-glass );
408 --os-ui-border: var( --_m-glass-border );
409 --os-ui-field-radius: 12px;
410 }
411
412 html[data-os-mode="mobile"] .os-mobile-home__search os-text-field {
413 backdrop-filter: blur( 14px ) saturate( 140% );
414 -webkit-backdrop-filter: blur( 14px ) saturate( 140% );
415 border-radius: 12px;
416 }
417
418 html[data-os-mode="mobile"] .os-mobile-home__scroll {
419 flex: 1 1 auto;
420 min-height: 0;
421 overflow-y: auto;
422 -webkit-overflow-scrolling: touch;
423 overscroll-behavior: contain;
424 }
425
426 /*
427 * The grid always scrolls, by at least one pixel. A phone only rubber-
428 * bands a surface that CAN scroll, and a home screen that fits — a
429 * dozen tiles on a tall phone — was a dead surface under the thumb.
430 * The 1px past the frame makes it a scroller, and the platform then
431 * gives it the platform's own bounce at both ends, exactly the drag
432 * every native home screen has. The bottom padding is here rather than
433 * on the scroller so the last row clears the tab bar inside the same
434 * measure.
435 */
436 html[data-os-mode="mobile"] .os-mobile-home__content {
437 box-sizing: border-box;
438 min-block-size: calc( 100% + 1px );
439 padding-block-end: 16px;
440 }
441
442 html[data-os-mode="mobile"] .os-mobile-home__heading {
443 margin: 12px 4px 6px;
444 font-size: 12px;
445 font-weight: 600;
446 letter-spacing: 0.04em;
447 text-transform: uppercase;
448 color: var( --_m-fg-muted );
449 text-shadow: var( --_m-label-shadow );
450 }
451
452 html[data-os-mode="mobile"] .os-mobile-home__empty {
453 margin: 24px 8px;
454 color: var( --_m-fg-muted );
455 text-align: center;
456 }
457
458 html[data-os-mode="mobile"] .os-mobile-grid {
459 display: grid;
460 grid-template-columns: repeat( auto-fill, minmax( 78px, 1fr ) );
461 gap: 10px 6px;
462 margin: 0;
463 padding: 0;
464 list-style: none;
465 }
466
467 html[data-os-mode="mobile"] .os-mobile-tile {
468 appearance: none;
469 background: transparent;
470 border: 0;
471 padding: 6px 2px;
472 width: 100%;
473 display: flex;
474 flex-direction: column;
475 align-items: center;
476 gap: 6px;
477 color: var( --_m-fg );
478 border-radius: 12px;
479 -webkit-tap-highlight-color: transparent;
480 }
481
482 html[data-os-mode="mobile"] .os-mobile-tile:active .os-mobile-tile__icon {
483 transform: scale( 0.92 );
484 }
485
486 html[data-os-mode="mobile"] .os-mobile-tile:focus-visible {
487 outline: 2px solid var( --_m-accent );
488 outline-offset: 2px;
489 }
490
491 html[data-os-mode="mobile"] .os-mobile-tile__icon {
492 position: relative;
493 width: 58px;
494 height: 58px;
495 display: inline-flex;
496 align-items: center;
497 justify-content: center;
498 border-radius: 16px;
499 background: var( --_m-glass );
500 border: 1px solid var( --_m-glass-border );
501 box-shadow: var( --_m-tile-shadow );
502 backdrop-filter: blur( 14px ) saturate( 140% );
503 -webkit-backdrop-filter: blur( 14px ) saturate( 140% );
504 transition: transform 120ms var( --_m-ease ), background 120ms var( --_m-ease );
505 color: var( --_m-fg );
506 }
507
508 html[data-os-mode="mobile"] .os-mobile-tile:active .os-mobile-tile__icon {
509 background: var( --_m-glass-strong );
510 }
511
512 html[data-os-mode="mobile"] .os-mobile-tile__glyph {
513 width: 30px;
514 height: 30px;
515 font-size: 30px;
516 line-height: 1;
517 }
518
519 html[data-os-mode="mobile"] .os-mobile-tile__glyph.dashicons {
520 width: 30px;
521 height: 30px;
522 font-size: 30px;
523 }
524
525 html[data-os-mode="mobile"] .os-mobile-tile__badge,
526 html[data-os-mode="mobile"] .os-mobile-tabs__badge {
527 position: absolute;
528 inset-block-start: -6px;
529 inset-inline-end: -6px;
530 min-width: 18px;
531 height: 18px;
532 padding-inline: 5px;
533 border-radius: 9px;
534 background: var( --_m-danger );
535 color: #fff;
536 font-size: 11px;
537 font-weight: 700;
538 line-height: 18px;
539 text-align: center;
540 box-shadow: 0 0 0 2px var( --_m-surface );
541 }
542
543 /*
544 * The switcher's glyph is the number of open apps inside a rounded
545 * square, the browser tab-switcher convention, so it reads as "this
546 * many open" and not as a notification (and the accent stays the
547 * active tab's alone). With nothing open the windows icon shows.
548 */
549 html[data-os-mode="mobile"] .os-mobile-tabs__count {
550 display: inline-flex;
551 align-items: center;
552 justify-content: center;
553 /* Stroke included: the neighbours' glyphs ink about 19px of their 22px box. */
554 box-sizing: border-box;
555 width: 20px;
556 height: 20px;
557 border: 2px solid currentColor;
558 border-radius: 5px;
559 font-size: 11px;
560 font-weight: 700;
561 line-height: 1;
562 letter-spacing: -0.02em;
563 color: inherit;
564 }
565
566 html[data-os-mode="mobile"] .os-mobile-tabs__count[hidden],
567 html[data-os-mode="mobile"] .os-mobile-tabs__icon--counted > :not( .os-mobile-tabs__count ) {
568 display: none;
569 }
570
571 html[data-os-mode="mobile"] .os-mobile-tile__label {
572 font-size: 11.5px;
573 line-height: 1.2;
574 color: var( --_m-fg );
575 text-align: center;
576 max-width: 100%;
577 display: -webkit-box;
578 -webkit-line-clamp: 2;
579 -webkit-box-orient: vertical;
580 overflow: hidden;
581 text-shadow: var( --_m-label-shadow );
582 }
583
584 /* ------------------------------------------------------------------
585 * 3c. The tab bar.
586 *
587 * Anchored to the viewport's bottom edge, not laid out as the last
588 * child of the shell. The shell is a viewport-sized fixed box, and a
589 * box of that shape is exactly what mobile Safari has, more than
590 * once, ended short of the screen's bottom edge (an installed app
591 * under one iOS release drew a band of nothing under the bar). A
592 * bar fixed to the edge is right whatever the shell's box does; the
593 * shell's body keeps the bar's footprint clear below it, so the
594 * window card and the home grid end where the bar begins.
595 * ---------------------------------------------------------------- */
596
597 html[data-os-mode="mobile"] .os-mobile-tabs {
598 position: fixed;
599 inset-inline: 0;
600 inset-block-end: 0;
601 display: grid;
602 grid-auto-flow: column;
603 grid-auto-columns: 1fr;
604 align-items: stretch;
605 min-height: var( --_m-tabs-h );
606 padding-block-end: var( --_m-safe-bottom );
607 padding-inline: var( --_m-safe-left ) var( --_m-safe-right );
608 background: var( --_m-surface );
609 border-block-start: 1px solid var( --_m-border );
610 z-index: 3000;
611 touch-action: pan-x;
612 }
613
614 html[data-os-mode="mobile"] .os-shell__body {
615 padding-block-end: var( --_m-tabs-total );
616 }
617
618 html[data-os-mode="mobile"] .os-mobile-tabs__item {
619 appearance: none;
620 background: transparent;
621 border: 0;
622 color: var( --_m-fg-muted );
623 display: flex;
624 flex-direction: column;
625 align-items: center;
626 justify-content: center;
627 gap: 2px;
628 min-height: var( --_m-tabs-h );
629 padding: 4px 2px;
630 font-size: 10.5px;
631 line-height: 1.1;
632 -webkit-tap-highlight-color: transparent;
633 }
634
635 html[data-os-mode="mobile"] .os-mobile-tabs__item[aria-current="page"] {
636 color: var( --_m-accent );
637 }
638
639 html[data-os-mode="mobile"] .os-mobile-tabs__item:focus-visible {
640 outline: 2px solid var( --_m-accent );
641 outline-offset: -2px;
642 border-radius: 10px;
643 }
644
645 html[data-os-mode="mobile"] .os-mobile-tabs__icon {
646 position: relative;
647 display: inline-flex;
648 width: 26px;
649 height: 26px;
650 align-items: center;
651 justify-content: center;
652 }
653
654 html[data-os-mode="mobile"] .os-mobile-tabs__glyph {
655 width: 22px;
656 height: 22px;
657 font-size: 22px;
658 line-height: 1;
659 }
660
661 html[data-os-mode="mobile"] .os-mobile-tabs__glyph.dashicons {
662 width: 22px;
663 height: 22px;
664 font-size: 22px;
665 }
666
667 html[data-os-mode="mobile"] .os-mobile-tabs__label {
668 max-width: 100%;
669 white-space: nowrap;
670 overflow: hidden;
671 text-overflow: ellipsis;
672 }
673
674 /* ------------------------------------------------------------------
675 * 3d. The switcher.
676 * ---------------------------------------------------------------- */
677
678 html[data-os-mode="mobile"] .os-mobile-switcher {
679 position: fixed;
680 inset: 0;
681 z-index: 4000;
682 display: flex;
683 flex-direction: column;
684 justify-content: flex-end;
685 }
686
687 html[data-os-mode="mobile"] .os-mobile-switcher[hidden] {
688 display: none;
689 }
690
691 html[data-os-mode="mobile"] .os-mobile-switcher__backdrop {
692 position: absolute;
693 inset: 0;
694 background: var( --_m-scrim );
695 animation: os-mobile-fade-in 160ms var( --_m-ease ) both;
696 }
697
698 @keyframes os-mobile-sheet-in {
699 from {
700 transform: translateY( 24px );
701 opacity: 0;
702 }
703 to {
704 transform: none;
705 opacity: 1;
706 }
707 }
708
709 html[data-os-mode="mobile"] .os-mobile-switcher__sheet {
710 position: relative;
711 max-height: min( 82vh, 82dvh );
712 display: flex;
713 flex-direction: column;
714 background: var( --_m-surface );
715 color: var( --_m-fg );
716 border-start-start-radius: var( --_m-radius );
717 border-start-end-radius: var( --_m-radius );
718 border: 1px solid var( --_m-border );
719 border-block-end: 0;
720 padding-block-end: var( --_m-safe-bottom );
721 box-shadow: var( --_m-sheet-shadow );
722 animation: os-mobile-sheet-in 200ms var( --_m-ease ) both;
723 }
724
725 html[data-os-mode="mobile"] .os-mobile-switcher__header {
726 display: flex;
727 align-items: center;
728 justify-content: space-between;
729 padding: 10px 8px 6px 16px;
730 }
731
732 /*
733 * Explicit colours on the headings: wp-admin's `common.css` paints
734 * every h2/h3 in the admin's heading colour, which is near-black under
735 * a light scheme and would sink into the sheet.
736 */
737 html[data-os-mode="mobile"] .os-mobile-switcher__title {
738 margin: 0;
739 font-size: 15px;
740 font-weight: 600;
741 color: var( --_m-fg );
742 }
743
744 html[data-os-mode="mobile"] .os-mobile-switcher__close {
745 appearance: none;
746 background: transparent;
747 border: 0;
748 color: inherit;
749 width: 40px;
750 height: 40px;
751 display: inline-flex;
752 align-items: center;
753 justify-content: center;
754 border-radius: 10px;
755 }
756
757 html[data-os-mode="mobile"] .os-mobile-switcher__close:focus-visible {
758 outline: 2px solid var( --_m-accent );
759 outline-offset: -2px;
760 }
761
762 /*
763 * The list is laid out bottom-up (`column-reverse`) so the deck sits
764 * at the bottom with its front card nearest the thumb, and a scroll
765 * container in this direction starts scrolled to the front. The DOM
766 * order is unchanged.
767 */
768 html[data-os-mode="mobile"] .os-mobile-switcher__list {
769 flex: 1 1 auto;
770 min-height: 0;
771 overflow-y: auto;
772 -webkit-overflow-scrolling: touch;
773 overscroll-behavior: contain;
774 padding: 4px 12px 12px;
775 display: flex;
776 flex-direction: column-reverse;
777 gap: 16px;
778 }
779
780 html[data-os-mode="mobile"] .os-mobile-deck {
781 display: flex;
782 flex-direction: column;
783 }
784
785 /*
786 * The pile. Cards are laid out bottom-up too, so the first card in
787 * the DOM (the most recent, the one in front) is the lowest; each
788 * card that follows is pulled down under the one before it by
789 * `--_m-card-peek` less than its height, so only its title bar shows.
790 * `switcher.ts` sets the z-order, front card highest.
791 */
792 html[data-os-mode="mobile"] .os-mobile-deck__cards {
793 display: flex;
794 flex-direction: column-reverse;
795 padding-block-end: 2px;
796 }
797
798 html[data-os-mode="mobile"] .os-mobile-switcher__empty {
799 margin: 24px 8px;
800 color: var( --_m-fg-muted );
801 text-align: center;
802 }
803
804 html[data-os-mode="mobile"] .os-mobile-switcher__footer {
805 display: flex;
806 justify-content: center;
807 padding: 8px 12px 10px;
808 border-block-start: 1px solid var( --_m-border );
809 }
810
811 html[data-os-mode="mobile"] .os-mobile-switcher__footer[hidden] {
812 display: none;
813 }
814
815 /*
816 * A card is a small window: an opaque surface (glass over the solid
817 * backstop, so the card behind never shows through), a title bar,
818 * and a lighter body. The shadow is cast upward, onto the card
819 * behind.
820 */
821 html[data-os-mode="mobile"] .os-mobile-card {
822 --_m-card-h: 132px;
823 --_m-card-peek: 58px;
824 position: relative;
825 display: block;
826 height: var( --_m-card-h );
827 background: linear-gradient( var( --_m-glass ), var( --_m-glass ) ) var( --_m-surface );
828 border: 1px solid var( --_m-border );
829 border-radius: var( --_m-radius );
830 box-shadow: var( --_m-card-shadow );
831 touch-action: pan-y;
832 will-change: transform;
833 transition: transform 200ms var( --_m-ease ), opacity 200ms var( --_m-ease );
834 }
835
836 html[data-os-mode="mobile"] .os-mobile-card + .os-mobile-card {
837 margin-block-end: calc( var( --_m-card-peek ) - var( --_m-card-h ) );
838 }
839
840 html[data-os-mode="mobile"] .os-mobile-card--dragging {
841 transition: none;
842 }
843
844 html[data-os-mode="mobile"] .os-mobile-card--out {
845 transform: translateX( calc( var( --os-mobile-card-dir, 1 ) * 110% ) ) !important;
846 opacity: 0 !important;
847 pointer-events: none;
848 }
849
850 /* The app on screen: outlined in the accent, labelled, not a target. */
851 html[data-os-mode="mobile"] .os-mobile-card--active {
852 border-color: var( --_m-accent-dim );
853 box-shadow: 0 0 0 1px var( --_m-accent-dim ), var( --_m-card-shadow );
854 }
855
856 html[data-os-mode="mobile"] .os-mobile-card__status {
857 display: inline-block;
858 margin-inline-start: 8px;
859 padding: 1px 7px;
860 border-radius: 999px;
861 font-size: 10px;
862 font-weight: 700;
863 letter-spacing: 0.04em;
864 text-transform: uppercase;
865 vertical-align: middle;
866 color: var( --_m-accent );
867 background: var( --_m-glass );
868 border: 1px solid var( --_m-accent-dim );
869 }
870
871 html[data-os-mode="mobile"] .os-mobile-card__body {
872 appearance: none;
873 background: transparent;
874 border: 0;
875 color: inherit;
876 position: absolute;
877 inset: 0;
878 display: flex;
879 flex-direction: column;
880 padding: 0;
881 text-align: start;
882 border-radius: inherit;
883 }
884
885 /* The title bar: exactly the strip that shows when the card is behind another. */
886 html[data-os-mode="mobile"] .os-mobile-card__bar {
887 flex: 0 0 auto;
888 height: var( --_m-card-peek );
889 display: flex;
890 align-items: center;
891 gap: 12px;
892 padding-inline: 12px 52px;
893 min-width: 0;
894 }
895
896 /* The body: a lighter page surface, its bottom under the card in front. */
897 html[data-os-mode="mobile"] .os-mobile-card__preview {
898 flex: 1 1 auto;
899 margin-inline: 10px;
900 border-radius: 10px 10px 0 0;
901 background: var( --_m-glass-strong );
902 border: 1px solid var( --_m-border );
903 border-block-end: 0;
904 }
905
906 html[data-os-mode="mobile"] .os-mobile-card__body:focus-visible {
907 outline: 2px solid var( --_m-accent );
908 outline-offset: -2px;
909 }
910
911 html[data-os-mode="mobile"] .os-mobile-card__icon {
912 flex: 0 0 auto;
913 width: 36px;
914 height: 36px;
915 display: inline-flex;
916 align-items: center;
917 justify-content: center;
918 border-radius: 10px;
919 background: var( --_m-glass );
920 border: 1px solid var( --_m-border );
921 color: var( --_m-fg );
922 }
923
924 html[data-os-mode="mobile"] .os-mobile-card__glyph {
925 width: 22px;
926 height: 22px;
927 font-size: 22px;
928 line-height: 1;
929 }
930
931 html[data-os-mode="mobile"] .os-mobile-card__text {
932 display: flex;
933 flex-direction: column;
934 min-width: 0;
935 gap: 2px;
936 }
937
938 html[data-os-mode="mobile"] .os-mobile-card__title {
939 font-size: 15px;
940 font-weight: 600;
941 white-space: nowrap;
942 overflow: hidden;
943 text-overflow: ellipsis;
944 }
945
946 html[data-os-mode="mobile"] .os-mobile-card__subtitle {
947 font-size: 12px;
948 color: var( --_m-fg-muted );
949 white-space: nowrap;
950 overflow: hidden;
951 text-overflow: ellipsis;
952 }
953
954 html[data-os-mode="mobile"] .os-mobile-card__close {
955 appearance: none;
956 background: var( --_m-glass );
957 border: 1px solid var( --_m-border );
958 color: var( --_m-fg-muted );
959 position: absolute;
960 inset-block-start: 11px;
961 inset-inline-end: 10px;
962 width: 36px;
963 height: 36px;
964 display: inline-flex;
965 align-items: center;
966 justify-content: center;
967 border-radius: 999px;
968 -webkit-tap-highlight-color: transparent;
969 }
970
971 html[data-os-mode="mobile"] .os-mobile-card__close:focus-visible {
972 outline: 2px solid var( --_m-accent );
973 outline-offset: -2px;
974 }
975
976 /* ------------------------------------------------------------------
977 * View transitions.
978 *
979 * The layer runs every open, back and switch through
980 * `document.startViewTransition()` (`src/mobile/layer.ts`). The thing
981 * the user touched and the thing it becomes share one
982 * `view-transition-name` (`os-mobile-hero`), so the browser morphs a
983 * tile into its full-screen window and the window back into its
984 * tile. The top bar and the tab bar carry their own names so they
985 * hold still instead of cross-fading with the rest of the root.
986 * ---------------------------------------------------------------- */
987
988 html[data-os-mode="mobile"] .os-mobile-top {
989 view-transition-name: os-mobile-top;
990 }
991
992 html[data-os-mode="mobile"] .os-mobile-tabs {
993 view-transition-name: os-mobile-tabs;
994 }
995
996 ::view-transition-group( os-mobile-hero ) {
997 animation-duration: 300ms;
998 animation-timing-function: cubic-bezier( 0.2, 0.8, 0.2, 1 );
999 }
1000
1001 ::view-transition-old( os-mobile-hero ),
1002 ::view-transition-new( os-mobile-hero ) {
1003 /* The two boxes are different shapes; stretch each to the group
1004 * box rather than letting the smaller one letterbox. */
1005 height: 100%;
1006 width: 100%;
1007 object-fit: cover;
1008 mix-blend-mode: normal;
1009 animation-duration: 300ms;
1010 }
1011
1012 ::view-transition-old( root ),
1013 ::view-transition-new( root ) {
1014 animation-duration: 220ms;
1015 }
1016
1017 ::view-transition-old( os-mobile-top ),
1018 ::view-transition-new( os-mobile-top ),
1019 ::view-transition-old( os-mobile-tabs ),
1020 ::view-transition-new( os-mobile-tabs ) {
1021 animation-duration: 160ms;
1022 }
1023
1024 /*
1025 * The tab bar's membership changing (a pin added, removed or
1026 * reordered in Preferences → Mobile). Each tab carries a name and
1027 * the `os-tab` transition class for the duration: a tab that stays
1028 * glides to its slot, a new one grows in from the bar, a removed one
1029 * shrinks out. `:only-child` is how a group with no counterpart —
1030 * an entry or an exit — is told apart from a morph.
1031 */
1032 @keyframes os-mobile-tab-in {
1033 from {
1034 opacity: 0;
1035 transform: translateY( 10px ) scale( 0.6 );
1036 }
1037 }
1038
1039 @keyframes os-mobile-tab-out {
1040 to {
1041 opacity: 0;
1042 transform: translateY( 10px ) scale( 0.6 );
1043 }
1044 }
1045
1046 ::view-transition-group( .os-tab ) {
1047 animation-duration: 280ms;
1048 animation-timing-function: cubic-bezier( 0.2, 0.8, 0.2, 1 );
1049 }
1050
1051 ::view-transition-new( .os-tab ):only-child {
1052 animation: os-mobile-tab-in 280ms cubic-bezier( 0.2, 0.8, 0.2, 1 ) both;
1053 }
1054
1055 ::view-transition-old( .os-tab ):only-child {
1056 animation: os-mobile-tab-out 200ms cubic-bezier( 0.2, 0.8, 0.2, 1 ) both;
1057 }
1058
1059 /* ------------------------------------------------------------------
1060 * Motion opt-out.
1061 * ---------------------------------------------------------------- */
1062
1063 @media ( prefers-reduced-motion: reduce ) {
1064 html[data-os-mode="mobile"] .os-window.os-mobile-enter,
1065 html[data-os-mode="mobile"] .os-mobile-home,
1066 html[data-os-mode="mobile"] .os-mobile-switcher__backdrop,
1067 html[data-os-mode="mobile"] .os-mobile-switcher__sheet {
1068 animation: none;
1069 }
1070
1071 html[data-os-mode="mobile"] .os-mobile-card,
1072 html[data-os-mode="mobile"] .os-mobile-tile__icon {
1073 transition: none;
1074 }
1075 }
1076