diff --git a/src/app/coach/conversations/page.tsx b/src/app/coach/conversations/page.tsx
index 32f0e0ce8..c12927c9c 100644
--- a/src/app/coach/conversations/page.tsx
+++ b/src/app/coach/conversations/page.tsx
@@ -331,7 +331,7 @@ export default function CoachConversationsPage() {
// ~50px on notched devices; restored to match exactly. v1.42 — the
// shell now takes that inset above the top bar at every width, so
// the desktop height subtracts it too, and `/coach/plans` matches.
- className="bg-background -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6 md:h-[calc(100dvh-4rem-env(safe-area-inset-top,0px))]"
+ className="bg-background shell-desktop:h-[calc(100dvh-4rem-env(safe-area-inset-top,0px))] -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6"
>
diff --git a/src/app/coach/plans/page.tsx b/src/app/coach/plans/page.tsx
index b02fddcdc..4df3e06b7 100644
--- a/src/app/coach/plans/page.tsx
+++ b/src/app/coach/plans/page.tsx
@@ -296,7 +296,7 @@ export default function CoachPlansPage() {
// mobile-only BottomNav band). The status-bar inset of an installed
// app comes off as well, at every width: the shell takes it above
// the top bar (`shell-safe-area`).
- className="bg-background -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6 md:h-[calc(100dvh-4rem-env(safe-area-inset-top,0px))]"
+ className="bg-background shell-desktop:h-[calc(100dvh-4rem-env(safe-area-inset-top,0px))] -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-top,0px)-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6"
>
diff --git a/src/app/globals.css b/src/app/globals.css
index 63361cc9f..81e798468 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -58,6 +58,30 @@
}
}
+/*
+ * `shell-desktop:` / `shell-mobile:` — which app shell the viewport gets.
+ * The desktop rail (sidebar, no bottom nav) from `md` up, EXCEPT a touch
+ * screen at most 520 px tall: a phone held sideways is 844–932 px wide but
+ * only 390–430 px tall, and the rail plus the top bar left it a sliver of
+ * content and no thumb-reachable navigation. It keeps the phone shell (top
+ * bar, bottom nav, capture button) instead. Everything that switches
+ * between the two shells keys on these, never on a bare `md:`, so the bars
+ * cannot disagree about which shell is showing.
+ */
+@custom-variant shell-desktop {
+ @media (min-width: 48rem) and (pointer: fine),
+ (min-width: 48rem) and (pointer: none),
+ (min-width: 48rem) and (min-height: 521px) {
+ @slot;
+ }
+}
+
+@custom-variant shell-mobile {
+ @media (max-width: 47.98rem), (pointer: coarse) and (max-height: 520px) {
+ @slot;
+ }
+}
+
@theme inline {
/* Extra-small breakpoint (30rem / 480px) for the narrowest phones — the
default scale starts at sm (640px), which leaves a gap between an iPhone SE
diff --git a/src/components/data-list/selection-action-bar.tsx b/src/components/data-list/selection-action-bar.tsx
index 5a275913a..f7bf6a1e2 100644
--- a/src/components/data-list/selection-action-bar.tsx
+++ b/src/components/data-list/selection-action-bar.tsx
@@ -82,7 +82,7 @@ export function SelectionActionBar({
// an `sm:` drop sat the bar behind the still-visible nav 640-767px.
// z-30 keeps the bar above page content without needing to beat the
// (now-cleared) nav's z-50.
- className="bg-card border-border sticky bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] z-30 mt-2 flex items-center justify-between gap-2 rounded-lg border p-2 shadow-lg md:bottom-4"
+ className="bg-card border-border sticky bottom-[calc(env(safe-area-inset-bottom,0px)+5rem)] z-30 mt-2 flex items-center justify-between gap-2 rounded-lg border p-2 shadow-lg shell-desktop:bottom-4"
>