diff --git a/client/src/components/Layout/Navbar.tsx b/client/src/components/Layout/Navbar.tsx index 1933982c..a8ec8f5c 100644 --- a/client/src/components/Layout/Navbar.tsx +++ b/client/src/components/Layout/Navbar.tsx @@ -89,6 +89,7 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: touchAction: 'manipulation', paddingTop: 'env(safe-area-inset-top, 0px)', height: 'var(--nav-h)', + top: 'var(--offline-banner-h, 0px)', transition: 'background 240ms cubic-bezier(0.23,1,0.32,1), backdrop-filter 240ms cubic-bezier(0.23,1,0.32,1), box-shadow 240ms cubic-bezier(0.23,1,0.32,1)', }} className="hidden md:flex items-center px-4 gap-4 fixed top-0 left-0 right-0 z-[200]"> {/* Left side */} diff --git a/client/src/components/Layout/OfflineBanner.tsx b/client/src/components/Layout/OfflineBanner.tsx index 04b58cf1..5c3b8010 100644 --- a/client/src/components/Layout/OfflineBanner.tsx +++ b/client/src/components/Layout/OfflineBanner.tsx @@ -40,6 +40,22 @@ export default function OfflineBanner(): React.ReactElement | null { }, []) const hidden = isOnline && pendingCount === 0 + + // When the banner is visible, reserve space at the top of the page so it + // doesn't cover the nav/header. Uses a CSS var on so we can offset + // via a global `body` rule instead of rewiring every layout. + useEffect(() => { + const root = document.documentElement + if (hidden) { + root.style.removeProperty('--offline-banner-h') + } else { + // 32px for icon+text row + the top safe-area inset that the banner adds + // in its own padding. Kept in one place so it's easy to tweak. + root.style.setProperty('--offline-banner-h', 'calc(env(safe-area-inset-top, 0px) + 32px)') + } + return () => { root.style.removeProperty('--offline-banner-h') } + }, [hidden]) + if (hidden) return null const offline = !isOnline diff --git a/client/src/index.css b/client/src/index.css index 4332ffdc..e67665c8 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -431,6 +431,8 @@ input[type="number"], input[type="time"], input[type="date"], input[type="dateti --safe-top: env(safe-area-inset-top, 0px); --nav-h: 0px; --bottom-nav-h: 0px; + /* Set by OfflineBanner when it's visible so body can reserve space. */ + --offline-banner-h: 0px; --font-system: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif; --sp-1: 4px; --sp-2: 8px; @@ -536,7 +538,9 @@ body { font-family: var(--font-system); background-color: var(--bg-primary); color: var(--text-primary); - transition: background-color 0.2s, color 0.2s; + transition: background-color 0.2s, color 0.2s, padding-top 0.15s ease; + /* Reserve space when OfflineBanner is visible; 0 when online. */ + padding-top: var(--offline-banner-h, 0px); } /* ── Marker cluster custom styling ────────────── */