diff --git a/apps/website/assets/css/voyage-layout.css b/apps/website/assets/css/voyage-layout.css index 5aa46e0..f4da441 100644 --- a/apps/website/assets/css/voyage-layout.css +++ b/apps/website/assets/css/voyage-layout.css @@ -37,7 +37,8 @@ /* Safe area helpers */ --safe-area-top: env(safe-area-inset-top, 0px); --safe-area-bottom: env(safe-area-inset-bottom, 0px); - --initial-safe-area-top: env(safe-area-inset-top, 0px); + --initial-safe-area-top: 0px; + --safe-area-cover-top: max(var(--safe-area-top), var(--initial-safe-area-top)); } @@ -228,17 +229,16 @@ html { background: rgba(255, 255, 255, 0); backdrop-filter: blur(0); transition: background 0.3s ease, color 0.3s ease, box-shadow 0.3s ease, backdrop-filter 0.3s ease; - --nav-safe-area-top: var(--safe-area-top); - --nav-padding-top: calc(var(--space-md) + var(--nav-safe-area-top)); - --nav-padding-bottom: var(--space-md); - padding: var(--nav-padding-top) 0 var(--nav-padding-bottom) 0; + --nav-safe-area-top: var(--safe-area-cover-top); + --nav-padding-top-base: var(--space-md); + --nav-padding-bottom-base: var(--space-md); + padding: calc(var(--nav-padding-top-base) + var(--nav-safe-area-top)) 0 var(--nav-padding-bottom-base) 0; } @supports (top: constant(safe-area-inset-top)) { :root { --safe-area-top: constant(safe-area-inset-top); --safe-area-bottom: constant(safe-area-inset-bottom); - --initial-safe-area-top: constant(safe-area-inset-top); } } @@ -246,16 +246,15 @@ html { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - --nav-safe-area-top: var(--initial-safe-area-top); - --nav-padding-top: calc(var(--space-sm) + var(--nav-safe-area-top)); - --nav-padding-bottom: var(--space-sm); - padding: var(--nav-padding-top) 0 var(--nav-padding-bottom) 0; + --nav-padding-top-base: var(--space-sm); + --nav-padding-bottom-base: var(--space-sm); } @media (max-width: 768px) { - .voyage-nav { - --nav-padding-top: calc(var(--space-sm) + var(--nav-safe-area-top)); - --nav-padding-bottom: var(--space-sm); + .voyage-nav, + .voyage-nav.scrolled { + --nav-padding-top-base: var(--space-sm); + --nav-padding-bottom-base: var(--space-sm); } } @@ -448,17 +447,20 @@ html { .hero-video-container { position: fixed; - inset: 0; - height: calc(100lvh + var(--safe-area-top)); - transform: translateY(calc(-1 * var(--safe-area-top))); - z-index: 0; /* Behind content but above page background */ + left: 0; + right: 0; + --hero-safe-area-top: var(--safe-area-cover-top); + top: calc(-1 * var(--hero-safe-area-top)); + height: calc(100lvh + var(--hero-safe-area-top)); + transform: translate3d(0, var(--parallax-offset, 0px), 0); + z-index: -1; /* Behind content */ pointer-events: none; } /* Fallback for browsers without lvh support */ @supports not (height: 100lvh) { .hero-video-container { - height: calc(100vh + var(--safe-area-top)); + height: calc(100vh + var(--hero-safe-area-top)); } } @@ -501,7 +503,7 @@ html { align-items: center; text-align: center; /* Apply safe area padding HERE to protect content from being obscured */ - padding: max(env(safe-area-inset-top), var(--space-md)) var(--space-md) max(env(safe-area-inset-bottom), var(--space-md)); + padding: max(var(--safe-area-cover-top), var(--space-md)) var(--space-md) max(var(--safe-area-bottom), var(--space-md)); } .trust-badge { diff --git a/apps/website/components/AppNavbar.vue b/apps/website/components/AppNavbar.vue index 0804a31..53b4687 100644 --- a/apps/website/components/AppNavbar.vue +++ b/apps/website/components/AppNavbar.vue @@ -57,16 +57,73 @@ const logoAlt = computed(() => isScrolled.value ? 'Harbor Smith Navy Logo' : 'Harbor Smith White Logo' ) +let maxSafeAreaTop = 0 + +const measureSafeAreaTop = () => { + if (typeof window === 'undefined') { + return 0 + } + + const viewport = window.visualViewport + const viewportInset = viewport?.offsetTop ?? 0 + let envInset = 0 + let legacyInset = 0 + + if (document.body) { + const probe = document.createElement('div') + probe.style.cssText = [ + 'position:absolute', + 'top:0', + 'left:0', + 'width:0', + 'height:env(safe-area-inset-top, 0px)', + 'pointer-events:none', + 'visibility:hidden' + ].join(';') + + document.body.appendChild(probe) + envInset = probe.getBoundingClientRect().height || 0 + + probe.style.height = 'constant(safe-area-inset-top)' + legacyInset = probe.getBoundingClientRect().height || 0 + document.body.removeChild(probe) + } + + return Math.max(0, viewportInset, envInset, legacyInset) +} + const setInitialSafeAreaTop = () => { if (typeof window === 'undefined') { return } const root = document.documentElement - const styles = getComputedStyle(root) - const safeAreaTop = styles.getPropertyValue('--safe-area-top').trim() + const measured = measureSafeAreaTop() - root.style.setProperty('--initial-safe-area-top', safeAreaTop || '0px') + if (measured > maxSafeAreaTop) { + maxSafeAreaTop = measured + root.style.setProperty('--initial-safe-area-top', `${measured}px`) + } +} + +const resetSafeAreaTop = () => { + if (typeof window === 'undefined') { + return + } + + maxSafeAreaTop = 0 + document.documentElement.style.setProperty('--initial-safe-area-top', '0px') + const remeasure = () => { + setInitialSafeAreaTop() + } + + if (typeof requestAnimationFrame === 'function') { + requestAnimationFrame(() => { + requestAnimationFrame(remeasure) + }) + } else { + setTimeout(remeasure, 50) + } } const handleScroll = () => { @@ -88,9 +145,13 @@ const scrollToTop = () => { onMounted(() => { setInitialSafeAreaTop() + setTimeout(() => { + setInitialSafeAreaTop() + }, 100) handleScroll() window.addEventListener('scroll', handleScroll, { passive: true }) - window.addEventListener('orientationchange', setInitialSafeAreaTop) + window.addEventListener('orientationchange', resetSafeAreaTop) + window.addEventListener('resize', setInitialSafeAreaTop) if (window.visualViewport) { window.visualViewport.addEventListener('resize', setInitialSafeAreaTop) @@ -98,11 +159,14 @@ onMounted(() => { }) onBeforeUnmount(() => { window.removeEventListener('scroll', handleScroll) - window.removeEventListener('orientationchange', setInitialSafeAreaTop) + window.removeEventListener('orientationchange', resetSafeAreaTop) + window.removeEventListener('resize', setInitialSafeAreaTop) if (window.visualViewport) { window.visualViewport.removeEventListener('resize', setInitialSafeAreaTop) } + + maxSafeAreaTop = 0 }) diff --git a/apps/website/composables/useParallax.js b/apps/website/composables/useParallax.js index 182962e..09daa74 100644 --- a/apps/website/composables/useParallax.js +++ b/apps/website/composables/useParallax.js @@ -1,18 +1,27 @@ import { ref, onMounted, onBeforeUnmount } from 'vue' export const useParallax = (elementRef, speed = 0.5) => { - const transform = ref('') + const offset = ref(0) let ticking = false + let isActive = true + + const updateOffset = (value) => { + offset.value = value + if (elementRef.value) { + elementRef.value.style.setProperty('--parallax-offset', `${value}px`) + } + } const handleScroll = () => { + if (!isActive || !elementRef.value) { + return + } + if (!ticking) { window.requestAnimationFrame(() => { - if (elementRef.value) { - const scrolled = window.pageYOffset - const rate = scrolled * speed - transform.value = `translateY(${rate}px)` - elementRef.value.style.transform = transform.value - } + const scrolled = window.pageYOffset + const rate = scrolled * speed + updateOffset(rate) ticking = false }) ticking = true @@ -20,17 +29,22 @@ export const useParallax = (elementRef, speed = 0.5) => { } onMounted(() => { - // Add will-change for performance - if (elementRef.value) { - elementRef.value.style.willChange = 'transform' + if (!elementRef.value) { + return } - // Check for reduced motion preference - const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + elementRef.value.style.willChange = 'transform' - if (!prefersReducedMotion) { + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches + const isNarrowViewport = window.innerWidth <= 768 + + isActive = !prefersReducedMotion && !isNarrowViewport + + if (isActive) { window.addEventListener('scroll', handleScroll, { passive: true }) - handleScroll() // Initial calculation + handleScroll() + } else { + updateOffset(0) } }) @@ -38,10 +52,11 @@ export const useParallax = (elementRef, speed = 0.5) => { window.removeEventListener('scroll', handleScroll) if (elementRef.value) { elementRef.value.style.willChange = 'auto' + elementRef.value.style.removeProperty('--parallax-offset') } }) return { - transform + transform: offset } -} \ No newline at end of file +}