/** * OpenStation — the phone layer. * * Every rule is scoped to `html[data-os-mode="mobile"]`, the * attribute the PHP head stamp writes before the body parses and the * mode controller keeps current. On a desktop this sheet matches * nothing; on a phone the first paint is already the phone layer. * * Three jobs: * * 1. Retire the desktop chrome — dock, widgets, wallpaper icons, * admin bar, notch, window title bars and resize handles. The * elements stay in the DOM (their listeners and registries keep * working); they just stop painting. * 2. Make every window the whole work area, between the top bar * and the tab bar, under the safe areas. * 3. Paint the four phone surfaces: top bar, home grid, tab bar, * app switcher. * * Every colour resolves through a palette token with the pre-brand * literal as its floor (see AGENTS.md, "The palette lives in * variables.css"). Motion is opt-out under `prefers-reduced-motion`. */ /* ------------------------------------------------------------------ * 1. The desktop chrome steps aside. * ---------------------------------------------------------------- */ html[data-os-mode="mobile"] #wpadminbar, html[data-os-mode="mobile"] #os-dock, html[data-os-mode="mobile"] #os-side-dock, html[data-os-mode="mobile"] #os-widgets, html[data-os-mode="mobile"] .os-icons, html[data-os-mode="mobile"] .os-files-layer, html[data-os-mode="mobile"] .os-notes, html[data-os-mode="mobile"] .os-mio, html[data-os-mode="mobile"] #os-mio, html[data-os-mode="mobile"] .os-notch, html[data-os-mode="mobile"] .os-window__titlebar, html[data-os-mode="mobile"] .os-window__resize-handle { display: none !important; } /* The 32px WordPress reserves for the admin bar. */ html[data-os-mode="mobile"].wp-toolbar { padding-top: 0 !important; } /* * The shell starts at the top edge, and the admin-bar height token * says so too: anything that still reads it (the notch's offset, a * plugin's own chrome) measures against a bar that is not there. */ html[data-os-mode="mobile"] .os-shell { inset-block-start: 0; --wp-admin--admin-bar--height: 0px; } /* * No page zoom. `pan-x pan-y` keeps every scroll and hands nothing * else to the browser: no pinch, no double-tap. The viewport meta * says the same (`maximum-scale=1`, `openstation_mode_viewport_meta()`) * and `src/mode/zoom-guard.ts` cancels the gesture where a browser * ignores both. `overscroll-behavior: none` is the rubber band at the * document's edges — the shell is fixed and has no document to pull. */ html[data-os-mode="mobile"], html[data-os-mode="mobile"] body { touch-action: pan-x pan-y; overscroll-behavior: none; } /* The work area is the whole area: no dock band to keep clear of. */ html[data-os-mode="mobile"] .os-area { padding: 0 !important; } /* ------------------------------------------------------------------ * Tokens for this layer alone. Private aliases read the palette so a * desktop theme that repaints the surfaces repaints the phone too. * ---------------------------------------------------------------- */ /* * The phone chrome is the DOCK's family, not the window's. The window * tokens describe a document surface (white body, dark text under most * admin colour schemes) and they are wrong for anything that sits on * the wallpaper: a white tile grid with grey labels on a dark desk. * The dock tokens describe exactly this situation — translucent dark * glass over the wallpaper, light glyphs — so every phone surface * reads them, and a desktop theme that reskins the dock reskins the * phone with it. Bars and sheets sit on the solid backstop, the same * floor the shell paints under everything. What the dock has no word * for — the glass a tile is cut from, the shadows, the scrims — is * the `--os-mobile-*` family in `variables.css`. */ html[data-os-mode="mobile"] .os-shell { --_m-surface: var( --os-backstop, #0c0b0f ); --_m-surface-raised: var( --os-dock-bg, rgba( 12, 11, 15, 0.72 ) ); --_m-glass: var( --os-mobile-glass, rgba( 255, 255, 255, 0.1 ) ); --_m-glass-strong: var( --os-mobile-glass-strong, rgba( 255, 255, 255, 0.16 ) ); --_m-glass-border: var( --os-mobile-glass-border, rgba( 255, 255, 255, 0.14 ) ); --_m-fg: var( --os-dock-icon-color-hover, #fffbff ); --_m-fg-muted: var( --os-dock-icon-color, rgba( 255, 251, 255, 0.72 ) ); --_m-border: var( --os-dock-border, rgba( 255, 251, 255, 0.1 ) ); --_m-accent: var( --os-ui-accent, #f252fc ); --_m-accent-dim: var( --os-ui-accent-dim, rgba( 242, 82, 252, 0.35 ) ); --_m-danger: var( --os-ui-danger, #d63638 ); --_m-tile-shadow: var( --os-mobile-tile-shadow, 0 8px 20px rgba( 0, 0, 0, 0.35 ) ); --_m-card-shadow: var( --os-mobile-card-shadow, 0 -8px 24px rgba( 0, 0, 0, 0.4 ) ); --_m-sheet-shadow: var( --os-mobile-sheet-shadow, 0 -12px 40px rgba( 0, 0, 0, 0.45 ) ); --_m-window-shadow: var( --os-mobile-window-shadow, 0 12px 32px rgba( 0, 0, 0, 0.45 ) ); --_m-scrim: var( --os-mobile-scrim, rgba( 0, 0, 0, 0.55 ) ); --_m-top-scrim: var( --os-mobile-top-scrim, rgba( 0, 0, 0, 0.45 ) ); --_m-label-shadow: var( --os-mobile-label-shadow, 0 1px 2px rgba( 0, 0, 0, 0.6 ) ); --_m-radius: 14px; --_m-gap: 8px; --_m-top-h: 48px; --_m-tabs-h: 56px; --_m-safe-top: env( safe-area-inset-top, 0px ); --_m-safe-bottom: env( safe-area-inset-bottom, 0px ); --_m-safe-left: env( safe-area-inset-left, 0px ); --_m-safe-right: env( safe-area-inset-right, 0px ); /* The tab bar's whole footprint: its items, and the home indicator under them. */ --_m-tabs-total: calc( var( --_m-tabs-h ) + 8px + var( --_m-safe-bottom ) ); --_m-ease: cubic-bezier( 0.2, 0.8, 0.2, 1 ); /* * Every kit field on the phone is 16px: below that, iOS zooms the * page into a focused control and never zooms back out. */ --os-ui-field-font-size: 16px; --os-ui-field-font-size-compact: 16px; } /* * The phone surfaces are DARK surfaces, and the kit components they * host (`` in the home search, `` in the * switcher footer) read the public `--os-ui-*` tokens, which under a * light admin scheme describe the window's white body and near-black * text. Re-pointing those public names on the surface itself is the * sanctioned move (a document-tree rule the component's `var()` * resolves through — see AGENTS.md, "Never declare a themeable token * on a component's :host"): every kit control inside reads the phone * palette, and a theme that recolours the dock recolours them too. * `--_m-*` are the private aliases declared on `.os-shell` above. */ html[data-os-mode="mobile"] .os-mobile-home, html[data-os-mode="mobile"] .os-mobile-top, html[data-os-mode="mobile"] .os-mobile-tabs, html[data-os-mode="mobile"] .os-mobile-switcher__sheet { --os-ui-fg: var( --_m-fg ); --os-ui-fg-muted: var( --_m-fg-muted ); --os-ui-border: var( --_m-border ); --os-ui-surface: var( --_m-surface ); --os-window-bg: var( --_m-surface ); --os-ui-hover: var( --_m-glass ); --os-ui-button-fg: var( --_m-fg ); --os-ui-button-bg: var( --_m-glass ); --os-ui-button-bg-hover: var( --_m-glass-strong ); --os-ui-button-border: 1px solid var( --_m-border ); } /* ------------------------------------------------------------------ * 2. Windows: one at a time, a card on the desk. * * The window is inset from the edges by `--_m-gap`, rounded and * shadowed, so the desk shows around it and the app reads as a card * sitting on something (the switcher draws it the same way). `inset` * with `!important` beats the inline left/top/width/height that * `maximize()` writes; the inline values stay what they were, which * is what the session hands back to the desktop. * ---------------------------------------------------------------- */ html[data-os-mode="mobile"] .os-window { border-radius: var( --_m-radius ) !important; border: 1px solid var( --_m-border ) !important; box-shadow: var( --_m-window-shadow ) !important; overflow: hidden; } html[data-os-mode="mobile"] .os-window--maximized { inset: var( --_m-gap ) calc( var( --_m-gap ) + var( --_m-safe-right ) ) var( --_m-gap ) calc( var( --_m-gap ) + var( --_m-safe-left ) ) !important; width: auto !important; height: auto !important; } /* * Only the topmost window paints. The rest are still open (their * iframes keep their state) but skip layout and paint entirely, so a * phone with six apps open composites one. */ html[data-os-mode="mobile"] .os-window:not( .os-window--focused ) { content-visibility: hidden; } /* * The tab strip (All Posts / Add New / Categories) stays: on a phone * it is the in-app navigation. It scrolls sideways instead of * wrapping. */ html[data-os-mode="mobile"] .os-window__tabs { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; scrollbar-width: none; padding-inline: var( --_m-safe-left ) var( --_m-safe-right ); } html[data-os-mode="mobile"] .os-window__tabs::-webkit-scrollbar { display: none; } /* A new app slides up from the tab bar. */ @keyframes os-mobile-app-in { from { opacity: 0; transform: translateY( 24px ) scale( 0.98 ); } to { opacity: 1; transform: none; } } html[data-os-mode="mobile"] .os-window.os-mobile-enter { animation: os-mobile-app-in 220ms var( --_m-ease ) both; } /* On the home screen nothing but the wallpaper and the grid paint. */ html[data-os-mode="mobile"] .os-shell[data-os-mobile-state="home"] .os-window { visibility: hidden; } /* ------------------------------------------------------------------ * The edge zones: thin strips the shell owns over both edges of the * area, so a pointer that starts there never belongs to the iframe. * The start edge is the Back gesture; both cancel the browser's * history swipe (`bindHistorySwipeGuard` in `gestures.ts`), so they * stay up on the home screen too — a swipe that navigated away would * navigate away from home as well. Above the windows and the home * grid, below the switcher sheet. * ---------------------------------------------------------------- */ html[data-os-mode="mobile"] .os-mobile-edge { position: absolute; inset-block: 0; width: 22px; z-index: 2000; touch-action: none; background: transparent; } html[data-os-mode="mobile"] .os-mobile-edge--start { inset-inline-start: 0; } html[data-os-mode="mobile"] .os-mobile-edge--end { inset-inline-end: 0; } /* ------------------------------------------------------------------ * 3a. The top bar. * ---------------------------------------------------------------- */ /* * The bar sits on the desk, not on the window: no fill and no rule * under it, only a scrim so the title reads over any wallpaper, and * the window card starts `--_m-gap` below it. Icon and name above a * card is the picture the switcher draws too. */ html[data-os-mode="mobile"] .os-mobile-top { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; min-height: var( --_m-top-h ); padding-block-start: var( --_m-safe-top ); padding-inline: calc( 4px + var( --_m-safe-left ) ) calc( 4px + var( --_m-safe-right ) ); background: linear-gradient( to bottom, var( --_m-top-scrim ), transparent ); color: var( --_m-fg ); position: relative; z-index: 3000; } html[data-os-mode="mobile"] .os-mobile-top[hidden] { display: none; } /* Icon + title, the app's identity. Follows an in-flight back gesture. */ html[data-os-mode="mobile"] .os-mobile-top__identity { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 10px; padding-inline-start: 10px; transition: transform 80ms linear; } html[data-os-mode="mobile"] .os-mobile-top--peeking .os-mobile-top__identity { transform: translateX( calc( var( --os-mobile-back-progress, 0 ) * 14px ) ); } /* * One control, a round glass ×: the app closes and morphs back into * its tile. The bar also answers a flick down by sending the app * home, window kept. */ html[data-os-mode="mobile"] .os-mobile-top__controls { display: inline-flex; align-items: center; gap: 8px; margin-inline-end: 6px; flex: 0 0 auto; } html[data-os-mode="mobile"] .os-mobile-top__button { appearance: none; border: 1px solid var( --_m-border ); background: var( --_m-glass ); color: inherit; width: 34px; height: 34px; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; flex: 0 0 auto; -webkit-tap-highlight-color: transparent; } html[data-os-mode="mobile"] .os-mobile-top__button:active { background: var( --_m-glass-strong ); } html[data-os-mode="mobile"] .os-mobile-top__button:focus-visible { outline: 2px solid var( --_m-accent ); outline-offset: 2px; } html[data-os-mode="mobile"] .os-mobile-top__icon { display: inline-flex; width: 24px; height: 24px; align-items: center; justify-content: center; flex: 0 0 auto; color: var( --_m-fg-muted ); } html[data-os-mode="mobile"] .os-mobile-top__icon > * { width: 22px; height: 22px; font-size: 22px; } html[data-os-mode="mobile"] .os-mobile-top__title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: 17px; font-weight: 600; line-height: 1.2; color: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-shadow: var( --_m-label-shadow ); } /* ------------------------------------------------------------------ * 3b. Home. * ---------------------------------------------------------------- */ html[data-os-mode="mobile"] .os-mobile-home { position: absolute; inset: 0; z-index: 1500; display: flex; flex-direction: column; color: var( --_m-fg ); padding-block-start: calc( 8px + var( --_m-safe-top ) ); padding-inline: calc( 12px + var( --_m-safe-left ) ) calc( 12px + var( --_m-safe-right ) ); animation: os-mobile-fade-in 160ms var( --_m-ease ) both; } html[data-os-mode="mobile"] .os-mobile-home[hidden] { display: none; } @keyframes os-mobile-fade-in { from { opacity: 0; } to { opacity: 1; } } /* * The search is a phone's search: a rounded glass field, 16px type * (the size under which iOS zooms the page into it), no hairline * box. The kit field reads the public tokens re-pointed here. */ html[data-os-mode="mobile"] .os-mobile-home__search { flex: 0 0 auto; padding-block: 4px 8px; --os-window-bg: var( --_m-glass ); --os-ui-border: var( --_m-glass-border ); --os-ui-field-radius: 12px; } html[data-os-mode="mobile"] .os-mobile-home__search os-text-field { backdrop-filter: blur( 14px ) saturate( 140% ); -webkit-backdrop-filter: blur( 14px ) saturate( 140% ); border-radius: 12px; } html[data-os-mode="mobile"] .os-mobile-home__scroll { flex: 1 1 auto; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; } /* * The grid always scrolls, by at least one pixel. A phone only rubber- * bands a surface that CAN scroll, and a home screen that fits — a * dozen tiles on a tall phone — was a dead surface under the thumb. * The 1px past the frame makes it a scroller, and the platform then * gives it the platform's own bounce at both ends, exactly the drag * every native home screen has. The bottom padding is here rather than * on the scroller so the last row clears the tab bar inside the same * measure. */ html[data-os-mode="mobile"] .os-mobile-home__content { box-sizing: border-box; min-block-size: calc( 100% + 1px ); padding-block-end: 16px; } html[data-os-mode="mobile"] .os-mobile-home__heading { margin: 12px 4px 6px; font-size: 12px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var( --_m-fg-muted ); text-shadow: var( --_m-label-shadow ); } html[data-os-mode="mobile"] .os-mobile-home__empty { margin: 24px 8px; color: var( --_m-fg-muted ); text-align: center; } html[data-os-mode="mobile"] .os-mobile-grid { display: grid; grid-template-columns: repeat( auto-fill, minmax( 78px, 1fr ) ); gap: 10px 6px; margin: 0; padding: 0; list-style: none; } html[data-os-mode="mobile"] .os-mobile-tile { appearance: none; background: transparent; border: 0; padding: 6px 2px; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 6px; color: var( --_m-fg ); border-radius: 12px; -webkit-tap-highlight-color: transparent; } html[data-os-mode="mobile"] .os-mobile-tile:active .os-mobile-tile__icon { transform: scale( 0.92 ); } html[data-os-mode="mobile"] .os-mobile-tile:focus-visible { outline: 2px solid var( --_m-accent ); outline-offset: 2px; } html[data-os-mode="mobile"] .os-mobile-tile__icon { position: relative; width: 58px; height: 58px; display: inline-flex; align-items: center; justify-content: center; border-radius: 16px; background: var( --_m-glass ); border: 1px solid var( --_m-glass-border ); box-shadow: var( --_m-tile-shadow ); backdrop-filter: blur( 14px ) saturate( 140% ); -webkit-backdrop-filter: blur( 14px ) saturate( 140% ); transition: transform 120ms var( --_m-ease ), background 120ms var( --_m-ease ); color: var( --_m-fg ); } html[data-os-mode="mobile"] .os-mobile-tile:active .os-mobile-tile__icon { background: var( --_m-glass-strong ); } html[data-os-mode="mobile"] .os-mobile-tile__glyph { width: 30px; height: 30px; font-size: 30px; line-height: 1; } html[data-os-mode="mobile"] .os-mobile-tile__glyph.dashicons { width: 30px; height: 30px; font-size: 30px; } html[data-os-mode="mobile"] .os-mobile-tile__badge, html[data-os-mode="mobile"] .os-mobile-tabs__badge { position: absolute; inset-block-start: -6px; inset-inline-end: -6px; min-width: 18px; height: 18px; padding-inline: 5px; border-radius: 9px; background: var( --_m-danger ); color: #fff; font-size: 11px; font-weight: 700; line-height: 18px; text-align: center; box-shadow: 0 0 0 2px var( --_m-surface ); } /* * The switcher's glyph is the number of open apps inside a rounded * square, the browser tab-switcher convention, so it reads as "this * many open" and not as a notification (and the accent stays the * active tab's alone). With nothing open the windows icon shows. */ html[data-os-mode="mobile"] .os-mobile-tabs__count { display: inline-flex; align-items: center; justify-content: center; /* Stroke included: the neighbours' glyphs ink about 19px of their 22px box. */ box-sizing: border-box; width: 20px; height: 20px; border: 2px solid currentColor; border-radius: 5px; font-size: 11px; font-weight: 700; line-height: 1; letter-spacing: -0.02em; color: inherit; } html[data-os-mode="mobile"] .os-mobile-tabs__count[hidden], html[data-os-mode="mobile"] .os-mobile-tabs__icon--counted > :not( .os-mobile-tabs__count ) { display: none; } html[data-os-mode="mobile"] .os-mobile-tile__label { font-size: 11.5px; line-height: 1.2; color: var( --_m-fg ); text-align: center; max-width: 100%; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-shadow: var( --_m-label-shadow ); } /* ------------------------------------------------------------------ * 3c. The tab bar. * * Anchored to the viewport's bottom edge, not laid out as the last * child of the shell. The shell is a viewport-sized fixed box, and a * box of that shape is exactly what mobile Safari has, more than * once, ended short of the screen's bottom edge (an installed app * under one iOS release drew a band of nothing under the bar). A * bar fixed to the edge is right whatever the shell's box does; the * shell's body keeps the bar's footprint clear below it, so the * window card and the home grid end where the bar begins. * ---------------------------------------------------------------- */ html[data-os-mode="mobile"] .os-mobile-tabs { position: fixed; inset-inline: 0; inset-block-end: 0; display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; align-items: stretch; min-height: var( --_m-tabs-h ); padding-block-end: var( --_m-safe-bottom ); padding-inline: var( --_m-safe-left ) var( --_m-safe-right ); background: var( --_m-surface ); border-block-start: 1px solid var( --_m-border ); z-index: 3000; touch-action: pan-x; } html[data-os-mode="mobile"] .os-shell__body { padding-block-end: var( --_m-tabs-total ); } html[data-os-mode="mobile"] .os-mobile-tabs__item { appearance: none; background: transparent; border: 0; color: var( --_m-fg-muted ); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; min-height: var( --_m-tabs-h ); padding: 4px 2px; font-size: 10.5px; line-height: 1.1; -webkit-tap-highlight-color: transparent; } html[data-os-mode="mobile"] .os-mobile-tabs__item[aria-current="page"] { color: var( --_m-accent ); } html[data-os-mode="mobile"] .os-mobile-tabs__item:focus-visible { outline: 2px solid var( --_m-accent ); outline-offset: -2px; border-radius: 10px; } html[data-os-mode="mobile"] .os-mobile-tabs__icon { position: relative; display: inline-flex; width: 26px; height: 26px; align-items: center; justify-content: center; } html[data-os-mode="mobile"] .os-mobile-tabs__glyph { width: 22px; height: 22px; font-size: 22px; line-height: 1; } html[data-os-mode="mobile"] .os-mobile-tabs__glyph.dashicons { width: 22px; height: 22px; font-size: 22px; } html[data-os-mode="mobile"] .os-mobile-tabs__label { max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* ------------------------------------------------------------------ * 3d. The switcher. * ---------------------------------------------------------------- */ html[data-os-mode="mobile"] .os-mobile-switcher { position: fixed; inset: 0; z-index: 4000; display: flex; flex-direction: column; justify-content: flex-end; } html[data-os-mode="mobile"] .os-mobile-switcher[hidden] { display: none; } html[data-os-mode="mobile"] .os-mobile-switcher__backdrop { position: absolute; inset: 0; background: var( --_m-scrim ); animation: os-mobile-fade-in 160ms var( --_m-ease ) both; } @keyframes os-mobile-sheet-in { from { transform: translateY( 24px ); opacity: 0; } to { transform: none; opacity: 1; } } html[data-os-mode="mobile"] .os-mobile-switcher__sheet { position: relative; max-height: min( 82vh, 82dvh ); display: flex; flex-direction: column; background: var( --_m-surface ); color: var( --_m-fg ); border-start-start-radius: var( --_m-radius ); border-start-end-radius: var( --_m-radius ); border: 1px solid var( --_m-border ); border-block-end: 0; padding-block-end: var( --_m-safe-bottom ); box-shadow: var( --_m-sheet-shadow ); animation: os-mobile-sheet-in 200ms var( --_m-ease ) both; } html[data-os-mode="mobile"] .os-mobile-switcher__header { display: flex; align-items: center; justify-content: space-between; padding: 10px 8px 6px 16px; } /* * Explicit colours on the headings: wp-admin's `common.css` paints * every h2/h3 in the admin's heading colour, which is near-black under * a light scheme and would sink into the sheet. */ html[data-os-mode="mobile"] .os-mobile-switcher__title { margin: 0; font-size: 15px; font-weight: 600; color: var( --_m-fg ); } html[data-os-mode="mobile"] .os-mobile-switcher__close { appearance: none; background: transparent; border: 0; color: inherit; width: 40px; height: 40px; display: inline-flex; align-items: center; justify-content: center; border-radius: 10px; } html[data-os-mode="mobile"] .os-mobile-switcher__close:focus-visible { outline: 2px solid var( --_m-accent ); outline-offset: -2px; } /* * The list is laid out bottom-up (`column-reverse`) so the deck sits * at the bottom with its front card nearest the thumb, and a scroll * container in this direction starts scrolled to the front. The DOM * order is unchanged. */ html[data-os-mode="mobile"] .os-mobile-switcher__list { flex: 1 1 auto; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; padding: 4px 12px 12px; display: flex; flex-direction: column-reverse; gap: 16px; } html[data-os-mode="mobile"] .os-mobile-deck { display: flex; flex-direction: column; } /* * The pile. Cards are laid out bottom-up too, so the first card in * the DOM (the most recent, the one in front) is the lowest; each * card that follows is pulled down under the one before it by * `--_m-card-peek` less than its height, so only its title bar shows. * `switcher.ts` sets the z-order, front card highest. */ html[data-os-mode="mobile"] .os-mobile-deck__cards { display: flex; flex-direction: column-reverse; padding-block-end: 2px; } html[data-os-mode="mobile"] .os-mobile-switcher__empty { margin: 24px 8px; color: var( --_m-fg-muted ); text-align: center; } html[data-os-mode="mobile"] .os-mobile-switcher__footer { display: flex; justify-content: center; padding: 8px 12px 10px; border-block-start: 1px solid var( --_m-border ); } html[data-os-mode="mobile"] .os-mobile-switcher__footer[hidden] { display: none; } /* * A card is a small window: an opaque surface (glass over the solid * backstop, so the card behind never shows through), a title bar, * and a lighter body. The shadow is cast upward, onto the card * behind. */ html[data-os-mode="mobile"] .os-mobile-card { --_m-card-h: 132px; --_m-card-peek: 58px; position: relative; display: block; height: var( --_m-card-h ); background: linear-gradient( var( --_m-glass ), var( --_m-glass ) ) var( --_m-surface ); border: 1px solid var( --_m-border ); border-radius: var( --_m-radius ); box-shadow: var( --_m-card-shadow ); touch-action: pan-y; will-change: transform; transition: transform 200ms var( --_m-ease ), opacity 200ms var( --_m-ease ); } html[data-os-mode="mobile"] .os-mobile-card + .os-mobile-card { margin-block-end: calc( var( --_m-card-peek ) - var( --_m-card-h ) ); } html[data-os-mode="mobile"] .os-mobile-card--dragging { transition: none; } html[data-os-mode="mobile"] .os-mobile-card--out { transform: translateX( calc( var( --os-mobile-card-dir, 1 ) * 110% ) ) !important; opacity: 0 !important; pointer-events: none; } /* The app on screen: outlined in the accent, labelled, not a target. */ html[data-os-mode="mobile"] .os-mobile-card--active { border-color: var( --_m-accent-dim ); box-shadow: 0 0 0 1px var( --_m-accent-dim ), var( --_m-card-shadow ); } html[data-os-mode="mobile"] .os-mobile-card__status { display: inline-block; margin-inline-start: 8px; padding: 1px 7px; border-radius: 999px; font-size: 10px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; vertical-align: middle; color: var( --_m-accent ); background: var( --_m-glass ); border: 1px solid var( --_m-accent-dim ); } html[data-os-mode="mobile"] .os-mobile-card__body { appearance: none; background: transparent; border: 0; color: inherit; position: absolute; inset: 0; display: flex; flex-direction: column; padding: 0; text-align: start; border-radius: inherit; } /* The title bar: exactly the strip that shows when the card is behind another. */ html[data-os-mode="mobile"] .os-mobile-card__bar { flex: 0 0 auto; height: var( --_m-card-peek ); display: flex; align-items: center; gap: 12px; padding-inline: 12px 52px; min-width: 0; } /* The body: a lighter page surface, its bottom under the card in front. */ html[data-os-mode="mobile"] .os-mobile-card__preview { flex: 1 1 auto; margin-inline: 10px; border-radius: 10px 10px 0 0; background: var( --_m-glass-strong ); border: 1px solid var( --_m-border ); border-block-end: 0; } html[data-os-mode="mobile"] .os-mobile-card__body:focus-visible { outline: 2px solid var( --_m-accent ); outline-offset: -2px; } html[data-os-mode="mobile"] .os-mobile-card__icon { flex: 0 0 auto; width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center; border-radius: 10px; background: var( --_m-glass ); border: 1px solid var( --_m-border ); color: var( --_m-fg ); } html[data-os-mode="mobile"] .os-mobile-card__glyph { width: 22px; height: 22px; font-size: 22px; line-height: 1; } html[data-os-mode="mobile"] .os-mobile-card__text { display: flex; flex-direction: column; min-width: 0; gap: 2px; } html[data-os-mode="mobile"] .os-mobile-card__title { font-size: 15px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } html[data-os-mode="mobile"] .os-mobile-card__subtitle { font-size: 12px; color: var( --_m-fg-muted ); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } html[data-os-mode="mobile"] .os-mobile-card__close { appearance: none; background: var( --_m-glass ); border: 1px solid var( --_m-border ); color: var( --_m-fg-muted ); position: absolute; inset-block-start: 11px; inset-inline-end: 10px; width: 36px; height: 36px; display: inline-flex; align-items: center; justify-content: center; border-radius: 999px; -webkit-tap-highlight-color: transparent; } html[data-os-mode="mobile"] .os-mobile-card__close:focus-visible { outline: 2px solid var( --_m-accent ); outline-offset: -2px; } /* ------------------------------------------------------------------ * View transitions. * * The layer runs every open, back and switch through * `document.startViewTransition()` (`src/mobile/layer.ts`). The thing * the user touched and the thing it becomes share one * `view-transition-name` (`os-mobile-hero`), so the browser morphs a * tile into its full-screen window and the window back into its * tile. The top bar and the tab bar carry their own names so they * hold still instead of cross-fading with the rest of the root. * ---------------------------------------------------------------- */ html[data-os-mode="mobile"] .os-mobile-top { view-transition-name: os-mobile-top; } html[data-os-mode="mobile"] .os-mobile-tabs { view-transition-name: os-mobile-tabs; } ::view-transition-group( os-mobile-hero ) { animation-duration: 300ms; animation-timing-function: cubic-bezier( 0.2, 0.8, 0.2, 1 ); } ::view-transition-old( os-mobile-hero ), ::view-transition-new( os-mobile-hero ) { /* The two boxes are different shapes; stretch each to the group * box rather than letting the smaller one letterbox. */ height: 100%; width: 100%; object-fit: cover; mix-blend-mode: normal; animation-duration: 300ms; } ::view-transition-old( root ), ::view-transition-new( root ) { animation-duration: 220ms; } ::view-transition-old( os-mobile-top ), ::view-transition-new( os-mobile-top ), ::view-transition-old( os-mobile-tabs ), ::view-transition-new( os-mobile-tabs ) { animation-duration: 160ms; } /* * The tab bar's membership changing (a pin added, removed or * reordered in Preferences → Mobile). Each tab carries a name and * the `os-tab` transition class for the duration: a tab that stays * glides to its slot, a new one grows in from the bar, a removed one * shrinks out. `:only-child` is how a group with no counterpart — * an entry or an exit — is told apart from a morph. */ @keyframes os-mobile-tab-in { from { opacity: 0; transform: translateY( 10px ) scale( 0.6 ); } } @keyframes os-mobile-tab-out { to { opacity: 0; transform: translateY( 10px ) scale( 0.6 ); } } ::view-transition-group( .os-tab ) { animation-duration: 280ms; animation-timing-function: cubic-bezier( 0.2, 0.8, 0.2, 1 ); } ::view-transition-new( .os-tab ):only-child { animation: os-mobile-tab-in 280ms cubic-bezier( 0.2, 0.8, 0.2, 1 ) both; } ::view-transition-old( .os-tab ):only-child { animation: os-mobile-tab-out 200ms cubic-bezier( 0.2, 0.8, 0.2, 1 ) both; } /* ------------------------------------------------------------------ * Motion opt-out. * ---------------------------------------------------------------- */ @media ( prefers-reduced-motion: reduce ) { html[data-os-mode="mobile"] .os-window.os-mobile-enter, html[data-os-mode="mobile"] .os-mobile-home, html[data-os-mode="mobile"] .os-mobile-switcher__backdrop, html[data-os-mode="mobile"] .os-mobile-switcher__sheet { animation: none; } html[data-os-mode="mobile"] .os-mobile-card, html[data-os-mode="mobile"] .os-mobile-tile__icon { transition: none; } }