(function () {
  'use strict';

  const defaults = {
    catalog: { categories: [], volume_loads: [] },
    currency: '$',
    areaFee: 0,
    popularCount: 6,
    morePageSize: 6,
    pricingMethods: ['specific', 'volume'],
    bookingForm: {},
    aiEstimator: {},
    estimateNote: 'Prices shown are estimates and may vary slightly upon physical inspection.',
    emptyTitle: 'No Items Selected',
    emptyMessage: 'Browse popular items below, or search for what you need.',
    strings: {},
  };

  const config = Object.assign({}, defaults, window.JunkrecoCalculatorData || {});
  config.strings = Object.assign(
    {
      specificTitle: 'Specific Item Based',
      specificHelp: 'Best if you know what items you want to remove',
      volumeTitle: 'Truck Volume Based',
      volumeHelp: "Best if you're not sure about item count",
      aiTitle: 'AI Photo Estimator',
      aiHelp: 'Best if you can upload photos of the job',
      aiResultTitle: 'AI photo estimate',
      methodLabel: 'Select a method to calculate your junk',
      selectItems: 'Please select your items',
      searchPlaceholder: 'Search your items here...',
      popularItems: 'Popular Items',
      moreItems: 'All Items',
      selectedItems: 'Selected items',
      subtotal: 'SUB Total Price -',
      areaFees: 'Area Fees -',
      total: 'Total Price -',
      startingAt: 'Starting at',
      volumeCtaPrefix: 'If you want to book, select your option and then',
      searchResults: 'Search Results',
      noResults: 'No items found matching',
      bookingBack: 'Back to estimate',
    },
    config.strings || {}
  );
  config.pricingMethods = normalizePricingMethods(config.pricingMethods);
  config.aiEstimator = Object.assign(
    {
      enabled: false,
      ajaxUrl: '',
      nonce: '',
      maxPhotos: 6,
      maxPhotoSize: 5242880,
      maxPhotoTotalSize: 5242880,
      compressPhotos: true,
      compressionMaxDimension: 2048,
      compressionQuality: 0.82,
      compressionTriggerSize: 1572864,
      enableNotes: true,
      introText: 'Upload photos of the junk you need removed. The AI estimator will analyze the load and return a recommended price.',
      submitLabel: 'Get AI Estimate',
      submittingText: 'Analyzing photos...',
      compressionText: 'Optimizing photos...',
      compressionDoneText: 'Optimized %d photo(s) for upload.',
      successText: 'Estimate ready.',
      resetLabel: 'Start a new estimate',
      errorText: 'The AI estimate could not be created. Please try again.',
      disclaimer: 'AI estimates are approximate and may change after an in-person review.',
      photoHelpText: 'Add clear photos from a few angles for the most useful estimate.',
      resultPhotosLabel: 'Uploaded photos',
      photoLimitText: 'You can upload up to %d photos.',
      photoSizeErrorText: 'Each photo must be %s or smaller.',
      photoTotalSizeErrorText: 'Selected photos must total %s or smaller.',
      photoTypeErrorText: 'Please upload JPG, PNG, GIF, WebP, HEIC, or HEIF photos.',
      notesLabel: 'Job notes',
      notesPlaceholder: 'Anything not obvious in the photos?',
      noPhotosText: 'Add at least one photo before requesting an AI estimate.',
      confidenceLabel: 'Confidence',
      cubicYardsLabel: 'Cubic yards',
      weightLabel: 'Estimated weight',
      truckPercentLabel: 'Truck usage',
      loadFractionLabel: 'Recommended load',
      landfillCostLabel: 'Estimated landfill cost',
      extraFeesLabel: 'Detected extra fees',
      extraFeesTotalLabel: 'Extra fees total',
      analysisNotesLabel: 'Analysis notes',
      itemBreakdownLabel: 'Detected items',
    },
    config.aiEstimator || {}
  );
  config.bookingForm = Object.assign(
    {
      mode: 'redirect',
      ajaxUrl: '',
      nonce: '',
      formTitle: 'Book your junk removal',
      formIntro: 'Send your contact details, pickup location, and preferred schedule. We will follow up to confirm the request.',
      submitLabel: 'Submit Request',
      successMessage: 'Your request has been sent successfully.',
      successRedirectUrl: '',
      consentText: 'By checking this box, you agree to receive SMS messages about this booking request.',
      enableZipEligibility: false,
      allowedZipCodes: [],
      enablePhotos: true,
      maxPhotos: 6,
      maxPhotoSize: 5242880,
      maxPhotoTotalSize: 5242880,
      requireAddress: true,
      requirePreferredDate: true,
      showPickupOptions: true,
      showStairs: true,
      showDisassembly: true,
      showDonation: true,
      showSpecialNote: true,
      requireTextConsent: true,
      submittingText: 'Submitting...',
      errorText: 'Something went wrong. Please try again.',
      photoHelpText: 'Add photos for a more accurate estimate.',
      photoLimitText: 'You can upload up to %d photos.',
      photoSizeErrorText: 'Each photo must be %s or smaller.',
      photoTotalSizeErrorText: 'Selected photos must total %s or smaller.',
      photoTypeErrorText: 'Please upload JPG, PNG, GIF, WebP, HEIC, or HEIF photos.',
      zipTitle: 'Check service availability',
      zipIntro: 'Enter your ZIP code to see if we can be available for you.',
      zipLabel: 'ZIP Code',
      zipPlaceholder: 'Enter ZIP code',
      zipSubmitLabel: 'Check Availability',
      zipEligibleMessage: 'Great news, we service your area.',
      zipIneligibleMessage: 'Sorry, we do not currently service that ZIP code.',
      zipInvalidMessage: 'Please enter a valid 5-digit ZIP code.',
    },
    config.bookingForm || {}
  );
  config.bookingForm.allowedZipCodes = normalizeZipCodes(config.bookingForm.allowedZipCodes);

  function normalizePricingMethods(value) {
    const raw = Array.isArray(value) ? value : String(value || '').split(/[\s,|]+/);
    const methods = [];

    raw.forEach((method) => {
      const normalized = String(method || '').trim().toLowerCase();
      if ((normalized === 'specific' || normalized === 'volume' || normalized === 'ai') && !methods.includes(normalized)) {
        methods.push(normalized);
      }
    });

    return methods.length ? methods : ['specific', 'volume'];
  }

  function escapeHtml(value) {
    return String(value || '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  function safeUrl(value, fallback, protocols) {
    const raw = String(value || '').trim();
    const allowed = Array.isArray(protocols) && protocols.length ? protocols : ['http:', 'https:'];

    if (!raw) {
      return fallback || '';
    }

    if (raw.charAt(0) === '#') {
      return raw;
    }

    try {
      const parsed = new URL(raw, window.location.href);
      return allowed.includes(parsed.protocol) ? raw : fallback || '';
    } catch (error) {
      return fallback || '';
    }
  }

  function money(value, decimals) {
    const amount = Number(value || 0);
    const precision = typeof decimals === 'number' ? decimals : 0;
    return config.currency + amount.toFixed(precision);
  }

  function formatNumber(value, decimals) {
    const amount = Number(value || 0);
    const precision = typeof decimals === 'number' ? decimals : 0;
    return amount.toLocaleString(undefined, {
      minimumFractionDigits: precision,
      maximumFractionDigits: precision,
    });
  }

  function formatText(template, values) {
    let index = 0;
    return String(template || '').replace(/%(\d+\$)?[ds]/g, (match, position) => {
      const value = position ? values[Number(position.slice(0, -1)) - 1] : values[index++];
      return typeof value === 'undefined' || value === null ? '' : String(value);
    });
  }

  function formatFileSize(bytes) {
    const size = Math.max(0, Number(bytes || 0));
    if (size >= 1024 * 1024) {
      return `${Math.round((size / 1024 / 1024) * 10) / 10} MB`;
    }

    return `${Math.max(1, Math.round(size / 1024))} KB`;
  }

  function normalizeZipCode(value) {
    const digits = String(value || '').replace(/\D/g, '');
    return digits.length >= 5 ? digits.slice(0, 5) : '';
  }

  function normalizeZipCodes(value) {
    const raw = Array.isArray(value) ? value : String(value || '').split(/[\s,;|]+/);
    const zipCodes = [];

    raw.forEach((item) => {
      const zip = normalizeZipCode(item);
      if (zip && !zipCodes.includes(zip)) {
        zipCodes.push(zip);
      }
    });

    return zipCodes;
  }

  function normalizeCatalog() {
    const catalog = config.catalog || {};
    const categories = Array.isArray(catalog.categories) ? catalog.categories : [];
    const loads = Array.isArray(catalog.volume_loads) ? catalog.volume_loads : [];

    return {
      categories: categories
        .map((category) => ({
          id: String(category.id || category.name),
          name: String(category.name || ''),
          image: String(category.image || ''),
          subItems: Array.isArray(category.sub_items)
            ? category.sub_items.map((item) => ({
                id: String(item.id || item.name),
                categoryId: String(category.id || category.name),
                name: String(item.name || ''),
                image: String(item.image || category.image || ''),
                price: Number(item.price || 0),
              }))
            : [],
        }))
        .filter((category) => category.name && category.subItems.length),
      loads: loads.map((load) => ({
        id: String(load.id || load.title),
        title: String(load.title || ''),
        items: String(load.items || ''),
        description: String(load.description || ''),
        image: String(load.image || ''),
        price: Number(load.price || 0),
      })),
    };
  }

  const catalog = normalizeCatalog();

  function initCalculator(root) {
    if (root.dataset.junkrecoReady === 'true') {
      return;
    }

    root.dataset.junkrecoReady = 'true';

    let availableMethods = normalizePricingMethods(root.dataset.pricingMethods || config.pricingMethods).filter((method) => {
      if (method === 'specific') {
        return catalog.categories.length > 0;
      }

      if (method === 'ai') {
        return Boolean(config.aiEstimator.enabled);
      }

      return catalog.loads.length > 0;
    });

    if (!availableMethods.length) {
      availableMethods = [];
      if (catalog.categories.length) {
        availableMethods.push('specific');
      }
      if (catalog.loads.length) {
        availableMethods.push('volume');
      }
      if (config.aiEstimator.enabled) {
        availableMethods.push('ai');
      }
      if (!availableMethods.length) {
        availableMethods = ['specific'];
      }
    }

    const state = {
      mode: availableMethods[0],
      selectedCategoryId: null,
      search: '',
      moreOffset: 0,
      cart: [],
      selectedLoadId: null,
      bookingOpen: false,
      bookingMethod: null,
      bookingStep: 'form',
      verifiedZip: '',
      selectedPhotos: [],
      aiPhotos: [],
      aiNotes: '',
      aiEstimate: null,
      aiLoading: false,
      aiCompressing: false,
      aiError: '',
      aiMessage: '',
    };

    const getBookingUrl = () => safeUrl(root.dataset.bookingUrl || '#', '#', ['http:', 'https:', 'mailto:', 'tel:']);
    const getButtonLabel = () => root.dataset.buttonLabel || 'Book Your Pickup';
    const getBookingMode = () => {
      const mode = String(root.dataset.bookingMode || config.bookingForm.mode || 'redirect').toLowerCase();
      return mode === 'form' ? 'form' : 'redirect';
    };
    const isDesignPreview = () => root.hasAttribute('data-junkreco-design-preview');
    const supportedPhotoTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif'];
    const supportedPhotoExtensionPattern = /\.(jpe?g|jpe|png|gif|webp|heic|heif)$/i;
    const photoPreviewUrls = new WeakMap();

    function getPhotoPreviewUrl(file) {
      if (!file || !window.URL || typeof window.URL.createObjectURL !== 'function') {
        return '';
      }

      if (!photoPreviewUrls.has(file)) {
        photoPreviewUrls.set(file, window.URL.createObjectURL(file));
      }

      return photoPreviewUrls.get(file);
    }

    function revokePhotoPreviewUrl(file) {
      const url = file && photoPreviewUrls.get(file);
      if (url && window.URL && typeof window.URL.revokeObjectURL === 'function') {
        window.URL.revokeObjectURL(url);
      }
      if (file) {
        photoPreviewUrls.delete(file);
      }
    }

    function clearSelectedPhotos() {
      state.selectedPhotos.forEach(revokePhotoPreviewUrl);
      state.selectedPhotos = [];
    }

    function clearAiPhotos() {
      state.aiPhotos.forEach(revokePhotoPreviewUrl);
      state.aiPhotos = [];
    }

    function getMaxPhotoCount() {
      return Math.max(1, Number(config.bookingForm.maxPhotos || 6));
    }

    function getMaxPhotoSize() {
      return Math.max(1, Number(config.bookingForm.maxPhotoSize || 5242880));
    }

    function getMaxPhotoTotalSize() {
      return Math.max(getMaxPhotoSize(), Number(config.bookingForm.maxPhotoTotalSize || getMaxPhotoSize()));
    }

    function getPhotoIdentity(file) {
      if (file && file.junkrecoOriginalIdentity) {
        return file.junkrecoOriginalIdentity;
      }

      return [file.name, file.size, file.lastModified, file.type].join(':');
    }

    function isSupportedPhoto(file) {
      return supportedPhotoTypes.includes(String(file.type || '').toLowerCase()) || supportedPhotoExtensionPattern.test(file.name || '');
    }

    function getSelectedPhotoTotal(nextPhotos) {
      return nextPhotos.reduce((total, file) => total + Number(file.size || 0), 0);
    }

    function getAiMaxPhotoCount() {
      return Math.max(1, Math.min(10, Number(config.aiEstimator.maxPhotos || 6)));
    }

    function getAiMaxPhotoSize() {
      return Math.max(1, Number(config.aiEstimator.maxPhotoSize || 5242880));
    }

    function getAiMaxPhotoTotalSize() {
      return Math.max(getAiMaxPhotoSize(), Number(config.aiEstimator.maxPhotoTotalSize || getAiMaxPhotoSize()));
    }

    function getAiCompressionMaxDimension() {
      return Math.max(512, Math.min(4096, Number(config.aiEstimator.compressionMaxDimension || 2048)));
    }

    function getAiCompressionQuality() {
      const quality = Number(config.aiEstimator.compressionQuality || 0.82);
      return Math.max(0.45, Math.min(0.92, quality));
    }

    function getAiCompressionTriggerSize() {
      return Math.max(1, Number(config.aiEstimator.compressionTriggerSize || 1572864));
    }

    function getPhotoLimitMessage() {
      return formatText(config.bookingForm.photoLimitText, [getMaxPhotoCount()]);
    }

    function getPhotoSizeMessage() {
      return formatText(config.bookingForm.photoSizeErrorText, [formatFileSize(getMaxPhotoSize())]);
    }

    function getPhotoTotalSizeMessage() {
      return formatText(config.bookingForm.photoTotalSizeErrorText, [formatFileSize(getMaxPhotoTotalSize())]);
    }

    function getAiPhotoLimitMessage() {
      return formatText(config.aiEstimator.photoLimitText, [getAiMaxPhotoCount()]);
    }

    function getAiPhotoSizeMessage() {
      return formatText(config.aiEstimator.photoSizeErrorText, [formatFileSize(getAiMaxPhotoSize())]);
    }

    function getAiPhotoTotalSizeMessage() {
      return formatText(config.aiEstimator.photoTotalSizeErrorText, [formatFileSize(getAiMaxPhotoTotalSize())]);
    }

    function findCategory(id) {
      return catalog.categories.find((category) => category.id === id) || null;
    }

    function findSubItem(id) {
      for (const category of catalog.categories) {
        const item = category.subItems.find((subItem) => subItem.id === id);
        if (item) {
          return item;
        }
      }
      return null;
    }

    function findLoad(id) {
      return catalog.loads.find((load) => load.id === id) || null;
    }

    function cartSubtotal() {
      return state.cart.reduce((total, item) => total + item.price * item.quantity, 0);
    }

    function getSearchableItems(query) {
      const normalizedQuery = query.trim().toLowerCase();
      const results = [];

      if (!normalizedQuery) {
        return results;
      }

      catalog.categories.forEach((category) => {
        category.subItems.forEach((item) => {
          const haystack = `${category.name} ${item.name}`.toLowerCase();
          if (haystack.includes(normalizedQuery)) {
            results.push(item);
          }
        });
      });

      return results.sort(compareByName);
    }

    function compareByName(first, second) {
      return String(first.name || '').localeCompare(String(second.name || ''), undefined, {
        numeric: true,
        sensitivity: 'base',
      });
    }

    function getPopularCount() {
      return Math.max(0, Math.floor(Number(config.popularCount || 6)));
    }

    function getMorePageSize() {
      return Math.max(1, Math.floor(Number(config.morePageSize || 6)));
    }

    function clampMoreOffset(total) {
      const pageSize = getMorePageSize();
      const maxOffset = Math.max(0, total - pageSize);
      state.moreOffset = Math.min(Math.max(0, state.moreOffset), maxOffset);
    }

    function renderImage(src, alt, className) {
      if (!src) {
        return `<span class="${className || ''} junkreco-image-fallback" aria-hidden="true">${escapeHtml(String(alt || '?').charAt(0))}</span>`;
      }

      return `<img class="${className || ''}" src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" loading="lazy" decoding="async" />`;
    }

    function renderMethodButton(mode, title, help) {
      const active = state.mode === mode;
      return `
        <button type="button" class="junkreco-method ${active ? 'is-active' : ''}" data-action="set-mode" data-mode="${mode}" aria-pressed="${active}">
          <span class="junkreco-radio" aria-hidden="true"></span>
          <span>
            <strong>${escapeHtml(title)}</strong>
            <small>${escapeHtml(help)}</small>
          </span>
        </button>
      `;
    }

    function renderMethodSelector() {
      if (availableMethods.length <= 1) {
        return '';
      }

      return `
        <div class="junkreco-method-card">
          <p>${escapeHtml(config.strings.methodLabel)}</p>
          <div class="junkreco-methods">
            ${
              availableMethods.includes('specific')
                ? renderMethodButton('specific', config.strings.specificTitle, config.strings.specificHelp)
                : ''
            }
            ${
              availableMethods.includes('volume')
                ? renderMethodButton('volume', config.strings.volumeTitle, config.strings.volumeHelp)
                : ''
            }
            ${
              availableMethods.includes('ai')
                ? renderMethodButton('ai', config.strings.aiTitle, config.strings.aiHelp)
                : ''
            }
          </div>
        </div>
      `;
    }

    function renderCategoryButton(category) {
      return `
        <button type="button" class="junkreco-category" data-action="open-category" data-category-id="${escapeHtml(category.id)}">
          <span class="junkreco-category-media">${renderImage(category.image, category.name, '')}</span>
          <span>${escapeHtml(category.name)}</span>
        </button>
      `;
    }

    function renderCategoryBrowser() {
      const query = state.search.trim().toLowerCase();
      const matches = (category) =>
        category.name.toLowerCase().includes(query) ||
        category.subItems.some((item) => item.name.toLowerCase().includes(query));
      const popular = catalog.categories.slice(0, getPopularCount()).filter((category) => !query || matches(category));
      const allItems = catalog.categories.slice().sort(compareByName).filter((category) => !query || matches(category));
      const pageSize = getMorePageSize();
      const itemResults = getSearchableItems(query);

      clampMoreOffset(allItems.length);

      const pagedAllItems = allItems.slice(state.moreOffset, state.moreOffset + pageSize);
      const noResults = popular.length === 0 && allItems.length === 0;

      if (query) {
        return `
          <label class="junkreco-search-wrap">
            <span class="junkreco-search-icon" aria-hidden="true"></span>
            <input class="junkreco-search" type="search" value="${escapeHtml(state.search)}" placeholder="${escapeHtml(config.strings.searchPlaceholder)}" aria-label="${escapeHtml(config.strings.searchPlaceholder)}" />
          </label>
          <div class="junkreco-browser">
            ${
              itemResults.length
                ? `
                  <p class="junkreco-kicker">${escapeHtml(config.strings.searchResults)}</p>
                  <div class="junkreco-sub-list">${itemResults.map(renderSubItemButton).join('')}</div>
                `
                : `<p class="junkreco-empty-line">${escapeHtml(config.strings.noResults)} "${escapeHtml(state.search)}"</p>`
            }
          </div>
        `;
      }

      return `
        <label class="junkreco-search-wrap">
          <span class="junkreco-search-icon" aria-hidden="true"></span>
          <input class="junkreco-search" type="search" value="${escapeHtml(state.search)}" placeholder="${escapeHtml(config.strings.searchPlaceholder)}" aria-label="${escapeHtml(config.strings.searchPlaceholder)}" />
        </label>
        <div class="junkreco-browser">
          ${
            popular.length
              ? `
                <p class="junkreco-kicker">${escapeHtml(config.strings.popularItems)}</p>
                <div class="junkreco-category-grid">${popular.map(renderCategoryButton).join('')}</div>
              `
              : ''
          }
          ${
            allItems.length
              ? `
                <div class="junkreco-more-header">
                  <p class="junkreco-kicker">${escapeHtml(config.strings.moreItems)}</p>
                  <div class="junkreco-pager" aria-label="${escapeHtml(config.strings.moreItems)} pagination">
                    <button type="button" data-action="more-prev" ${state.moreOffset <= 0 ? 'disabled' : ''} aria-label="Previous items">&lt;</button>
                    <button type="button" data-action="more-next" ${state.moreOffset >= Math.max(0, allItems.length - pageSize) ? 'disabled' : ''} aria-label="Next items">&gt;</button>
                  </div>
                </div>
                <div class="junkreco-category-grid is-compact">${pagedAllItems.map(renderCategoryButton).join('')}</div>
              `
              : ''
          }
          ${noResults ? `<p class="junkreco-empty-line">${escapeHtml(config.strings.noResults)} "${escapeHtml(state.search)}"</p>` : ''}
        </div>
      `;
    }

    function renderSubItemButton(item) {
      return `
        <button type="button" class="junkreco-sub-item" data-action="add-item" data-item-id="${escapeHtml(item.id)}">
          <span class="junkreco-sub-item-media">${renderImage(item.image, item.name, '')}</span>
          <span>${escapeHtml(item.name)} <strong>(${money(item.price, 0)})</strong></span>
        </button>
      `;
    }

    function renderCategoryDetails() {
      const category = findCategory(state.selectedCategoryId);
      if (!category) {
        state.selectedCategoryId = null;
        return renderCategoryBrowser();
      }

      const selectedIds = new Set(state.cart.map((item) => item.id));
      const availableItems = category.subItems.filter((item) => !selectedIds.has(item.id));

      return `
        <div class="junkreco-detail-head">
          <button type="button" class="junkreco-back" data-action="back-to-categories" aria-label="Back to item categories">&lt;</button>
          <p>${escapeHtml(category.name)}</p>
        </div>
        <div class="junkreco-sub-list">
          ${
            availableItems.length
              ? availableItems.map(renderSubItemButton).join('')
              : '<p class="junkreco-empty-line">All items from this category have been added.</p>'
          }
        </div>
      `;
    }

    function currentEstimate(method) {
      if (method === 'ai') {
        if (!state.aiEstimate) {
          return null;
        }

        return {
          method: 'ai',
          methodLabel: config.strings.aiTitle,
          title: state.aiEstimate.recommended_load_fraction || config.strings.aiResultTitle,
          itemsLabel: `${formatNumber(state.aiEstimate.cubic_yards, 2)} cubic yards · ${formatNumber(state.aiEstimate.truck_percentage, 0)}% truck usage`,
          total: state.aiEstimate.recommended_price_with_fees || state.aiEstimate.recommended_price,
          areaFee: state.aiEstimate.landfill_cost,
          items: [],
          aiDetails: state.aiEstimate,
        };
      }

      if (method === 'volume') {
        const load = findLoad(state.selectedLoadId);
        if (!load) {
          return null;
        }

        return {
          method: 'volume',
          methodLabel: config.strings.volumeTitle,
          title: load.title,
          itemsLabel: load.items,
          total: load.price,
          areaFee: 0,
          items: [],
        };
      }

      if (!state.cart.length) {
        return null;
      }

      const subtotal = cartSubtotal();
      const areaFee = Number(config.areaFee || 0);

      return {
        method: 'specific',
        methodLabel: config.strings.specificTitle,
        title: config.strings.selectedItems,
        itemsLabel: '',
        total: subtotal + areaFee,
        areaFee,
        items: state.cart.map((item) => ({
          name: item.name,
          quantity: item.quantity,
          price: item.price,
          total: item.price * item.quantity,
        })),
      };
    }

    function renderBookingAction(method, disabled) {
      if (getBookingMode() !== 'form') {
        return `<a class="junkreco-book-link" href="${escapeHtml(getBookingUrl())}">${escapeHtml(getButtonLabel())}</a>`;
      }

      return `
        <button type="button" class="junkreco-book-link" data-action="open-booking" data-booking-method="${escapeHtml(method)}" ${disabled ? 'disabled' : ''}>
          ${escapeHtml(getButtonLabel())}
        </button>
      `;
    }

    function renderBookingEstimateSummary(estimate) {
      if (!estimate) {
        return '';
      }

      return `
        <div class="junkreco-booking-summary">
          <p class="junkreco-kicker">${escapeHtml(estimate.methodLabel || 'Estimate')}</p>
          <h3>${escapeHtml(estimate.title || 'Selected estimate')}</h3>
          ${estimate.itemsLabel ? `<p>${escapeHtml(estimate.itemsLabel)}</p>` : ''}
          ${
            estimate.items && estimate.items.length
              ? `
                <div class="junkreco-booking-items">
                  ${estimate.items
                    .map(
                      (item) => `
                        <div>
                          <span>${escapeHtml(item.name)} x ${item.quantity}</span>
                          <strong>${money(item.total, 2)}</strong>
                        </div>
                      `
                    )
                    .join('')}
                </div>
              `
              : ''
          }
          ${estimate.aiDetails ? renderAiEstimateDetails(estimate.aiDetails, true) : ''}
          <div class="junkreco-booking-total">
            <span>${escapeHtml(config.strings.total)}</span>
            <strong>${money(estimate.total, 2)}</strong>
          </div>
          ${config.bookingForm.enablePhotos ? renderPhotoUpload() : ''}
        </div>
      `;
    }

    function renderPhotoUpload() {
      const maxPhotos = getMaxPhotoCount();
      const previews = renderPhotoPreviewSlots();

      return `
        <div class="junkreco-photo-upload">
          <div class="junkreco-photo-picker">
            <input class="junkreco-photo-input" type="file" name="photos[]" accept="image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif,.jpg,.jpeg,.jpe,.png,.gif,.webp,.heic,.heif" multiple data-junkreco-photo-input />
            <span class="junkreco-photo-grid" data-junkreco-photo-grid ${state.selectedPhotos.length ? '' : 'hidden'}>${previews}</span>
          </div>
          <button type="button" class="junkreco-photo-add" data-action="choose-photos">Add Photos</button>
          <p>${escapeHtml(config.bookingForm.photoHelpText)} ${maxPhotos ? `Max ${maxPhotos}. ${escapeHtml(formatFileSize(getMaxPhotoSize()))} each.` : ''}</p>
          <div class="junkreco-photo-status" data-junkreco-photo-status aria-live="polite" hidden></div>
          <ul class="junkreco-photo-benefits">
            <li>Free, no-obligation estimates</li>
            <li>All inclusive price-no hidden fees</li>
            <li>Fast, efficient, and friendly service</li>
          </ul>
        </div>
      `;
    }

    function renderPhotoPreviewSlots(files, actionName) {
      const action = actionName || 'remove-photo';

      return (files || state.selectedPhotos)
        .map((file, index) => {
          const previewUrl = getPhotoPreviewUrl(file);

          return `
            <span class="junkreco-photo-slot is-filled">
              ${
                previewUrl
                  ? `<img src="${escapeHtml(previewUrl)}" alt="${escapeHtml(file.name)}" />`
                  : '<span class="junkreco-photo-icon" aria-hidden="true"></span>'
              }
              <button type="button" class="junkreco-photo-remove" data-action="${escapeHtml(action)}" data-photo-index="${index}" aria-label="Remove ${escapeHtml(file.name || 'photo')}">&times;</button>
            </span>
          `;
        })
        .join('');
    }

    function showPhotoStatus(upload, message, type) {
      const status = upload ? upload.querySelector('[data-junkreco-photo-status]') : null;
      if (!status) {
        return;
      }

      status.hidden = !message;
      status.className = `junkreco-photo-status${message ? ` is-${type || 'info'}` : ''}`;
      status.textContent = message || '';
    }

    function updatePhotoUpload(upload) {
      if (!upload) {
        return;
      }

      const count = state.selectedPhotos.length;
      const previewGrid = upload.querySelector('[data-junkreco-photo-grid]');

      if (previewGrid) {
        previewGrid.hidden = count === 0;
        previewGrid.innerHTML = renderPhotoPreviewSlots();
      }
    }

    function addSelectedPhotos(upload, files) {
      const incoming = Array.from(files || []);
      const errors = new Set();
      const accepted = [];
      const maxPhotos = getMaxPhotoCount();
      const maxPhotoSize = getMaxPhotoSize();
      const maxTotalSize = getMaxPhotoTotalSize();
      const existingIds = new Set(state.selectedPhotos.map(getPhotoIdentity));

      incoming.forEach((file) => {
        if (!isSupportedPhoto(file)) {
          errors.add(config.bookingForm.photoTypeErrorText);
          return;
        }

        if (Number(file.size || 0) > maxPhotoSize) {
          errors.add(getPhotoSizeMessage());
          return;
        }

        if (state.selectedPhotos.length + accepted.length >= maxPhotos) {
          errors.add(getPhotoLimitMessage());
          return;
        }

        const fileId = getPhotoIdentity(file);
        if (existingIds.has(fileId) || accepted.some((photo) => getPhotoIdentity(photo) === fileId)) {
          return;
        }

        if (getSelectedPhotoTotal(state.selectedPhotos.concat(accepted, file)) > maxTotalSize) {
          errors.add(getPhotoTotalSizeMessage());
          return;
        }

        accepted.push(file);
      });

      if (accepted.length) {
        state.selectedPhotos.push(...accepted);
      }

      updatePhotoUpload(upload);
      showPhotoStatus(upload, Array.from(errors).join(' '), errors.size ? 'error' : '');
    }

    function validateSelectedPhotos(upload) {
      if (!state.selectedPhotos.length) {
        return true;
      }

      if (state.selectedPhotos.length > getMaxPhotoCount()) {
        showPhotoStatus(upload, getPhotoLimitMessage(), 'error');
        return false;
      }

      if (state.selectedPhotos.some((file) => !isSupportedPhoto(file))) {
        showPhotoStatus(upload, config.bookingForm.photoTypeErrorText, 'error');
        return false;
      }

      if (state.selectedPhotos.some((file) => Number(file.size || 0) > getMaxPhotoSize())) {
        showPhotoStatus(upload, getPhotoSizeMessage(), 'error');
        return false;
      }

      if (getSelectedPhotoTotal(state.selectedPhotos) > getMaxPhotoTotalSize()) {
        showPhotoStatus(upload, getPhotoTotalSizeMessage(), 'error');
        return false;
      }

      showPhotoStatus(upload, '', '');
      return true;
    }

    function canCompressAiPhoto(file) {
      if (!config.aiEstimator.compressPhotos || !file || !/^image\//i.test(String(file.type || ''))) {
        return false;
      }

      if (String(file.type || '').toLowerCase() === 'image/gif') {
        return false;
      }

      const canvas = document.createElement('canvas');
      return Boolean(canvas.getContext && (canvas.toBlob || canvas.toDataURL));
    }

    function loadPhotoForCanvas(file) {
      if (window.createImageBitmap) {
        return window
          .createImageBitmap(file, { imageOrientation: 'from-image' })
          .then((bitmap) => ({
            image: bitmap,
            width: bitmap.width,
            height: bitmap.height,
            close: () => {
              if (typeof bitmap.close === 'function') {
                bitmap.close();
              }
            },
          }))
          .catch(() => loadPhotoElementForCanvas(file));
      }

      return loadPhotoElementForCanvas(file);
    }

    function loadPhotoElementForCanvas(file) {
      return new Promise((resolve, reject) => {
        if (!window.URL || typeof window.URL.createObjectURL !== 'function') {
          reject(new Error('Image previews are not available in this browser.'));
          return;
        }

        const url = window.URL.createObjectURL(file);
        const image = new Image();
        image.onload = () => {
          resolve({
            image,
            width: image.naturalWidth || image.width,
            height: image.naturalHeight || image.height,
            close: () => window.URL.revokeObjectURL(url),
          });
        };
        image.onerror = () => {
          window.URL.revokeObjectURL(url);
          reject(new Error('The selected photo could not be optimized.'));
        };
        image.src = url;
      });
    }

    function canvasToBlob(canvas, mimeType, quality) {
      return new Promise((resolve) => {
        if (typeof canvas.toBlob === 'function') {
          canvas.toBlob(resolve, mimeType, quality);
          return;
        }

        try {
          const dataUrl = canvas.toDataURL(mimeType, quality);
          const parts = dataUrl.split(',');
          const byteString = window.atob(parts[1] || '');
          const bytes = new Uint8Array(byteString.length);
          for (let index = 0; index < byteString.length; index++) {
            bytes[index] = byteString.charCodeAt(index);
          }
          resolve(new Blob([bytes], { type: mimeType }));
        } catch (error) {
          resolve(null);
        }
      });
    }

    function optimizedPhotoName(name) {
      const original = String(name || 'photo').replace(/\.[^.]+$/, '');
      return `${original || 'photo'}-optimized.jpg`;
    }

    function makeOptimizedPhoto(original, blob) {
      const name = optimizedPhotoName(original.name);
      let optimized;

      try {
        optimized = new File([blob], name, {
          type: blob.type || 'image/jpeg',
          lastModified: original.lastModified || Date.now(),
        });
      } catch (error) {
        optimized = blob;
        optimized.name = name;
        optimized.lastModified = original.lastModified || Date.now();
      }

      optimized.junkrecoCompressed = true;
      optimized.junkrecoOriginalSize = Number(original.size || 0);
      optimized.junkrecoOriginalIdentity = getPhotoIdentity(original);
      return optimized;
    }

    async function compressAiPhoto(file) {
      if (!canCompressAiPhoto(file)) {
        return { file, compressed: false };
      }

      let loaded = null;
      try {
        loaded = await loadPhotoForCanvas(file);
        const maxDimension = getAiCompressionMaxDimension();
        const largestSide = Math.max(loaded.width, loaded.height);
        const scale = largestSide > maxDimension ? maxDimension / largestSide : 1;
        const targetWidth = Math.max(1, Math.round(loaded.width * scale));
        const targetHeight = Math.max(1, Math.round(loaded.height * scale));

        if (scale >= 1 && Number(file.size || 0) < getAiCompressionTriggerSize()) {
          return { file, compressed: false };
        }

        const canvas = document.createElement('canvas');
        canvas.width = targetWidth;
        canvas.height = targetHeight;

        const context = canvas.getContext('2d', { alpha: false });
        if (!context) {
          return { file, compressed: false };
        }

        context.fillStyle = '#fff';
        context.fillRect(0, 0, targetWidth, targetHeight);
        context.drawImage(loaded.image, 0, 0, targetWidth, targetHeight);

        const maxSize = getAiMaxPhotoSize();
        const baseQuality = getAiCompressionQuality();
        const qualities = Array.from(new Set([baseQuality, Math.min(baseQuality, 0.76), 0.68, 0.6]));
        let bestBlob = null;

        for (const quality of qualities) {
          const blob = await canvasToBlob(canvas, 'image/jpeg', quality);
          if (!blob) {
            continue;
          }

          if (!bestBlob || blob.size < bestBlob.size) {
            bestBlob = blob;
          }

          if (blob.size <= maxSize) {
            bestBlob = blob;
            break;
          }
        }

        if (!bestBlob || bestBlob.size >= Number(file.size || 0)) {
          return { file, compressed: false };
        }

        return { file: makeOptimizedPhoto(file, bestBlob), compressed: true };
      } catch (error) {
        return { file, compressed: false };
      } finally {
        if (loaded && typeof loaded.close === 'function') {
          loaded.close();
        }
      }
    }

    function renderAiPhotoUpload() {
      const count = state.aiPhotos.length;
      const previews = renderPhotoPreviewSlots(state.aiPhotos, 'remove-ai-photo');

      return `
        <div class="junkreco-photo-upload junkreco-ai-photo-upload">
          <div class="junkreco-photo-picker">
            <input class="junkreco-photo-input" type="file" name="ai_photos[]" accept="image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif,.jpg,.jpeg,.jpe,.png,.gif,.webp,.heic,.heif" multiple data-junkreco-ai-photo-input />
            <span class="junkreco-photo-grid" data-junkreco-ai-photo-grid ${count ? '' : 'hidden'}>${previews}</span>
          </div>
          <button type="button" class="junkreco-photo-add" data-action="choose-ai-photos" ${state.aiCompressing ? 'disabled' : ''}>Add Photos</button>
          <p>${escapeHtml(config.aiEstimator.photoHelpText)} Max ${getAiMaxPhotoCount()}. ${escapeHtml(formatFileSize(getAiMaxPhotoSize()))} each.</p>
        </div>
      `;
    }

    async function addAiPhotos(files) {
      const incoming = Array.from(files || []);
      const errors = new Set();
      const accepted = [];
      const maxPhotos = getAiMaxPhotoCount();
      const maxPhotoSize = getAiMaxPhotoSize();
      const maxTotalSize = getAiMaxPhotoTotalSize();
      const existingIds = new Set(state.aiPhotos.map(getPhotoIdentity));
      let compressedCount = 0;

      if (!incoming.length || state.aiCompressing) {
        return;
      }

      state.aiCompressing = true;
      state.aiError = '';
      state.aiMessage = '';
      render(false);

      try {
        for (const file of incoming) {
          if (!isSupportedPhoto(file)) {
            errors.add(config.aiEstimator.photoTypeErrorText);
            continue;
          }

          if (state.aiPhotos.length + accepted.length >= maxPhotos) {
            errors.add(getAiPhotoLimitMessage());
            continue;
          }

          const fileId = getPhotoIdentity(file);
          if (existingIds.has(fileId) || accepted.some((photo) => getPhotoIdentity(photo) === fileId)) {
            continue;
          }

          const optimized = await compressAiPhoto(file);
          const nextFile = optimized.file;
          if (optimized.compressed) {
            compressedCount += 1;
          }

          if (Number(nextFile.size || 0) > maxPhotoSize) {
            errors.add(getAiPhotoSizeMessage());
            continue;
          }

          if (getSelectedPhotoTotal(state.aiPhotos.concat(accepted, nextFile)) > maxTotalSize) {
            errors.add(getAiPhotoTotalSizeMessage());
            continue;
          }

          accepted.push(nextFile);
        }

        if (accepted.length) {
          state.aiPhotos.push(...accepted);
          state.aiEstimate = null;
          state.aiMessage = compressedCount ? formatText(config.aiEstimator.compressionDoneText, [compressedCount]) : '';
        }

        state.aiError = Array.from(errors).join(' ');
      } catch (error) {
        state.aiError = config.aiEstimator.errorText;
      } finally {
        state.aiCompressing = false;
        render(false);
      }
    }

    function validateAiPhotos() {
      if (!state.aiPhotos.length) {
        state.aiError = config.aiEstimator.noPhotosText;
        return false;
      }

      if (state.aiPhotos.length > getAiMaxPhotoCount()) {
        state.aiError = getAiPhotoLimitMessage();
        return false;
      }

      if (state.aiPhotos.some((file) => !isSupportedPhoto(file))) {
        state.aiError = config.aiEstimator.photoTypeErrorText;
        return false;
      }

      if (state.aiPhotos.some((file) => Number(file.size || 0) > getAiMaxPhotoSize())) {
        state.aiError = getAiPhotoSizeMessage();
        return false;
      }

      if (getSelectedPhotoTotal(state.aiPhotos) > getAiMaxPhotoTotalSize()) {
        state.aiError = getAiPhotoTotalSizeMessage();
        return false;
      }

      state.aiError = '';
      return true;
    }

    function normalizeAiEstimate(raw) {
      const estimate = raw && typeof raw === 'object' ? raw : {};
      const itemBreakdown = Array.isArray(estimate.item_breakdown)
        ? estimate.item_breakdown
            .map((item) => ({
              item: String(item.item || ''),
              quantity: Number(item.quantity || 0),
              cubicYards: Number(item.cubicYards || item.cubic_yards || 0),
              weight: Number(item.weight || 0),
            }))
            .filter((item) => item.item)
        : [];
      const rawExtraFees = Array.isArray(estimate.extra_fees)
        ? estimate.extra_fees
        : Array.isArray(estimate.detected_extra_fees)
          ? estimate.detected_extra_fees
          : [];
      const detectedExtraFees = rawExtraFees.length
        ? rawExtraFees
            .map((fee) => ({
              id: String(fee.id || ''),
              name: String(fee.name || fee.fee_name || fee.item || fee.label || ''),
              quantity: Number(fee.quantity || fee.count || fee.units || 1),
              per_unit_price: Number(fee.per_unit_price || fee.unit_price || fee.price || 0),
              total: Number(fee.subtotal || fee.total || fee.amount || fee.fee || fee.line_total || 0),
              reason: String(fee.reason || ''),
              instruction: String(fee.instruction || ''),
            }))
            .map((fee) => {
              if (!fee.total && fee.per_unit_price) {
                fee.total = fee.per_unit_price * Math.max(1, fee.quantity);
              }
              return fee;
            })
            .filter((fee) => fee.name || fee.total)
        : [];
      const extraFeesTotal =
        Number(estimate.extra_fees_total || estimate.detected_extra_fees_total || 0) ||
        detectedExtraFees.reduce((total, fee) => total + Number(fee.total || 0), 0);
      const recommendedPrice = Number(estimate.recommended_price || 0);
      const recommendedPriceWithFees =
        Number(estimate.recommended_price_with_fees || 0) || (extraFeesTotal ? recommendedPrice + extraFeesTotal : recommendedPrice);

      return {
        estimate_id: String(estimate.estimate_id || estimate.id || ''),
        cubic_yards: Number(estimate.cubic_yards || 0),
        estimated_weight_lbs: Number(estimate.estimated_weight_lbs || estimate.estimated_weight || 0),
        confidence: Math.max(0, Math.min(100, Number(estimate.confidence || 0))),
        landfill_cost: Number(estimate.landfill_cost || 0),
        recommended_price: recommendedPrice,
        recommended_price_with_fees: recommendedPriceWithFees,
        total_cost: Number(estimate.total_cost || 0),
        recommended_load_fraction: String(estimate.recommended_load_fraction || ''),
        truck_percentage: Math.max(0, Math.min(100, Number(estimate.truck_percentage || 0))),
        item_breakdown: itemBreakdown,
        detected_extra_fees: detectedExtraFees,
        extra_fees_total: extraFeesTotal,
        analysis_notes: String(estimate.analysis_notes || ''),
        created_at: String(estimate.created_at || ''),
      };
    }

    function getPreviewAiEstimate() {
      return normalizeAiEstimate({
        cubic_yards: 5.2,
        estimated_weight_lbs: 950,
        confidence: 78,
        landfill_cost: 30.88,
        recommended_price: 495,
        recommended_load_fraction: '1/4 Load',
        truck_percentage: 33,
        item_breakdown: [
          { item: 'Couch', quantity: 1, cubicYards: 2.5, weight: 180 },
          { item: 'Mixed boxes', quantity: 6, cubicYards: 2.7, weight: 770 },
        ],
        detected_extra_fees: [{ name: 'Mattress disposal', quantity: 1, total: 35 }],
        extra_fees_total: 35,
        analysis_notes: 'Preview estimate shown for design testing.',
      });
    }

    function renderAiMetric(label, value) {
      return `
        <div>
          <span>${escapeHtml(label)}</span>
          <strong>${escapeHtml(value)}</strong>
        </div>
      `;
    }

    function formatQuantity(value) {
      const quantity = Number(value || 0);
      return Number.isInteger(quantity) ? String(quantity) : formatNumber(quantity, 1);
    }

    function renderAiEstimateDetails(estimate, compact) {
      if (!estimate) {
        return '';
      }

      return `
        <div class="${compact ? 'junkreco-ai-details is-compact' : 'junkreco-ai-details'}">
          <div class="junkreco-ai-metrics">
            ${renderAiMetric(config.aiEstimator.loadFractionLabel, estimate.recommended_load_fraction || config.strings.aiResultTitle)}
            ${renderAiMetric(config.aiEstimator.cubicYardsLabel, `${formatNumber(estimate.cubic_yards, 2)} cu yd`)}
            ${renderAiMetric(config.aiEstimator.weightLabel, `${formatNumber(estimate.estimated_weight_lbs, 0)} lbs`)}
            ${renderAiMetric(config.aiEstimator.truckPercentLabel, `${formatNumber(estimate.truck_percentage, 0)}%`)}
            ${renderAiMetric(config.aiEstimator.confidenceLabel, `${formatNumber(estimate.confidence, 0)}%`)}
            ${renderAiMetric(config.aiEstimator.landfillCostLabel, money(estimate.landfill_cost, 2))}
            ${estimate.extra_fees_total > 0 ? renderAiMetric(config.aiEstimator.extraFeesTotalLabel, money(estimate.extra_fees_total, 2)) : ''}
          </div>
          ${
            estimate.item_breakdown.length
              ? `
                <div class="junkreco-ai-breakdown">
                  <p class="junkreco-kicker">${escapeHtml(config.aiEstimator.itemBreakdownLabel)}</p>
                  ${estimate.item_breakdown
                    .map(
                      (item) => `
                        <div>
                          <span>${escapeHtml(item.item)} x ${formatQuantity(item.quantity)}</span>
                          <small>${formatNumber(item.cubicYards, 2)} cu yd · ${formatNumber(item.weight, 0)} lbs</small>
                        </div>
                      `
                    )
                    .join('')}
                </div>
              `
              : ''
          }
          ${
            estimate.detected_extra_fees.length
              ? `
                <div class="junkreco-ai-breakdown junkreco-ai-extra-fees">
                  <p class="junkreco-kicker">${escapeHtml(config.aiEstimator.extraFeesLabel)}</p>
                  ${estimate.detected_extra_fees
                    .map(
                      (fee) => `
                        <div>
                          <span>${escapeHtml(fee.name || 'Extra fee')}${fee.quantity ? ` x ${formatQuantity(fee.quantity)}` : ''}</span>
                          <small>${money(fee.total, 2)}${fee.per_unit_price ? ` · ${money(fee.per_unit_price, 2)} each` : ''}</small>
                          ${fee.reason ? `<small>${escapeHtml(fee.reason)}</small>` : ''}
                        </div>
                      `
                    )
                    .join('')}
                </div>
              `
              : ''
          }
          ${
            estimate.analysis_notes
              ? `<p class="junkreco-ai-notes"><strong>${escapeHtml(config.aiEstimator.analysisNotesLabel)}:</strong> ${escapeHtml(estimate.analysis_notes)}</p>`
              : ''
          }
        </div>
      `;
    }

    function renderAiEstimatePhotos() {
      if (!state.aiPhotos.length) {
        return '';
      }

      return `
        <div class="junkreco-ai-result-photos" aria-label="${escapeHtml(config.aiEstimator.resultPhotosLabel)}">
          ${state.aiPhotos
            .map((file) => {
              const previewUrl = getPhotoPreviewUrl(file);
              return `
                <span>
                  ${
                    previewUrl
                      ? `<img src="${escapeHtml(previewUrl)}" alt="${escapeHtml(file.name)}" />`
                      : '<i class="junkreco-photo-icon" aria-hidden="true"></i>'
                  }
                </span>
              `;
            })
            .join('')}
        </div>
      `;
    }

    function renderAiResult() {
      if (!state.aiEstimate) {
        return `
          <div class="junkreco-ai-empty">
            <div class="junkreco-empty-graphic" aria-hidden="true"></div>
            <p>${escapeHtml(config.strings.aiResultTitle)}</p>
            <span>${escapeHtml(config.aiEstimator.disclaimer)}</span>
          </div>
        `;
      }

      return `
        <div class="junkreco-ai-result">
          <p class="junkreco-kicker">${escapeHtml(config.strings.aiTitle)}</p>
          <h3>${money(state.aiEstimate.recommended_price_with_fees || state.aiEstimate.recommended_price, 2)}</h3>
          <span>${escapeHtml(config.aiEstimator.disclaimer)}</span>
          ${renderAiEstimatePhotos()}
          ${renderAiEstimateDetails(state.aiEstimate, false)}
          <div class="junkreco-ai-result-actions">
            ${renderBookingAction('ai', false)}
            <button type="button" class="junkreco-ai-reset" data-action="reset-ai-estimate">${escapeHtml(config.aiEstimator.resetLabel)}</button>
          </div>
        </div>
      `;
    }

    function renderAiMode() {
      const statusClass = state.aiCompressing || state.aiLoading ? 'is-loading' : state.aiError ? 'is-error' : state.aiMessage ? 'is-success' : '';
      const statusText = state.aiCompressing ? config.aiEstimator.compressionText : state.aiLoading ? config.aiEstimator.submittingText : state.aiError || state.aiMessage;

      if (state.aiEstimate && !state.aiCompressing && !state.aiLoading) {
        return `
          <div class="junkreco-ai is-result-only">
            <div class="junkreco-ai-output">
              ${renderAiResult()}
            </div>
          </div>
        `;
      }

      return `
        <form class="junkreco-ai" data-junkreco-ai-form novalidate>
          <div class="junkreco-ai-inputs">
            <h3>${escapeHtml(config.strings.aiTitle)}</h3>
            <p>${escapeHtml(config.aiEstimator.introText)}</p>
            ${renderAiPhotoUpload()}
            ${
              config.aiEstimator.enableNotes
                ? `
                  <label class="junkreco-ai-notes-field">
                    <span>${escapeHtml(config.aiEstimator.notesLabel)}</span>
                    <textarea name="notes" rows="4" maxlength="2000" data-junkreco-ai-notes placeholder="${escapeHtml(config.aiEstimator.notesPlaceholder)}">${escapeHtml(state.aiNotes)}</textarea>
                  </label>
                `
                : ''
            }
            <div class="junkreco-booking-status ${statusClass}" aria-live="polite">${escapeHtml(statusText || '')}</div>
            <button type="submit" class="junkreco-booking-submit" ${state.aiLoading || state.aiCompressing ? 'disabled' : ''}>${escapeHtml(state.aiCompressing ? config.aiEstimator.compressionText : state.aiLoading ? config.aiEstimator.submittingText : config.aiEstimator.submitLabel)}</button>
          </div>
          <div class="junkreco-ai-output">
            ${renderAiResult()}
          </div>
        </form>
      `;
    }

    function renderYesNoToggle(name, title, help) {
      return `
        <label class="junkreco-toggle-card">
          <span>
            <strong>${escapeHtml(title)}</strong>
            <small>${escapeHtml(help)}</small>
          </span>
          <span class="junkreco-segmented-toggle">
            <input type="checkbox" name="${escapeHtml(name)}" value="1" />
            <b>No</b>
            <b>Yes</b>
          </span>
        </label>
      `;
    }

    function requiredAttr(enabled) {
      return enabled ? 'required' : '';
    }

    function updateStairsFlightsVisibility(form) {
      if (!form) {
        return;
      }

      const stairsToggle = form.querySelector('input[name="has_stairs"]');
      const stairsCount = form.querySelector('.junkreco-stairs-count');
      const stairsInput = form.querySelector('input[name="stairs_flights"]');

      if (!stairsToggle || !stairsCount || !stairsInput) {
        return;
      }

      const showFlights = Boolean(stairsToggle.checked);
      stairsCount.hidden = !showFlights;
      stairsInput.disabled = !showFlights;
    }

    function usesZipEligibility() {
      return Boolean(config.bookingForm.enableZipEligibility) && config.bookingForm.allowedZipCodes.length > 0;
    }

    function isAllowedZip(zip) {
      return config.bookingForm.allowedZipCodes.includes(zip);
    }

    function renderBookingBackButton() {
      return `
        <button type="button" class="junkreco-booking-back" data-action="close-booking">
          <span aria-hidden="true">&lt;</span>
          <span>${escapeHtml(config.strings.bookingBack)}</span>
        </button>
      `;
    }

    function renderZipGate() {
      if (!state.bookingOpen) {
        return '';
      }

      const estimate = currentEstimate(state.bookingMethod || state.mode);
      if (!estimate) {
        return '';
      }

      const previewMode = isDesignPreview();
      const zipFormTag = previewMode ? 'div' : 'form';
      const zipFormAttrs = previewMode ? 'data-junkreco-zip-form data-junkreco-preview-form role="form"' : 'data-junkreco-zip-form novalidate';

      return `
        <section class="junkreco-booking-panel junkreco-zip-panel" aria-label="${escapeHtml(config.bookingForm.zipTitle)}">
          <div class="junkreco-booking-head">
            <div>
              <p>${escapeHtml(estimate.methodLabel || config.strings.methodLabel)}</p>
              <h2>${escapeHtml(config.bookingForm.zipTitle)}</h2>
              <span>${escapeHtml(config.bookingForm.zipIntro)}</span>
            </div>
          </div>
          <${zipFormTag} class="junkreco-zip-form" ${zipFormAttrs}>
            <div class="junkreco-zip-card">
              <label>
                <span>${escapeHtml(config.bookingForm.zipLabel)}</span>
                <input class="junkreco-zip-input" name="zip_code" type="text" inputmode="numeric" autocomplete="postal-code" maxlength="10" placeholder="${escapeHtml(config.bookingForm.zipPlaceholder)}" value="${escapeHtml(state.verifiedZip)}" required />
              </label>
              <div class="junkreco-booking-status" data-junkreco-zip-status aria-live="polite"></div>
              <button type="${previewMode ? 'button' : 'submit'}" class="junkreco-zip-submit">${escapeHtml(config.bookingForm.zipSubmitLabel)}</button>
            </div>
          </${zipFormTag}>
        </section>
      `;
    }

    function renderBookingForm() {
      if (!state.bookingOpen) {
        return '';
      }

      const method = state.bookingMethod || state.mode;
      const estimate = currentEstimate(method);
      if (!estimate) {
        return '';
      }

      const requireAddress = Boolean(config.bookingForm.requireAddress);
      const requireDate = Boolean(config.bookingForm.requirePreferredDate);
      const requireConsent = Boolean(config.bookingForm.requireTextConsent);
      const previewMode = isDesignPreview();
      const bookingFormTag = previewMode ? 'div' : 'form';
      const bookingFormAttrs = previewMode
        ? 'data-junkreco-booking-form data-junkreco-preview-form role="form"'
        : 'data-junkreco-booking-form enctype="multipart/form-data"';

      return `
        <section class="junkreco-booking-panel" aria-label="${escapeHtml(config.bookingForm.formTitle)}">
          <div class="junkreco-booking-head">
            <div>
              <p>${escapeHtml(config.strings.methodLabel)}</p>
              <h2>${escapeHtml(config.bookingForm.formTitle)}</h2>
              <span>${escapeHtml(config.bookingForm.formIntro)}</span>
            </div>
          </div>
          <${bookingFormTag} class="junkreco-booking-form" ${bookingFormAttrs}>
            ${renderBookingEstimateSummary(estimate)}
            <div class="junkreco-booking-fields">
              <fieldset>
                <legend>Contact information</legend>
                <div class="junkreco-field-grid">
                  <label><span>First Name</span><input name="first_name" type="text" placeholder="First name" required /></label>
                  <label><span>Last Name</span><input name="last_name" type="text" placeholder="Last name" required /></label>
                  <label><span>Email</span><input name="email" type="email" placeholder="Email address" required /></label>
                  <label><span>Phone Number</span><input name="phone" type="tel" placeholder="Phone number" required /></label>
                </div>
              </fieldset>

              <fieldset>
                <legend>Booking location</legend>
                <label><span>Street Address</span><input name="street" type="text" placeholder="Street address" ${requiredAttr(requireAddress)} /></label>
                <div class="junkreco-field-grid is-three">
                  <label><span>City</span><input name="city" type="text" placeholder="City" ${requiredAttr(requireAddress)} /></label>
                  <label><span>State</span><input name="state" type="text" maxlength="2" placeholder="State" ${requiredAttr(requireAddress)} /></label>
                  <label><span>Postal Code</span><input name="postal_code" type="text" inputmode="numeric" placeholder="Zip code" value="${escapeHtml(state.verifiedZip)}" ${requiredAttr(requireAddress)} /></label>
                </div>
              </fieldset>

              <fieldset>
                <legend>Order information</legend>
                <div class="junkreco-field-grid">
                  <label><span>Pickup Date</span><input name="pickup_date" type="date" ${requiredAttr(requireDate)} /></label>
                  <label>
                    <span>Pickup Time</span>
                    <select name="pickup_time">
                      <option value="">Select time slot</option>
                      <option value="morning">Morning (8AM - 12PM)</option>
                      <option value="afternoon">Afternoon (12PM - 4PM)</option>
                      <option value="evening">Evening (4PM - 8PM)</option>
                      <option value="all-day">All Day (8AM - 8PM)</option>
                    </select>
                  </label>
                </div>

                ${
                  config.bookingForm.showPickupOptions
                    ? `
                      <div class="junkreco-choice-group">
                        <p>How should we collect your items?</p>
                        <label><input type="radio" name="pickup_type" value="in-home" checked /> <span>In-home pickup <small>Our team will remove items from inside your home</small></span></label>
                        <label><input type="radio" name="pickup_type" value="outdoor" /> <span>Outdoor pickup <small>Leave items outside. No need to be present.</small></span></label>
                      </div>
                    `
                    : ''
                }

                ${
                  config.bookingForm.showStairs || config.bookingForm.showDisassembly || config.bookingForm.showDonation
                    ? `
                      <div class="junkreco-toggle-list">
                        ${
                          config.bookingForm.showStairs
                            ? `
                              ${renderYesNoToggle('has_stairs', 'Are stairs involved there?', '6 or more steps inside or outside your home')}
                              <label class="junkreco-stairs-count" hidden><span>Flights of Stairs</span><input type="number" name="stairs_flights" min="1" max="20" value="1" disabled /></label>
                            `
                            : ''
                        }
                        ${config.bookingForm.showDisassembly ? renderYesNoToggle('needs_disassembly', 'Do items need disassembly?', "Don't worry, our team can handle it") : ''}
                        ${
                          config.bookingForm.showDonation
                            ? `
                              ${renderYesNoToggle('donate_items', 'Donate items instead of discarding?', "We'll try to donate your usable items.")}
                              <label><span>Choose a Preferred Donation Organization (Optional)</span><input type="text" name="donation_organization" placeholder="Donation organization" /></label>
                            `
                            : ''
                        }
                      </div>
                    `
                    : ''
                }
              </fieldset>

              ${
                config.bookingForm.showSpecialNote
                  ? `
                    <fieldset>
                      <legend>Special Note</legend>
                      <textarea name="special_note" rows="4" maxlength="500" placeholder="Any special instructions..."></textarea>
                    </fieldset>
                  `
                  : ''
              }

              <label class="junkreco-consent">
                <input type="checkbox" name="text_consent" value="1" ${requiredAttr(requireConsent)} />
                <span>${escapeHtml(config.bookingForm.consentText)}</span>
              </label>

              <div class="junkreco-booking-status" data-junkreco-booking-status aria-live="polite"></div>
              <button type="${previewMode ? 'button' : 'submit'}" class="junkreco-booking-submit">${escapeHtml(config.bookingForm.submitLabel)}</button>
            </div>
          </${bookingFormTag}>
        </section>
      `;
    }

    function renderBookingStep() {
      const panel = state.bookingStep === 'zip' && usesZipEligibility() ? renderZipGate() : renderBookingForm();
      if (!panel) {
        return '';
      }

      return `
        <div class="junkreco-booking-nav">
          ${renderBookingBackButton()}
        </div>
        ${panel}
      `;
    }

    function renderCart() {
      if (!state.cart.length) {
        return `
          <div class="junkreco-cart-empty">
            <div class="junkreco-empty-graphic" aria-hidden="true"></div>
            <p>${escapeHtml(config.emptyTitle)}</p>
            <span>${escapeHtml(config.emptyMessage)}</span>
          </div>
        `;
      }

      const subtotal = cartSubtotal();
      const total = subtotal + Number(config.areaFee || 0);

      return `
        <div class="junkreco-cart-list">
          ${state.cart
            .map(
              (item) => `
                <div class="junkreco-cart-row">
                  <span class="junkreco-cart-media">${renderImage(item.image, item.name, '')}</span>
                  <span class="junkreco-cart-name">
                    <strong>${escapeHtml(item.name)}</strong>
                    <em>${money(item.price, 0)}</em>
                  </span>
                  <span class="junkreco-qty">
                    <button type="button" data-action="decrease-item" data-item-id="${escapeHtml(item.id)}" aria-label="Decrease ${escapeHtml(item.name)}">-</button>
                    <b>${item.quantity}</b>
                    <button type="button" data-action="increase-item" data-item-id="${escapeHtml(item.id)}" aria-label="Increase ${escapeHtml(item.name)}">+</button>
                  </span>
                  <strong class="junkreco-line-price">${money(item.price * item.quantity, 0)}</strong>
                  <button type="button" class="junkreco-remove" data-action="remove-item" data-item-id="${escapeHtml(item.id)}" aria-label="Remove ${escapeHtml(item.name)}">x</button>
                </div>
              `
            )
            .join('')}
        </div>
        <div class="junkreco-total-box">
          <div><span>${escapeHtml(config.strings.subtotal)}</span><strong>${money(subtotal, 2)}</strong></div>
          <div><span>${escapeHtml(config.strings.areaFees)}</span><strong>${money(config.areaFee || 0, 2)}</strong></div>
          <div class="junkreco-total-line"><span>${escapeHtml(config.strings.total)}</span><strong>${money(total, 2)}</strong></div>
        </div>
        <p class="junkreco-note"><span>*</span><strong>Note:</strong> ${escapeHtml(config.estimateNote)}</p>
        ${renderBookingAction('specific', false)}
      `;
    }

    function renderSpecificMode() {
      return `
        <div class="junkreco-specific">
          <div class="junkreco-select-panel">
            <h3>${escapeHtml(config.strings.selectItems)}</h3>
            ${state.selectedCategoryId ? renderCategoryDetails() : renderCategoryBrowser()}
          </div>
          <div class="junkreco-cart-panel">
            <h3>${escapeHtml(config.strings.selectedItems)}</h3>
            <hr />
            ${renderCart()}
          </div>
        </div>
      `;
    }

    function renderLoad(load) {
      const selected = state.selectedLoadId === load.id;
      return `
        <button type="button" class="junkreco-load ${selected ? 'is-selected' : ''}" data-action="select-load" data-load-id="${escapeHtml(load.id)}" aria-pressed="${selected}">
          <span class="junkreco-load-art">
            ${renderImage(load.image, load.title, '')}
          </span>
          <span class="junkreco-load-copy">
            <strong>${escapeHtml(load.title)} <em>${escapeHtml(load.items)}</em></strong>
            <small>${escapeHtml(load.description)}</small>
            <b>${escapeHtml(config.strings.startingAt)} ${money(load.price, 0)}</b>
          </span>
        </button>
      `;
    }

    function renderVolumeMode() {
      const selectedLoad = findLoad(state.selectedLoadId);

      return `
        <div class="junkreco-volume">
          ${
            catalog.loads.length
              ? `<div class="junkreco-load-grid">${catalog.loads.map(renderLoad).join('')}</div>`
              : `<p class="junkreco-empty-line">No truck volume options are configured yet.</p>`
          }
          ${
            selectedLoad
              ? `<p class="junkreco-load-summary">Selected estimate: <strong>${escapeHtml(selectedLoad.title)} ${money(selectedLoad.price, 0)}</strong></p>`
              : ''
          }
          <p class="junkreco-volume-cta">
            ${escapeHtml(config.strings.volumeCtaPrefix)}
            ${renderBookingAction('volume', !selectedLoad)}
          </p>
        </div>
      `;
    }

    function render(preserveSearchFocus) {
      const title = root.dataset.title || 'Calculate Your Junk Removal Cost';
      const intro = root.dataset.intro || '';
      if (!availableMethods.includes(state.mode)) {
        state.mode = availableMethods[0];
      }

      let bookingMarkup = state.bookingOpen ? renderBookingStep() : '';
      if (state.bookingOpen && !bookingMarkup) {
        state.bookingOpen = false;
        bookingMarkup = '';
      }
      root.classList.toggle('has-booking-open', Boolean(bookingMarkup));

      const mainMarkup = `
        <div class="junkreco-heading">
          <h2>${escapeHtml(title)}</h2>
          ${intro ? `<p>${escapeHtml(intro)}</p>` : ''}
        </div>
        ${renderMethodSelector()}
        ${state.mode === 'specific' ? renderSpecificMode() : state.mode === 'volume' ? renderVolumeMode() : renderAiMode()}
      `;

      root.innerHTML = `
        <section class="junkreco-shell ${bookingMarkup ? 'is-booking-step' : ''}" aria-label="${escapeHtml(bookingMarkup ? (state.bookingStep === 'zip' ? config.bookingForm.zipTitle : config.bookingForm.formTitle) : title)}">
          ${bookingMarkup || mainMarkup}
        </section>
      `;

      if (preserveSearchFocus && !bookingMarkup) {
        const input = root.querySelector('.junkreco-search');
        if (input) {
          input.focus();
          input.setSelectionRange(input.value.length, input.value.length);
        }
      }
    }

    function openBookingForm(method) {
      state.bookingMethod = availableMethods.includes(method) ? method : state.mode;
      state.bookingOpen = true;
      state.bookingStep = usesZipEligibility() ? 'zip' : 'form';
      state.verifiedZip = '';
      clearSelectedPhotos();
      render(false);

      window.setTimeout(() => {
        const panel = root.querySelector('.junkreco-booking-panel');
        const scrollTarget = root.querySelector('.junkreco-booking-nav') || panel;
        if (panel) {
          scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
          const firstInput = panel.querySelector('input[name="zip_code"], input[name="first_name"], input, select, textarea, button');
          if (firstInput) {
            firstInput.focus({ preventScroll: true });
          }
        }
      }, 20);
    }

    function resetBookingStep() {
      state.bookingOpen = false;
      state.bookingStep = 'form';
      state.verifiedZip = '';
      clearSelectedPhotos();
    }

    function resetAiEstimate() {
      clearAiPhotos();
      state.aiNotes = '';
      state.aiEstimate = null;
      state.aiLoading = false;
      state.aiCompressing = false;
      state.aiError = '';
      state.aiMessage = '';
      render(false);
    }

    function submitZipGate(form) {
      const input = form.querySelector('input[name="zip_code"]');
      const status = form.querySelector('[data-junkreco-zip-status]');
      const zip = normalizeZipCode(input ? input.value : '');

      if (!zip) {
        if (status) {
          status.className = 'junkreco-booking-status is-error';
          status.textContent = config.bookingForm.zipInvalidMessage;
        }
        if (input) {
          input.focus();
        }
        return;
      }

      if (!isAllowedZip(zip)) {
        if (status) {
          status.className = 'junkreco-booking-status is-error';
          status.textContent = config.bookingForm.zipIneligibleMessage;
        }
        if (input) {
          input.focus();
          input.select();
        }
        return;
      }

      if (status) {
        status.className = 'junkreco-booking-status is-success';
        status.textContent = config.bookingForm.zipEligibleMessage;
      }

      state.verifiedZip = zip;
      state.bookingStep = 'form';
      render(false);

      window.setTimeout(() => {
        const firstInput = root.querySelector('input[name="first_name"], input, select, textarea, button');
        if (firstInput) {
          firstInput.focus({ preventScroll: true });
        }
      }, 20);
    }

    function collectBookingPayload(form) {
      const data = new FormData(form);
      const checked = (name) => {
        const input = form.elements[name];
        return Boolean(input && input.checked);
      };
      const hasStairs = checked('has_stairs');

      return {
        first_name: String(data.get('first_name') || ''),
        last_name: String(data.get('last_name') || ''),
        email: String(data.get('email') || ''),
        phone: String(data.get('phone') || ''),
        street: String(data.get('street') || ''),
        city: String(data.get('city') || ''),
        state: String(data.get('state') || ''),
        postal_code: String(data.get('postal_code') || ''),
        pickup_date: String(data.get('pickup_date') || ''),
        pickup_time: String(data.get('pickup_time') || ''),
        pickup_type: String(data.get('pickup_type') || ''),
        has_stairs: hasStairs,
        stairs_flights: hasStairs ? String(data.get('stairs_flights') || '1') : '1',
        needs_disassembly: checked('needs_disassembly'),
        donate_items: checked('donate_items'),
        donation_organization: String(data.get('donation_organization') || ''),
        special_note: String(data.get('special_note') || ''),
        text_consent: checked('text_consent'),
        page_url: window.location.href,
        estimate: currentEstimate(state.bookingMethod || state.mode),
      };
    }

    async function submitBookingForm(form) {
      const status = form.querySelector('[data-junkreco-booking-status]');
      const submitButton = form.querySelector('.junkreco-booking-submit');
      const photoUpload = form.querySelector('.junkreco-photo-upload');

      if (!config.bookingForm.ajaxUrl || !config.bookingForm.nonce) {
        if (status) {
          status.className = 'junkreco-booking-status is-error';
          status.textContent = config.bookingForm.errorText;
        }
        return;
      }

      if (!validateSelectedPhotos(photoUpload)) {
        return;
      }

      const payload = new FormData(form);
      payload.delete('photos[]');
      state.selectedPhotos.forEach((file) => {
        payload.append('photos[]', file, file.name);
      });
      payload.append('action', 'junkreco_submit_booking');
      payload.append('nonce', config.bookingForm.nonce);
      payload.append('booking', JSON.stringify(collectBookingPayload(form)));

      if (submitButton) {
        submitButton.disabled = true;
        submitButton.textContent = config.bookingForm.submittingText;
      }
      if (status) {
        status.className = 'junkreco-booking-status is-loading';
        status.textContent = config.bookingForm.submittingText;
      }

      try {
        const response = await fetch(config.bookingForm.ajaxUrl, {
          method: 'POST',
          body: payload,
          credentials: 'same-origin',
        });
        const json = await response.json();

        if (!response.ok || !json || !json.success) {
          throw new Error((json && json.data && json.data.message) || config.bookingForm.errorText);
        }

        const message = (json.data && json.data.message) || config.bookingForm.successMessage;
        if (status) {
          status.className = 'junkreco-booking-status is-success';
          status.textContent = message;
        }

        const redirectUrl = safeUrl(json.data && json.data.redirectUrl ? String(json.data.redirectUrl) : '', '', ['http:', 'https:']);
        if (redirectUrl) {
          window.setTimeout(() => {
            window.location.href = redirectUrl;
          }, 600);
        }
      } catch (error) {
        if (status) {
          status.className = 'junkreco-booking-status is-error';
          status.textContent = error && error.message ? error.message : config.bookingForm.errorText;
        }
        if (submitButton) {
          submitButton.disabled = false;
          submitButton.textContent = config.bookingForm.submitLabel;
        }
      }
    }

    async function submitAiEstimate(form) {
      if (config.aiEstimator.enableNotes) {
        const notes = form.querySelector('[data-junkreco-ai-notes]');
        state.aiNotes = notes ? notes.value : state.aiNotes;
      }

      if (state.aiCompressing) {
        render(false);
        return;
      }

      if (!validateAiPhotos()) {
        render(false);
        return;
      }

      if (isDesignPreview()) {
        state.aiLoading = true;
        state.aiError = '';
        state.aiMessage = '';
        render(false);
        window.setTimeout(() => {
          state.aiEstimate = getPreviewAiEstimate();
          state.aiLoading = false;
          state.aiMessage = config.aiEstimator.successText;
          render(false);
        }, 500);
        return;
      }

      if (!config.aiEstimator.ajaxUrl || !config.aiEstimator.nonce) {
        state.aiError = config.aiEstimator.errorText;
        render(false);
        return;
      }

      const payload = new FormData();
      payload.append('action', 'junkreco_create_ai_estimate');
      payload.append('nonce', config.aiEstimator.nonce);
      payload.append('notes', state.aiNotes);
      state.aiPhotos.forEach((file) => {
        payload.append('photos[]', file, file.name);
      });

      state.aiLoading = true;
      state.aiError = '';
      state.aiMessage = '';
      render(false);

      try {
        const response = await fetch(config.aiEstimator.ajaxUrl, {
          method: 'POST',
          body: payload,
          credentials: 'same-origin',
        });
        const json = await response.json();

        if (!response.ok || !json || !json.success) {
          throw new Error((json && json.data && json.data.message) || config.aiEstimator.errorText);
        }

        state.aiEstimate = normalizeAiEstimate(json.data && json.data.estimate);
        state.aiMessage = config.aiEstimator.successText;
      } catch (error) {
        state.aiError = error && error.message ? error.message : config.aiEstimator.errorText;
      } finally {
        state.aiLoading = false;
        render(false);
      }
    }

    root.addEventListener('click', (event) => {
      const target = event.target.closest('[data-action]');
      if (!target || !root.contains(target)) {
        return;
      }

      const action = target.dataset.action;

      if (action === 'open-booking') {
        openBookingForm(target.dataset.bookingMethod || state.mode);
        return;
      }

      if (action === 'close-booking') {
        resetBookingStep();
        render(false);
        return;
      }

      if (action === 'choose-photos') {
        const upload = target.closest('.junkreco-photo-upload');
        const input = upload ? upload.querySelector('[data-junkreco-photo-input]') : null;
        if (input) {
          input.click();
        }
        return;
      }

      if (action === 'choose-ai-photos') {
        if (state.aiCompressing) {
          return;
        }
        const input = root.querySelector('[data-junkreco-ai-photo-input]');
        if (input) {
          input.click();
        }
        return;
      }

      if (action === 'remove-photo') {
        const index = Number(target.dataset.photoIndex);
        const upload = target.closest('.junkreco-photo-upload');
        if (Number.isInteger(index) && index >= 0 && index < state.selectedPhotos.length) {
          revokePhotoPreviewUrl(state.selectedPhotos[index]);
          state.selectedPhotos.splice(index, 1);
          updatePhotoUpload(upload);
          showPhotoStatus(upload, '', '');
        }
        return;
      }

      if (action === 'remove-ai-photo') {
        const index = Number(target.dataset.photoIndex);
        if (Number.isInteger(index) && index >= 0 && index < state.aiPhotos.length) {
          revokePhotoPreviewUrl(state.aiPhotos[index]);
          state.aiPhotos.splice(index, 1);
          state.aiEstimate = null;
          state.aiMessage = '';
          state.aiError = '';
          render(false);
        }
        return;
      }

      if (action === 'reset-ai-estimate') {
        resetAiEstimate();
        return;
      }

      if (action === 'set-mode') {
        const nextMode = String(target.dataset.mode || '');
        if (!availableMethods.includes(nextMode)) {
          return;
        }

        state.mode = nextMode;
        resetBookingStep();
        render(false);
        return;
      }

      if (action === 'open-category') {
        state.selectedCategoryId = target.dataset.categoryId || null;
        state.search = '';
        state.moreOffset = 0;
        render(false);
        return;
      }

      if (action === 'back-to-categories') {
        state.selectedCategoryId = null;
        state.moreOffset = 0;
        render(false);
        return;
      }

      if (action === 'add-item') {
        const item = findSubItem(target.dataset.itemId);
        if (item) {
          const existing = state.cart.find((cartItem) => cartItem.id === item.id);
          if (existing) {
            existing.quantity += 1;
          } else {
            state.cart.push(Object.assign({}, item, { quantity: 1 }));
          }
          render(false);
        }
        return;
      }

      if (action === 'increase-item' || action === 'decrease-item' || action === 'remove-item') {
        const id = target.dataset.itemId;
        const index = state.cart.findIndex((item) => item.id === id);
        if (index >= 0) {
          if (action === 'increase-item') {
            state.cart[index].quantity += 1;
          } else if (action === 'decrease-item') {
            state.cart[index].quantity -= 1;
            if (state.cart[index].quantity <= 0) {
              state.cart.splice(index, 1);
            }
          } else {
            state.cart.splice(index, 1);
          }
          render(false);
        }
        return;
      }

      if (action === 'more-prev') {
        state.moreOffset = Math.max(0, state.moreOffset - getMorePageSize());
        render(false);
        return;
      }

      if (action === 'more-next') {
        state.moreOffset += getMorePageSize();
        render(false);
        return;
      }

      if (action === 'select-load') {
        state.selectedLoadId = target.dataset.loadId || null;
        resetBookingStep();
        render(false);
      }
    });

    root.addEventListener('submit', (event) => {
      const aiForm = event.target.closest('[data-junkreco-ai-form]');
      if (aiForm && root.contains(aiForm)) {
        event.preventDefault();
        submitAiEstimate(aiForm);
        return;
      }

      const zipForm = event.target.closest('[data-junkreco-zip-form]');
      if (zipForm && root.contains(zipForm)) {
        event.preventDefault();
        submitZipGate(zipForm);
        return;
      }

      const form = event.target.closest('[data-junkreco-booking-form]');
      if (!form || !root.contains(form)) {
        return;
      }

      event.preventDefault();
      submitBookingForm(form);
    });

    root.addEventListener('change', (event) => {
      if (event.target.matches('input[name="has_stairs"]')) {
        updateStairsFlightsVisibility(event.target.closest('[data-junkreco-booking-form]'));
        return;
      }

      if (event.target.matches('[data-junkreco-ai-photo-input]')) {
        addAiPhotos(event.target.files);
        event.target.value = '';
        return;
      }

      if (!event.target.matches('[data-junkreco-photo-input]')) {
        return;
      }

      const upload = event.target.closest('.junkreco-photo-upload');
      addSelectedPhotos(upload, event.target.files);
      event.target.value = '';
    });

    root.addEventListener('input', (event) => {
      if (event.target.matches('.junkreco-zip-input')) {
        const status = event.target.closest('[data-junkreco-zip-form]')?.querySelector('[data-junkreco-zip-status]');
        if (status) {
          status.className = 'junkreco-booking-status';
          status.textContent = '';
        }
        return;
      }

      if (event.target.matches('[data-junkreco-ai-notes]')) {
        state.aiNotes = event.target.value || '';
        return;
      }

      if (!event.target.classList.contains('junkreco-search')) {
        return;
      }

      state.search = event.target.value || '';
      state.moreOffset = 0;
      render(true);
    });

    render(false);
  }

  function boot() {
    document.querySelectorAll('.junkreco-calculator').forEach(initCalculator);
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', boot);
  } else {
    boot();
  }
})();
