(function () {
'use strict';
var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var heroCinematic = document.getElementById('top');
function clamp01(value) { return Math.max(0, Math.min(1, value)); }
function smoothstep(value) { value = clamp01(value); return value * value * (3 - 2 * value); }
/* ── scroll-aware nav ─────────────────────────────────────
Keeps the nav visible while the oversized opening state compacts. */
var nav = document.getElementById('nav');
var navProgress = document.getElementById('nav-progress');
var navToggle = document.querySelector('.nav-toggle');
var navLinksWrap = document.getElementById('nav-links');
var navAnchors = navLinksWrap.querySelectorAll('a[href^="#"]');
var stickyCta = document.getElementById('sticky-cta');
var ticking = false;
function mix(from, to, progress) {
return from + ((to - from) * progress);
}
/* The opening masthead collapses over the first 220px of real scroll.
Values finish at the original compact nav measurements. */
function paintNavScale(y) {
var mobileNav = window.innerWidth <= 900;
var progress = reduceMotion ? (y > 12 ? 1 : 0) : smoothstep(clamp01(y / 220));
var compactLogo = Math.min(188, Math.max(146, window.innerWidth * 0.17));
var introLogo = mobileNav
? Math.min(240, Math.max(190, window.innerWidth * 0.45))
: Math.min(310, Math.max(230, window.innerWidth * 0.24));
var introHeight = mobileNav
? 104
: Math.min(138, Math.max(112, window.innerHeight * 0.12));
nav.style.setProperty('--nav-live-height', mix(introHeight, 76, progress).toFixed(2) + 'px');
nav.style.setProperty('--nav-live-logo', mix(introLogo, compactLogo, progress).toFixed(2) + 'px');
nav.style.setProperty('--nav-live-call-font', mix(16, 13.76, progress).toFixed(2) + 'px');
nav.style.setProperty('--nav-live-call-y', mix(14, 11, progress).toFixed(2) + 'px');
nav.style.setProperty('--nav-live-call-x', mix(22, 18, progress).toFixed(2) + 'px');
nav.style.setProperty('--nav-live-icon', mix(18, 15, progress).toFixed(2) + 'px');
}
function onScroll() {
var y = window.scrollY;
var max = document.documentElement.scrollHeight - window.innerHeight;
if (window.voldPaintHero) window.voldPaintHero();
paintNavScale(y);
nav.classList.toggle('is-stuck', y > 12);
navProgress.style.transform = 'scaleX(' + (max > 0 ? Math.min(y / max, 1) : 0) + ')';
stickyCta.classList.toggle('is-up', heroCinematic && y > heroCinematic.offsetTop + heroCinematic.offsetHeight);
ticking = false;
}
window.addEventListener('scroll', function () {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(onScroll);
}, { passive: true });
window.addEventListener('resize', function () {
if (ticking) return;
ticking = true;
window.requestAnimationFrame(onScroll);
});
onScroll();
/* ── mobile menu ──────────────────────────────────────── */
navToggle.addEventListener('click', function () {
var open = navToggle.getAttribute('aria-expanded') === 'true';
navToggle.setAttribute('aria-expanded', String(!open));
navToggle.setAttribute('aria-label', open ? 'Open menu' : 'Close menu');
nav.classList.toggle('is-open', !open);
});
function closeMenu() {
navToggle.setAttribute('aria-expanded', 'false');
navToggle.setAttribute('aria-label', 'Open menu');
nav.classList.remove('is-open');
}
navLinksWrap.addEventListener('click', function (e) {
if (e.target.closest('a')) closeMenu();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && nav.classList.contains('is-open')) {
closeMenu();
navToggle.focus();
}
});
document.addEventListener('click', function (e) {
if (!nav.classList.contains('is-open') || nav.contains(e.target)) return;
closeMenu();
});
/* ── active section in the nav ────────────────────────── */
var sections = [];
navAnchors.forEach(function (a) {
var el = document.querySelector(a.getAttribute('href'));
if (el) sections.push({ el: el, link: a });
});
if (sections.length && 'IntersectionObserver' in window) {
var spy = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
navAnchors.forEach(function (a) { a.classList.remove('is-current'); });
var match = sections.find(function (s) { return s.el === entry.target; });
if (match) match.link.classList.add('is-current');
});
}, { rootMargin: '-45% 0px -50% 0px' });
sections.forEach(function (s) { spy.observe(s.el); });
}
/* ── reveal on scroll ─────────────────────────────────── */
var reveals = document.querySelectorAll('.reveal');
if (reduceMotion || !('IntersectionObserver' in window)) {
reveals.forEach(function (el) { el.classList.add('is-in'); });
} else {
var revealObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
entry.target.classList.add('is-in');
revealObserver.unobserve(entry.target);
});
}, { threshold: 0.12, rootMargin: '0px 0px -6% 0px' });
reveals.forEach(function (el) { revealObserver.observe(el); });
}
/* ── the wand line ────────────────────────────────────── */
document.querySelectorAll('.wand').forEach(function (wand) {
var wandRange = wand.querySelector('.wand-range');
function paintWand() {
wand.style.setProperty('--pos', wandRange.value + '%');
var beforePercent = Math.round(Number(wandRange.value));
wandRange.setAttribute('aria-valuetext', beforePercent + '% before, ' + (100 - beforePercent) + '% after');
}
wandRange.addEventListener('input', paintWand);
paintWand();
});
/* ── gallery lightbox ─────────────────────────────────── */
var lightbox = document.getElementById('lightbox');
if (lightbox && document.getElementById('work-grid')) {
var lightboxImg = document.getElementById('lightbox-img');
var lightboxClose = lightbox.querySelector('.lightbox-close');
var lastFocused = null;
var workGrid = document.getElementById('work-grid');
workGrid.addEventListener('click', function (event) {
var tile = event.target.closest('.work-tile');
if (!tile || !workGrid.contains(tile)) return;
var img = tile.querySelector('img');
if (!img) return;
lastFocused = tile;
lightboxImg.src = img.dataset.full || img.currentSrc || img.src;
lightboxImg.alt = img.alt;
lightbox.classList.add('is-open');
document.body.classList.add('is-locked');
lightboxClose.focus();
});
function renderManagedPhotos(manifest) {
if (!manifest || manifest.version !== 1 || !Array.isArray(manifest.images)) return;
if (Array.isArray(manifest.comparisons)) {
manifest.comparisons.forEach(function (comparison) {
var project = Array.prototype.find.call(
document.querySelectorAll('[data-comparison-id]'),
function (item) { return item.dataset.comparisonId === comparison.id; }
);
if (!project) return;
['before', 'after'].forEach(function (side) {
var media = comparison[side];
var image = project.querySelector('[data-comparison-side="' + side + '"]');
if (!image || !media || !media.src || !media.alt) return;
image.src = new URL(media.src, window.location.origin + '/').href;
image.alt = media.alt;
});
});
}
var fragment = document.createDocumentFragment();
manifest.images.forEach(function (item) {
if (!item || !item.src || !item.caption || !item.alt) return;
var tile = document.createElement('button');
tile.className = 'work-tile';
tile.type = 'button';
var image = document.createElement('img');
image.src = new URL(item.thumb || item.src, window.location.origin + '/').href;
image.alt = item.alt;
image.loading = 'lazy';
image.decoding = 'async';
image.dataset.full = new URL(item.src, window.location.origin + '/').href;
var cap = document.createElement('span');
cap.className = 'work-cap';
var title = document.createElement('b');
title.textContent = item.caption;
cap.appendChild(title);
tile.appendChild(image);
tile.appendChild(cap);
fragment.appendChild(tile);
});
workGrid.replaceChildren(fragment);
}
fetch(workGrid.dataset.galleryManifest, { credentials: 'same-origin' })
.then(function (response) {
if (!response.ok) throw new Error('Gallery request failed');
return response.json();
})
.then(renderManagedPhotos)
.catch(function () {
// Keep the server-rendered photos in place as the resilient fallback.
});
function closeLightbox() {
lightbox.classList.remove('is-open');
document.body.classList.remove('is-locked');
lightboxImg.removeAttribute('src');
lightboxImg.alt = '';
if (lastFocused) lastFocused.focus();
}
lightboxClose.addEventListener('click', closeLightbox);
lightbox.addEventListener('click', function (e) {
if (e.target === lightbox) closeLightbox();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && lightbox.classList.contains('is-open')) closeLightbox();
});
}
/* ── quote form ─────────────────────────────────────────── */
var form = document.getElementById('quote-form');
if (form) {
var success = document.getElementById('form-success');
var successName = document.getElementById('success-name');
var formError = document.getElementById('form-error');
var submitButton = document.getElementById('quote-submit');
var photoInput = document.getElementById('q-photos');
var photoList = document.getElementById('q-photo-list');
var photoStatus = document.getElementById('q-photos-status');
var maxPhotos = 5;
var maxPhotoBytes = 25 * 1024 * 1024;
var maxTotalPhotoBytes = 90 * 1024 * 1024;
var selectedPhotos = [];
function clearFormError() {
formError.hidden = true;
formError.textContent = '';
}
function showFormError(message) {
formError.textContent = message;
formError.hidden = false;
}
function formatPhotoStatus(files) {
if (!files.length) return '';
return files.length + ' of ' + maxPhotos + (files.length === 1 ? ' photo is' : ' photos are') + ' ready to send.';
}
function photoValidationMessage(files) {
var totalBytes = files.reduce(function (total, file) { return total + file.size; }, 0);
var oversizedPhoto = files.find(function (file) { return file.size > maxPhotoBytes; });
var nonPhoto = files.find(function (file) {
return file.type && file.type.indexOf('image/') !== 0;
});
if (files.length > maxPhotos) {
return 'Choose no more than 5 photos. Remove one before adding another.';
}
if (oversizedPhoto) {
return oversizedPhoto.name + ' is larger than 25 MB and was not added.';
}
if (totalBytes > maxTotalPhotoBytes) {
return 'Keep the combined photo size under 90 MB. Those photos were not added.';
}
if (nonPhoto) return 'Choose image files only.';
return '';
}
function formatPhotoSize(bytes) {
if (bytes < 1024 * 1024) return Math.max(1, Math.round(bytes / 1024)) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1).replace('.0', '') + ' MB';
}
function photoKey(file) {
return [file.name, file.size, file.lastModified, file.type].join('|');
}
function renderPhotos(message) {
photoList.textContent = '';
selectedPhotos.forEach(function (file, index) {
var item = document.createElement('li');
var fileDetails = document.createElement('span');
var fileName = document.createElement('span');
var fileSize = document.createElement('span');
var removeButton = document.createElement('button');
item.className = 'photo-item';
fileDetails.className = 'photo-file';
fileName.className = 'photo-name';
fileName.textContent = file.name;
fileSize.className = 'photo-size';
fileSize.textContent = 'Photo ' + (index + 1) + ' · ' + formatPhotoSize(file.size);
removeButton.className = 'photo-remove';
removeButton.type = 'button';
removeButton.dataset.photoIndex = index;
removeButton.setAttribute('aria-label', 'Remove ' + file.name);
removeButton.textContent = 'Remove';
fileDetails.appendChild(fileName);
fileDetails.appendChild(fileSize);
item.appendChild(fileDetails);
item.appendChild(removeButton);
photoList.appendChild(item);
});
photoList.hidden = selectedPhotos.length === 0;
photoStatus.textContent = message || formatPhotoStatus(selectedPhotos);
}
function validatePhotos() {
var message = photoValidationMessage(selectedPhotos);
photoInput.setCustomValidity(message);
if (message) {
photoStatus.textContent = message;
return false;
}
photoStatus.textContent = formatPhotoStatus(selectedPhotos);
return true;
}
function formspreeErrorMessage(data, status) {
var errors = data && Array.isArray(data.errors) ? data.errors : [];
var codes = errors.map(function (error) { return error.code; });
if (status === 429) {
return 'Too many requests were sent at once. Wait a minute, then try again.';
}
if (codes.indexOf('NO_FILE_UPLOADS') !== -1) {
return 'Photo uploads are not enabled for this form yet. Remove the photos and try again, or call or text (954) 401-3301.';
}
if (codes.indexOf('TOO_MANY_FILES') !== -1) {
return 'Too many photos were attached. Choose up to 5 and try again.';
}
if (codes.indexOf('FILES_TOO_BIG') !== -1) {
return 'One or more photos are too large. Choose smaller files and try again.';
}
var messages = errors.map(function (error) { return error.message; }).filter(Boolean);
if (messages.length) return messages.join(' ');
return 'Your request could not be sent. Check your connection and try again, or call or text (954) 401-3301.';
}
photoInput.addEventListener('change', function () {
clearFormError();
var incomingPhotos = Array.prototype.slice.call(photoInput.files || []);
var selectedKeys = selectedPhotos.map(photoKey);
var duplicateCount = 0;
photoInput.value = '';
incomingPhotos = incomingPhotos.filter(function (file) {
var key = photoKey(file);
if (selectedKeys.indexOf(key) !== -1) {
duplicateCount += 1;
return false;
}
selectedKeys.push(key);
return true;
});
var candidatePhotos = selectedPhotos.concat(incomingPhotos);
var message = photoValidationMessage(candidatePhotos);
photoInput.setCustomValidity('');
if (message) {
renderPhotos(message);
return;
}
selectedPhotos = candidatePhotos;
renderPhotos(duplicateCount
? (duplicateCount === 1 ? 'That photo is already selected. ' : 'Those photos are already selected. ') + formatPhotoStatus(selectedPhotos)
: '');
});
photoList.addEventListener('click', function (e) {
var removeButton = e.target.closest('.photo-remove');
if (!removeButton) return;
var index = Number(removeButton.dataset.photoIndex);
if (!Number.isInteger(index) || !selectedPhotos[index]) return;
selectedPhotos.splice(index, 1);
photoInput.setCustomValidity('');
clearFormError();
renderPhotos();
});
form.addEventListener('submit', async function (e) {
e.preventDefault();
clearFormError();
if (!validatePhotos() || !form.checkValidity()) {
form.reportValidity();
return;
}
var submittedName = form.querySelector('#q-name').value.trim();
var originalButtonText = submitButton.textContent;
form.setAttribute('aria-busy', 'true');
submitButton.disabled = true;
submitButton.textContent = 'Sending request…';
try {
var formData = new FormData(form);
formData.delete(photoInput.name);
selectedPhotos.forEach(function (file) {
formData.append(photoInput.name, file, file.name);
});
var response = await fetch(form.action, {
method: form.method,
body: formData,
headers: { 'Accept': 'application/json' }
});
var data = await response.json().catch(function () { return null; });
if (!response.ok) {
throw { formspree: true, data: data, status: response.status };
}
successName.textContent = submittedName || 'we’ve got it';
form.reset();
selectedPhotos = [];
renderPhotos();
form.hidden = true;
success.hidden = false;
success.focus();
} catch (error) {
var message = error && error.formspree
? formspreeErrorMessage(error.data, error.status)
: 'Your request could not be sent. Check your connection and try again, or call or text (954) 401-3301.';
showFormError(message);
} finally {
form.removeAttribute('aria-busy');
submitButton.disabled = false;
submitButton.textContent = originalButtonText;
}
});
}
document.getElementById('year').textContent = new Date().getFullYear();
})();