PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / trunk
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin vtrunk
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 0.8.6 All 33 releases
desktop-mode / assets / css / mobile.css

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

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