New Driveway Company logo New Driveway Company Driveway Visualizer

Visualise Your New Kitchen

Upload a photo and preview changes in 60 seconds.

How it works

1) Upload Step 1
Upload an image of your kitchen.
2) Describe Step 2
Tell us what to change.
3) See it Done Step 3
Get a photorealistic result.
/************* STATE *************/ let currentFile = null; let originalImageUrl = null; let letterboxedDataUrl = null; let origW = 0, origH = 0, scale = 1, finalWidth = 0, finalHeight = 0, paddedX = 0, paddedY = 0; let resultRestoredUrl = null; let lastJobId = null; let isGenerating = false; // double-click guard // lead memory const mem = { email: localStorage.getItem("sid_email") || "", consent: localStorage.getItem("sid_consent") === "true", token: localStorage.getItem("sid_lead_token") || "" }; /************* ELEMS *************/ const $ = (s)=>document.querySelector(s); const brandHome = $('#brandHome'); const logoImg = $('#logoImg'); const logoFallback= $('#logoFallback'); const brandName = $('#brandName'); const pageTitle = $('#pageTitle'); const pageSub = $('#pageSub'); const verticalPill= $('#verticalPill'); const sectionA = $('#sectionA'); const sectionB = $('#sectionB'); const sectionC = $('#sectionC'); const loadingPanel = $('#loadingPanel'); const uploadBtn = $('#uploadBtn'); const fileInput = $('#fileInput'); const changeImageBtn = $('#changeImageBtn'); const prompt2Input = $('#prompt2Input'); const generateNowBtn = $('#generateNowBtn'); const resultImage = $('#resultImage'); const originalImage = $('#originalImage'); const previewSmall = $('#previewSmall'); const howImg1 = $('#howImg1'); const howImg2 = $('#howImg2'); const howImg3 = $('#howImg3'); const toast = $('#toast'); /* modals */ const leadModal = $('#leadModal'); const leadEmail = $('#leadEmail'); const leadConsent = $('#leadConsent'); const leadContinue = $('#leadContinue'); const leadPostcode = document.getElementById('leadPostcode'); const optinModal = $('#optinModal'); const optinEmail = $('#optinEmail'); const optinConsent = $('#optinConsent'); const optinSend = $('#optinSend'); const optinCancel = $('#optinCancel'); /* community slider */ const slider = $('#communitySlider'); const cBefore = $('#cBefore'); const cAfter = $('#cAfter'); const nav = $('#communityNav'); const prevSlide = $('#prevSlide'); const nextSlide = $('#nextSlide'); let slideIndex = 0; /* next steps controls (fixed IDs) */ const getQuoteBtn = document.getElementById('getQuote'); const copyLinkBtn = document.getElementById('copyLink'); const generateMoreBtn = document.getElementById('generateMore'); const downloadBtn = document.getElementById('downloadAfter'); const nextMsg = document.getElementById('nextMsg'); const lateOptInBtn = document.getElementById('lateOptInBtn'); const leadModalContent = document.getElementById('leadModalContent'); const leadForm = document.getElementById('leadForm'); const leadError = document.getElementById('leadError'); const leadClose = document.getElementById('leadClose'); /************* HERO STATE *************/ function setHeroState(state) { const titleEls = document.querySelectorAll('#pageTitle, .hero h1'); const subEls = document.querySelectorAll('#pageSub, .hero .lead'); const setText = (els, text)=> els.forEach(el => { if (el) el.textContent = text; }); document.body.classList.remove('is-home','is-editing','is-loading','is-ready'); if (state === 'home') { document.body.classList.add('is-home'); const homeTitle = (CONFIG.TITLE && CONFIG.TITLE.trim()) ? CONFIG.TITLE.trim() : `Visualise Your New ${ (CONFIG.AREA_NOUN || 'Space').trim() }`; setText(titleEls, homeTitle); setText(subEls, 'Upload a photo and preview changes in 60 seconds.'); return; } if (state === 'editing') { document.body.classList.add('is-editing'); setText(titleEls, 'Describe your changes'); setText(subEls, ''); return; } if (state === 'loading') { document.body.classList.add('is-loading'); setText(titleEls, 'Our AI fairies are working on your image!'); // put the gif under the title subEls.forEach(el => { el.innerHTML = ` Animated fairy `; }); return; } if (state === 'ready') { document.body.classList.add('is-ready'); setText(titleEls, 'Your Image is Ready'); setText(subEls, ''); return; } } /************* BRANDING *************/ /************* BRANDING *************/ (function initBranding(){ try { // theme colors document.documentElement.style.setProperty('--brand', CONFIG.PRIMARY); document.documentElement.style.setProperty('--brand-2',CONFIG.ACCENT); // derive title from AREA_NOUN if TITLE is blank const derivedTitle = (CONFIG.TITLE && CONFIG.TITLE.trim()) ? CONFIG.TITLE.trim() : `Visualise Your New ${ (CONFIG.AREA_NOUN || 'Space').trim() }`; // apply brand text if (brandName) brandName.textContent = CONFIG.BRAND_NAME || 'See It Done'; if (pageTitle) pageTitle.textContent = derivedTitle; if (pageSub) pageSub.textContent = "Upload a photo and preview changes in 60 seconds."; // pill text e.g., "Driveway Visualizer" if (verticalPill) verticalPill.textContent = `${CONFIG.AREA_NOUN} Visualizer`; // nouns inside body text const noun = (CONFIG.AREA_NOUN || 'Space').toLowerCase(); const el2 = document.getElementById('areaWord2Label'); if (el2) el2.textContent = noun; const el1 = document.getElementById('areaWord1'); if (el1) el1.textContent = noun; // prompt placeholder const promptBox = document.getElementById('prompt2Input'); if (promptBox && CONFIG.PROMPT_PLACEHOLDER) promptBox.placeholder = CONFIG.PROMPT_PLACEHOLDER; // logo if (CONFIG.LOGO_URL && logoImg) { logoImg.src = CONFIG.LOGO_URL; logoImg.style.display = 'block'; if (logoFallback) logoFallback.style.display = 'none'; } // "How it works" images with cache-bust + fallback [howImg1, howImg2, howImg3].forEach((el, i) => { const url = CONFIG.HOW_STEP_IMAGES?.[i]; if (!url || !el) return; const bust = (url.includes('?') ? '&' : '?') + 'v=' + Date.now(); el.src = url + bust; el.style.display = 'block'; el.loading = 'lazy'; el.decoding = 'async'; el.onerror = () => { el.style.display = 'none'; const fallback = document.createElement('div'); fallback.textContent = `Step ${i+1}`; fallback.style.cssText = 'width:100%;aspect-ratio:16/9;border:1px solid #e8eef7;border-radius:10px;background:#f8fafc;display:flex;align-items:center;justify-content:center;color:#6b7280;font-weight:700;'; el.parentElement?.appendChild(fallback); }; }); } catch (e) { console.error('Branding init failed:', e); } finally { // always show Upload CTA by entering 'home' state setHeroState('home'); } })(); /************* HELPERS *************/ function show(section){ [sectionA, sectionB, sectionC, loadingPanel].forEach(el=>el.style.display='none'); section.style.display = 'block'; } function showToast(msg, type){ toast.textContent = msg; toast.className = 'toast' + (type==='error' ? ' error' : ''); requestAnimationFrame(()=> toast.classList.add('show')); setTimeout(()=> toast.classList.remove('show'), 2500); } function resetToStart(){ prompt2Input.value = ''; if (originalImageUrl) { URL.revokeObjectURL(originalImageUrl); } fileInput.value = ''; currentFile = null; originalImageUrl = null; letterboxedDataUrl = null; resultRestoredUrl = null; lastJobId = null; previewSmall.removeAttribute('src'); resultImage.removeAttribute('src'); originalImage.removeAttribute('src'); show(sectionA); setHeroState('home'); window.scrollTo({ top: 0, behavior: 'smooth' }); } async function fileToDataUrl(file) { return await new Promise((resolve, reject) => { const fr = new FileReader(); fr.onload = () => resolve(fr.result); fr.onerror = () => reject(new Error('Failed to read file')); fr.readAsDataURL(file); }); } // --- UK postcode helpers --- function normalisePostcode(p){ p = String(p||"").toUpperCase().trim().replace(/\s+/g,""); // Insert a space before the last 3 chars if length > 3 (cosmetic) if (p.length > 3) p = p.slice(0, -3) + " " + p.slice(-3); return p; } // Extract the letters at the start (1–2 letters for UK areas, e.g. OX, RG, N, W, NW, etc.) function extractAreaPrefix(p){ const raw = String(p || "").toUpperCase(); const m = raw.match(/^([A-Z]{1,2})\d/i); if (m && m[1]) return m[1]; // e.g. "OX", "RG", "N", "W", "NW" // Fallback: just take leading letters if no digit present yet const m2 = raw.match(/^([A-Z]{1,2})/); return m2 ? m2[1] : ""; } function isAllowedPostcode(p){ const prefix = extractAreaPrefix(p); return CONFIG.ALLOWED_PREFIXES.includes(prefix); } // ---- Out-of-area persistent block helpers ---- function setAreaBlocked() { const hours = Number(CONFIG.BLOCK_OUT_OF_AREA_PERSIST_HOURS || 24); const until = Date.now() + hours * 60 * 60 * 1000; localStorage.setItem("sid_block_area_until", String(until)); } function isAreaBlocked() { const v = Number(localStorage.getItem("sid_block_area_until") || 0); return v && Date.now() < v; } // If you ever need to clear the block in testing: // localStorage.removeItem("sid_block_area_until"); /* prevent accidental navigation mid-generation */ window.addEventListener('beforeunload', (e)=>{ if (isGenerating) { e.preventDefault(); e.returnValue = ''; } }); /************* COMMUNITY SLIDER *************/ const isTouchDevice = () => typeof window !== "undefined" && ("ontouchstart" in window || navigator.maxTouchPoints > 0); function buildCards(trackEl, slides){ trackEl.innerHTML = ""; slides.forEach((s) => { const before = s.before; const after = s.after; const caption = s.caption || ""; const card = document.createElement("div"); card.className = "cc-card"; card.setAttribute("role","listitem"); const img = document.createElement("img"); img.className = "cc-img"; img.alt = "Community renovation"; img.src = after; // start on AFTER img.dataset.after = after; img.dataset.before = before; img.dataset.state = "after"; // "after" | "before" const badges = document.createElement("div"); badges.className = "cc-badges"; const badgeAfter = document.createElement("div"); badgeAfter.className = "cc-badge cc-badge--after"; badgeAfter.textContent = "After"; const hint = document.createElement("div"); hint.className = "cc-hint-pill"; hint.textContent = isTouchDevice() ? "Tap to see before" : "Hover to see before"; const badgeBefore = document.createElement("div"); badgeBefore.className = "cc-badge cc-badge--before"; badgeBefore.textContent = "Before"; badgeBefore.style.display = "none"; badges.appendChild(badgeAfter); if (before) badges.appendChild(hint); badges.appendChild(badgeBefore); const cap = document.createElement("div"); cap.className = "cc-caption"; if (caption) cap.textContent = `“${caption}”`; const showBefore = () => { if (!before) return; img.src = img.dataset.before; img.dataset.state = "before"; badgeAfter.style.display = "none"; hint.style.display = "none"; badgeBefore.style.display = "inline-block"; }; const showAfter = () => { img.src = img.dataset.after; img.dataset.state = "after"; badgeAfter.style.display = "inline-block"; if (before) hint.style.display = "inline-block"; badgeBefore.style.display = "none"; }; if (isTouchDevice()) { card.addEventListener("click", (e) => { e.stopPropagation(); img.dataset.state === "after" ? showBefore() : showAfter(); }); } else { card.addEventListener("mouseenter", showBefore); card.addEventListener("mouseleave", showAfter); } card.appendChild(img); card.appendChild(badges); card.appendChild(cap); trackEl.appendChild(card); }); } function initSlider(){ const container = document.getElementById("communityCarousel"); const track = document.getElementById("ccTrack"); const hint = document.getElementById("ccHint"); if (!container || !track) return; hint.style.display = isTouchDevice() ? "flex" : "none"; hint.querySelector("svg").style.color = getComputedStyle(document.documentElement).getPropertyValue("--brand") || CONFIG.PRIMARY || "#bc1821"; const slides = (CONFIG.COMMUNITY_SLIDES || []).map(s => ({ before: s.before, after: s.after, caption: s.caption || "" })); if (!slides.length) return; buildCards(track, slides); const step = () => { const card = track.querySelector(".cc-card"); if (!card) return 320; const gap = 12; return card.getBoundingClientRect().width + gap; }; const prev = document.getElementById("ccPrev"); const next = document.getElementById("ccNext"); if (prev && next){ prev.onclick = () => track.scrollBy({ left: -step(), behavior: "smooth" }); next.onclick = () => track.scrollBy({ left: step(), behavior: "smooth" }); } const io = new IntersectionObserver((entries)=>{ entries.forEach(e=>{ const img = e.target.querySelector(".cc-img"); if (!img) return; if (e.isIntersecting && img.dataset.state !== "after") { img.src = img.dataset.after; img.dataset.state = "after"; const b = e.target.querySelector(".cc-badge--before"); const a = e.target.querySelector(".cc-badge--after"); const h = e.target.querySelector(".cc-hint-pill"); if (a) a.style.display = "inline-block"; if (h) h.style.display = "inline-block"; if (b) b.style.display = "none"; } }); }, { root: document.getElementById("ccTrack"), threshold: 0.5 }); track.querySelectorAll(".cc-card").forEach(c => io.observe(c)); container.style.display = "block"; } /************* LEAD (after file pick) *************/ function openLeadModal(){ // If previously blocked, show the error view and lock the UI if (isAreaBlocked()) { if (leadForm) leadForm.style.display = 'none'; if (leadError) leadError.style.display = 'block'; leadModal.classList.add('show'); return; } // Normal form view if (leadForm) leadForm.style.display = ''; if (leadError) leadError.style.display = 'none'; leadEmail.value = mem.email || ""; leadConsent.checked = !!mem.consent; leadModal.classList.add('show'); } function closeLeadModal(){ leadModal.classList.remove('show'); } async function submitLead(shouldConsent){ const email = (leadEmail.value||"").trim(); const consent = shouldConsent && leadConsent.checked; const postcodeInput = (leadPostcode.value || "").trim(); // Require postcode always if (!postcodeInput){ showToast("Please enter your postcode","error"); return; } const postcode = normalisePostcode(postcodeInput); // Gate by allowed area prefixes // Gate by allowed area prefixes if (!isAllowedPostcode(postcode)){ setAreaBlocked(); // persist across refresh if (leadForm) leadForm.style.display = 'none'; if (leadError) leadError.style.display = 'block'; return; } // If they tick consent, require email too (unchanged rule) if (consent && !email){ showToast("Enter your email to receive designs","error"); return; } try { // Save lead if email provided (optional) if (email){ const r = await fetch(`${CONFIG.WORKER_URL}/lead`, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ name:"", email, postcode, // <-- SAVE POSTCODE marketing_consent: !!consent, consent_text: consent ? "Agreed at upload" : "" }) }); const j = await r.json(); if (j.ok && j.lead_token){ mem.token = j.lead_token; localStorage.setItem("sid_lead_token", j.lead_token); } mem.email = email; localStorage.setItem("sid_email", email); mem.consent = !!consent; localStorage.setItem("sid_consent", String(!!consent)); } // Store postcode locally too (optional for later use) localStorage.setItem("sid_postcode", postcode); } catch {} // All checks passed closeLeadModal(); } /************* IMAGE HANDLING *************/ async function handleFileSelect(e){ const file = e?.target?.files?.[0]; if (!file) return; if (!/^image\//.test(file.type) && !/\.(heic|heif)$/i.test(file.name||'')) { return showToast('Please select an image file','error'); } currentFile = await normalizeImageFile(file); if (originalImageUrl) URL.revokeObjectURL(originalImageUrl); originalImageUrl = URL.createObjectURL(currentFile); const img = new Image(); img.onload = async ()=>{ origW = img.naturalWidth; origH = img.naturalHeight; const canvas = document.createElement('canvas'); canvas.width = 1024; canvas.height = 1024; const ctx = canvas.getContext('2d'); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.clearRect(0,0,1024,1024); scale = Math.min(1024 / origW, 1024 / origH); finalWidth = Math.round(origW * scale); finalHeight = Math.round(origH * scale); paddedX = Math.round((1024 - finalWidth) / 2); paddedY = Math.round((1024 - finalHeight) / 2); ctx.drawImage(img, paddedX, paddedY, finalWidth, finalHeight); letterboxedDataUrl = canvas.toDataURL('image/jpeg', 0.92); previewSmall.src = originalImageUrl; show(sectionB); setHeroState('editing'); openLeadModal(); }; img.onerror = ()=> showToast('Could not read image','error'); img.src = originalImageUrl; } async function normalizeImageFile(file){ const name = (file.name||'').toLowerCase(); const isHeic = name.endsWith('.heic') || name.endsWith('.heif') || (file.type||'').includes('heic'); if (isHeic && window.heic2any){ try{ const converted = await window.heic2any({ blob:file, toType:'image/jpeg', quality:0.95 }); return new File([converted], (file.name||'image').replace(/\.(heic|heif)$/i,'.jpg'), { type:'image/jpeg' }); }catch(e){} } return file; } /************* GENERATE *************/ async function generateNow(){ if (isGenerating) return; isGenerating = true; generateNowBtn.disabled = true; if (!currentFile){ showToast(`Upload a ${CONFIG.AREA_NOUN.toLowerCase()} photo first`,'error'); isGenerating=false; generateNowBtn.disabled=false; return; } const userDetail = (prompt2Input.value || '').trim(); if (!userDetail){ showToast('Please describe your changes','error'); isGenerating=false; generateNowBtn.disabled=false; return; } const areaToEdit = (CONFIG.DEFAULT_AREA && CONFIG.DEFAULT_AREA !== 'auto' ? CONFIG.DEFAULT_AREA : (CONFIG.AREA_NOUN || 'kitchen')).toLowerCase(); const combinedPrompt = `${CONFIG.PRESET_PROMPT}\n\nBrand: ${CONFIG.BRAND_NAME}\nVertical: ${CONFIG.AREA_NOUN}\nUser request: ${userDetail}`; setHeroState('loading'); show(loadingPanel); initSlider(); const ctrl = new AbortController(); const kill = setTimeout(()=> ctrl.abort(), CONFIG.REQUEST_TIMEOUT_MS); try { const maskRes = await fetch(`${CONFIG.WORKER_URL}/generate-mask`,{ method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ imageBase64: letterboxedDataUrl, areaToEdit, imageMetadata: { origW, origH, finalWidth, finalHeight, paddedX, paddedY, scale } }), signal: ctrl.signal }); if (!maskRes.ok){ const err = await maskRes.json().catch(()=>({})); throw new Error(err.error || err.message || 'Mask generation failed'); } const { maskBase64 } = await maskRes.json(); if (!maskBase64) throw new Error('No mask returned from server'); const paintRes = await fetch(`${CONFIG.WORKER_URL}/inpainting`,{ method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ prompt: combinedPrompt, init_image: letterboxedDataUrl, mask_image: maskBase64, email: mem.email || "", consented: !!mem.consent }), signal: ctrl.signal }); if (!paintRes.ok){ const err = await paintRes.json().catch(()=>({})); throw new Error(err.error || err.message || 'Inpainting failed'); } const paintJson = await paintRes.json(); if (!paintJson.images || !paintJson.images[0]) throw new Error('No image returned from inpainting'); lastJobId = paintJson.jobId || null; const result1024 = `data:image/png;base64,${paintJson.images[0]}`; const out = new Image(); out.onload = async ()=>{ const c = document.createElement('canvas'); c.width = origW; c.height = origH; const ctx = c.getContext('2d'); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(out, paddedX, paddedY, finalWidth, finalHeight, 0, 0, origW, origH); resultRestoredUrl = c.toDataURL('image/png', 1.0); resultImage.src = resultRestoredUrl; originalImage.src = originalImageUrl; show(sectionC); setHeroState('ready'); window.scrollTo({ top: 0, behavior: 'smooth' }); if (lateOptInBtn) lateOptInBtn.style.display = (!mem.consent ? 'inline-flex' : 'none'); showToast('Image generated successfully!'); // Save corrected images server-side so quote page & emails use them try { const beforeDataUrl = await fileToDataUrl(currentFile); // original upload (data URL) const afterDataUrl = resultRestoredUrl; // restored (data URL) await fetch(`${CONFIG.WORKER_URL}/job/patch-images`, { method: 'POST', headers: { 'content-type':'application/json' }, body: JSON.stringify({ jobId: lastJobId, beforeDataUrl, afterDataUrl }) }); } catch (e) { console.debug('patch-images failed (non-fatal):', e?.message || e); } }; out.onerror = ()=> { throw new Error('Failed to decode result image'); }; out.src = result1024; } catch (err) { show(sectionB); setHeroState('home'); showToast(err.message || 'Something went wrong','error'); } finally { clearTimeout(kill); isGenerating=false; generateNowBtn.disabled=false; } } /************* DOWNLOAD *************/ function downloadImage(){ if (!resultRestoredUrl) return showToast('Generate an image first','error'); const a = document.createElement('a'); const ts = new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'); a.href = resultRestoredUrl; a.download = `see-it-done-${ts}.png`; document.body.appendChild(a); a.click(); a.remove(); } /************* NEXT STEPS *************/ async function sendQuoteLink(){ if (!lastJobId) { showToast("No job found","error"); return; } try{ const r = await fetch(`${CONFIG.WORKER_URL}/quote/link`,{ method:"POST", headers:{ "content-type":"application/json" }, body: JSON.stringify({ jobId:lastJobId, email: mem.email || "" }) }); const j = await r.json(); if (!j.ok || !j.url) throw new Error(j.error || "Could not create link"); // Go straight to the quote page: location.href = j.url; }catch(e){ showToast(e.message || "Could not open quote page","error"); } } /************* LATE OPT-IN *************/ function openOptin(){ optinEmail.value = mem.email || ""; optinConsent.checked = false; optinModal.classList.add('show'); } function closeOptin(){ optinModal.classList.remove('show'); } async function sendOptin(){ const email = (optinEmail.value||"").trim(); const consent = !!optinConsent.checked; if (!email || !consent){ showToast("Add your email and tick consent to proceed","error"); return; } if (!lastJobId){ showToast("No job found to send","error"); return; } try{ const r = await fetch(`${CONFIG.WORKER_URL}/consent`,{ method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ email, consent_text:"Agreed on results page", jobId: lastJobId }) }); const j = await r.json(); if (j.ok){ mem.email = email; localStorage.setItem("sid_email", email); mem.consent = true; localStorage.setItem("sid_consent","true"); showToast("Sent! Check your inbox."); if (lateOptInBtn) lateOptInBtn.style.display = 'none'; closeOptin(); }else{ showToast(j.error || "Could not send","error"); } }catch{ showToast("Network error","error"); } } /************* EVENTS *************/ uploadBtn.addEventListener('click', () => { fileInput.value=""; fileInput.click(); }); fileInput.addEventListener('change', handleFileSelect); /* Consent modal controls */ leadContinue.addEventListener('click', ()=> submitLead(true)); /* Change image */ changeImageBtn.addEventListener('click', ()=> fileInput.click()); /* Generate / Next steps buttons */ generateNowBtn.addEventListener('click', generateNow); if (generateMoreBtn) generateMoreBtn.addEventListener('click', resetToStart); if (downloadBtn) downloadBtn.addEventListener('click', downloadImage); if (getQuoteBtn) getQuoteBtn.addEventListener('click', sendQuoteLink); if (copyLinkBtn) copyLinkBtn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(location.href); nextMsg.textContent = "Link copied to clipboard."; nextMsg.style.display = "block"; } catch { nextMsg.textContent = "Could not copy link."; nextMsg.style.display = "block"; } }); if (lateOptInBtn) lateOptInBtn.addEventListener('click', openOptin); optinSend.addEventListener('click', sendOptin); optinCancel.addEventListener('click', closeOptin); /* Brand click returns home */ brandHome.addEventListener('click', resetToStart); brandHome.addEventListener('keydown', (e)=>{ if(e.key==='Enter'||e.key===' ') resetToStart(); }); /* ESC to close modals + basic focus management */ [leadModal, optinModal].forEach(modal=>{ modal?.addEventListener('keydown', (e)=>{ if(e.key==='Escape') modal.classList.remove('show'); }); // simple focus trap function trapFocus(m){ const f = m.querySelectorAll('button,[href],input,textarea,select,[tabindex]:not([tabindex="-1"])'); if(!f.length) return; const first=f[0], last=f[f.length-1]; m.addEventListener('keydown', (e)=>{ if(e.key!=='Tab') return; if(e.shiftKey && document.activeElement===first){ last.focus(); e.preventDefault(); } else if(!e.shiftKey && document.activeElement===last){ first.focus(); e.preventDefault(); } }); } if (modal) trapFocus(modal); }); /************* STATUS PING (optional) *************/ (async function ping(){ try{ const r = await fetch(`${CONFIG.WORKER_URL}/health`); console.debug('API status:', r.ok ? 'connected' : 'error'); } catch{ console.debug('API status: unreachable'); } // If blocked from a previous attempt, lock immediately on load (function enforceBlockOnLoad(){ if (isAreaBlocked()) { // Ensure hero shows, but overlay locks everything setHeroState('home'); // Switch modal to error view and open it if (leadForm) leadForm.style.display = 'none'; if (leadError) leadError.style.display = 'block'; leadModal.classList.add('show'); } })(); })();