patx/voldservices

    (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();
    })();