// Seasonal imagery, ambient audio, cursor-reactive parallax, video hero. // All features are additive — degrades gracefully without JS. const { useState, useEffect, useRef, useMemo } = React; // ─── Season-specific imagery bank // Each season has its own hero video, hero-right still, ambient bands, program bgs. // Videos: Coverr / Pexels CC-licensed nature loops (transcoded to mp4). // If you have custom brand video later, swap the urls in SEASONS. const SEASONS = { spring: { label: 'Spring', accent: '#A8B87A', glow: 'rgba(168, 184, 122, 0.12)', // Spring: Ken Burns still (video unreliable for vibrant spring foliage) — lush meadow with wildflowers heroVideo: null, heroPoster: 'https://images.unsplash.com/photo-1490750967868-88aa4486c946?w=1800&q=80&auto=format&fit=crop', heroRight: 'https://images.unsplash.com/photo-1465146344425-f00d5f5c8f07?w=1400&q=80&auto=format&fit=crop', band1: 'https://images.unsplash.com/photo-1490750967868-88aa4486c946?w=1800&q=80&auto=format&fit=crop', band2: 'https://images.unsplash.com/photo-1501854140801-50d01698950b?w=1800&q=80&auto=format&fit=crop', finalCta: 'https://images.unsplash.com/photo-1438565434616-3ef039228b15?w=1800&q=80&auto=format&fit=crop', heroEyebrow: 'NH · VT · MA · CT · NE · NY · MD', heroCaption: 'Spring 2026', }, summer: { label: 'Summer', accent: '#C9A875', glow: 'rgba(201, 168, 117, 0.10)', heroVideo: 'https://assets.mixkit.co/videos/529/529-720.mp4', heroPoster: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=1800&q=80&auto=format&fit=crop', heroRight: 'https://images.unsplash.com/photo-1511497584788-876760111969?w=1400&q=80&auto=format&fit=crop', band1: 'https://images.unsplash.com/photo-1518495973542-4542c06a5843?w=1800&q=80&auto=format&fit=crop', band2: 'https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?w=1800&q=80&auto=format&fit=crop', finalCta: 'https://images.unsplash.com/photo-1483728642387-6c3bdd6c93e5?w=1800&q=80&auto=format&fit=crop', heroEyebrow: 'NH · VT · MA · CT · NE · NY · MD', heroCaption: 'Summer 2026', }, autumn: { label: 'Autumn', accent: '#D49261', glow: 'rgba(212, 146, 97, 0.12)', // Autumn: Ken Burns still of peak NE foliage — vibrant red/orange/yellow canopy heroVideo: null, heroPoster: 'https://images.unsplash.com/photo-1507783548227-544c3b8fc065?w=1800&q=80&auto=format&fit=crop', heroRight: 'https://images.unsplash.com/photo-1506126613408-eca07ce68773?w=1400&q=80&auto=format&fit=crop', band1: 'https://images.unsplash.com/photo-1507041957456-9c397ce39c97?w=1800&q=80&auto=format&fit=crop', band2: 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=1800&q=80&auto=format&fit=crop', finalCta: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1800&q=80&auto=format&fit=crop', heroEyebrow: 'NH · VT · MA · CT · NE · NY · MD', heroCaption: 'Autumn 2026', }, winter: { label: 'Winter', accent: '#B8C4D4', glow: 'rgba(184, 196, 212, 0.10)', heroVideo: 'https://assets.mixkit.co/videos/1172/1172-720.mp4', heroPoster: 'https://images.unsplash.com/photo-1418985991508-e47386d96a71?w=1800&q=80&auto=format&fit=crop', heroRight: 'https://images.unsplash.com/photo-1482192505345-5655af888cc4?w=1400&q=80&auto=format&fit=crop', band1: 'https://images.unsplash.com/photo-1483921020237-2ff51e8e4b22?w=1800&q=80&auto=format&fit=crop', band2: 'https://images.unsplash.com/photo-1418985991508-e47386d96a71?w=1800&q=80&auto=format&fit=crop', finalCta: 'https://images.unsplash.com/photo-1491002052546-bf38f186af56?w=1800&q=80&auto=format&fit=crop', heroEyebrow: 'NH · VT · MA · CT · NE · NY · MD', heroCaption: 'Winter 2026', }, }; // Auto-detect season from current month (Northern Hemisphere). const autoSeason = () => { const m = new Date().getMonth(); // 0..11 if (m >= 2 && m <= 4) return 'spring'; if (m >= 5 && m <= 7) return 'summer'; if (m >= 8 && m <= 10) return 'autumn'; return 'winter'; }; // ─── Season context: drives --season-accent variable and imagery bank. function useSeasonEffect(season) { useEffect(() => { const s = SEASONS[season] || SEASONS.summer; document.documentElement.style.setProperty('--season-accent', s.accent); document.documentElement.style.setProperty('--season-glow', s.glow); document.documentElement.setAttribute('data-season', season); }, [season]); } // ─── Cursor-reactive tilt: subtle mouse-follow transform on a target element. // Keep subtle (4–8deg max) to avoid motion sickness. function useCursorTilt(ref, { max = 6, scale = 1.04 } = {}) { useEffect(() => { const el = ref.current; if (!el) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; let raf = null; let tx = 0, ty = 0, targetX = 0, targetY = 0; const onMove = (e) => { const r = el.getBoundingClientRect(); const cx = r.left + r.width / 2; const cy = r.top + r.height / 2; targetX = ((e.clientX - cx) / (r.width / 2)) * max; targetY = ((e.clientY - cy) / (r.height / 2)) * max; schedule(); }; const onLeave = () => { targetX = 0; targetY = 0; schedule(); }; const schedule = () => { if (raf) return; raf = requestAnimationFrame(tick); }; const tick = () => { tx += (targetX - tx) * 0.08; ty += (targetY - ty) * 0.08; el.style.transform = `perspective(1200px) rotateY(${tx}deg) rotateX(${-ty}deg) scale(${scale})`; if (Math.abs(tx - targetX) > 0.05 || Math.abs(ty - targetY) > 0.05) { raf = requestAnimationFrame(tick); } else { raf = null; } }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseout', onLeave); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseout', onLeave); if (raf) cancelAnimationFrame(raf); if (el) el.style.transform = ''; }; }, [ref, max, scale]); } // ─── Ambient audio: forest/stream ASMR loop. Off by default, user-toggle only. // Per Chrome autoplay policy, audio requires user gesture — we only start on click. const AudioToggle = ({ src = 'https://cdn.pixabay.com/download/audio/2022/03/15/audio_8a6d62be03.mp3', label = 'Ambient' }) => { const [on, setOn] = useState(false); const [ready, setReady] = useState(false); const audioRef = useRef(null); useEffect(() => { if (!audioRef.current) { const a = new Audio(src); a.loop = true; a.volume = 0; a.preload = 'none'; audioRef.current = a; a.addEventListener('canplay', () => setReady(true)); a.addEventListener('error', () => setReady(false)); } return () => { if (audioRef.current) { try { audioRef.current.pause(); } catch(e) {} } }; }, [src]); const toggle = async () => { const a = audioRef.current; if (!a) return; if (on) { fadeVol(a, 0, 600, () => { try { a.pause(); } catch(e) {} }); setOn(false); } else { try { a.currentTime = a.currentTime || 0; await a.play(); fadeVol(a, 0.35, 1400); setOn(true); } catch (err) { console.warn('audio play failed', err); } } }; return ( ); }; function fadeVol(audio, target, ms, done) { const start = audio.volume; const t0 = performance.now(); const step = (now) => { const t = Math.min(1, (now - t0) / ms); audio.volume = start + (target - start) * t; if (t < 1) requestAnimationFrame(step); else if (done) done(); }; requestAnimationFrame(step); } // ─── HeroVideo: auto-playing looping nature video with poster fallback. // Uses muted+playsInline for autoplay; respects reduced-motion (shows poster only). const HeroVideo = ({ src, poster, opacity = 0.55 }) => { const ref = useRef(null); const [loaded, setLoaded] = useState(false); const [failed, setFailed] = useState(false); const reducedMotion = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; useEffect(() => { const v = ref.current; if (!v || reducedMotion) return; const onCanPlay = () => setLoaded(true); const onErr = () => setFailed(true); v.addEventListener('canplay', onCanPlay); v.addEventListener('error', onErr); // Force-play in case autoplay is blocked but muted v.play().catch(() => {}); return () => { v.removeEventListener('canplay', onCanPlay); v.removeEventListener('error', onErr); }; }, [src, reducedMotion]); return (
); }; Object.assign(window, { SEASONS, autoSeason, useSeasonEffect, useCursorTilt, AudioToggle, HeroVideo, });