/* ───────── js/main.js ───────── Dynamically builds the project grid + injects about/contact/crew copy. -----------------------------------------------------------------*/ // Page loader overlay (function(){ const overlay=document.createElement('div'); overlay.id='loader-overlay'; overlay.innerHTML='
'; document.addEventListener('DOMContentLoaded',()=>document.body.appendChild(overlay)); window.addEventListener('load',()=>{ overlay.classList.add('hide'); setTimeout(()=>overlay.remove(),700); }); })(); // Mobile 100‑vh fix function setVh(){document.documentElement.style.setProperty('--vh',window.innerHeight*0.01);}setVh();window.addEventListener('resize',setVh); // Header height fix function setHeaderOffset(){ const headerEl = document.querySelector('header.nav'); if(!headerEl) return; // Header not in DOM yet – abort and wait const h = headerEl.offsetHeight; document.documentElement.style.setProperty('--header-h', `${h}px`); } setHeaderOffset(); // Run once more after a short delay to catch late-injected nav fragments setTimeout(setHeaderOffset, 500); window.addEventListener('resize', setHeaderOffset); // Fade‑in intersection observer const reveal = new IntersectionObserver((entries,o)=>{ entries.forEach(e=>{if(e.isIntersecting){e.target.classList.add('loaded');o.unobserve(e.target);}}); },{threshold:.3}); // Grab the grid once so every scope has it const grid = document.getElementById('projects'); // Only run project-grid logic on pages that actually have the grid element if(grid){ // Build card element function card({id,title,thumbnail,size='small'}){ const a = document.createElement('a'); a.href = `project.html?id=${encodeURIComponent(id)}`; a.className = 'item'; a.dataset.span = size; // <— MAGIC LINE a.innerHTML = `

${title}

`; reveal.observe(a); return a; } // Load manifest fetch('projects/projects.json') .then(r=>{if(!r.ok)throw new Error('manifest missing');return r.json();}) .then(list=>{ if(!Array.isArray(list))throw new Error('manifest must be an array'); list.forEach(p=>grid.appendChild(card(p))); }) .catch(err=>{ console.error('Manifest issue:',err.message); grid.innerHTML=`

No projects manifest found.

`; }); } // Inject About & Contact copy ['about','contact'].forEach(key=>{ const el=document.getElementById(key); if(!el)return; fetch(`written/${key}_us.txt`) .then(r=>r.ok?r.text():Promise.reject()) .then(txt=>{ el.innerHTML=`

${key}

${txt.replace(/\n/g,'
')}

`; }) .catch(()=>{}); }); // Inject Crew copy ['crew'].forEach(key=>{ const el=document.getElementById(key); if(!el)return; fetch(`written/${key}.txt`) .then(r=>r.ok?r.text():Promise.reject()) .then(txt=>{ el.innerHTML=`

${txt.replace(/\n/g,'
')}

`; }) .catch(()=>{}); }); // ───────── SIMPLE PARALLAX HANDLER ───────── // Applies translateY based on scroll position to any element with a data-speed attribute. document.addEventListener('DOMContentLoaded', () => { const layers = Array.from(document.querySelectorAll('[data-speed]')); if(!layers.length) return; const update = () => { const scrollY = window.scrollY; layers.forEach(el => { const speed = parseFloat(el.dataset.speed) || 0; // Negative so layers move opposite to scroll for depth illusion el.style.transform = `translateY(${ -scrollY * speed }px)`; }); }; update(); window.addEventListener('scroll', update, { passive: true }); window.addEventListener('resize', update, { passive: true }); });