diff --git a/src/app/components/stream-header/StreamHeader.css.ts b/src/app/components/stream-header/StreamHeader.css.ts
index 6a5cff1d..b2303c47 100644
--- a/src/app/components/stream-header/StreamHeader.css.ts
+++ b/src/app/components/stream-header/StreamHeader.css.ts
@@ -228,8 +228,17 @@ export const handleBar = style({
// up over the inline form. The DirectSelfRow ending up immediately
// above the keyboard would block the user's view of the form they're
// typing into.
+//
+// `paddingBottom: env(safe-area-inset-bottom)` lifts DirectSelfRow /
+// WorkspaceFooter above Android 3-button nav in edge-to-edge mode.
+// The inset zone paints in the curtain's `Background.Container` bg
+// (curtain has `overflow: hidden`), so the system bar reads as a
+// continuation of the curtain tone — no visible seam. The keyboard
+// `height: 0; overflow: hidden` collapse above also clips the
+// padding region — keyboard handling preserved.
export const bottomPinnedSlot = style({
flexShrink: 0,
+ paddingBottom: 'env(safe-area-inset-bottom, 0px)',
});
// Segment button (Direct / Channels / Bots).
diff --git a/src/app/features/room/RoomView.css.ts b/src/app/features/room/RoomView.css.ts
index 03085920..d9c1020c 100644
--- a/src/app/features/room/RoomView.css.ts
+++ b/src/app/features/room/RoomView.css.ts
@@ -1,10 +1,6 @@
import { globalStyle, style } from '@vanilla-extract/css';
import { color, toRem } from 'folds';
-import {
- Editor,
- EditorTextarea,
- EditorTextareaScroll,
-} from '../../components/editor/Editor.css';
+import { Editor, EditorTextarea, EditorTextareaScroll } from '../../components/editor/Editor.css';
import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
// Main chat composer — Dawn canon (stream-v2-dawn.jsx line 285-307): a
@@ -28,6 +24,13 @@ export const ChatComposer = style({});
// slide/fade transition driven by the `data-hidden` attribute set from
// React state. CSS class (not inline `transition`) so the
// `prefers-reduced-motion` media query can disable the motion.
+//
+// `env(safe-area-inset-bottom)` for Android 3-button-nav clearance lives
+// on the INNER `ChatComposer` div (see `RoomView.tsx`), not here. The
+// wrapper is the `ResizeObserver` target for `composerHeight`, and
+// `entry.contentRect` returns content-box (excludes the observed node's
+// own padding); padding here would inflate border-box without growing
+// the reported height, letting the last message slip behind the inset.
export const ComposerOverlay = style({
position: 'absolute',
left: 0,
diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx
index 4b4b0198..3428055e 100644
--- a/src/app/features/room/RoomView.tsx
+++ b/src/app/features/room/RoomView.tsx
@@ -174,8 +174,13 @@ export function RoomView({ eventId }: { eventId?: string }) {
>
{tombstoneEvent ? (
diff --git a/src/app/features/room/RoomViewTyping.css.ts b/src/app/features/room/RoomViewTyping.css.ts
index 7c01249d..467aa514 100644
--- a/src/app/features/room/RoomViewTyping.css.ts
+++ b/src/app/features/room/RoomViewTyping.css.ts
@@ -14,6 +14,12 @@ export const RoomViewTyping = style([
DefaultReset,
{
padding: `0 ${config.space.S500}`,
+ // Lift typing text above the Android 3-button nav for the
+ // composer-hidden window (thread drawer open / scroll-hide). The
+ // main composer sits at `bottom: 0` with its own inset and fully
+ // covers this strip at rest, so the duplicate inset is invisible
+ // in normal flow.
+ paddingBottom: 'env(safe-area-inset-bottom, 0px)',
width: '100%',
backgroundColor: color.SurfaceVariant.Container,
color: color.Surface.OnContainer,
diff --git a/src/app/features/room/ThreadDrawer.css.ts b/src/app/features/room/ThreadDrawer.css.ts
index 1144749e..a4572469 100644
--- a/src/app/features/room/ThreadDrawer.css.ts
+++ b/src/app/features/room/ThreadDrawer.css.ts
@@ -1,9 +1,6 @@
import { style } from '@vanilla-extract/css';
import { color, config, toRem } from 'folds';
-import {
- VOJO_HORSESHOE_GAP_PX,
- VOJO_HORSESHOE_RADIUS_PX,
-} from '../../styles/horseshoe';
+import { VOJO_HORSESHOE_GAP_PX, VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
// Desktop wrapper for the resizable thread drawer. Sizing and the
// absolutely-positioned resize handle live here; the inner aside
@@ -231,9 +228,15 @@ export const ThreadCounterText = style({
// reapplied to the inner wrap in `ThreadDrawer.tsx` so the same
// `globalStyle` rules (`Surface.Container` bg, 32px radius, dark
// touch-hover gate) reach the Editor inside.
+//
+// Bottom side adds `env(safe-area-inset-bottom)` so the Send button
+// clears Android 3-button nav in edge-to-edge mode, mirroring the
+// main `ChatComposer` in `RoomView.tsx`.
export const ThreadComposer = style({
flexShrink: 0,
- padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} ${toRem(VOJO_HORSESHOE_GAP_PX)}`,
+ padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} calc(${toRem(
+ VOJO_HORSESHOE_GAP_PX
+ )} + env(safe-area-inset-bottom, 0px))`,
});
// Bubble chrome itself lives in `Channel.css.ts` and applies via the
diff --git a/src/app/styles/global.css.ts b/src/app/styles/global.css.ts
index 409554b0..13852b23 100644
--- a/src/app/styles/global.css.ts
+++ b/src/app/styles/global.css.ts
@@ -17,8 +17,16 @@ import { color } from 'folds';
// 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.
+// themselves (e.g. `SyncIndicator.css.ts`,
+// `StreamHeader.bottomPinnedSlot` for DirectSelfRow / WorkspaceFooter,
+// `RoomView.tsx` ChatComposer inner div, the `panelContent` rules in
+// MobileSettings / ChannelsWorkspace / MobileMediaViewer horseshoes).
+// The chat overlay wrapper (`ComposerOverlay`) itself stays `bottom: 0`
+// flush — the inset lives on the inner card padding so
+// `ResizeObserver.contentRect` keeps `composerHeight` in sync with
+// the visible overlay height for `RoomTimeline.bottomOverlayHeight`.
+// Bot widgets are responsible for their own gesture-pill clearance
+// (see `BotShell.css.ts::Shell`).
globalStyle(':root', {
vars: {
'--vojo-safe-top': 'env(safe-area-inset-top, 0px)',