From 149382299a3ab072767c0662b2761bc31ae90d49 Mon Sep 17 00:00:00 2001 From: heaven Date: Tue, 12 May 2026 01:54:30 +0300 Subject: [PATCH] feat(safe-area): extend Android edge-to-edge top inset via --vojo-safe-top var and collapse profile horseshoe header with measured height --- src/app/components/Modal500.tsx | 12 +++- src/app/components/page/Page.tsx | 13 ++++ src/app/features/bots/BotShell.css.ts | 14 ++++ src/app/features/room/Room.tsx | 40 +++++------ .../features/room/RoomViewProfilePanel.css.ts | 63 ++++++++++++++--- .../features/room/RoomViewProfilePanel.tsx | 68 +++++++++++++++---- src/app/features/room/ThreadDrawer.css.ts | 11 +++ src/app/pages/auth/styles.css.ts | 9 +++ src/app/styles/global.css.ts | 62 +++++++++++------ src/index.css | 16 ++--- 10 files changed, 232 insertions(+), 76 deletions(-) diff --git a/src/app/components/Modal500.tsx b/src/app/components/Modal500.tsx index a4c3128c..43dca162 100644 --- a/src/app/components/Modal500.tsx +++ b/src/app/components/Modal500.tsx @@ -20,7 +20,17 @@ export function Modal500({ requestClose, children }: Modal500Props) { escapeDeactivates: stopPropagation, }} > - + {/* PageRoot rendered inside the dialog (Settings, SpaceSettings, RoomSettings) would otherwise pick up the web horseshoe layout — void column + rounded diff --git a/src/app/components/page/Page.tsx b/src/app/components/page/Page.tsx index 39d9d289..4e16b8ff 100644 --- a/src/app/components/page/Page.tsx +++ b/src/app/components/page/Page.tsx @@ -141,6 +141,15 @@ export function PageNav({ grow="Yes" direction="Column" className={horseshoe ? css.PageNavInnerWebHorseshoe : undefined} + // Top inset for native: `#root` no longer reserves the status-bar + // height (src/index.css), so the page-nav extends to the screen + // top. The padding here pushes the page-nav header (workspace + // tabs, etc.) below the status-bar icons. Applied at the inner + // column rather than at the `PageNavHeader` recipe because the + // recipe uses a Folds `
` with a fixed height — + // padding there would clip the header content. `--vojo-safe-top` + // is 0 on web and inside Modal500-hosted dialogs. + style={{ paddingTop: 'var(--vojo-safe-top, 0px)' }} > {children} @@ -268,6 +277,10 @@ function ResizablePageNav({ children }: { children: ReactNode }) { grow="Yes" direction="Column" className={horseshoe ? css.PageNavInnerWebHorseshoe : undefined} + // Same native safe-top inset as the regular PageNav above — + // `var(--vojo-safe-top)` is 0 on web (where resizable is used) + // but kept here for symmetry / future-proofing. + style={{ paddingTop: 'var(--vojo-safe-top, 0px)' }} > {children} diff --git a/src/app/features/bots/BotShell.css.ts b/src/app/features/bots/BotShell.css.ts index 723cc9cf..dfdeb291 100644 --- a/src/app/features/bots/BotShell.css.ts +++ b/src/app/features/bots/BotShell.css.ts @@ -21,6 +21,20 @@ export const Shell = style([ flexDirection: 'column', backgroundColor: color.SurfaceVariant.Container, overflow: 'hidden', + // Native safe-top: `#root` no longer reserves the status-bar inset + // (src/index.css), so BotShell extends to the screen top. The + // padding keeps the Hero (avatar + title + actions) clear of the + // system icons. Shell bg already matches the widget body tone, so + // the padding zone reads as a continuation of the bot surface. On + // web `--vojo-safe-top` is 0. + // + // Bottom inset is intentionally NOT added here: the iframe inside + // `Frame` paints its own body bg (#181a20, see widget-telegram + // styles.css) and the widget is responsible for the gesture-pill + // clearance of its own action rows. Padding Shell here exposed a + // visible seam between the iframe area and the env-bottom-tall + // strip below it on Android. + paddingTop: 'var(--vojo-safe-top, 0px)', }, ]); diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx index 80919810..96486cdb 100644 --- a/src/app/features/room/Room.tsx +++ b/src/app/features/room/Room.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback } from 'react'; import { Box, Line, toRem } from 'folds'; import { useMatch, useParams } from 'react-router-dom'; import { isKeyHotkey } from 'is-hotkey'; @@ -112,25 +112,6 @@ export function Room({ renderRoomView }: RoomProps) { const profileOpen = !!useAtomValue(userRoomProfileAtom); const showProfileHorseshoe = profileOpen && !isMobile && !showThreadDrawer; - // Match the Android edge-to-edge safe-area zones (status bar + gesture - // pill) to the chat surface while a Room is mounted: `body` reads - // `--vojo-safe-area-bg`, which `app/styles/global.css.ts` defaults to - // `#0d0e11` (chat-list tone). We retune to `#181a20` - // (`SurfaceVariant.Container`, the chat tone painted by RoomView's - // ``) for the lifetime of Room and `removeProperty` on cleanup - // so the chat-list view goes back to its dark tone. Hardcoded because - // folds tokens are scoped to the `.dark-theme` class on `body` — - // writing `var(--xxx)` on `:root` (where `--vojo-safe-area-bg` lives) - // would leave it unresolved and body would fall back to the literal - // `#0d0e11` in index.css. - useEffect(() => { - const root = document.documentElement; - root.style.setProperty('--vojo-safe-area-bg', '#181a20'); - return () => { - root.style.removeProperty('--vojo-safe-area-bg'); - }; - }, []); - useKeyDown( window, useCallback( @@ -173,8 +154,13 @@ export function Room({ renderRoomView }: RoomProps) { grow="Yes" direction="Column" className={ - showProfileHorseshoe ? ContainerColor({ variant: 'Background' }) : undefined + showProfileHorseshoe + ? ContainerColor({ variant: 'Background' }) + : undefined } + // No chat-column padding-top / bg: the silhouette inside + // `MobileProfileHorseshoe` owns the safe-top inset and bg. + // See the !callView twin block below for the rationale. > }> @@ -188,8 +174,18 @@ export function Room({ renderRoomView }: RoomProps) { grow="Yes" direction="Column" className={ - showProfileHorseshoe ? ContainerColor({ variant: 'Background' }) : undefined + showProfileHorseshoe + ? ContainerColor({ variant: 'Background' }) + : undefined } + // No padding-top / bg on mobile chat column: the silhouette + // inside `MobileProfileHorseshoe` permanently sits at body_top + // with its own `padding-top: var(--vojo-safe-top)` keeping + // chat-header / panel content below the status-bar icons. + // The silhouette's bg fades between chat-surface tone (when + // closed) and user-card tone (when fully open) as the user + // drags. Desktop branch still works because `--vojo-safe-top` + // resolves to 0 on web. > }> diff --git a/src/app/features/room/RoomViewProfilePanel.css.ts b/src/app/features/room/RoomViewProfilePanel.css.ts index 3831f24f..c9401b8f 100644 --- a/src/app/features/room/RoomViewProfilePanel.css.ts +++ b/src/app/features/room/RoomViewProfilePanel.css.ts @@ -9,8 +9,10 @@ export const HORSESHOE_GAP_PX = 12; // Outer container — flex column hosting the silhouette (panel + // header live inside) and the chat body below. `overflow: hidden` // clips the chat body's rounded top corners against the container's -// own edges; `background` paints the dark «void» behind the gap -// between silhouette and chat body when the drawer is open. +// own edges when the user-card rail is open and the chatBody margin- +// top void gap opens up; without this clip the rounded-corner cut +// areas would reveal whatever's behind the chat column instead of +// the horseshoe void colour painted by `containerStyle`. export const container = style({ position: 'relative', display: 'flex', @@ -70,11 +72,26 @@ export const panelViewport = style({ userSelect: 'none', }); +// Anchor at the TOP of `panelViewport` so the card slides downward as +// `panelViewport.height` grows: hero avatar visible first, info rows +// behind it, drag handle last. Previously `bottom: 0` made the handle +// reveal first and the hero last — which read as "dark rising from the +// chat header" once the silhouette also extended through the status-bar +// zone above. Trade-off: drag handle is hidden mid-drag, only visible +// when fully open; the panel surface stays drag-sensitive throughout. +// +// `padding-top: var(--vojo-safe-top)` keeps the user-card content (hero +// avatar + info rows) clear of the Android status-bar icons in the +// open state. The padding zone shows the silhouette's bg +// (`Background.Container`, #0d0e11) through panelContent's transparent +// background, so the status-bar zone reads as part of the dark user +// card when the panel is open. export const panelContent = style({ position: 'absolute', - bottom: 0, + top: 0, left: 0, right: 0, + paddingTop: 'var(--vojo-safe-top, 0px)', }); export const panelInner = style({ @@ -160,22 +177,46 @@ export const avatarFullFallback = style({ }); // === Header viewport === Holds the chat header as the silhouette's -// bottom child. Same CSS-grid `1fr → 0fr` row-track trick the legacy -// `headerWrap` used so the row animates smoothly without measuring -// the header's intrinsic height in JS. The inner div clips the -// header content as the track collapses. `userSelect: none` keeps a -// mouse drag-down-to-open gesture from racing native text-selection -// on the chat title — same rationale as on `panelViewport`. +// bottom child. Animated via an explicit `height` set inline (driven +// by `(1 - expandedFraction) * headerNaturalHeight`, where +// `headerNaturalHeight` is measured via `useLayoutEffect` + +// `ResizeObserver` on the inner block). The legacy +// `grid-template-rows: 1fr → 0fr` trick was abandoned because Folds +// `
` enforces a `min-height` that prevented the +// grid track from collapsing to 0 — leaving a light-blue strip at +// the bottom of the user card. `overflow: hidden` keeps the chat +// header content clipped while the viewport height interpolates. +// `userSelect: none` keeps a mouse drag-down-to-open gesture from +// racing native text-selection on the chat title — same rationale +// as on `panelViewport`. export const headerViewport = style({ - display: 'grid', flexShrink: 0, - willChange: 'grid-template-rows', + overflow: 'hidden', + willChange: 'height', userSelect: 'none', }); +// `padding-top: var(--vojo-safe-top)` pushes the chat header's content +// (back arrow, avatar, name) below the Android status-bar icons in +// edge-to-edge mode — `#root` no longer reserves the inset itself +// (src/index.css), so the chat header would otherwise land directly +// under the system icons. The padding zone is painted with +// `SurfaceVariant.Container` (the chat-header bg below it) so the +// visible strip against the status-bar icons reads as one continuous +// header band — no seam at the inset boundary. +// +// `box-sizing: border-box` is load-bearing for the height animation +// that `headerViewport` runs (see comment there): when the outer height +// reaches 0 px on full rail-open, the `padding-top` is included in the +// 0-tall box and the SurfaceVariant strip collapses with it. With +// `content-box` it would persist as a `var(--vojo-safe-top)`-tall band +// at the bottom of the user card. export const headerViewportInner = style({ + boxSizing: 'border-box', minHeight: 0, overflow: 'hidden', + paddingTop: 'var(--vojo-safe-top, 0px)', + backgroundColor: color.SurfaceVariant.Container, }); // === Chat body === Timeline + input below the silhouette. The diff --git a/src/app/features/room/RoomViewProfilePanel.tsx b/src/app/features/room/RoomViewProfilePanel.tsx index 97cac006..a29dd542 100644 --- a/src/app/features/room/RoomViewProfilePanel.tsx +++ b/src/app/features/room/RoomViewProfilePanel.tsx @@ -18,7 +18,7 @@ // `RoomViewProfileSidePanel`, which `Room.tsx` mounts as a sibling // of the chat column. -import React, { ReactNode, useEffect, useRef, useState } from 'react'; +import React, { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useAtomValue } from 'jotai'; import { config } from 'folds'; import FocusTrap from 'focus-trap-react'; @@ -38,6 +38,7 @@ import { getMxIdLocalPart, guessDmRoomUserId, mxcUrlToHttp } from '../../utils/m import { UserRoomProfile } from '../../components/user-profile/UserRoomProfile'; import { stopPropagation } from '../../utils/keyboard'; import colorMXID from '../../../util/colorMXID'; +import { VOJO_HORSESHOE_VOID_COLOR } from '../../styles/horseshoe'; import * as css from './RoomViewProfilePanel.css'; // Card height as a fraction of the viewport. @@ -128,6 +129,19 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps) const headerRef = useRef(null); const panelRef = useRef(null); + const headerInnerRef = useRef(null); + // Measured natural height of the chat-header inner block (incl. its + // `padding-top: var(--vojo-safe-top)`). Drives the explicit `height` + // animation on `headerViewport` — replaces the legacy `grid-template- + // rows: 1fr → 0fr` trick because Folds `
` enforces + // a `min-height` that prevents the grid track from collapsing fully + // to 0, leaving a light-blue strip at the bottom of the user card + // when the rail is fully open. Measured via `useLayoutEffect` so the + // first paint already uses the right value (no flicker on cold + // mount) and re-measured by ResizeObserver whenever the chat-header + // content reflows (e.g. online tag appears, env-inset changes on + // rotation). + const [headerNaturalHeight, setHeaderNaturalHeight] = useState(0); // Close profile when the room changes — atom is global state and // would otherwise carry the previous room's userId into this room. @@ -150,6 +164,25 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps) return () => window.removeEventListener('resize', onResize); }, []); + // Measure the chat header's natural height (incl. its safe-top padding). + // `scrollHeight` returns the content size even while the outer is + // constrained by our animated `height` — so we keep getting the right + // natural value across reflows. `useLayoutEffect` does the first + // measurement synchronously after DOM mutation, before paint, so the + // first frame already uses the measured value. + useLayoutEffect(() => { + const el = headerInnerRef.current; + if (!el) return undefined; + const measure = () => { + const next = el.scrollHeight; + if (next > 0) setHeaderNaturalHeight(next); + }; + measure(); + const ro = new ResizeObserver(measure); + ro.observe(el); + return () => ro.disconnect(); + }, []); + const open = !!profileState; const baseExpanded = open ? railHeightPx : 0; const expandedPx = drag @@ -372,7 +405,7 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps) : `height ${ANIMATION_MS}ms ${VAUL_EASING}`; const headerViewportTransition = isDragging ? 'none' - : `grid-template-rows ${ANIMATION_MS}ms ${VAUL_EASING}`; + : `height ${ANIMATION_MS}ms ${VAUL_EASING}`; const silhouetteTransition = isDragging ? 'none' : `border-bottom-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-bottom-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`; @@ -381,7 +414,7 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps) : `margin-top ${ANIMATION_MS}ms ${VAUL_EASING}, border-top-left-radius ${ANIMATION_MS}ms ${VAUL_EASING}, border-top-right-radius ${ANIMATION_MS}ms ${VAUL_EASING}`; const containerStyle: React.CSSProperties = { - backgroundColor: horseshoeActive ? '#090909' : undefined, + backgroundColor: horseshoeActive ? VOJO_HORSESHOE_VOID_COLOR : undefined, }; return ( @@ -482,15 +515,22 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps) ref={headerRef} className={css.headerViewport} style={{ - // Collapse the chat header to 0 height as the panel - // expands — same `1fr → 0fr` row-track trick the legacy - // layout used. CSS-grid track interpolation is animatable - // (unlike `height: auto`) and avoids measuring the - // header's intrinsic height in JS. As the row collapses, - // the silhouette's bottom edge moves from header-bottom - // up to panel-bottom; the rounded corners stay anchored - // there because they belong to the silhouette wrapper. - gridTemplateRows: `${1 - expandedFraction}fr`, + // Animate the chat header's height from its measured natural + // size down to 0 as the user-card panel expands. Linear scale + // by `(1 - expandedFraction)` so the panel and the header + // grow / shrink in lockstep. Falls back to `auto` until the + // first measurement lands (cold mount before useLayoutEffect + // runs) so the closed-state header isn't briefly squashed to + // 0. Replaces the legacy `grid-template-rows: 1fr → 0fr` + // trick because Folds `
` enforces a + // `min-height` that prevented the grid track from collapsing + // to 0, leaving a light-blue strip at the bottom of the + // user card. With explicit `height` + `overflow: hidden` on + // the viewport (see css), `height: 0` is forced honest. + height: + headerNaturalHeight > 0 + ? `${(1 - expandedFraction) * headerNaturalHeight}px` + : 'auto', transition: headerViewportTransition, // While the rail is mostly open the (collapsed) header // shouldn't intercept drag-back-to-open gestures — @@ -499,7 +539,9 @@ function MobileProfileHorseshoe({ header, children }: RoomViewProfilePanelProps) touchAction: headerDragEnabled ? 'pan-x' : undefined, }} > -
{header}
+
+ {header} +
diff --git a/src/app/features/room/ThreadDrawer.css.ts b/src/app/features/room/ThreadDrawer.css.ts index e7995430..9674ef64 100644 --- a/src/app/features/room/ThreadDrawer.css.ts +++ b/src/app/features/room/ThreadDrawer.css.ts @@ -19,6 +19,16 @@ export const ThreadDrawer = style({ }); // Mobile push: drawer occupies the whole content column, not a side pane. +// `padding-top: var(--vojo-safe-top)` keeps the drawer's `ThreadDrawerHeader` +// (back arrow + thread title + counter) clear of the Android status-bar +// icons. `#root` no longer reserves the inset itself (`src/index.css`), +// and the chat-column wrapper that hosts this drawer on mobile has no +// safe-top of its own (the silhouette in `RoomViewProfilePanel` owns the +// inset for the chat header — but the thread drawer mounts as a sibling +// and would otherwise land directly under the system icons). The desktop +// `ThreadDrawer` style sits inside the chat row that has `env()` left/right +// only, so it inherits no top inset and doesn't need this padding (and +// would just leave dead space below the chat-column-wrapper's chrome). export const ThreadDrawerMobile = style({ flexGrow: 1, width: '100%', @@ -27,6 +37,7 @@ export const ThreadDrawerMobile = style({ flexDirection: 'column', backgroundColor: color.Surface.Container, minHeight: 0, + paddingTop: 'var(--vojo-safe-top, 0px)', }); export const ThreadDrawerHeader = style({ diff --git a/src/app/pages/auth/styles.css.ts b/src/app/pages/auth/styles.css.ts index 35fd06aa..9551731a 100644 --- a/src/app/pages/auth/styles.css.ts +++ b/src/app/pages/auth/styles.css.ts @@ -18,6 +18,15 @@ export const AuthLayout = style({ // chat surface so cold start / auth read as one continuous canvas. background: '#0d0e11', color: '#e8e4df', + // Status-bar inset for native: `#root` no longer reserves it + // (src/index.css), so the AuthLayout backdrop now extends through the + // status-bar zone. Padding pushes the mascot / modal stack below the + // system icons. AuthLayout reads `#root`'s computed paddingTop into + // `padTop` (AuthLayout.tsx) for its anchor math; with that 0 the math + // sees the full body height and positions content against the local + // padding instead. + paddingTop: 'var(--vojo-safe-top, 0px)', + boxSizing: 'border-box', '@media': { 'only screen and (max-width: 768px)': { diff --git a/src/app/styles/global.css.ts b/src/app/styles/global.css.ts index 7b12a424..f578a87a 100644 --- a/src/app/styles/global.css.ts +++ b/src/app/styles/global.css.ts @@ -1,27 +1,49 @@ import { globalStyle } from '@vanilla-extract/css'; +import { color } from 'folds'; -// Vojo-owned safe-area background. Used by `body { background-color: ... }` -// in `src/index.css` to paint Android edge-to-edge cutout / nav-bar zones. -// Matches `Background.Container` from the Dawn palette in `src/colors.css.ts` -// (`#0d0e11`, canon DAWN.bg2). The safe-area zone reads as a continuation of -// the sidebar / nav panels — no visible color seam at the system-bar boundary. +// Android edge-to-edge top inset exposed as a plain CSS var so the +// top-anchored UIs that need it (PageNav inner column, +// `RoomViewProfilePanel.headerViewportInner` / `.panelContent`, +// `BotShell.Shell`, `AuthLayout`, `ThreadDrawerMobile`) can pad +// themselves down by exactly the status-bar height without +// re-evaluating `env(...)` at each call site. `#root` no longer +// reserves this inset itself (`src/index.css`); the chat surface / +// chat-list shell extend their own bg through the status-bar zone and +// push visible content below the system icons via this var. `Modal500` +// resets the var to `0px` inside the dialog subtree so internal +// headers don't double-pad. On web `env()` resolves to `0px`, so the +// var is a no-op there. // -// Why a custom var (and not `color.Background.Container`): -// 1. Folds emits its color tokens scoped to a theme class (e.g. `--oq6d070` -// lives inside the `.lightTheme` selector). Before useTheme appends a -// theme class to body, `var(--oq6d070)` resolves to its initial value -// and `background-color` falls back to transparent — system bars show -// through unpainted on Android edge-to-edge. A `:root`-level var here -// resolves from the very first paint. -// 2. `--oq6d070` is a folds@2.6 internal vanilla-extract debugId hash and -// will rotate on any folds upgrade. -// -// The hardcoded #0d0e11 in `index.css` is the matching fallback for the -// instant before this CSS file is parsed. -// -// See docs/plans/dm_1x1_redesign.md §6.6 / R13. +// Bottom inset is owned per-component: surfaces that anchor +// interactive content at the screen bottom and need 3-button-nav / +// home-indicator clearance read `env(safe-area-inset-bottom)` +// themselves (e.g. `SyncIndicator.css.ts`). The chat surface / +// composer extend flush to `body_bottom` by design. globalStyle(':root', { vars: { - '--vojo-safe-area-bg': '#0d0e11', + '--vojo-safe-top': 'env(safe-area-inset-top, 0px)', + }, +}); + +// Vojo-owned safe-area background. Used by `body { background-color: ... }` +// in `src/index.css` to paint the body region behind any `env()` cut-out +// zone (display notch, left/right inset) — defended in depth even though +// `#root` covers most of the layout edge-to-edge now. +// +// Bound to the folds `Background.Container` token (Dawn `#0d0e11` in +// dark; will reshade automatically when a light theme is added). The +// var lives on `body` rather than `:root` because folds attaches the +// theme class (`.dark-theme` / `.lightTheme`) to `document.body` — +// `var(--folds-token)` only resolves at scopes the theme class +// dominates. A `:root` host would freeze the var at the folds token's +// initial value (`transparent` in dark / `#fff` in light) and the +// safe-area would flash unpainted before `ThemeManager` mounts. Hosting +// on body keeps the var resolution in lockstep with whatever theme +// `ThemeManager` has applied; the literal `#0d0e11` fallback in +// `index.css`'s `body { background-color }` declaration covers the +// cold-start window before this stylesheet parses. +globalStyle('body', { + vars: { + '--vojo-safe-area-bg': color.Background.Container, }, }); diff --git a/src/index.css b/src/index.css index 656e09bf..026fe438 100644 --- a/src/index.css +++ b/src/index.css @@ -60,15 +60,13 @@ body { height: 100%; display: flex; flex-direction: column; - /* Top / left / right insets keep app chrome clear of system bars and - display cutouts. Bottom inset is intentionally zero so the chat - surface (RoomView's ``) and the chat-list shell (PageRoot's - Background-coloured outer Box) extend all the way to the screen - edge — same edge-to-edge pattern WhatsApp / Telegram use. Without - this, the bottom env zone showed a different colour from the - painted surface, and the composer's wrap-padding-bottom kept - overlapping the gesture-pill area, visually "eating" the form. */ - padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left); + /* Top & bottom insets are intentionally zero: each screen extends + its own bg through the system-bar zones (status bar at top, gesture + pill at bottom). Top-anchored UI pads itself down by `--vojo-safe-top` + (see `app/styles/global.css.ts`) — covered: PageHeader, PageNavHeader + (chat / chat-list headers via recipe), AuthLayout root. Side insets + stay so app chrome clears display cutouts on devices with one. */ + padding: 0 env(safe-area-inset-right) 0 env(safe-area-inset-left); } *,