// asana-form-builder/assets/form.js
(function ($) {
  const afbPreparedUploadFiles = new WeakMap();
  const afbPendingUploadPrepJobs = new Set();

  function afbTrackUploadPrepJob(jobPromise) {
    if (!jobPromise || typeof jobPromise.then !== 'function') return;

    afbPendingUploadPrepJobs.add(jobPromise);
    const clear = function () {
      afbPendingUploadPrepJobs.delete(jobPromise);
    };

    jobPromise.then(clear, clear);
  }

  async function afbWaitForUploadPrepJobs() {
    if (!afbPendingUploadPrepJobs.size) return;
    await Promise.allSettled(Array.from(afbPendingUploadPrepJobs));
  }

  function afbGetPreparedUploadFile(inputEl) {
    if (!inputEl) return null;

    const cached = afbPreparedUploadFiles.get(inputEl);
    if (cached && cached.file) {
      return cached.file;
    }

    const fallback = inputEl.files && inputEl.files[0] ? inputEl.files[0] : null;
    return fallback || null;
  }

  function afbClearPreparedUploadFile(inputEl) {
    if (!inputEl) return;
    afbPreparedUploadFiles.delete(inputEl);
  }

  function afbParsePositiveUploadNumber(raw) {
    const n = parseFloat(String(raw == null ? '' : raw).trim());
    return Number.isFinite(n) && n > 0 ? n : 0;
  }

  function afbNormalizeUploadCompressionQuality(raw) {
    let n = parseFloat(String(raw == null ? '' : raw).trim());
    if (!Number.isFinite(n) || n <= 0) {
      return 0.6;
    }

    if (n > 1) {
      n = n / 100;
    }

    return Math.max(0.01, Math.min(1, n));
  }

  function afbNormalizeUploadMaxWidth(raw) {
    const n = parseFloat(String(raw == null ? '' : raw).trim());
    if (!Number.isFinite(n) || n <= 0) {
      return 1200;
    }

    return Math.max(100, Math.min(4000, Math.round(n)));
  }

  function afbFormatUploadMb(raw) {
    const n = afbParsePositiveUploadNumber(raw);
    if (!n) return '';
    const fixed = n.toFixed(2);
    return fixed.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '');
  }

  function afbFormatUploadBytes(bytes) {
    const n = Number(bytes);
    if (!Number.isFinite(n) || n <= 0) return '0 B';

    const units = ['B', 'KB', 'MB', 'GB'];
    let value = n;
    let idx = 0;
    while (value >= 1024 && idx < units.length - 1) {
      value /= 1024;
      idx += 1;
    }

    const decimals = idx === 0 ? 0 : (value >= 10 ? 1 : 2);
    return `${value.toFixed(decimals)} ${units[idx]}`;
  }

  function afbFormatUploadCompressionSummary(originalBytes, finalBytes) {
    const original = Number(originalBytes);
    const final = Number(finalBytes);

    if (!Number.isFinite(final) || final <= 0) {
      return 'Ready';
    }

    const finalLabel = afbFormatUploadBytes(final);
    if (!Number.isFinite(original) || original <= 0 || original === final) {
      return `Ready: ${finalLabel}`;
    }

    const deltaPct = Math.round(Math.abs((original - final) / original) * 100);
    const originalLabel = afbFormatUploadBytes(original);

    if (final < original) {
      return `Ready: ${finalLabel} (from ${originalLabel}, -${deltaPct}%)`;
    }

    return `Ready: ${finalLabel} (from ${originalLabel}, +${deltaPct}%)`;
  }

  function afbCanCompressUploadImage(file) {
    if (!file) return false;

    const type = String(file.type || '').toLowerCase();
    if (!type.startsWith('image/')) return false;

    // Keep to common browser-safe formats for on-canvas recompression.
    return ['image/jpeg', 'image/jpg', 'image/pjpeg', 'image/png', 'image/webp'].includes(type);
  }

  function afbLoadImageFromFile(file) {
    return new Promise((resolve, reject) => {
      const objectUrl = URL.createObjectURL(file);
      const img = new Image();

      img.onload = function () {
        resolve({
          img,
          width: img.naturalWidth || img.width || 0,
          height: img.naturalHeight || img.height || 0,
          cleanup: function () {
            URL.revokeObjectURL(objectUrl);
          }
        });
      };

      img.onerror = function () {
        URL.revokeObjectURL(objectUrl);
        reject(new Error('Image decode failed'));
      };

      img.src = objectUrl;
    });
  }

  function afbCanvasToBlob(canvas, mimeType, quality) {
    return new Promise((resolve, reject) => {
      try {
        canvas.toBlob(function (blob) {
          if (!blob) {
            reject(new Error('Canvas export failed'));
            return;
          }
          resolve(blob);
        }, mimeType, quality);
      } catch (err) {
        reject(err);
      }
    });
  }

  function afbTryReplaceInputFile(inputEl, file) {
    if (!inputEl || !file || typeof DataTransfer === 'undefined') return;

    try {
      const dt = new DataTransfer();
      dt.items.add(file);
      inputEl.files = dt.files;
    } catch (_) {
      // Keep fallback cache path when direct file replacement is unsupported.
    }
  }

  async function afbCompressImageFileForUpload(file, maxFileBytes, requestedQuality, requestedMaxWidth) {
    if (!afbCanCompressUploadImage(file)) {
      return { file, compressed: false };
    }

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

    const sourceType = String(file.type || '').toLowerCase();
    const outputType = sourceType.includes('png')
      ? 'image/png'
      : (sourceType.includes('webp') ? 'image/webp' : 'image/jpeg');
    const maxWidthPx = afbNormalizeUploadMaxWidth(requestedMaxWidth);
    const compressionQuality = afbNormalizeUploadCompressionQuality(requestedQuality);

    const loaded = await afbLoadImageFromFile(file);

    try {
      const width = Math.max(1, Number(loaded.width || 0));
      const height = Math.max(1, Number(loaded.height || 0));
      const longestEdge = Math.max(width, height);
      if (!longestEdge) {
        return { file, compressed: false };
      }

      const widthNeedsReduction = width > maxWidthPx;
      let scale = widthNeedsReduction ? (maxWidthPx / width) : 1;
      if (!Number.isFinite(scale) || scale <= 0) {
        scale = 1;
      }

      const targetBytes = maxFileBytes > 0
        ? Math.min(Math.floor(maxFileBytes * 0.92), Math.floor(sourceSize * 0.8))
        : Math.floor(sourceSize * 0.72);
      const canvas = document.createElement('canvas');

      const renderBlobAtScale = async function (renderScale) {
        const w = Math.max(1, Math.round(width * renderScale));
        const h = Math.max(1, Math.round(height * renderScale));
        canvas.width = w;
        canvas.height = h;

        const ctx = canvas.getContext('2d', { alpha: outputType === 'image/png' });
        if (!ctx) {
          throw new Error('Canvas 2D context unavailable');
        }

        ctx.drawImage(loaded.img, 0, 0, w, h);

        if (outputType === 'image/png') {
          return afbCanvasToBlob(canvas, outputType);
        }

        return afbCanvasToBlob(canvas, outputType, compressionQuality);
      };

      let bestBlob = await renderBlobAtScale(scale);
      let attempts = 0;
      while (
        bestBlob
        && maxFileBytes > 0
        && bestBlob.size > maxFileBytes
        && scale > 0.35
        && attempts < 3
      ) {
        attempts += 1;
        scale *= 0.85;
        const candidate = await renderBlobAtScale(scale);
        if (!candidate || candidate.size >= bestBlob.size) {
          break;
        }
        bestBlob = candidate;
      }

      if (!bestBlob) {
        return { file, compressed: false };
      }

      // Keep the original file unless we got a meaningful reduction.
      // If width was above 1200px, always keep the resized output.
      if (!widthNeedsReduction && bestBlob.size >= sourceSize * 0.97 && (maxFileBytes <= 0 || bestBlob.size > maxFileBytes)) {
        return { file, compressed: false };
      }

      const optimizedFile = new File([bestBlob], file.name, {
        type: bestBlob.type || outputType,
        lastModified: Date.now(),
      });

      return {
        file: optimizedFile,
        compressed: optimizedFile.size < sourceSize,
        originalSize: sourceSize,
        finalSize: optimizedFile.size,
      };
    } finally {
      if (loaded && typeof loaded.cleanup === 'function') {
        loaded.cleanup();
      }
    }
  }

  /* Helper: collect chosen files from the "Optional File Uploads" UI */
  function afbGetSelectedFiles($form) {
    const files = [];
    const seen = new Set();
    $form.find('.afb-upload-block .file-list input[type="file"]').each(function () {
      if (this.disabled) return;
      const f = afbGetPreparedUploadFile(this);
      if (!f) return;

      const blockId = String($(this).closest('.afb-upload-block').data('field-id') || '').trim();
      const sig = [
        blockId,
        String(f.name || '').trim(),
        String(f.size || 0),
        String(f.lastModified || 0),
        String(f.type || '').trim(),
      ].join('|');

      if (sig && seen.has(sig)) return;
      if (sig) seen.add(sig);

      files.push({
        file: f,
        uploadSig: sig,
        uploadFieldId: blockId,
      });
    });
    return files;
  }

  function afbParseUploadAcceptTokens(rawAccept) {
    const out = [];
    const seen = {};

    String(rawAccept || '').split(',').forEach((part) => {
      let token = String(part || '').trim().toLowerCase();
      if (!token) return;

      token = token.replace(/\s+/g, '');
      if (!token) return;

      if (token.charAt(0) === '.') {
        token = '.' + token.slice(1).replace(/[^a-z0-9]/g, '');
        if (token.length <= 1) return;
      } else if (/^[a-z0-9!#$&^_.+-]+\/(?:[a-z0-9!#$&^_.+-]+|\*)$/.test(token)) {
        // Valid MIME token.
      } else if (/^[a-z0-9]{1,16}$/.test(token)) {
        token = '.' + token;
      } else {
        return;
      }

      if (!seen[token]) {
        seen[token] = true;
        out.push(token);
      }
    });

    return out;
  }

  function afbFileMatchesUploadAccept(file, acceptTokens) {
    const tokens = Array.isArray(acceptTokens) ? acceptTokens : [];
    if (!tokens.length) return true;
    if (!file) return false;

    const name = String(file.name || '').toLowerCase();
    const mime = String(file.type || '').toLowerCase();
    let ext = '';
    const dotIndex = name.lastIndexOf('.');
    if (dotIndex > -1 && dotIndex < name.length - 1) {
      ext = name.slice(dotIndex);
    }

    return tokens.some((token) => {
      if (!token) return false;

      if (token.charAt(0) === '.') {
        return !!ext && ext === token;
      }

      if (token.endsWith('/*')) {
        const prefix = token.slice(0, -1);
        return !!mime && mime.indexOf(prefix) === 0;
      }

      return !!mime && mime === token;
    });
  }

  function afbDescribeUploadAcceptTokens(acceptTokens) {
    const tokens = Array.isArray(acceptTokens) ? acceptTokens : [];
    if (!tokens.length) return '';
    return tokens.join(', ');
  }

  function afbValidateUploadBlocks($scope, $statusText, $progressBarContainer, $submitButton, setBusyState) {
    const $blocks = $scope.find('.afb-upload-block').filter(':visible');
    if (!$blocks.length) return true;

    let $firstUploadError = null;
    let uploadErrorMessage = '';
    let $firstMissing = null;
    let $firstInvalid = null;
    let $firstTooLarge = null;
    let invalidFileName = '';
    let invalidAllowedText = '';
    let tooLargeFileName = '';
    let tooLargeLimitText = '';
    $blocks.each(function () {
      const $block = $(this);
      const acceptTokens = afbParseUploadAcceptTokens($block.attr('data-accept-types') || '');
      const maxFileMB = afbParsePositiveUploadNumber($block.attr('data-max-file-mb') || '');
      const maxFileBytes = maxFileMB > 0 ? Math.round(maxFileMB * 1024 * 1024) : 0;
      const maxFileLabel = afbFormatUploadMb(maxFileMB);
      const $fileInputs = $block.find('.file-list input[type="file"]:enabled');
      const $errorRows = $block.find('.afb-upload-file-row[data-afb-upload-error="1"]:visible');
      let validFileCount = 0;

      if ($errorRows.length) {
        $firstUploadError = $block;
        uploadErrorMessage = String($errorRows.first().attr('data-afb-upload-error-msg') || '').trim();
        return false;
      }

      $fileInputs.each(function () {
        const f = afbGetPreparedUploadFile(this);
        if (!f) return;

        if (acceptTokens.length && !afbFileMatchesUploadAccept(f, acceptTokens)) {
          if (!$firstInvalid) {
            $firstInvalid = $block;
            invalidFileName = String(f.name || '').trim();
            invalidAllowedText = afbDescribeUploadAcceptTokens(acceptTokens);
          }
          return false;
        }

        if (maxFileBytes > 0 && Number(f.size || 0) > maxFileBytes) {
          if (!$firstTooLarge) {
            $firstTooLarge = $block;
            tooLargeFileName = String(f.name || '').trim();
            tooLargeLimitText = maxFileLabel;
          }
          return false;
        }

        validFileCount += 1;
      });

      if ($firstInvalid) {
        return false;
      }

      if ($firstTooLarge) {
        return false;
      }

      if ($firstUploadError) {
        return false;
      }

      const minFiles = Math.max(0, parseInt($block.data('min-files'), 10) || 0);
      if (minFiles <= 0) return;

      if (validFileCount < minFiles) {
        $firstMissing = $block;
        return false;
      }
    });

    if ($firstUploadError) {
      const title = ($firstUploadError.find('.afb-upload-heading').first().text() || 'this upload area').trim();
      const detail = uploadErrorMessage || 'Please fix the selected file before submitting.';
      const msg = `Please fix upload errors for "${title}": ${detail}`;

      if ($statusText && $statusText.length) {
        $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${msg}`);
      }
      if ($progressBarContainer && $progressBarContainer.length) {
        $progressBarContainer.addClass('hidden');
      }
      if ($submitButton && $submitButton.length) {
        $submitButton.prop('disabled', false)
          .removeAttr('aria-busy aria-disabled')
          .removeClass('opacity-60 cursor-not-allowed')
          .text('Submit');
      }
      if (typeof setBusyState === 'function') {
        setBusyState(false);
      }

      try {
        $firstUploadError[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
      } catch (_) {}

      return false;
    }

    if ($firstInvalid) {
      const title = ($firstInvalid.find('.afb-upload-heading').first().text() || 'this upload area').trim();
      let msg = invalidFileName
        ? `"${invalidFileName}" is not allowed for "${title}".`
        : `A selected file is not allowed for "${title}".`;

      if (invalidAllowedText) {
        msg += ` Allowed types: ${invalidAllowedText}.`;
      }

      if ($statusText && $statusText.length) {
        $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${msg}`);
      }
      if ($progressBarContainer && $progressBarContainer.length) {
        $progressBarContainer.addClass('hidden');
      }
      if ($submitButton && $submitButton.length) {
        $submitButton.prop('disabled', false)
          .removeAttr('aria-busy aria-disabled')
          .removeClass('opacity-60 cursor-not-allowed')
          .text('Submit');
      }
      if (typeof setBusyState === 'function') {
        setBusyState(false);
      }

      try {
        $firstInvalid[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
      } catch (_) {}

      return false;
    }

    if ($firstTooLarge) {
      const title = ($firstTooLarge.find('.afb-upload-heading').first().text() || 'this upload area').trim();
      const msg = tooLargeFileName
        ? `"${tooLargeFileName}" is too large for "${title}".${tooLargeLimitText ? ` Max ${tooLargeLimitText} MB per file.` : ''}`
        : `A selected file is too large for "${title}".`;

      if ($statusText && $statusText.length) {
        $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${msg}`);
      }
      if ($progressBarContainer && $progressBarContainer.length) {
        $progressBarContainer.addClass('hidden');
      }
      if ($submitButton && $submitButton.length) {
        $submitButton.prop('disabled', false)
          .removeAttr('aria-busy aria-disabled')
          .removeClass('opacity-60 cursor-not-allowed')
          .text('Submit');
      }
      if (typeof setBusyState === 'function') {
        setBusyState(false);
      }

      try {
        $firstTooLarge[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
      } catch (_) {}

      return false;
    }

    if (!$firstMissing) return true;

    const title = ($firstMissing.find('.afb-upload-heading').first().text() || 'this upload area').trim();
    const minFiles = Math.max(0, parseInt($firstMissing.data('min-files'), 10) || 0);
    const msg = minFiles > 1
      ? `Please upload at least ${minFiles} files for "${title}".`
      : `Please upload a file for "${title}".`;

    if ($statusText && $statusText.length) {
      $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${msg}`);
    }
    if ($progressBarContainer && $progressBarContainer.length) {
      $progressBarContainer.addClass('hidden');
    }
    if ($submitButton && $submitButton.length) {
      $submitButton.prop('disabled', false)
        .removeAttr('aria-busy aria-disabled')
        .removeClass('opacity-60 cursor-not-allowed')
        .text('Submit');
    }
    if (typeof setBusyState === 'function') {
      setBusyState(false);
    }

    try {
      $firstMissing[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
    } catch (_) {}

    return false;
  }

  /* Helper: collect custom fields (single + multi-enum) */
  function collectCustomFields($root) {
    const out = {};

    // single-value inputs (text/number/date/enum)
    $root.find('[name^="custom_fields["]').each(function () {
      const name = this.name;
      const m = name.match(/^custom_fields\[([^\]]+)\](?:\[\])?$/);
      if (!m) return;
      const gid = m[1];

      // multi-enum handled below
      if (name.endsWith('[]')) return;
      if (this.type === 'checkbox') return;
      if (this.disabled) return;

      const raw = $(this).val();
      if (raw == null) return;

      const value = (typeof raw === 'string') ? raw.trim() : raw;
      if (value === '') return;

      out[gid] = value;
    });

// multi-enum checkbox groups
$root.find('div[data-multienum="1"]').each(function () {
  const checked = $(this)
    .find('input[type="checkbox"][name^="custom_fields["]:checked:enabled')
    .map(function () { return (this.value || '').trim(); })
    .get();

  const cleaned = checked.filter(Boolean);

  if (cleaned.length) {
    const firstName = $(this).find('input[type="checkbox"]').first().attr('name') || '';
    const m = firstName.match(/^custom_fields\[([^\]]+)\]/);
    if (m) out[m[1]] = cleaned;
  }
});

    // pseudo inputs synced to Asana text custom fields
    $root.find('[data-afb-sync-target]').each(function () {
      const $input = $(this);
      if ($input.prop('disabled')) return;

      const gid = String($input.attr('data-afb-sync-target') || '').trim();
      if (!gid) return;

      const raw = $input.val();
      const value = String(raw == null ? '' : raw).trim();
      if (!value) return;

      // Keep explicit custom_fields[] values if already present.
      if (typeof out[gid] === 'undefined') {
        out[gid] = value;
      }
    });

    // pseudo multi-select groups synced to Asana text custom fields
    $root.find('[data-afb-sync-target-group]').each(function () {
      const $group = $(this);
      if ($group.find(':enabled').length === 0) return;

      const gid = String($group.attr('data-afb-sync-target-group') || '').trim();
      if (!gid) return;

      const selectedVals = $group
        .find('input[type="checkbox"]:checked:enabled')
        .map(function () { return String(this.value || '').trim(); })
        .get()
        .filter(Boolean);

      if (!selectedVals.length) return;

      if (typeof out[gid] === 'undefined') {
        out[gid] = selectedVals.join(', ');
      }
    });


    return out;
  }

  function collectDescriptionInputs($root) {
    const out = [];

    $root.find('[data-afb-description-input="1"]').each(function () {
      const $input = $(this);
      if ($input.prop('disabled')) return;

      const value = String($input.val() || '').trim();
      if (!value) return;

      const id = String($input.attr('data-afb-description-id') || '').trim();
      const label = String($input.attr('data-afb-description-label') || '').trim();
      out.push({ id, label, value });
    });

    $root.find('[data-afb-description-group="1"]').each(function () {
      const $group = $(this);
      if ($group.find(':enabled').length === 0) return;

      const selectedVals = $group
        .find('input[type="checkbox"]:checked:enabled')
        .map(function () { return String(this.value || '').trim(); })
        .get()
        .filter(Boolean);

      if (!selectedVals.length) return;

      const id = String($group.attr('data-afb-description-id') || '').trim();
      const label = String($group.attr('data-afb-description-label') || '').trim();
      out.push({ id, label, value: selectedVals.join(', ') });
    });

    return out;
  }

  function collectAgreementChecks($root) {
    const out = {};

    $root.find('input[data-afb-agreement="1"]').each(function () {
      const $input = $(this);
      if ($input.prop('disabled')) return;

      const rawName = String($input.attr('name') || '');
      const match = rawName.match(/^agreement_checks\[(.+)\]$/);
      if (!match || !match[1]) return;

      const agreementId = String(match[1] || '').trim();
      if (!agreementId) return;

      out[agreementId] = $input.is(':checked') ? '1' : '';
    });

    return out;
  }

  function collectDueDateInput($root) {
    const out = { value: '', fieldId: '' };

    $root.find('[data-afb-due-date-source]').each(function () {
      if (out.value) return;

      const $input = $(this);
      if ($input.prop('disabled')) return;

      const value = String($input.val() || '').trim();
      if (!value) return;

      const fieldId = String($input.closest('[data-field-id]').attr('data-field-id') || '').trim();
      out.value = value;
      out.fieldId = fieldId;
    });

    return out;
  }

  function clampRepeaterRowLimit(rawValue) {
    const parsed = parseInt(rawValue, 10);
    if (!Number.isFinite(parsed) || parsed < 0) return 0;
    return Math.max(0, Math.min(250, parsed));
  }

  function parseRepeaterScalarValue(rawValue, fieldType) {
    const type = String(fieldType || '').trim().toLowerCase();
    const value = String(rawValue == null ? '' : rawValue).trim();
    if (!value) return null;

    if (type === 'number') {
      const parsed = Number(value);
      if (!Number.isFinite(parsed)) {
        return null;
      }
      return parsed;
    }

    return value;
  }

  function readRepeaterFieldValue($field) {
    if (!$field || !$field.length) return null;

    const fieldType = String($field.attr('data-afb-repeater-type') || '').trim().toLowerCase();

    if (fieldType === 'multi_enum') {
      const $multiOptions = $field.find('input[type="checkbox"][data-afb-repeater-option]');
      if ($multiOptions.length === 0) {
        // Fallback path when options are unavailable and we rendered a scalar input.
      } else {
        const selected = [];
        const selectedLabels = [];

        $field.find('input[type="checkbox"][data-afb-repeater-option]:checked:enabled').each(function () {
          const optionValue = String(this.value || '').trim();
          if (!optionValue) return;

          selected.push(optionValue);
          const label = String($(this).attr('data-afb-repeater-option-label') || optionValue).trim();
          selectedLabels.push(label || optionValue);
        });

        if (!selected.length) return null;

        return {
          raw: Array.from(new Set(selected)),
          display: Array.from(new Set(selectedLabels)).join(', '),
        };
      }
    }

    const $input = $field.find('input, select, textarea').filter(':enabled').first();
    if (!$input.length) return null;

    if ($input.is(':checkbox')) {
      if (!$input.is(':checked')) return null;
      const checkedValue = String($input.val() || '').trim() || 'Yes';
      return { raw: checkedValue, display: checkedValue };
    }

    const rawValue = String($input.val() == null ? '' : $input.val()).trim();
    if (!rawValue) return null;

    if (fieldType === 'enum') {
      const selectedLabel = String($input.find('option:selected').text() || rawValue).trim() || rawValue;
      return { raw: rawValue, display: selectedLabel };
    }

    const parsedValue = parseRepeaterScalarValue(rawValue, fieldType);
    if (parsedValue === null) return null;

    return {
      raw: parsedValue,
      display: String(rawValue),
    };
  }

  function normalizeRepeaterKey(label, fallbackIndex) {
    const raw = String(label || '').trim().toLowerCase();
    const slug = raw.replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
    if (slug) return slug;
    return `value_${Math.max(1, (parseInt(fallbackIndex, 10) || 0) + 1)}`;
  }

  function collectRepeaterRowsFromBlock($block) {
    const out = [];
    if (!$block || !$block.length) return out;

    const repeaterId = String($block.attr('data-afb-repeater-id') || '').trim();
    const repeaterLabel = String($block.attr('data-afb-repeater-label') || '').trim();
    const repeaterSaveMode = String($block.attr('data-afb-repeater-save-mode') || '').trim() === 'description'
      ? 'description'
      : 'subtasks';

    $block.find('.afb-repeater-row').each(function (rowIndex) {
      const $row = $(this);
      const display = {};
      const values = {};
      const customFields = {};
      let hasValue = false;

      $row.find('.afb-repeater-col-field').each(function (colIndex) {
        const $field = $(this);
        const colLabel = String($field.attr('data-afb-repeater-col') || '').trim();
        const colKeyRaw = String($field.attr('data-afb-repeater-key') || '').trim();
        const colKey = colKeyRaw || normalizeRepeaterKey(colLabel, colIndex);

        const bundle = readRepeaterFieldValue($field);
        if (!bundle) return;

        hasValue = true;
        if (colLabel) display[colLabel] = bundle.display;
        if (colKey) values[colKey] = bundle.display;

        const customFieldGid = String($field.attr('data-afb-repeater-gid') || '').trim();
        if (customFieldGid && /^\d+$/.test(customFieldGid)) {
          customFields[customFieldGid] = bundle.raw;
        }
      });

      if (!hasValue) return;

      const rowPayload = {
        repeater_id: repeaterId,
        repeater_label: repeaterLabel,
        save_mode: repeaterSaveMode,
        row_number: rowIndex + 1,
        display,
        values,
      };

      if (Object.keys(customFields).length) {
        rowPayload.custom_fields = customFields;
      }

      out.push(rowPayload);
    });

    return out;
  }

  function collectRepeaterRows($root) {
    const out = [];

    $root.find('[data-afb-repeater="1"]').each(function () {
      const $block = $(this);
      const $fieldWrap = $block.closest('[data-field-id]');
      if (!$fieldWrap.length) return;
      if ($fieldWrap.attr('data-afb-condition-hidden') === '1') return;
      if ($fieldWrap.closest('.afb-step-panel').attr('data-afb-step-condition-hidden') === '1') return;
      if ($block.find(':enabled').length === 0) return;

      const rows = collectRepeaterRowsFromBlock($block);
      if (!rows.length) return;
      out.push(...rows);
    });

    return out;
  }

  function validateRepeaterBlocks($scope) {
    const result = { valid: true, message: '', $block: null };

    const $blocks = $scope.find('[data-afb-repeater="1"]').filter(':visible');
    if (!$blocks.length) return result;

    for (let i = 0; i < $blocks.length; i += 1) {
      const $block = $($blocks[i]);
      if ($block.find(':enabled').length === 0) continue;

      const label = String($block.attr('data-afb-repeater-label') || '').trim() || 'this repeater';
      const isRequired = String($block.attr('data-afb-repeater-required') || '') === '1';
      const minRows = clampRepeaterRowLimit($block.attr('data-afb-repeater-min-rows'));
      const requiredRows = minRows > 0 ? minRows : (isRequired ? 1 : 0);

      const rows = collectRepeaterRowsFromBlock($block);
      if (requiredRows > 0 && rows.length < requiredRows) {
        result.valid = false;
        result.$block = $block;
        result.message = requiredRows === 1
          ? `Please add at least one row in "${label}".`
          : `Please add at least ${requiredRows} rows in "${label}".`;
        return result;
      }
    }

    return result;
  }

  function collectTaskNameFieldValues($root) {
    const out = {};

    $root.find('[data-field-id]').each(function () {
      const $fieldWrap = $(this);
      const fieldId = String($fieldWrap.attr('data-field-id') || '').trim();
      if (!fieldId) return;

      if (!/^calendar_|^single_|^dropdown_/.test(fieldId)) return;
      if ($fieldWrap.find(':enabled').length === 0) return;

      // For multi-select groups, store selected values as a readable joined string.
      const $multiGroup = $fieldWrap.find('[data-multienum="1"]').first();
      if ($multiGroup.length) {
        const selectedVals = $multiGroup
          .find('input[type="checkbox"]:checked:enabled')
          .map(function () { return String(this.value || '').trim(); })
          .get()
          .filter(Boolean);

        if (selectedVals.length) {
          out[fieldId] = selectedVals.join(', ');
        }
        return;
      }

      const $control = $fieldWrap
        .find('input:not([type="checkbox"]), select, textarea')
        .filter(':enabled')
        .first();

      if (!$control.length) return;

      const value = String($control.val() || '').trim();
      if (!value) return;

      out[fieldId] = value;
    });

    return out;
  }

  /* Helper: snapshot of triage state (no sessionStorage fallback) */
  function getTriageStateSnapshot() {
    try {
      if (typeof afb_ajax !== 'undefined' &&
          afb_ajax.triage_state &&
          typeof afb_ajax.triage_state === 'object' &&
          Object.keys(afb_ajax.triage_state).length > 0) {
        return afb_ajax.triage_state;
      }
    } catch (_) {}

    try {
      if (window.__triage && typeof window.__triage.getState === 'function') {
        const s = window.__triage.getState();
        if (s && typeof s === 'object') return s;
      }
    } catch (_) {}

    // Persisted fallback used after page navigation from triage -> form page
    try {
      const raw = localStorage.getItem('triage_state');
      if (raw) {
        const parsed = JSON.parse(raw);
        if (parsed && typeof parsed === 'object') return parsed;
      }
    } catch (_) {}

    // Minimal identity-only fallback
    try {
      const rawId = localStorage.getItem('triage_identity_v1');
      if (rawId) {
        const id = JSON.parse(rawId);
        if (id && typeof id === 'object') {
          return {
            answers: {
              first_name: id.first_name || '',
              last_name: id.last_name || '',
              work_email: id.work_email || id.email || '',
              full_name: id.full_name || ''
            },
            user: id
          };
        }
      }
    } catch (_) {}

    return {};
  }

  function getIdentityFromState() {
    const snap = getTriageStateSnapshot() || {};
    const answers = snap.answers || {};
    const user = snap.user || snap.contact || snap.meta || {};
    return {
      first_name: String(answers.first_name || user.first_name || '').trim(),
      last_name: String(answers.last_name || user.last_name || '').trim(),
      work_email: String(answers.work_email || user.work_email || user.email || '').trim(),
    };
  }

  /*───────────────────────────────────────────────────────*
   * 1) INIT DYNAMIC FORMS
   *───────────────────────────────────────────────────────*/
  function initAsanaDynamicForms() {
    $('.asana-dynamic-form').each(function () {
      const $form = $(this);
      if ($form.data('afbInitDone')) return;
      $form.data('afbInitDone', true);

      const $fieldWrapper = $form.find('.asana-fields');
      const $submitButton = $form.find('button[type="submit"]');
      const defaultSubmitButtonText = String($submitButton.first().text() || '').trim() || 'Submit';

      // ✅ Safe: remove vertical stack helpers that fight the grid (safe if not present)
      $form.removeClass('space-y-4');
      $fieldWrapper.removeClass('space-y-4');

      const $statusBox = $('<div class="mt-4 text-sm space-y-2 afb-submit-status"></div>');
      const $statusText = $('<div class="text-blue-600"></div>');
      const $progressBarContainer = $('<div class="w-full bg-gray-200 rounded-full h-2.5 hidden afb-progress-container">');
      const $progressBar = $('<div class="h-2.5 rounded-full text-blue-600 afb-progress-bar" style="width:0%"></div>');
      const $errorDetails = $('<div class="hidden afb-error-details text-xs text-red-700 whitespace-pre-wrap"></div>');

      $progressBarContainer.append($progressBar);
      $statusBox.append($statusText, $progressBarContainer, $errorDetails);
      $form.append($statusBox);

      function setCompactSubmitView(isCompact) {
        $form.toggleClass('afb-submitting-compact', !!isCompact);
        $form.closest('.asana-form-container').find('.asana-title-wrap').toggle(!isCompact);
      }

      let projectId  = $form.find('.project-picker').val();
      let projectPat = $form.find('.project-picker option:selected').data('pat') || '';
      let inFlight   = false; // prevent double submits
      let pendingSubmissionRequestId = '';
      let afbWizard  = null;
      let afbSyncSubmitBlockState = function () { return false; };

      $fieldWrapper.hide();
      $form.find('.header-and-submit').addClass('hidden');

      // Instant title update before the fetch completes
      const cfgMap = afb_ajax.field_flow_config || {};
      const cfg    = cfgMap[projectId] || {};
      const $h1    = $form.closest('.asana-form-container').find('.js-project-title, .asana-title-wrap h1').first();
      const txt    = (cfg.form_title || $form.find('.project-picker option:selected').text() || '').trim();
      if ($h1.length && txt && txt !== '-- Choose a project --') $h1.text(txt);

      /* ---------- PROJECT PICKER ---------- */
      $form.find('.project-picker').on('change', function () {
        projectId  = $(this).val();
        projectPat = $(this).find('option:selected').data('pat') || '';
        pendingSubmissionRequestId = '';
        if (!projectId) return;
        $form.find('.header-and-submit').addClass('hidden');
        loadProjectFields(projectId, projectPat);
      });

      /* ---------- FIELD LOADER ---------- */
      function loadProjectFields(projectId, pat) {
        const $container = $fieldWrapper;

        $container.hide().html(`
<div class="w-full flex justify-center mt-4">
  <div class="flex items-center text-sm text-blue-600">
    <svg class="animate-spin h-5 w-5 text-blue-500 mr-2" viewBox="0 0 24 24">
      <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
      <path class="opacity-75" fill="currentColor" d="M4 12a 8 8 0 018-8v8z"></path>
    </svg>
    Loading Form fields…
  </div>
</div>`).fadeIn();

        $.post(
          afb_ajax.ajax_url,
          { action: 'afb_get_project_fields', project_id: projectId, pat: pat },
          function (response) {
            $container.empty();

            if (!response.success) {
              $container.html('<p class="text-red-600">⚠️ Failed to load fields.</p>');
              return;
            }

            /* ---------- CONFIG MAPS ---------- */
            const fieldConfig  = afb_ajax.field_flow_config || {};
            const config       = fieldConfig[projectId] || {};

            // ---- Set page title from Field Flow Config (fallback to project name) ----
            const $titleH1  = $form.closest('.asana-form-container')
              .find('.js-project-title, .asana-title-wrap h1').first();
            if ($titleH1.length) {
              const cfgTitle = (config.form_title || '').trim();
              if (cfgTitle) {
                $titleH1.text(cfgTitle);
              } else {
                const fallback = ($form.find('.project-picker option:selected').text() || '').trim();
                if (fallback && fallback !== '-- Choose a project --') $titleH1.text(fallback);
              }
            }

            function normalizeStringList(raw) {
              const out = [];

              if (Array.isArray(raw)) {
                raw.forEach((val, idx) => {
                  if (typeof val === 'string' && val.trim()) {
                    out.push(val.trim());
                    return;
                  }

                  if (typeof idx === 'string' && idx.trim()) {
                    out.push(idx.trim());
                  }
                });
              } else if (raw && typeof raw === 'object') {
                Object.keys(raw).forEach((key) => {
                  const v = raw[key];
                  if (typeof v === 'string' && v.trim()) {
                    out.push(v.trim());
                    return;
                  }
                  if (typeof key === 'string' && key.trim()) {
                    out.push(key.trim());
                  }
                });
              }

              return Array.from(new Set(out.filter(Boolean)));
            }

            // Width layout lists from admin config
            const half = normalizeStringList(config.half_width_fields);
            const third = normalizeStringList(config.third_width_fields);

            // Inject scoped grid CSS (6-column desktop grid allows 1/2 and 1/3 widths)
            // Scope styles to this specific form wrapper so multiple forms don't clash
            let $wrap = $form.closest('.asana-form-wrapper');
            let wrapperId = $wrap.attr('id') || '';

            // Ensure wrapper has an id so scoping always works
            if (!wrapperId) {
              wrapperId = 'afb-wrap-' + Math.floor(Math.random() * 1e9);
              $wrap.attr('id', wrapperId);
            }

            const styleId = 'afb-grid-' + wrapperId;
            const oldStyle = document.getElementById(styleId);
            if (oldStyle) oldStyle.remove();

            const rules = [
              `#${wrapperId} .asana-fields{display:block}`,
              `#${wrapperId} .asana-fields .afb-step-fields{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:16px 20px}`,
              `#${wrapperId} .asana-fields .afb-step-fields > *{grid-column:span 6 / span 6}`,
              // neutralize any "space-y" style if present
              `#${wrapperId} .asana-fields .afb-step-fields > * + *{margin-top:0 !important}`,
              // per-gid width rules
              ...half.map(gid => `#${wrapperId} .asana-fields .afb-step-fields > [data-field-id="${gid}"]{grid-column:span 3 / span 3}`),
              ...third.map(gid => `#${wrapperId} .asana-fields .afb-step-fields > [data-field-id="${gid}"]{grid-column:span 2 / span 2}`),
              `@media (max-width:768px){
                #${wrapperId} .asana-fields .afb-step-fields{grid-template-columns:1fr}
                #${wrapperId} .asana-fields .afb-step-fields > *{grid-column:1 / -1}
               }`
            ].join('');

            const style = document.createElement('style');
            style.id = styleId;
            style.textContent = rules;
            document.head.appendChild(style);

            const allowed      = normalizeStringList(config.used_fields);
            const customParas  = config.custom_paragraphs || {};
            const customTitles = config.custom_paragraph_titles || {};
            const customSections = config.custom_sections || {};
            const customSectionTitles = config.custom_section_titles || {};
            const customAgreementLabels = config.custom_agreement_labels || {};
            const customAgreementDetails = config.custom_agreement_details || {};
            const customUploadTitles = config.custom_upload_titles || {};
            const customUploadDetails = config.custom_upload_details || {};
            const customUploadMinFiles = config.custom_upload_min_files || {};
            const customUploadAcceptTypes = config.custom_upload_accept_types || {};
            const customUploadCompressImages = normalizeStringList(config.custom_upload_compress_images || []);
            const customUploadCompressConfigured = Object.prototype.hasOwnProperty.call(config, 'custom_upload_compress_images');
            const customUploadMaxFileMB = (config.custom_upload_max_file_mb && typeof config.custom_upload_max_file_mb === 'object')
              ? config.custom_upload_max_file_mb
              : {};
            const customUploadMaxWidthPx = (config.custom_upload_max_width_px && typeof config.custom_upload_max_width_px === 'object')
              ? config.custom_upload_max_width_px
              : {};
            const customUploadCompressQuality = (config.custom_upload_compress_quality && typeof config.custom_upload_compress_quality === 'object')
              ? config.custom_upload_compress_quality
              : {};
            const customSingleInputLabels = config.custom_single_input_labels || {};
            const customSingleInputDescriptions = config.custom_single_input_descriptions || {};
            const customSingleInputPlaceholders = config.custom_single_input_placeholders || {};
            const customSingleInputSyncFields = config.custom_single_input_sync_fields || {};
            const customSingleInputModes = config.custom_single_input_modes || {};
            const customSingleInputMaxWordsEnabled = normalizeStringList(config.custom_single_input_max_words_enabled || []);
            const customSingleInputMaxWords = (config.custom_single_input_max_words && typeof config.custom_single_input_max_words === 'object')
              ? config.custom_single_input_max_words
              : {};
            const customSingleInputAiEnabled = normalizeStringList(config.custom_single_input_ai_enabled || []);
            const customDropdownLabels = config.custom_dropdown_labels || {};
            const customDropdownDescriptions = config.custom_dropdown_descriptions || {};
            const customDropdownPlaceholders = config.custom_dropdown_placeholders || {};
            const customDropdownOptions = config.custom_dropdown_options || {};
            const customDropdownSelectionModes = config.custom_dropdown_selection_modes || {};
            const customDropdownSyncFields = config.custom_dropdown_sync_fields || {};
            const customCalendarLabels = config.custom_calendar_labels || {};
            const customCalendarDescriptions = config.custom_calendar_descriptions || {};
            const customCalendarPlaceholders = config.custom_calendar_placeholders || {};
            const customCalendarSyncModes = config.custom_calendar_sync_modes || {};
            const customCalendarSyncFields = config.custom_calendar_sync_fields || {};
            const customTextSyncFields = config.custom_text_sync_fields || {};
            const customCalendarPresetModes = config.custom_calendar_preset_modes || {};
            const customCalendarPresetOptions = config.custom_calendar_preset_options || {};
            const customRepeaterLabels = config.custom_repeater_labels || {};
            const customRepeaterDescriptions = config.custom_repeater_descriptions || {};
            const customRepeaterColumns = (config.custom_repeater_columns && typeof config.custom_repeater_columns === 'object')
              ? config.custom_repeater_columns
              : {};
            const customRepeaterMinRows = (config.custom_repeater_min_rows && typeof config.custom_repeater_min_rows === 'object')
              ? config.custom_repeater_min_rows
              : {};
            const customRepeaterMaxRows = (config.custom_repeater_max_rows && typeof config.custom_repeater_max_rows === 'object')
              ? config.custom_repeater_max_rows
              : {};
            const customRepeaterFieldMap = (config.custom_repeater_field_map && typeof config.custom_repeater_field_map === 'object')
              ? config.custom_repeater_field_map
              : {};
            const customRepeaterSaveModes = (config.custom_repeater_save_mode && typeof config.custom_repeater_save_mode === 'object')
              ? config.custom_repeater_save_mode
              : {};
            const visibleEnumOptions = config.visible_enum_options || {};
            const optionVisibilityRules = config.option_visibility_rules || {};
            const conditions   = config.conditions || {};
            const required     = normalizeStringList(config.required_fields);
            const agreementRequiredConfigured = Object.prototype.hasOwnProperty.call(config, 'agreement_required_fields');
            const agreementRequired = normalizeStringList(config.agreement_required_fields || []);
            const submitBlockFields = normalizeStringList(config.submit_block_fields || []);
            const hidden       = normalizeStringList(config.hidden_fields);
            const readonly     = normalizeStringList(config.readonly_fields);
            const labels       = config.labels || {};
            const descriptions = config.descriptions || {};
            const paragraph    = normalizeStringList(config.paragraph_fields);
            const legacyAllowUploads = !!config.allow_uploads;

            function normalizeFieldOrder(rawOrder) {
              const list = [];
              if (Array.isArray(rawOrder)) {
                rawOrder.forEach((id) => {
                  if (typeof id === 'string' && id.trim()) list.push(id.trim());
                });
              } else if (rawOrder && typeof rawOrder === 'object') {
                Object.keys(rawOrder).forEach((id) => {
                  if (typeof id === 'string' && id.trim()) list.push(id.trim());
                });
              }

              if (!list.length) {
                Object.keys(customTitles).forEach((id) => { if (id) list.push(id); });
                Object.keys(customSections).forEach((id) => { if (id) list.push(id); });
                Object.keys(customAgreementLabels).forEach((id) => { if (id) list.push(id); });
                Object.keys(customUploadTitles).forEach((id) => { if (id) list.push(id); });
                Object.keys(customSingleInputLabels).forEach((id) => { if (id) list.push(id); });
                Object.keys(customDropdownLabels).forEach((id) => { if (id) list.push(id); });
                Object.keys(customCalendarLabels).forEach((id) => { if (id) list.push(id); });
                Object.keys(customRepeaterLabels).forEach((id) => { if (id) list.push(id); });
                allowed.forEach((id) => {
                  if (typeof id === 'string' && id.trim()) list.push(id.trim());
                });
              }

              return Array.from(new Set(list));
            }

            const order = normalizeFieldOrder(config.field_order);

            const fieldMap = {};
            (response.data.fields || []).forEach((setting) => {
              fieldMap[setting.custom_field.gid] = setting;
            });

/* ---------- ORDER SORT (Unified) ---------- */
const NOTES_ID = '__NOTES_HTML__';

const sortedFields = order.map((gid) => {
  if (gid.startsWith('para_')) {
    return {
      custom_field: {
        gid,
        type: 'paragraph',
        title: customTitles[gid] || '',
        text: customParas[gid] || '',
      }
    };
  }

  if (gid.startsWith('section_')) {
    return {
      custom_field: {
        gid,
        type: 'section_break',
        title: customSectionTitles[gid] || '',
        text: customSections[gid] || '',
      }
    };
  }

  if (gid.startsWith('agree_')) {
    return {
      custom_field: {
        gid,
        type: 'agreement',
        label: customAgreementLabels[gid] || '',
        text: customAgreementDetails[gid] || '',
      }
    };
  }

  if (gid.startsWith('upload_')) {
    return {
      custom_field: {
        gid,
        type: 'upload',
        title: customUploadTitles[gid] || '',
        text: customUploadDetails[gid] || '',
        min_files: customUploadMinFiles[gid] || 0,
        accept_types: customUploadAcceptTypes[gid] || '',
        compress_images: customUploadCompressConfigured ? customUploadCompressImages.includes(gid) : true,
        max_file_mb: customUploadMaxFileMB[gid] || 0,
        compress_max_width: customUploadMaxWidthPx[gid] || 1200,
        compress_quality: customUploadCompressQuality[gid] || 60,
      }
    };
  }

  if (gid.startsWith('single_')) {
    return {
      custom_field: {
        gid,
        type: 'single_input',
        label: customSingleInputLabels[gid] || '',
        text: customSingleInputDescriptions[gid] || '',
        placeholder: customSingleInputPlaceholders[gid] || '',
        sync_field: customSingleInputSyncFields[gid] || '',
        input_mode: customSingleInputModes[gid] || 'text',
        max_words_enabled: customSingleInputMaxWordsEnabled.includes(gid),
        max_words: customSingleInputMaxWords[gid] || 0,
        ai_enabled: customSingleInputAiEnabled.includes(gid),
      }
    };
  }

  if (gid.startsWith('dropdown_')) {
    return {
      custom_field: {
        gid,
        type: 'dropdown_input',
        label: customDropdownLabels[gid] || '',
        text: customDropdownDescriptions[gid] || '',
        placeholder: customDropdownPlaceholders[gid] || '',
        options_text: customDropdownOptions[gid] || '',
        selection_mode: customDropdownSelectionModes[gid] || '',
        sync_field: customDropdownSyncFields[gid] || '',
      }
    };
  }

  if (gid.startsWith('calendar_')) {
    return {
      custom_field: {
        gid,
        type: 'calendar_input',
        label: customCalendarLabels[gid] || '',
        text: customCalendarDescriptions[gid] || '',
        placeholder: customCalendarPlaceholders[gid] || '',
        sync_mode: customCalendarSyncModes[gid] || '',
        sync_field: customCalendarSyncFields[gid] || '',
        preset_mode: customCalendarPresetModes[gid] || '',
        preset_options: Array.isArray(customCalendarPresetOptions[gid]) ? customCalendarPresetOptions[gid] : [],
      }
    };
  }

  if (gid.startsWith('repeater_')) {
    return {
      custom_field: {
        gid,
        type: 'repeater_input',
        label: customRepeaterLabels[gid] || '',
        text: customRepeaterDescriptions[gid] || '',
        columns_text: customRepeaterColumns[gid] || '',
        min_rows: customRepeaterMinRows[gid] || 0,
        max_rows: customRepeaterMaxRows[gid] || 0,
        save_mode: customRepeaterSaveModes[gid] || 'subtasks',
        field_map: Array.isArray(customRepeaterFieldMap[gid])
          ? customRepeaterFieldMap[gid].map((entry) => String(entry || '').trim()).filter(Boolean)
          : [],
      }
    };
  }

  // ✅ Pseudo field: Notes (writes to task description as html_notes)
  if (gid === NOTES_ID) {
    return {
      custom_field: {
        gid: NOTES_ID,
        type: 'notes', // pseudo type handled in render below
        name: (labels[NOTES_ID] || 'Notes')
      }
    };
  }

  return fieldMap[gid] || null;
}).filter(Boolean);

// ✅ If admin enabled Notes but forgot to put it in order, render it at the end.
if ((allowed || []).includes(NOTES_ID) && !sortedFields.some(s => (s.custom_field?.gid === NOTES_ID))) {
  sortedFields.push({
    custom_field: {
      gid: NOTES_ID,
      type: 'notes',
      name: (labels[NOTES_ID] || 'Notes')
    }
  });
}

// Legacy migration: old configs used allow_uploads without upload_* blocks.
// Normalize to a regular upload block so frontend always uses one upload model.
if (legacyAllowUploads && !sortedFields.some((s) => String(s.custom_field?.gid || '').startsWith('upload_'))) {
  sortedFields.push({
    custom_field: {
      gid: 'upload_legacy_optional',
      type: 'upload',
      title: (customUploadTitles.upload_legacy_optional || 'File Uploads'),
      text: (customUploadDetails.upload_legacy_optional || 'Please upload any relevant documents to this request. We have a maximum upload size of 64MB.'),
      min_files: 0,
      accept_types: customUploadAcceptTypes.upload_legacy_optional || '',
      compress_images: customUploadCompressConfigured
        ? customUploadCompressImages.includes('upload_legacy_optional')
        : true,
      max_file_mb: customUploadMaxFileMB.upload_legacy_optional || 0,
      compress_max_width: customUploadMaxWidthPx.upload_legacy_optional || 1200,
      compress_quality: customUploadCompressQuality.upload_legacy_optional || 60,
    }
  });
}


            $fieldWrapper.show();

            /* ---------- INLINE TRIAGE SUMMARY FROM SAVED STATE ---------- */

            (function injectInlineTriageSummary() {
              // Only show triage summary when this form was opened from a triage request flow
              let fromTriage = false;
              try {
                const params = new URLSearchParams(window.location.search || '');
                const flag   = params.get('afb_triage') || params.get('triage_mode');
                if (flag && (flag === '1' || flag === 'request')) {
                  fromTriage = true;
                }
              } catch (_) {}

              if (!fromTriage) return;

              const snap    = getTriageStateSnapshot();
              const answers = snap.answers || {};

              // ── Build a dynamic summary from ALL keyed answers ──
              // Ordered fields first (known keys), then any leftover keys
              const orderedKeys = [
                ['Service',            ['service', 'service_name']],
                ['Subservice',         ['subservice', 'subservice_name']],
                ['Broadcast Type',     ['broadcast_type']],
                ['Audience',           ['audience', 'target_group', 'staff_relevance']],
                ['Audience Segment',   ['audience_segment']],
                ['Subject',            ['broadcast_title', 'ealert_heading', 'title']],
                ['Requested Date',     ['broadcast_date', 'date', 'date_qh']],
                ['Approval',           ['approval']],
                ['Approver Name',      ['approver_name']],
                ['Approver Position',  ['approver_position']],
                ['Start Date',         ['start_date']],
                ['End Date',           ['end_date']],
                ['Stage',              ['stage']],
                ['DDG',                ['ddg_other', 'qh_update_ddg']],
                ['Content Source',     ['broadcast_content_source', 'ealert_content_source']],
                ['Asset Ready',        ['asset_ready']],
                ['Created by SCB',     ['created_by_scb']],
                ['Brand Check',        ['brand_check']],
                ['Asset Size',         ['asset_size']],
                ['Asset Link',         ['asset_link']],
                ['Related Service',    ['related_service']],
              ];

              // Keys to never show (internal / meta / identity / raw HTML)
              const skipKeys = new Set([
                'triage_summary', 'triage_summary_html',
                'submitted_copy_html', 'submitted_copy_plain',
                'first_name', 'last_name', 'work_email', 'full_name',
                'triage_ref', 'session_key',
              ]);

              const usedKeys = new Set();
              const items = [];
              const seenPairs = new Set();

              const normalizeValue = (raw) => {
                if (Array.isArray(raw)) {
                  return raw.map(v => String(v ?? '').trim()).filter(Boolean).join(', ');
                }
                return raw == null ? '' : String(raw).trim();
              };

              // Skip mapped alias keys like site_suburb_brisbane when site_suburb
              // exists with the same value; this avoids duplicate site bullets.
              const hasEquivalentBaseKey = (key, value) => {
                if (!key || !key.includes('_') || value === '') return false;

                return Object.keys(answers).some(otherKey => {
                  if (!otherKey || otherKey === key) return false;
                  if (skipKeys.has(otherKey) || usedKeys.has(otherKey)) return false;
                  if (!key.startsWith(otherKey + '_')) return false;

                  const otherVal = normalizeValue(answers[otherKey]);
                  return otherVal !== '' && otherVal === value;
                });
              };

              // 1) Ordered known fields
              for (const [label, keys] of orderedKeys) {
                let val = '';
                for (const k of keys) {
                  usedKeys.add(k);
                  const raw = answers[k];
                  const normalized = normalizeValue(raw);
                  if (normalized !== '') { val = normalized; break; }
                }
                if (val) {
                  const sig = label + '::' + val;
                  if (!seenPairs.has(sig)) {
                    seenPairs.add(sig);
                    items.push({ label, val });
                  }
                }
              }

              // 2) Remaining answer keys (dynamically captured fields)
              Object.keys(answers).forEach(k => {
                if (usedKeys.has(k) || skipKeys.has(k)) return;
                if (/^follow_up_\d+$/i.test(k)) return;
                const val = normalizeValue(answers[k]);
                if (val === '') return;
                if (hasEquivalentBaseKey(k, val)) return;
                // Convert snake_case key to Title Case label
                const label = k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
                const sig = label + '::' + val;
                if (seenPairs.has(sig)) return;
                seenPairs.add(sig);
                items.push({ label, val });
              });

              if (items.length === 0) return;

              // Build the heading
              const svc = answers.subservice || answers.subservice_name || answers.service || answers.service_name || 'service';
              const sourceHeading = 'This request was submitted through QH Forms';
              const heading = 'You have submitted a request:';

              let listHtml = '<p>' + sourceHeading + '</p><p>' + heading + '</p><ul style="list-style:disc;padding-left:1.4em;margin:0.5em 0;">';
              for (const { label, val } of items) {
                const safeLabel = $('<span>').text(label).html();
                const safeVal   = $('<span>').text(val).html();
                listHtml += '<li>' + safeLabel + ': ' + safeVal + '</li>';
              }

              // Triage ref
              const triageRef = (snap.meta && snap.meta.triage_ref) || answers.triage_ref || '';
              if (triageRef) {
                const safeRef = $('<span>').text(triageRef).html();
                listHtml += '<li>Triage Ref: ' + safeRef + '</li>';
              }
              listHtml += '</ul>';

              const $block = $(`
                <div class="afb-triage-summary mt-4" data-triage-summary="1">
                  <h3>Request summary</h3>
                  <div class="afb-triage-summary-body"></div>
                </div>
              `);

              $block.find('.afb-triage-summary-body').html(listHtml);
              $container.prepend($block);
            })();

            /* ---------- Move Triage Summary above submit button ---------- */
            (function moveSummaryAboveSubmit() {
              const $submitWrap = $form.find('.header-and-submit');
              let $summary = $container.find('#triage-summary, .triage-summary, [data-triage-summary]').first();
              if (!$summary.length) {
                const $blocks = $container.find('.afb-paragraph-block, .field-block, div, section');
                $blocks.each(function () {
                  if ($summary.length) return;
                  const $el = $(this);
                  const headTxt = ($el.find('.afb-paragraph-title, h1, h2, h3, h4, strong').first().text() || '').trim().toLowerCase();
                  const txt = (headTxt || $el.text() || '').trim().toLowerCase();
                  if (txt.startsWith('triage summary')) $summary = $el;
                });
              }
              if ($summary.length && $submitWrap.length && !$summary.is($submitWrap.prev())) {
                $submitWrap.before($summary);
              }
            })();

            // ---------- Strip inner "Triage Summary" heading from the card ----------
            (function stripInnerTriageHeading() {
              $container.find('h1,h2,h3,h4,h5,h6,strong').each(function () {
                const txt = ($(this).text() || '').trim().toLowerCase();
                if (txt === 'triage summary') {
                  $(this).remove();
                }
              });
            })();

            /* ---------- TASK NAME ---------- */
            $container.append('<input type="hidden" name="task_name" value="" />');
            $container.append('<input type="hidden" id="submitter_name"  name="submitter_name"  value="">');
            $container.append('<input type="hidden" id="submitter_email" name="submitter_email" value="">');

            // Job number — prefix per project, fallback TASK-
            const pad = (n) => String(n).padStart(2, '0');
            const now = new Date();
            const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;

            // Use the currently selected projectId and the prefix map injected into afb_ajax
            const prefixMap = (window.afb_ajax && window.afb_ajax.prefix_map) ? window.afb_ajax.prefix_map : {};
            const pfx = (projectId && prefixMap && prefixMap[projectId]) ? prefixMap[projectId] : 'TASK';

            const jobNumber = `${pfx}-${ts}`;
            $container.append(`<input type="hidden" name="job_number" value="${jobNumber}" />`);
            $container.find('input[name="task_name"]').val(jobNumber);

            // Track rendered field elements for conditional logic
            const fieldInputs = {};
            const optionFilteredInputs = {};
            const steps = [];

            const $stepper = $('<div class="afb-stepper hidden mb-4"></div>');
            const $stepPanelsWrap = $('<div class="afb-step-panels"></div>');
            $container.append($stepper, $stepPanelsWrap);

            function createStep(title, text, stepTargetId = '', stepCondition = { logic: 'all', rules: [] }) {
              const index = steps.length;
              const resolvedTitle = (title || '').trim() || `Step ${index + 1}`;
              const resolvedText = (text || '').trim();
              const hasIntro = resolvedText.length > 0 || (title || '').trim().length > 0;
              const normalizedTargetId = String(stepTargetId || '').trim();

              const $panel = $(`
                <section class="afb-step-panel" data-step-index="${index}">
                  <div class="afb-step-intro ${hasIntro ? '' : 'hidden'}">
                    <h3 class="afb-step-title">${resolvedTitle}</h3>
                    <p class="afb-step-desc ${resolvedText ? '' : 'hidden'}">${resolvedText}</p>
                  </div>
                  <div class="afb-step-fields"></div>
                </section>
              `);

              const step = {
                title: resolvedTitle,
                text: resolvedText,
                targetId: normalizedTargetId,
                condition: (stepCondition && typeof stepCondition === 'object') ? stepCondition : { logic: 'all', rules: [] },
                $panel,
                $fields: $panel.find('.afb-step-fields').first(),
              };

              if (normalizedTargetId) {
                step.$panel.attr('data-afb-step-target', normalizedTargetId);
              }

              steps.push(step);
              $stepPanelsWrap.append($panel);
              return step;
            }

            let activeStep = createStep('', '', '', { logic: 'all', rules: [] });

            function normalizeCondition(cond) {
              const raw = (cond && typeof cond === 'object') ? { ...cond } : {};
              const logic = raw.logic === 'any' ? 'any' : 'all';

              const normalizeRule = (rule) => {
                const out = (rule && typeof rule === 'object') ? { ...rule } : {};
                const dependsOn = String(out.depends_on || '').trim();
                if (!dependsOn) return null;

                let vals = [];
                if (Array.isArray(out.values)) {
                  vals = out.values;
                } else if (typeof out.value !== 'undefined' && out.value !== null && out.value !== '') {
                  vals = [out.value];
                }

                const normalizedValues = vals
                  .map((v) => String(v || '').trim())
                  .filter(Boolean);

                return {
                  depends_on: dependsOn,
                  values: Array.from(new Set(normalizedValues)),
                };
              };

              let rules = [];
              if (Array.isArray(raw.rules)) {
                rules = raw.rules.map(normalizeRule).filter(Boolean);
              } else {
                const legacyRule = normalizeRule(raw);
                if (legacyRule) rules = [legacyRule];
              }

              return {
                logic,
                rules,
              };
            }

            function normalizeOptionRule(rule) {
              const out = (rule && typeof rule === 'object') ? { ...rule } : {};
              out.depends_on = String(out.depends_on || '').trim();
              const map = (out.map && typeof out.map === 'object') ? out.map : {};
              const normalizedMap = {};
              Object.keys(map).forEach((sourceValue) => {
                const sourceKey = String(sourceValue || '').trim();
                if (!sourceKey) return;
                const vals = Array.isArray(map[sourceValue]) ? map[sourceValue] : [];
                const clean = vals
                  .map((v) => String(v || '').trim())
                  .filter(Boolean);
                if (clean.length) {
                  normalizedMap[sourceKey] = Array.from(new Set(clean));
                }
              });
              out.map = normalizedMap;
              return out;
            }

            function countWords(value) {
              const text = String(value || '').trim();
              if (!text) return 0;
              const tokens = text.match(/\S+/g);
              return Array.isArray(tokens) ? tokens.length : 0;
            }

            function bindSingleInputWordLimit($input, maxWords) {
              if (!$input || !$input.length) return;

              const max = Math.max(0, Number.parseInt(maxWords, 10) || 0);
              if (max <= 0) return;

              const $status = $('<p class="mt-1 text-xs text-slate-500 afb-single-word-limit" aria-live="polite"></p>');
              $input.after($status);

              const el = $input.get(0);
              if (!el || typeof el.setCustomValidity !== 'function') return;

              const update = function () {
                if ($input.prop('disabled')) {
                  el.setCustomValidity('');
                  return;
                }

                const words = countWords($input.val());
                const delta = max - words;

                if (delta < 0) {
                  const overBy = Math.abs(delta);
                  const suffix = overBy === 1 ? '' : 's';
                  const err = `You are over the limit by ${overBy} word${suffix} (${words}/${max}).`;
                  $status.removeClass('text-slate-500').addClass('text-red-600').text(err);
                  el.setCustomValidity(`Please remove ${overBy} word${suffix}. Maximum is ${max} words.`);
                  return;
                }

                if (delta === 0) {
                  $status.removeClass('text-red-600').addClass('text-slate-500').text(`You are at the limit (${words}/${max} words).`);
                } else {
                  const suffix = delta === 1 ? '' : 's';
                  $status.removeClass('text-red-600').addClass('text-slate-500').text(`You are under the limit by ${delta} word${suffix} (${words}/${max}).`);
                }

                el.setCustomValidity('');
              };

              $input.on('input change', update);
              update();
            }

            /* ---------- RENDER FIELDS ---------- */
 sortedFields.forEach((setting) => {
  const field = setting.custom_field;

  if (field.type === 'section_break') {
    const sectionId = String(field.gid || '').trim();
    const sectionCondition = normalizeCondition(conditions[sectionId] || {});
    const title = (field.title || '').trim();
    const text  = (field.text || '').trim();

    if (steps.length === 1 && activeStep.$fields.children().length === 0) {
      const fallbackTitle = title || 'Step 1';
      activeStep.title = fallbackTitle;
      activeStep.text = text;
      activeStep.targetId = sectionId;
      activeStep.condition = sectionCondition;
      activeStep.$panel.attr('data-afb-step-target', sectionId);
      activeStep.$panel.find('.afb-step-title').text(fallbackTitle);
      activeStep.$panel.find('.afb-step-desc').text(text).toggleClass('hidden', !text);
      activeStep.$panel.find('.afb-step-intro').toggleClass('hidden', !(title || text));
    } else {
      activeStep = createStep(title, text, sectionId, sectionCondition);
    }

    return;
  }

  // ✅ Pseudo "Notes" field (Task Description) — full width
  if (field.type === 'notes' || field.gid === NOTES_ID) {
    

    // Only render if admin enabled it (used_fields) or explicitly ordered
    if (!(allowed || []).includes(NOTES_ID) && !(order || []).includes(NOTES_ID)) return;

    const isReq = (required || []).includes(NOTES_ID);
    const label = (labels && labels[NOTES_ID]) ? labels[NOTES_ID] : 'Notes';
    const desc  = (descriptions && descriptions[NOTES_ID]) ? descriptions[NOTES_ID] : 'Anything we should know?';

    activeStep.$fields.append(`
      <label class="block mb-2" data-field-id="${NOTES_ID}">
        <span class="text-gray-700">${label}${isReq ? ' *' : ''}</span>
        ${desc ? `<p class="text-sm text-gray-500 mb-1">${desc}</p>` : ''}
        <textarea name="asana_html_notes" rows="6"
          class="mt-1 block w-full rounded border-gray-300 shadow-sm afb-notes-textarea"
          ${isReq ? 'required' : ''}></textarea>
      </label>
    `);

    fieldInputs[NOTES_ID] = {
      element: activeStep.$fields.find(`[data-field-id="${NOTES_ID}"]`).last(),
      condition: normalizeCondition(conditions[NOTES_ID] || {}),
      blockSubmit: submitBlockFields.includes(NOTES_ID),
    };

    return;
  }

  if (field.type === 'single_input') {
    const singleId = String(field.gid || '').trim();
    if (!singleId) return;

    const isReq = required.includes(singleId);
    const singleLabel = (field.label || '').trim() || 'Additional detail';
    const singleText = (field.text || '').trim();
    const singlePlaceholder = (field.placeholder || '').trim();
    const singleSyncField = String(field.sync_field || '').trim();
    const singleModeRaw = String(field.input_mode || 'text').trim();
    const singleMode = singleModeRaw === 'textarea' ? 'textarea' : 'text';
    const singleMaxWordsEnabled = !!field.max_words_enabled;
    const singleMaxWords = Math.max(0, Number.parseInt(field.max_words, 10) || 0);
    const singleAiEnabled = !!field.ai_enabled;
    const enforceSingleMaxWords = singleMode === 'textarea' && singleMaxWordsEnabled && singleMaxWords > 0;
    const singleCond = normalizeCondition(conditions[singleId] || {});

    const $singleWrap = $('<label class="block mb-2"></label>').attr('data-field-id', singleId);
    $singleWrap.append($('<span class="text-gray-700"></span>').text(isReq ? `${singleLabel} *` : singleLabel));

    if (singleText) {
      $singleWrap.append($('<p class="text-sm text-gray-500 mb-1"></p>').text(singleText));
    }

    const $singleInput = singleMode === 'textarea'
      ? $('<textarea rows="1" class="mt-1 block w-full rounded border-gray-300 shadow-sm afb-single-input-textarea"></textarea>')
      : $('<input type="text" class="mt-1 block w-full rounded border-gray-300 shadow-sm" />');

    if (singleMode === 'textarea' && singleMaxWords > 0) {
      $singleInput.attr('data-afb-max-words', String(singleMaxWords));
    }

    $singleInput.attr(singleSyncField ? 'data-afb-sync-target' : 'data-afb-description-input', singleSyncField ? singleSyncField : '1');

    if (!singleSyncField) {
      $singleInput
        .attr('name', `description_inputs[${singleId}]`)
        .attr('data-afb-description-id', singleId)
        .attr('data-afb-description-label', singleLabel);
    }

    if (isReq) {
      $singleInput.prop('required', true);
    }

    if (singlePlaceholder) {
      $singleInput.attr('placeholder', singlePlaceholder);
    }

    $singleWrap.append($singleInput);

    if (singleAiEnabled) {
      const $aiHelper = $('<div class="afb-single-ai-helper mt-2"></div>');
      const $aiHead = $('<div class="afb-single-ai-head"></div>');
      const $aiBadge = $('<span class="afb-single-ai-badge">AI help</span>');
      const $aiControls = $('<div class="afb-single-ai-controls"></div>');
      const $aiPreset = $(
        '<select class="afb-single-ai-preset" data-afb-ai-preset="1" aria-label="Rewrite style">'
        + '<option value="improve" selected>Improve clarity</option>'
        + '<option value="shorter">Make it shorter</option>'
        + '<option value="formal">More formal</option>'
        + '<option value="friendly">Friendlier</option>'
        + '<option value="plain">Plain English</option>'
        + '</select>'
      );
      const $aiButton = $('<button type="button" class="afb-single-ai-btn" data-afb-ai-rewrite="1">Rewrite with AI</button>')
        .attr('data-afb-ai-mode', singleMode);

      if (enforceSingleMaxWords) {
        $aiButton.attr('data-afb-ai-max-words', String(singleMaxWords));
      }

      const $aiNote = $('<p class="afb-single-ai-note text-xs text-slate-500 mt-1 mb-0">Use AI to improve wording while keeping your meaning.</p>');

      $aiControls.append($aiPreset, $aiButton);
      $aiHead.append($aiBadge, $aiControls);
      $aiHelper.append($aiHead, $aiNote);
      $singleWrap.append($aiHelper);
    }

    if (enforceSingleMaxWords) {
      bindSingleInputWordLimit($singleInput, singleMaxWords);
    }

    activeStep.$fields.append($singleWrap);
    fieldInputs[singleId] = { element: $singleWrap, condition: singleCond, blockSubmit: submitBlockFields.includes(singleId) };
    return;
  }

  if (field.type === 'dropdown_input') {
    const dropdownId = String(field.gid || '').trim();
    if (!dropdownId) return;

    const isReq = required.includes(dropdownId);
    const dropdownLabel = (field.label || '').trim() || 'Choose an option';
    const dropdownText = (field.text || '').trim();
    const dropdownPlaceholder = (field.placeholder || '').trim() || '-- Select --';
    const dropdownSyncField = String(field.sync_field || '').trim();
    const dropdownSelectionModeRaw = String(field.selection_mode || '').trim();
    const dropdownSelectionMode = dropdownSelectionModeRaw === 'multi' ? 'multi' : 'single';
    const dropdownCond = normalizeCondition(conditions[dropdownId] || {});
    const dropdownOptions = String(field.options_text || '')
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter(Boolean);

    const $dropdownWrap = $('<label class="block mb-2"></label>').attr('data-field-id', dropdownId);
    $dropdownWrap.append($('<span class="text-gray-700"></span>').text(dropdownLabel));

    if (dropdownText) {
      $dropdownWrap.append($('<p class="text-sm text-gray-500 mb-1"></p>').text(dropdownText));
    }

    if (dropdownSelectionMode === 'multi') {
      const $group = $('<div class="mt-2 flex flex-col gap-2" data-multienum="1"></div>');

      if (isReq) {
        $group.attr('data-required', '1');
        $dropdownWrap.find('span.text-gray-700').first().text(`${dropdownLabel} *`);
      }

      if (dropdownSyncField) {
        $group.attr('data-afb-sync-target-group', dropdownSyncField);
      } else {
        $group
          .attr('data-afb-description-group', '1')
          .attr('data-afb-description-id', dropdownId)
          .attr('data-afb-description-label', dropdownLabel);
      }

      dropdownOptions.forEach((optText) => {
        const optEsc = String(optText || '').trim();
        if (!optEsc) return;

        const $opt = $('<label class="inline-flex items-center space-x-2"></label>');
        const $checkbox = $('<input type="checkbox" class="rounded border-gray-300">').val(optEsc);
        const $text = $('<span></span>').text(optEsc);
        $opt.append($checkbox, $text);
        $group.append($opt);
      });

      $dropdownWrap.append($group);
    } else {
      const $dropdown = $('<select class="mt-1 block w-full rounded border-gray-300 shadow-sm"></select>');
      $dropdown.append($('<option></option>').val('').text(dropdownPlaceholder));
      dropdownOptions.forEach((optText) => {
        $dropdown.append($('<option></option>').val(optText).text(optText));
      });

      if (isReq) {
        $dropdown.prop('required', true);
        $dropdownWrap.find('span.text-gray-700').first().text(`${dropdownLabel} *`);
      }

      if (dropdownSyncField) {
        $dropdown.attr('data-afb-sync-target', dropdownSyncField);
      } else {
        $dropdown
          .attr('name', `description_inputs[${dropdownId}]`)
          .attr('data-afb-description-input', '1')
          .attr('data-afb-description-id', dropdownId)
          .attr('data-afb-description-label', dropdownLabel);
      }

      $dropdownWrap.append($dropdown);
    }

    activeStep.$fields.append($dropdownWrap);
    fieldInputs[dropdownId] = { element: $dropdownWrap, condition: dropdownCond, blockSubmit: submitBlockFields.includes(dropdownId) };
    return;
  }

  if (field.type === 'calendar_input') {
    const calendarId = String(field.gid || '').trim();
    if (!calendarId) return;

    const isReq = required.includes(calendarId);
    const calendarLabel = (field.label || '').trim() || 'Select a date';
    const calendarText = (field.text || '').trim();
    const calendarPlaceholder = (field.placeholder || '').trim() || '-- Select a date --';
    const calendarSyncField = String(field.sync_field || '').trim();
    const calendarSyncModeRaw = String(field.sync_mode || '').trim();
    const calendarPresetMode = String(field.preset_mode || '').trim();
    const calendarPresetOptions = Array.isArray(field.preset_options) ? field.preset_options : [];
    const calendarSyncMode = ['none', 'text', 'due_date'].includes(calendarSyncModeRaw)
      ? calendarSyncModeRaw
      : (calendarSyncField ? 'text' : 'none');
    const calendarCond = normalizeCondition(conditions[calendarId] || {});

    const $calendarWrap = $('<label class="block mb-2"></label>').attr('data-field-id', calendarId);
    $calendarWrap.append($('<span class="text-gray-700"></span>').text(calendarLabel));

    if (calendarText) {
      $calendarWrap.append($('<p class="text-sm text-gray-500 mb-1"></p>').text(calendarText));
    }

    const usePresetSelect = calendarPresetMode === 'ealert_next_4_wednesdays' && calendarPresetOptions.length > 0;
    let $calendarInput;

    if (usePresetSelect) {
      $calendarInput = $('<select class="mt-1 block w-full rounded border-gray-300 shadow-sm"></select>');
      $calendarInput.append($('<option></option>').val('').text(calendarPlaceholder));
      calendarPresetOptions.forEach((opt) => {
        const value = String(opt || '').trim();
        if (!value) return;
        $calendarInput.append($('<option></option>').val(value).text(value));
      });
    } else {
      $calendarInput = $('<input type="date" class="mt-1 block w-full rounded border-gray-300 shadow-sm" />');
      $calendarInput.attr('placeholder', calendarPlaceholder);
    }

    if (isReq) {
      $calendarInput.prop('required', true);
      $calendarWrap.find('span.text-gray-700').first().text(`${calendarLabel} *`);
    }

    if (calendarSyncMode === 'due_date') {
      $calendarInput.attr('data-afb-due-date-source', '1');
    } else if (calendarSyncMode === 'text' && calendarSyncField) {
      $calendarInput.attr('data-afb-sync-target', calendarSyncField);
    } else {
      $calendarInput
        .attr('name', `description_inputs[${calendarId}]`)
        .attr('data-afb-description-input', '1')
        .attr('data-afb-description-id', calendarId)
        .attr('data-afb-description-label', calendarLabel);
    }

    $calendarWrap.append($calendarInput);
    activeStep.$fields.append($calendarWrap);
    fieldInputs[calendarId] = { element: $calendarWrap, condition: calendarCond, blockSubmit: submitBlockFields.includes(calendarId) };
    return;
  }

  if (field.type === 'repeater_input') {
    const repeaterId = String(field.gid || '').trim();
    if (!repeaterId) return;

    const isReq = required.includes(repeaterId);
    const repeaterLabel = (field.label || '').trim() || 'Repeater List';
    const repeaterText = (field.text || '').trim();
    const repeaterSaveMode = String(field.save_mode || '').trim() === 'description' ? 'description' : 'subtasks';
    const repeaterCond = normalizeCondition(conditions[repeaterId] || {});

    const parseRepeaterColumns = function (rawColumns) {
      const lines = String(rawColumns || '')
        .split(/\r?\n/)
        .map((line) => String(line || '').trim())
        .filter(Boolean);

      const unique = [];
      const seen = {};
      lines.forEach((line) => {
        const key = line.toLowerCase();
        if (seen[key]) return;
        seen[key] = true;
        unique.push(line);
      });

      return unique;
    };

    const defaultRepeaterColumns = ['Name', 'Surname', 'Email', 'Phone', 'Agreement'];
    const repeaterColumns = parseRepeaterColumns(field.columns_text || field.columns || '');

    const mappedFieldIds = Array.isArray(field.field_map)
      ? field.field_map.map((entry) => String(entry || '').trim()).filter(Boolean)
      : [];

    const normalizeEnumOptions = function (rawOptions) {
      if (!Array.isArray(rawOptions)) return [];

      const out = [];
      const seen = {};
      rawOptions.forEach((opt) => {
        if (!opt || opt.enabled === false) return;

        const gid = String(opt.gid || '').trim();
        if (!gid || seen[gid]) return;
        seen[gid] = true;

        const name = String(opt.name || gid).trim() || gid;
        out.push({ gid, name });
      });

      return out;
    };

    const mappedFields = [];
    const seenMappedGids = {};
    const appendMappedFieldByGid = function (rawGid) {
      const mappedGid = String(rawGid || '').trim();
      if (!/^\d+$/.test(mappedGid) || seenMappedGids[mappedGid]) return false;
      seenMappedGids[mappedGid] = true;

      const setting = fieldMap[mappedGid] && fieldMap[mappedGid].custom_field
        ? fieldMap[mappedGid].custom_field
        : null;

      let mappedType = String((setting && setting.type) || 'text').trim().toLowerCase();
      if (!['text', 'number', 'enum', 'multi_enum', 'date'].includes(mappedType)) {
        mappedType = 'text';
      }

      const mappedLabel = String(labels[mappedGid] || (setting && setting.name) || ('Field ' + mappedGid)).trim() || ('Field ' + mappedGid);
      const mappedOptions = normalizeEnumOptions(setting && setting.enum_options);

      mappedFields.push({
        gid: mappedGid,
        type: mappedType,
        label: mappedLabel,
        options: mappedOptions,
      });

      return true;
    };

    mappedFieldIds.forEach((mappedGidRaw) => {
      appendMappedFieldByGid(mappedGidRaw);
    });

    // Legacy compatibility: numeric column labels are treated as mapped Asana field gids.
    repeaterColumns.forEach((columnLabelRaw) => {
      const maybeGid = String(columnLabelRaw || '').trim();
      if (!/^\d+$/.test(maybeGid)) return;
      appendMappedFieldByGid(maybeGid);
    });

    const plainColumnsSource = repeaterColumns.length ? repeaterColumns : defaultRepeaterColumns;
    const plainColumns = plainColumnsSource.filter((columnLabelRaw) => {
      const label = String(columnLabelRaw || '').trim();
      if (!label) return false;

      return !(/^\d+$/.test(label) && seenMappedGids[label]);
    });

    const columns = mappedFields.length
      ? mappedFields.map((entry) => String(entry.label || '').trim()).filter(Boolean).concat(plainColumns)
      : plainColumns;

    const minRows = clampRepeaterRowLimit(field.min_rows);
    let maxRows = clampRepeaterRowLimit(field.max_rows);
    if (maxRows > 0 && maxRows < minRows) {
      maxRows = minRows;
    }

    const requiredRows = minRows > 0 ? minRows : (isReq ? 1 : 0);
    const initialRows = maxRows > 0
      ? Math.min(Math.max(1, requiredRows), maxRows)
      : Math.max(1, requiredRows);

    const inferRepeaterInputKind = function (label) {
      const lower = String(label || '').toLowerCase();
      if (/email/.test(lower)) return 'email';
      if (/(phone|mobile|tel)/.test(lower)) return 'tel';
      if (/(agreement|agree|consent|approval|approved|permission)/.test(lower)) return 'checkbox';
      return 'text';
    };

    const appendLegacyRepeaterColumnField = function ($grid, values, columnLabel, columnIndex) {
      const label = String(columnLabel || '').trim();
      if (!label) return;

      const key = normalizeRepeaterKey(label, columnIndex);
      const inputKind = inferRepeaterInputKind(label);
      const preset = String(values[label] || values[key] || '').trim();

      const $fieldWrap = $('<label class="afb-repeater-col-field block"></label>')
        .attr('data-afb-repeater-col', label)
        .attr('data-afb-repeater-key', key)
        .attr('data-afb-repeater-type', inputKind === 'checkbox' ? 'checkbox' : 'text');

      if (inputKind === 'checkbox') {
        const checked = /^(1|yes|true|y|agreed|checked)$/i.test(preset);
        const $checkboxWrap = $('<span class="inline-flex items-start gap-2 text-sm text-slate-700"></span>');
        const $checkbox = $('<input type="checkbox" class="mt-1 rounded border-gray-300">')
          .val('Yes')
          .prop('checked', checked);
        $checkboxWrap.append($checkbox, $('<span></span>').text(label));
        $fieldWrap.append($checkboxWrap);
      } else {
        const $caption = $('<span class="mb-1 block text-xs text-slate-600"></span>').text(label);
        const $input = $('<input class="mt-1 block w-full rounded border-gray-300 shadow-sm" />')
          .attr('type', inputKind)
          .attr('placeholder', label)
          .val(preset);
        $fieldWrap.append($caption, $input);
      }

      $grid.append($fieldWrap);
    };

    const $wrap = $('<div class="block mb-2"></div>').attr('data-field-id', repeaterId);
    const $header = $('<div class="min-w-0"></div>');
    $header.append($('<p class="font-semibold text-gray-700"></p>').text(isReq ? `${repeaterLabel} *` : repeaterLabel));
    if (repeaterText) {
      $header.append($('<p class="text-sm text-gray-500 mb-1"></p>').text(repeaterText));
    }

    const $block = $('<div class="afb-repeater-block mt-2 rounded bg-slate-50/50 p-3"></div>')
      .attr('data-afb-repeater', '1')
      .attr('data-afb-repeater-id', repeaterId)
      .attr('data-afb-repeater-label', repeaterLabel)
      .attr('data-afb-repeater-save-mode', repeaterSaveMode)
      .attr('data-afb-repeater-required', isReq ? '1' : '0')
      .attr('data-afb-repeater-min-rows', String(minRows))
      .attr('data-afb-repeater-max-rows', String(maxRows))
      .attr('data-afb-repeater-columns', JSON.stringify(columns))
      .attr('data-afb-repeater-field-map', JSON.stringify(mappedFields.map((entry) => entry.gid)));

    const $rows = $('<div class="afb-repeater-rows space-y-3"></div>');
    const $actions = $('<div class="mt-3 flex flex-wrap items-center gap-2"></div>');
    const $addRow = $('<button type="button" class="afb-repeater-add-row rounded bg-slate-900 px-3 py-1 text-sm text-white">+ Add row</button>');
    const $meta = $('<p class="text-xs text-slate-500 mb-0"></p>');

    const buildRow = function (prefillValues) {
      const values = (prefillValues && typeof prefillValues === 'object') ? prefillValues : {};
      const $row = $('<div class="afb-repeater-row rounded bg-white p-3"></div>');
      const $rowHeader = $('<div class="mb-2 flex items-center justify-between gap-2"></div>');
      const $rowTitle = $('<p class="afb-repeater-row-title mb-0 text-xs font-semibold uppercase tracking-wide text-slate-500"></p>');
      const $removeRow = $('<button type="button" class="afb-repeater-remove-row rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-700">Remove</button>');
      $rowHeader.append($rowTitle, $removeRow);

      const $grid = $('<div class="grid gap-3 md:grid-cols-2"></div>');

      if (mappedFields.length) {
        mappedFields.forEach((mappedField, columnIndex) => {
          const label = String(mappedField.label || '').trim();
          if (!label) return;

          const key = normalizeRepeaterKey(label, columnIndex);
          const mappedType = String(mappedField.type || 'text').trim().toLowerCase() || 'text';
          const presetRaw = values[mappedField.gid] != null ? values[mappedField.gid] : (values[key] != null ? values[key] : values[label]);
          const preset = Array.isArray(presetRaw)
            ? presetRaw.map((entry) => String(entry || '').trim()).filter(Boolean)
            : String(presetRaw == null ? '' : presetRaw).trim();

          const $fieldWrap = $('<label class="afb-repeater-col-field block"></label>')
            .attr('data-afb-repeater-col', label)
            .attr('data-afb-repeater-key', key)
            .attr('data-afb-repeater-gid', mappedField.gid)
            .attr('data-afb-repeater-type', mappedType);

          const $caption = $('<span class="mb-1 block text-xs text-slate-600"></span>').text(label);
          $fieldWrap.append($caption);

          if (mappedType === 'enum' && mappedField.options.length) {
            const $select = $('<select class="mt-1 block w-full rounded border-gray-300 shadow-sm"></select>');
            $select.append($('<option></option>').val('').text(`Select ${label}`));
            mappedField.options.forEach((opt) => {
              const optValue = String(opt.gid || '').trim();
              if (!optValue) return;

              const $opt = $('<option></option>').val(optValue).text(String(opt.name || optValue));
              if (!Array.isArray(preset) && String(preset || '') === optValue) {
                $opt.prop('selected', true);
              }
              $select.append($opt);
            });
            $fieldWrap.append($select);
            $grid.append($fieldWrap);
            return;
          }

          if (mappedType === 'multi_enum' && mappedField.options.length) {
            const selectedSet = Array.isArray(preset)
              ? new Set(preset)
              : new Set(String(preset || '').split(',').map((entry) => String(entry || '').trim()).filter(Boolean));
            const $group = $('<div class="mt-1 space-y-1 rounded bg-white p-2"></div>');

            mappedField.options.forEach((opt) => {
              const optValue = String(opt.gid || '').trim();
              if (!optValue) return;

              const optLabel = String(opt.name || optValue).trim() || optValue;
              const $rowOption = $('<label class="inline-flex items-start gap-2 text-sm text-slate-700"></label>');
              const $checkbox = $('<input type="checkbox" class="mt-1 rounded border-gray-300">')
                .attr('data-afb-repeater-option', '1')
                .attr('data-afb-repeater-option-label', optLabel)
                .val(optValue)
                .prop('checked', selectedSet.has(optValue));
              $rowOption.append($checkbox, $('<span></span>').text(optLabel));
              $group.append($rowOption);
            });

            $fieldWrap.append($group);
            $grid.append($fieldWrap);
            return;
          }

          const inputType = mappedType === 'number'
            ? 'number'
            : (mappedType === 'date' ? 'date' : 'text');
          const $input = $('<input class="mt-1 block w-full rounded border-gray-300 shadow-sm" />')
            .attr('type', inputType)
            .attr('placeholder', label)
            .val(Array.isArray(preset) ? '' : preset || '');

          if (inputType === 'number') {
            $input.attr('step', 'any');
          }

          $fieldWrap.append($input);
          $grid.append($fieldWrap);
        });

        plainColumns.forEach((columnLabel, plainIndex) => {
          appendLegacyRepeaterColumnField($grid, values, columnLabel, mappedFields.length + plainIndex);
        });
      } else {
        plainColumns.forEach((columnLabel, columnIndex) => {
          appendLegacyRepeaterColumnField($grid, values, columnLabel, columnIndex);
        });
      }

      $row.append($rowHeader, $grid);
      return $row;
    };

    const updateRepeaterUi = function () {
      const $allRows = $rows.children('.afb-repeater-row');
      const rowCount = $allRows.length;
      const minVisibleRows = Math.max(1, requiredRows);
      const canRemove = rowCount > minVisibleRows;

      $allRows.each(function (index) {
        $(this).find('.afb-repeater-row-title').first().text(`Row ${index + 1}`);
      });

      $allRows.find('.afb-repeater-remove-row')
        .prop('disabled', !canRemove)
        .toggleClass('opacity-60 cursor-not-allowed', !canRemove);

      const canAdd = maxRows <= 0 || rowCount < maxRows;
      $addRow
        .prop('disabled', !canAdd)
        .toggleClass('opacity-60 cursor-not-allowed', !canAdd);

      const rowWord = rowCount === 1 ? 'row' : 'rows';
      const minNote = requiredRows > 0 ? `minimum ${requiredRows}` : 'optional';
      const maxNote = maxRows > 0 ? `, max ${maxRows}` : '';
      $meta.text(`${rowCount} ${rowWord} ready (${minNote}${maxNote}).`);
    };

    $addRow.on('click', function () {
      const currentRows = $rows.children('.afb-repeater-row').length;
      if (maxRows > 0 && currentRows >= maxRows) {
        updateRepeaterUi();
        return;
      }

      const $newRow = buildRow({});
      $rows.append($newRow);
      updateRepeaterUi();
    });

    $rows.on('click', '.afb-repeater-remove-row', function () {
      const $row = $(this).closest('.afb-repeater-row');
      if (!$row.length) return;

      const minVisibleRows = Math.max(1, requiredRows);
      const rowCount = $rows.children('.afb-repeater-row').length;
      if (rowCount <= minVisibleRows) {
        $row.find('input[type="text"], input[type="email"], input[type="tel"], input[type="number"], input[type="date"], textarea').val('');
        $row.find('select').prop('selectedIndex', 0);
        $row.find('input[type="checkbox"]').prop('checked', false);
        updateRepeaterUi();
        return;
      }

      $row.remove();
      updateRepeaterUi();
    });

    for (let i = 0; i < initialRows; i += 1) {
      $rows.append(buildRow({}));
    }

    updateRepeaterUi();

    $actions.append($addRow, $meta);
    $block.append($rows, $actions);
    $wrap.append($header, $block);

    activeStep.$fields.append($wrap);
    fieldInputs[repeaterId] = { element: $wrap, condition: repeaterCond, blockSubmit: submitBlockFields.includes(repeaterId) };
    return;
  }

  // Admin-defined agreement checkbox (required, not sent to Asana custom fields)
  if (field.type === 'agreement') {
    const agreementId = field.gid;
    const agreementLabel = (field.label || '').trim() || 'I agree';
    const agreementText = (field.text || '').trim();
    const isAgreementRequired = agreementRequiredConfigured
      ? agreementRequired.includes(String(agreementId || '').trim())
      : true;

    const $agreement = $(`
      <div class="afb-agreement-block" data-field-id="${agreementId}">
        <label class="inline-flex items-start gap-2 text-gray-700">
          <input
            type="checkbox"
            name="agreement_checks[${agreementId}]"
            value="1"
            ${isAgreementRequired ? 'required' : ''}
            data-afb-agreement="1"
            class="mt-1 rounded border-gray-300"
          />
          <span>${agreementLabel}</span>
        </label>
        ${agreementText ? `<p class="text-sm text-gray-500 mt-1 mb-0">${agreementText}</p>` : ''}
      </div>
    `);

    activeStep.$fields.append($agreement);
    fieldInputs[agreementId] = { element: $agreement, condition: normalizeCondition(conditions[agreementId] || {}), blockSubmit: submitBlockFields.includes(String(agreementId || '').trim()) };

    return;
  }

  if (field.type === 'upload') {
    const uploadId = field.gid;
    const uploadTitle = (field.title || '').trim() || 'Upload Area';
    const uploadText = (field.text || '').trim();
    const minFiles = Math.max(0, parseInt(field.min_files, 10) || 0);
    const acceptTokens = afbParseUploadAcceptTokens(field.accept_types || '');
    const acceptValue = acceptTokens.join(',');
    const acceptLabel = afbDescribeUploadAcceptTokens(acceptTokens);
    const compressImages = !(field.compress_images === false || String(field.compress_images || '').trim() === '0');
    const compressMaxWidthRaw = afbParsePositiveUploadNumber(field.compress_max_width);
    const compressMaxWidthPx = afbNormalizeUploadMaxWidth(compressMaxWidthRaw || 1200);
    const compressQualityPctRaw = afbParsePositiveUploadNumber(field.compress_quality);
    const compressQualityPct = Math.max(1, Math.min(100, Math.round(compressQualityPctRaw || 60)));
    const maxFileMB = afbParsePositiveUploadNumber(field.max_file_mb);
    const maxFileBytes = maxFileMB > 0 ? Math.round(maxFileMB * 1024 * 1024) : 0;
    const maxFileLabel = afbFormatUploadMb(maxFileMB);
    const initialRows = minFiles > 0 ? minFiles : 0;

    const $block = $(`
      <div class="afb-upload-block" data-field-id="${uploadId}" data-min-files="${minFiles}">
        <div class="afb-upload-block-inner">
          <div class="afb-upload-header flex items-start justify-between gap-3">
            <div class="min-w-0">
              <p class="afb-upload-heading font-semibold text-gray-700 mb-1">${uploadTitle}</p>
              ${uploadText ? `<p class="text-sm text-gray-600 mb-0">${uploadText}</p>` : ''}
            </div>
            ${minFiles > 0 ? `<span class="afb-upload-required-badge text-xs font-semibold uppercase tracking-wide whitespace-nowrap">${minFiles > 1 ? `Required: ${minFiles}+` : 'Required'}</span>` : ''}
          </div>
          <div class="upload-fields mt-3 space-y-2">
            <div class="file-list space-y-2"></div>
            <button type="button" class="add-file-btn bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600">+ Add File</button>
          </div>
        </div>
      </div>
    `);

    $block.attr('data-compress-images', compressImages ? '1' : '0');
    $block.attr('data-compress-max-width', String(compressMaxWidthPx));
    $block.attr('data-compress-quality', String(compressQualityPct));

    if (acceptValue) {
      $block.attr('data-accept-types', acceptValue);
    }
    if (maxFileMB > 0) {
      $block.attr('data-max-file-mb', maxFileLabel);
    }

    if (acceptLabel) {
      const $acceptHint = $('<p class="text-xs text-gray-500 mb-0 mt-1"></p>').text(`Accepted file types: ${acceptLabel}`);
      $block.find('.afb-upload-header .min-w-0').append($acceptHint);
    }
    if (compressImages) {
      const $compressHint = $('<p class="text-xs text-gray-500 mb-0 mt-1"></p>').text(`Images are compressed automatically on upload (max ${compressMaxWidthPx}px wide, ~${compressQualityPct}% quality).`);
      $block.find('.afb-upload-header .min-w-0').append($compressHint);
    }
    if (maxFileMB > 0) {
      const $maxHint = $('<p class="text-xs text-gray-500 mb-0 mt-1"></p>').text(`Max file size per file: ${maxFileLabel} MB`);
      $block.find('.afb-upload-header .min-w-0').append($maxHint);
    }

    const $fileList = $block.find('.file-list');
    const addFileRow = function () {
      const row = $(`
        <div class="afb-upload-file-row flex items-center justify-between gap-2 bg-white border border-gray-200 p-2 rounded">
          <input type="file" class="file-input block w-full text-sm text-gray-700" />
          <div class="file-meta afb-upload-file-meta ml-2 text-xs text-gray-500"></div>
          <button type="button" class="remove-file-btn text-xs px-3 py-1 rounded-full transition">Remove</button>
        </div>
      `);
      const $input = row.find('input[type="file"]');
      const $meta = row.find('.file-meta');
      const inputEl = $input.get(0);

      const setRowError = function (message) {
        const msg = String(message || '').trim();
        row.addClass('border-red-300');
        row.attr('data-afb-upload-error', '1');
        row.attr('data-afb-upload-error-msg', msg);
        $meta.text(msg || 'File upload error.');
      };

      const clearRowError = function () {
        row.removeClass('border-red-300');
        row.removeAttr('data-afb-upload-error');
        row.removeAttr('data-afb-upload-error-msg');
      };

      if (acceptValue) {
        $input.attr('accept', acceptValue);
      }

      $input.on('change', function () {
        const currentInput = this;
        const prepToken = `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
        currentInput.dataset.afbPrepToken = prepToken;
        afbClearPreparedUploadFile(currentInput);

        const prepJob = (async function () {
          const isStale = function () {
            return String(currentInput.dataset.afbPrepToken || '') !== prepToken;
          };

          const selectedFile = currentInput.files && currentInput.files[0] ? currentInput.files[0] : null;

          if (isStale()) return;

          if (!selectedFile) {
            if (row.attr('data-afb-upload-error') === '1') {
              return;
            }
            clearRowError();
            $meta.text('');
            return;
          }

          if (acceptTokens.length && !afbFileMatchesUploadAccept(selectedFile, acceptTokens)) {
            currentInput.value = '';
            setRowError(acceptLabel ? `Type not allowed. Allowed: ${acceptLabel}` : 'Type not allowed.');
            return;
          }

          let finalFile = selectedFile;
          let compressed = false;
          let originalSize = Number(selectedFile.size || 0);
          const compressMaxWidth = afbNormalizeUploadMaxWidth($block.attr('data-compress-max-width') || '');
          const compressQuality = afbNormalizeUploadCompressionQuality($block.attr('data-compress-quality') || '');

          if (compressImages && afbCanCompressUploadImage(selectedFile)) {
            $meta.text('Optimizing image...');
            try {
              const optimized = await afbCompressImageFileForUpload(selectedFile, maxFileBytes, compressQuality, compressMaxWidth);
              if (isStale()) return;
              if (optimized && optimized.file) {
                finalFile = optimized.file;
                compressed = !!optimized.compressed;
                if (Number.isFinite(optimized.originalSize)) {
                  originalSize = Number(optimized.originalSize);
                }
              }
            } catch (_) {
              if (isStale()) return;
              finalFile = selectedFile;
            }
          }

          if (isStale()) return;

          if (acceptTokens.length && !afbFileMatchesUploadAccept(finalFile, acceptTokens)) {
            afbClearPreparedUploadFile(currentInput);
            currentInput.value = '';
            setRowError(acceptLabel ? `Type not allowed. Allowed: ${acceptLabel}` : 'Type not allowed.');
            return;
          }

          if (maxFileBytes > 0 && Number(finalFile.size || 0) > maxFileBytes) {
            afbClearPreparedUploadFile(currentInput);
            currentInput.value = '';
            setRowError(`Final size ${afbFormatUploadBytes(finalFile.size)} exceeds ${maxFileLabel} MB limit.`);
            return;
          }

          afbPreparedUploadFiles.set(currentInput, { file: finalFile });
          if (finalFile !== selectedFile) {
            afbTryReplaceInputFile(currentInput, finalFile);
          }

          clearRowError();
          if (compressed || Number(finalFile.size || 0) !== Number(originalSize || 0)) {
            $meta.text(afbFormatUploadCompressionSummary(originalSize, finalFile.size));
          } else {
            $meta.text(`Ready: ${afbFormatUploadBytes(finalFile.size)}`);
          }
        })();

        afbTrackUploadPrepJob(prepJob);
      });

      row.find('.remove-file-btn').on('click', () => {
        afbClearPreparedUploadFile(inputEl);
        row.remove();
      });
      $fileList.append(row);
    };

    $block.find('.add-file-btn').on('click', addFileRow);

    for (let i = 0; i < initialRows; i += 1) {
      addFileRow();
    }

    activeStep.$fields.append($block);
    fieldInputs[uploadId] = { element: $block, condition: normalizeCondition(conditions[uploadId] || {}), blockSubmit: submitBlockFields.includes(String(uploadId || '').trim()) };
    return;
  }

  // Paragraph blocks from admin
  if (field.type === 'paragraph') {
    const rawParagraph = String(field.text || '');
    const looksLikeHtml = /<[^>]+>/.test(rawParagraph);
    const paragraphHtml = looksLikeHtml
      ? rawParagraph
      : rawParagraph.replace(/\n/g, '<br>');

    const $para = $(`
      <div class="afb-paragraph-block" data-paragraph-id="${field.gid}">
        ${field.title ? `<h3>${field.title}</h3>` : ''}
        <div class="afb-paragraph-content">${paragraphHtml}</div>
      </div>
    `);

    $para.attr('data-field-id', field.gid);
    activeStep.$fields.append($para);
    fieldInputs[field.gid] = { element: $para, condition: normalizeCondition(conditions[field.gid] || {}), blockSubmit: submitBlockFields.includes(String(field.gid || '').trim()) };
    return;
  }

  const id = field.gid;
  const fieldName = (field.name || '').toLowerCase();

  const isAllowed = allowed.length ? allowed.includes(id) : true;
  if ((!isAllowed) || hidden.includes(id) || fieldName.includes('dashboard')) return;

  const labelText = labels[id] || field.name;
  const descText  = descriptions[id] || '';
  const type      = field.type;
  const textSyncTarget = type === 'text' ? String(customTextSyncFields[id] || '').trim() : '';
  const hasTextSyncTarget = type === 'text' && /^\d+$/.test(textSyncTarget) && textSyncTarget !== String(id);
  const visibleForField = Array.isArray(visibleEnumOptions[id])
    ? visibleEnumOptions[id].map((v) => String(v))
    : [];
  const useVisibleFilter = visibleForField.length > 0;
  const visibleSet = useVisibleFilter ? new Set(visibleForField) : null;
  const options   = (field.enum_options || []).filter((o) => {
    if (!o || o.enabled === false) return false;
    if (!visibleSet) return true;
    return visibleSet.has(String(o.gid));
  });
  const isReq     = required.includes(id);
  const isRO      = readonly.includes(id);
  const isPara    = paragraph.includes(id);
  const cond      = conditions[id];

  const $label = $(`<label class="block mb-2" data-field-id="${id}"></label>`);
  $label.append(`<span class="text-gray-700">${labelText}${isReq ? ' *' : ''}</span>`);
  if (descText) $label.append(`<p class="text-sm text-gray-500 mb-1">${descText}</p>`);

  let $input;
  if (type === 'text') {
    if (isPara) {
      $input = $(`<textarea ${hasTextSyncTarget ? '' : `name="custom_fields[${id}]"`} rows="4"
        class="mt-1 block w-full rounded border-gray-300 shadow-sm afb-paragraph-input"
        ${isReq ? 'required' : ''}></textarea>`);
    } else {
      const inputType = labelText.toLowerCase().includes('email') ? 'email' : 'text';
      $input = $(`<input type="${inputType}" ${hasTextSyncTarget ? '' : `name="custom_fields[${id}]"`}
        class="mt-1 block w-full rounded border-gray-300 shadow-sm"
        ${isReq ? 'required' : ''} />`);
    }

    if (hasTextSyncTarget) {
      $input.attr('data-afb-sync-target', textSyncTarget);
    }
  } else if (type === 'enum') {
    $input = $(`<select name="custom_fields[${id}]"
      class="mt-1 block w-full rounded border-gray-300 shadow-sm"
      ${isReq ? 'required' : ''}>
        <option value="">-- Select --</option>
      </select>`);
    options.forEach((opt) => $input.append(`<option value="${opt.gid}" data-afb-option-gid="${opt.gid}">${opt.name}</option>`));
  } else if (type === 'multi_enum') {
    $input = $('<div class="mt-2 flex flex-wrap gap-4" data-multienum="1"></div>');
    if (isReq) $input.attr('data-required', '1');
    options.forEach((opt) => {
      $input.append(`
        <label class="inline-flex items-center space-x-2" data-afb-option-gid="${opt.gid}">
          <input type="checkbox" name="custom_fields[${id}][]" value="${opt.gid}" class="rounded border-gray-300">
          <span>${opt.name}</span>
        </label>
      `);
    });
  } else if (type === 'number') {
    $input = $(`<input type="number" name="custom_fields[${id}]"
      class="mt-1 block w-full rounded border-gray-300 shadow-sm"
      ${isReq ? 'required' : ''} />`);
  } else if (type === 'date') {
    $input = $(`<input type="date" name="custom_fields[${id}]"
      class="mt-1 block w-full rounded border-gray-300 shadow-sm"
      ${isReq ? 'required' : ''} />`);
  } else {
    $input = $(`<input type="text" disabled value="Unsupported field type"
      class="mt-1 block w-full text-gray-400" />`);
  }

  if (isRO) {
    $input.prop('disabled', true).addClass('bg-gray-100');
    if ($input.is('div')) $input.find('input').prop('disabled', true);
  }

  $label.append($input);
  activeStep.$fields.append($label);
  const normCond = normalizeCondition(cond || {});
  fieldInputs[id] = { element: $label, condition: normCond, blockSubmit: submitBlockFields.includes(id) };

  const optionRule = normalizeOptionRule(optionVisibilityRules[id] || {});
  if ((type === 'enum' || type === 'multi_enum') && optionRule.depends_on) {
    if (type === 'enum') {
      $input.data('afb-base-required', !!isReq);
    }
    if (type === 'multi_enum') {
      $input.data('afb-base-required', !!isReq);
    }

    optionFilteredInputs[id] = {
      type,
      $input,
      rule: optionRule,
      isReadOnly: !!isRO,
      baseRequired: !!isReq,
    };
  }

});


            /* ---------- PREFILL IDENTITY FROM TRIAGE STATE ---------- */
            const globalMap = window.GlobalFieldMap || {};
            if (Object.keys(globalMap).length) {
              const snap = getTriageStateSnapshot();
              const ans  = snap.answers || {};
              const user = snap.user || snap.contact || snap.meta || {};

              Object.entries(globalMap).forEach(([key, fieldId]) => {
                const value = ans[key] || user[key] || snap[key] || '';
                if (!value) return;

                let $target = $();
                const $targetWrap = $form.find(`[data-field-id="${fieldId}"]`).first();
                if ($targetWrap.length) {
                  $target = $targetWrap
                    .find('input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), textarea, select')
                    .filter(':enabled')
                    .first();
                }

                if (!$target.length) {
                  $target = $form.find(`[name="custom_fields[${fieldId}]"]`).first();
                }

                if ($target.length) {
                  $target.val(value).trigger('change');
                }
              });
            }

            // Fallback: if mapping is missing or stale, fill visible identity fields by label/name.
            (function prefillIdentityFallbackByLabel() {
              const identity = getIdentityFromState();
              if (!identity.first_name && !identity.last_name && !identity.work_email) return;

              const setIfEmpty = ($el, value) => {
                if (!$el || !$el.length || !value) return;
                const cur = String($el.val() || '').trim();
                if (!cur) $el.val(value).trigger('change');
              };

              const $blocks = $container.find('label[data-field-id]');
              $blocks.each(function () {
                const $label = $(this);
                const txt = ($label.find('> span.text-gray-700').first().text() || '').toLowerCase();
                const $input = $label.find('input[type="text"], input[type="email"], textarea').first();
                if (!$input.length) return;

                if (/(^|\b)first\s*name(\b|$)/i.test(txt)) {
                  setIfEmpty($input, identity.first_name);
                  return;
                }
                if (/(^|\b)(last\s*name|surname)(\b|$)/i.test(txt)) {
                  setIfEmpty($input, identity.last_name);
                  return;
                }
                if (/(^|\b)(work\s*)?email(\b|$)/i.test(txt) || $input.attr('type') === 'email') {
                  setIfEmpty($input, identity.work_email);
                }
              });
            })();

           /* ---------- CONDITIONAL DISPLAY ---------- */

// Helper: does one rule match current source value(s)?
function afbMatchesConditionRule(current, rule = {}) {
  const expected = Array.isArray(rule.values) && rule.values.length
    ? rule.values.map((v) => String(v))
    : [];

  // If a rule exists but there are no expected values, treat it as not matched.
  if (expected.length === 0) return false;

  if (Array.isArray(current)) {
    return current.map((v) => String(v)).some((v) => expected.includes(v));
  }

  return expected.includes(String(current ?? ''));
}

function afbMatchesConditionSet(cond = {}) {
  const rules = Array.isArray(cond.rules) ? cond.rules : [];
  if (!rules.length) return true;

  const logic = cond.logic === 'any' ? 'any' : 'all';

  const ruleResults = rules.map((rule) => {
    const source = String((rule && rule.depends_on) || '').trim();
    if (!source) return true;

    const hasSource = $form.find(
      `[name="custom_fields[${source}]"], [name="custom_fields[${source}][]"], [data-field-id="${source}"]`
    ).length > 0;

    // Be resilient to stale config: if dependency source no longer exists,
    // do not hide the target.
    if (!hasSource) return true;

    const current = getCurrentFieldValue(source);
    return afbMatchesConditionRule(current, rule || {});
  });

  if (!ruleResults.length) return true;
  return logic === 'any' ? ruleResults.some(Boolean) : ruleResults.every(Boolean);
}

// Make show/hide robust: disable inputs when hidden so "required" won't block submit
function toggleField($wrap, show) {
  $wrap.toggle(!!show);
  const $inputs = $wrap.find('input, select, textarea');

  if (show) {
    $wrap.removeAttr('data-afb-condition-hidden');
    $inputs.each(function () {
      if ($(this).data('was-required')) this.required = true;
    });
    $inputs.prop('disabled', false);
  } else {
    $wrap.attr('data-afb-condition-hidden', '1');
    $inputs.each(function () {
      $(this).data('was-required', this.required);
      this.required = false;
    });
    $inputs.prop('disabled', true);
  }
}

let afbSyncWizardState = null;

// (re)computes show/hide for all fields with conditions
function evaluateConditions() {
  Object.entries(fieldInputs).forEach(([id, entry]) => {
    const el = entry.element;
    const cond = entry.condition || {};

    const show = afbMatchesConditionSet(cond);
    toggleField(el, show);
  });
}

function evaluateStepConditions() {
  steps.forEach((step) => {
    const cond = step.condition || {};
    const show = afbMatchesConditionSet(cond);
    const $panel = step.$panel;
    const $inputs = $panel.find('input, select, textarea');
    const wasHidden = $panel.attr('data-afb-step-condition-hidden') === '1';

    if (show) {
      if (wasHidden) {
        $inputs.each(function () {
          const $input = $(this);
          const prevDisabled = $input.data('afb-step-prev-disabled');
          const prevRequired = $input.data('afb-step-prev-required');

          if (typeof prevDisabled !== 'undefined') this.disabled = !!prevDisabled;
          if (typeof prevRequired !== 'undefined') this.required = !!prevRequired;

          $input.removeData('afb-step-prev-disabled');
          $input.removeData('afb-step-prev-required');
        });
      }

      $panel.removeAttr('data-afb-step-condition-hidden');
      return;
    }

    if (!wasHidden) {
      $inputs.each(function () {
        const $input = $(this);
        $input.data('afb-step-prev-disabled', this.disabled);
        $input.data('afb-step-prev-required', this.required);
      });
    }

    $panel.attr('data-afb-step-condition-hidden', '1');
    $inputs.each(function () {
      this.required = false;
      this.disabled = true;
    });
  });

  // Safety fallback: never let all steps disappear due to invalid conditions.
  if (!getVisibleStepIndexes().length && steps.length) {
    const fallbackStep = steps[0];
    const $panel = fallbackStep.$panel;
    $panel.removeAttr('data-afb-step-condition-hidden');
    $panel.find('input, select, textarea').each(function () {
      const $input = $(this);
      const prevDisabled = $input.data('afb-step-prev-disabled');
      const prevRequired = $input.data('afb-step-prev-required');

      if (typeof prevDisabled !== 'undefined') this.disabled = !!prevDisabled;
      else this.disabled = false;

      if (typeof prevRequired !== 'undefined') this.required = !!prevRequired;

      $input.removeData('afb-step-prev-disabled');
      $input.removeData('afb-step-prev-required');
    });
  }
}

function getVisibleStepIndexes() {
  return steps
    .map((step, i) => ({ step, i }))
    .filter(({ step }) => step.$panel.attr('data-afb-step-condition-hidden') !== '1')
    .map(({ i }) => i);
}

function getActiveSubmissionBlockFields() {
  return Object.entries(fieldInputs)
    .filter(([, entry]) => {
      if (!entry || !entry.blockSubmit || !entry.element || !entry.element.length) return false;
      return entry.element.attr('data-afb-condition-hidden') !== '1';
    })
    .map(([fieldId, entry]) => ({
      fieldId,
      entry,
    }));
}

const submitBlockStatusPrefix = 'Submission blocked:';

function clearSubmitBlockStatus() {
  const current = String($statusText.text() || '').trim();
  if (!current.startsWith(submitBlockStatusPrefix)) {
    return;
  }

  $statusText.text('');
  $statusText.removeClass('text-red-600').addClass('text-blue-600');
}

function syncSubmitBlockState() {
  if (inFlight) return false;

  const blockingFields = getActiveSubmissionBlockFields();
  const shouldBlockSubmit = blockingFields.length > 0;
  const canInteractWithSubmit = $submitButton.is(':visible');

  if (!shouldBlockSubmit || !canInteractWithSubmit) {
    $submitButton
      .prop('disabled', false)
      .removeAttr('aria-disabled')
      .removeClass('opacity-60 cursor-not-allowed')
      .text(defaultSubmitButtonText);
    clearSubmitBlockStatus();
    return false;
  }

  const fieldTitles = blockingFields
    .map(({ entry, fieldId }) => {
      const $label = entry.element.find('> span, .afb-upload-heading, .afb-item-title, h3, p.font-semibold').first();
      const text = String($label.text() || '').trim();
      return text || fieldId;
    })
    .filter(Boolean);
  const blockerLabel = fieldTitles.length ? ` (${fieldTitles.join(', ')})` : '';
  const message = `${submitBlockStatusPrefix} this request cannot be submitted while the current answers match a blocked field${blockerLabel}.`;

  $submitButton
    .prop('disabled', true)
    .attr('aria-disabled', 'true')
    .addClass('opacity-60 cursor-not-allowed')
    .text('Submission blocked');

  $statusText
    .removeClass('text-blue-600')
    .addClass('text-red-600')
    .text(message);

  return true;
}

afbSyncSubmitBlockState = syncSubmitBlockState;

function getCurrentFieldValue(fieldGid) {
  if (!fieldGid) return '';

  const $wrap = $form.find(`[data-field-id="${fieldGid}"]`).first();
  if ($wrap.length) {
    const $controls = $wrap
      .find('input, select, textarea')
      .filter(':enabled')
      .filter(function () {
        const el = this;
        const type = String(el.type || '').toLowerCase();
        return type !== 'hidden';
      });

    if ($controls.length) {
      const $checks = $controls.filter(':checkbox, :radio');
      if ($checks.length) {
        return $checks.filter(':checked').map((_, el) => String(el.value || '')).get();
      }

      const $first = $controls.first();
      if ($first.is('select[multiple]')) {
        const vals = $first.val();
        return Array.isArray(vals) ? vals.map((v) => String(v || '')) : [];
      }

      return String($first.val() || '');
    }
  }

  const $dep = $form.find(
    `[name="custom_fields[${fieldGid}]"], [name="custom_fields[${fieldGid}][]"]`
  ).filter(':enabled');

  if (!$dep.length) return '';

  if ($dep.first().is(':checkbox')) {
    return $dep.filter(':checked').map((_, el) => String(el.value || '')).get();
  }

  return String($dep.first().val() || '');
}

function applyOptionVisibility(targetGid) {
  const meta = optionFilteredInputs[targetGid];
  if (!meta || !meta.rule || !meta.rule.depends_on) return;

  const sourceCurrent = getCurrentFieldValue(meta.rule.depends_on);
  const selectedSourceValues = Array.isArray(sourceCurrent)
    ? sourceCurrent.map((v) => String(v || '').trim()).filter(Boolean)
    : [String(sourceCurrent || '').trim()].filter(Boolean);

  const allowed = new Set();
  selectedSourceValues.forEach((sourceVal) => {
    const vals = Array.isArray(meta.rule.map[sourceVal]) ? meta.rule.map[sourceVal] : [];
    vals.forEach((v) => allowed.add(String(v || '').trim()));
  });

  const hasSourceSelection = selectedSourceValues.length > 0;
  const hasAllowedOptions = allowed.size > 0;
  const $fieldWrap = meta.$input.closest('[data-field-id]');
  const hiddenByCondition = $fieldWrap.attr('data-afb-condition-hidden') === '1';
  const hiddenByStepCondition = $fieldWrap.closest('.afb-step-panel').attr('data-afb-step-condition-hidden') === '1';
  const shouldEnable = hasSourceSelection && hasAllowedOptions && !meta.isReadOnly && !hiddenByCondition && !hiddenByStepCondition;

  if (meta.type === 'enum') {
    const $select = meta.$input;
    const $placeholder = $select.find('option[value=""]').first();

    $select.find('option[data-afb-option-gid]').each(function () {
      const $opt = $(this);
      const gid = String($opt.data('afb-option-gid') || '').trim();
      const isAllowed = allowed.has(gid);
      $opt.prop('disabled', !isAllowed);
      $opt.prop('hidden', !isAllowed);
    });

    const currentValue = String($select.val() || '').trim();
    if (currentValue && !allowed.has(currentValue)) {
      $select.val('');
    }

    if ($placeholder.length) {
      if (!hasSourceSelection) {
        $placeholder.text('-- Select previous field first --');
      } else if (!hasAllowedOptions) {
        $placeholder.text('-- No matching options --');
      } else {
        $placeholder.text('-- Select --');
      }
    }

    $select.prop('disabled', !shouldEnable);
    $select.prop('required', shouldEnable && !!meta.baseRequired);
    return;
  }

  if (meta.type === 'multi_enum') {
    const $group = meta.$input;
    let enabledCount = 0;

    $group.find('[data-afb-option-gid]').each(function () {
      const $optWrap = $(this);
      const gid = String($optWrap.data('afb-option-gid') || '').trim();
      const isAllowed = allowed.has(gid);
      const $checkbox = $optWrap.find('input[type="checkbox"]').first();

      if (!isAllowed) {
        $checkbox.prop('checked', false).prop('disabled', true);
        $optWrap.hide();
        return;
      }

      $optWrap.show();
      $checkbox.prop('disabled', !shouldEnable);
      if (shouldEnable) enabledCount += 1;
    });

    if (meta.baseRequired && shouldEnable && enabledCount > 0) {
      $group.attr('data-required', '1');
    } else {
      $group.removeAttr('data-required');
    }
  }
}

function evaluateOptionVisibility() {
  Object.keys(optionFilteredInputs).forEach((targetGid) => {
    applyOptionVisibility(targetGid);
  });
}

function evaluateDynamicVisibility() {
  evaluateConditions();
  evaluateStepConditions();
  evaluateOptionVisibility();
  if (typeof afbSyncWizardState === 'function') {
    afbSyncWizardState();
  }
  afbSyncSubmitBlockState();
}

$form.on('change', 'input, select, textarea', evaluateDynamicVisibility);
evaluateDynamicVisibility();

$form.on('click', '[data-afb-ai-rewrite="1"]', async function (e) {
  e.preventDefault();

  const $btn = $(this);
  if ($btn.data('busy')) return;

  const $fieldWrap = $btn.closest('label[data-field-id]');
  if (!$fieldWrap.length) return;

  const $input = $fieldWrap.find('input[type="text"], textarea').first();
  if (!$input.length || $input.prop('disabled')) return;

  const $note = $fieldWrap.find('.afb-single-ai-note').first();
  const currentText = String($input.val() || '').trim();
  if (!currentText) {
    if ($note.length) {
      $note.removeClass('text-slate-500 text-green-600').addClass('text-red-600').text('Type some text first, then run AI rewrite.');
    }
    return;
  }

  const ajaxUrl = (window.afb_ajax && afb_ajax.ajax_url) ? String(afb_ajax.ajax_url) : '';
  const nonce = (window.afb_ajax && afb_ajax.ai_rewrite_nonce) ? String(afb_ajax.ai_rewrite_nonce) : '';
  const action = (window.afb_ajax && afb_ajax.ai_rewrite_action) ? String(afb_ajax.ai_rewrite_action) : 'afb_rewrite_single_input_copy';
  if (!ajaxUrl || !nonce) {
    if ($note.length) {
      $note.removeClass('text-slate-500 text-green-600').addClass('text-red-600').text('AI rewrite is unavailable right now.');
    }
    return;
  }

  const mode = String($btn.attr('data-afb-ai-mode') || 'textarea').trim() === 'text' ? 'text' : 'textarea';
  const maxWords = String($btn.attr('data-afb-ai-max-words') || '').trim();
  const $preset = $fieldWrap.find('[data-afb-ai-preset="1"]').first();
  const preset = String($preset.val() || 'improve').trim().toLowerCase();

  const beforeLabel = String($btn.text() || 'Rewrite with AI');
  $btn.data('busy', true).prop('disabled', true).text('Rewriting...');
  if ($preset.length) {
    $preset.prop('disabled', true);
  }
  if ($note.length) {
    $note.removeClass('text-red-600 text-green-600').addClass('text-slate-500').text('AI is rewriting your text...');
  }

  try {
    const body = new FormData();
    body.append('action', action);
    body.append('nonce', nonce);
    body.append('text', currentText);
    body.append('mode', mode);
    body.append('preset', preset);
    if (maxWords) {
      body.append('max_words', maxWords);
    }

    const resp = await fetch(ajaxUrl, {
      method: 'POST',
      credentials: 'same-origin',
      body,
    });

    let payload = null;
    try {
      payload = await resp.json();
    } catch (_jsonErr) {
      payload = null;
    }

    if (!resp.ok || !payload || !payload.success) {
      const msg = payload && payload.data && payload.data.message
        ? String(payload.data.message)
        : 'AI rewrite failed. Please try again.';
      throw new Error(msg);
    }

    const rewritten = payload.data && typeof payload.data.rewritten === 'string'
      ? payload.data.rewritten.trim()
      : '';
    const parsedWordCount = Number(payload && payload.data ? payload.data.word_count : 0);
    const finalWordCount = Number.isFinite(parsedWordCount) ? Math.max(0, Math.floor(parsedWordCount)) : 0;
    const parsedMaxWords = Number(payload && payload.data ? payload.data.max_words : 0);
    const finalMaxWords = Number.isFinite(parsedMaxWords) ? Math.max(0, Math.floor(parsedMaxWords)) : 0;
    const limitApplied = Boolean(payload && payload.data && payload.data.limit_applied);
    const usedHardTrim = Boolean(payload && payload.data && payload.data.used_hard_trim);

    if (!rewritten) {
      throw new Error('AI returned empty text. Please try again.');
    }

    $input.val(rewritten).trigger('input').trigger('change');
    if ($note.length) {
      let successMessage = 'AI rewrite applied. Review and edit as needed.';
      if (limitApplied && finalMaxWords > 0 && finalWordCount > 0) {
        successMessage = `AI rewrite applied (${finalWordCount}/${finalMaxWords} words).`;
        if (usedHardTrim) {
          successMessage += ' Final trim was used to fit the limit.';
        }
      }
      $note.removeClass('text-slate-500 text-red-600').addClass('text-green-600').text(successMessage);
    }
  } catch (err) {
    const msg = (err && err.message) ? String(err.message) : 'AI rewrite failed. Please try again.';
    if ($note.length) {
      $note.removeClass('text-slate-500 text-green-600').addClass('text-red-600').text(msg);
    }
  } finally {
    $btn.data('busy', false).prop('disabled', false).text(beforeLabel);
    if ($preset.length) {
      $preset.prop('disabled', false);
    }
  }
});

            $form.find('.afb-step-nav').remove();

            const validateStep = function (index) {
              const step = steps[index];
              if (!step) return true;

              const controls = step.$panel.find('input, select, textarea').filter(':enabled:visible');
              for (let i = 0; i < controls.length; i++) {
                const el = controls[i];
                if (typeof el.checkValidity === 'function' && !el.checkValidity()) {
                  el.reportValidity();
                  return false;
                }
              }

              const $requiredGroup = step.$panel.find('div[data-multienum="1"][data-required="1"]').filter(':visible').filter(function () {
                const $enabled = $(this).find('input[type="checkbox"]:enabled');
                if (!$enabled.length) return false;
                return $enabled.filter(':checked').length === 0;
              }).first();

              if ($requiredGroup.length) {
                const labelText = $requiredGroup.closest('label').find('> span.text-gray-700').text().replace(/\s*\*$/, '');
                const msg = `Please select at least one option for "${labelText || 'this field'}".`;
                $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${msg}`);
                return false;
              }

              const repeaterValidation = validateRepeaterBlocks(step.$panel);
              if (!repeaterValidation.valid) {
                $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${repeaterValidation.message}`);
                if (repeaterValidation.$block && repeaterValidation.$block.length) {
                  try {
                    repeaterValidation.$block[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
                  } catch (_) {}
                }
                return false;
              }

              if (!afbValidateUploadBlocks(step.$panel, $statusText, $progressBarContainer, $submitButton)) {
                return false;
              }

              return true;
            };

            if (steps.length > 1) {
              const $stepNav = $(`
                <div class="afb-step-nav mb-3 flex items-center justify-between gap-2">
                  <button type="button" class="afb-step-back px-3 py-2 rounded border border-slate-300 bg-white text-slate-700 text-sm">Back</button>
                  <button type="button" class="afb-step-next px-3 py-2 rounded bg-slate-900 text-white text-sm">Next</button>
                </div>
              `);

              $form.find('.header-and-submit').prepend($stepNav);

              const $back = $stepNav.find('.afb-step-back');
              const $next = $stepNav.find('.afb-step-next');
              let activeIndex = 0;

              const renderStepper = function (index, visibleIndexes) {
                const visible = Array.isArray(visibleIndexes) ? visibleIndexes : [];
                if (visible.length <= 1) {
                  $stepper.addClass('hidden').empty();
                  return;
                }

                const activePos = visible.indexOf(index);
                $stepper.removeClass('hidden').empty();
                visible.forEach((stepIndex, orderIndex) => {
                  const step = steps[stepIndex];
                  const activeClass = stepIndex === index ? 'is-active' : '';
                  const doneClass = activePos > orderIndex ? 'is-done' : '';
                  const stepTitle = (step && step.title) || ('Step ' + (orderIndex + 1));
                  const $pill = $(`
                    <button type="button" class="afb-step-pill ${activeClass} ${doneClass}" data-step="${stepIndex}">
                      <span class="afb-step-pill-index">${orderIndex + 1}</span>
                      <span class="afb-step-pill-title">${stepTitle}</span>
                    </button>
                  `);
                  $stepper.append($pill);
                });
              };

              const setActiveStep = function (nextIndex) {
                const visibleIndexes = getVisibleStepIndexes();
                if (!visibleIndexes.length) {
                  steps.forEach((step) => step.$panel.hide());
                  $back.hide();
                  $next.hide();
                  $submitButton.hide();
                  $stepNav.hide();
                  $stepper.addClass('hidden').empty();
                  return;
                }

                let resolvedIndex = Number(nextIndex);
                if (!Number.isFinite(resolvedIndex)) {
                  resolvedIndex = visibleIndexes[0];
                }

                if (!visibleIndexes.includes(resolvedIndex)) {
                  const nextVisible = visibleIndexes.find((i) => i >= resolvedIndex);
                  resolvedIndex = typeof nextVisible === 'number'
                    ? nextVisible
                    : visibleIndexes[visibleIndexes.length - 1];
                }

                activeIndex = resolvedIndex;
                steps.forEach((step, i) => {
                  const shouldShow = i === activeIndex && visibleIndexes.includes(i);
                  step.$panel.toggle(shouldShow);
                });

                const activePos = visibleIndexes.indexOf(activeIndex);
                const hasMultipleVisible = visibleIndexes.length > 1;

                $stepNav.toggle(hasMultipleVisible);
                $back.toggle(hasMultipleVisible && activePos > 0);
                $next.toggle(hasMultipleVisible && activePos < visibleIndexes.length - 1);
                $submitButton.toggle(activePos === visibleIndexes.length - 1);
                renderStepper(activeIndex, visibleIndexes);
                afbSyncSubmitBlockState();
              };

              const syncWizardState = function () {
                const visibleIndexes = getVisibleStepIndexes();
                if (!visibleIndexes.length) {
                  setActiveStep(-1);
                  return;
                }

                if (visibleIndexes.includes(activeIndex)) {
                  setActiveStep(activeIndex);
                  return;
                }

                const firstAfter = visibleIndexes.find((i) => i > activeIndex);
                const fallback = typeof firstAfter === 'number'
                  ? firstAfter
                  : visibleIndexes[visibleIndexes.length - 1];
                setActiveStep(fallback);
              };

              $back.on('click', function () {
                const visibleIndexes = getVisibleStepIndexes();
                const activePos = visibleIndexes.indexOf(activeIndex);
                if (activePos <= 0) return;
                setActiveStep(visibleIndexes[activePos - 1]);
              });

              $next.on('click', function () {
                if (!validateStep(activeIndex)) return;
                const visibleIndexes = getVisibleStepIndexes();
                const activePos = visibleIndexes.indexOf(activeIndex);
                if (activePos === -1 || activePos >= visibleIndexes.length - 1) return;
                setActiveStep(visibleIndexes[activePos + 1]);
              });

              $stepper.on('click', '.afb-step-pill', function () {
                const i = Number($(this).data('step'));
                if (Number.isNaN(i)) return;
                const visibleIndexes = getVisibleStepIndexes();
                const activePos = visibleIndexes.indexOf(activeIndex);
                const targetPos = visibleIndexes.indexOf(i);
                if (targetPos === -1) return;
                if (targetPos > activePos && !validateStep(activeIndex)) return;
                setActiveStep(i);
              });

              afbSyncWizardState = syncWizardState;
              setActiveStep(0);
              afbWizard = {
                validateAll: function () {
                  const visibleIndexes = getVisibleStepIndexes();
                  for (let i = 0; i < visibleIndexes.length; i++) {
                    const stepIndex = visibleIndexes[i];
                    if (!validateStep(stepIndex)) {
                      setActiveStep(stepIndex);
                      return false;
                    }
                  }
                  return true;
                }
              };
            } else {
              afbWizard = null;
              afbSyncWizardState = null;
              $stepper.addClass('hidden').empty();
              steps.forEach((step) => step.$panel.show());
              $submitButton.show();
              afbSyncSubmitBlockState();
            }

            $form.find('.header-and-submit').removeClass('hidden');
          }
        );
      }

      /* ---------- FORM SUBMIT (phased, modular) ---------- */
      $form.on('submit', async function (e) {
        e.preventDefault();
        if (inFlight) return;
        if (afbSyncSubmitBlockState()) return;
        if (afbWizard && typeof afbWizard.validateAll === 'function' && !afbWizard.validateAll()) return;

        if (afbPendingUploadPrepJobs.size) {
          $statusText.removeClass('text-red-600').addClass('text-blue-600').text('Preparing uploads...');
          $progressBarContainer.removeClass('hidden');
        }
        await afbWaitForUploadPrepJobs();

        if (!afbValidateUploadBlocks($form, $statusText, $progressBarContainer, $submitButton)) return;
        inFlight = true;
        setCompactSubmitView(true);

        const hasUploadBlocks = $form.find('.afb-upload-block').length > 0;

        // Reset UI
        $statusText.removeClass('text-red-600').addClass('text-blue-600');
        $progressBarContainer.removeClass('hidden');
        $errorDetails.addClass('hidden').empty();
        $statusText.text(hasUploadBlocks ? 'Starting…' : 'Submitting…');
        $progressBar.removeClass('afb-progress-bar--indeterminate').css('width', '0%');

        // Button busy state
        $submitButton
          .prop('disabled', true)
          .attr({ 'aria-busy': 'true', 'aria-disabled': 'true' })
          .addClass('opacity-60 cursor-not-allowed')
          .html(`<span class="flex items-center space-x-3">
            <svg class="animate-spin h-5 w-5 text-white" viewBox="0 0 24 24">
              <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
              <path class="opacity-75" d="M4 12a8 8 0 018-8v8H4z" fill="currentColor"></path>
            </svg><span class="ml-3">Processing your request…</span></span>`);

        // Use request number as task name (fallback/default naming strategy)
        $form.find('input[name="task_name"]').val(
          $form.find('input[name="job_number"]').val() || ''
        );

        // Required multi-enum validation
        const $firstEmptyGroup = $form.find('div[data-multienum][data-required="1"]').filter(function () {
          const $enabled = $(this).find('input[type="checkbox"]:enabled');
          if (!$enabled.length) return false;
          return $enabled.filter(':checked').length === 0;
        }).first();
        if ($firstEmptyGroup.length) {
          const labelText = $firstEmptyGroup.closest('label').find('> span.text-gray-700').text().replace(/\s*\*$/, '');
          const msg = `Please select at least one option for "${labelText || 'this field'}".`;
          $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${msg}`);
          $progressBarContainer.addClass('hidden');
          $submitButton.prop('disabled', false)
            .removeAttr('aria-busy aria-disabled')
            .removeClass('opacity-60 cursor-not-allowed')
            .text(defaultSubmitButtonText);
          inFlight = false;
          return;
        }

        const repeaterValidation = validateRepeaterBlocks($form);
        if (!repeaterValidation.valid) {
          const msg = repeaterValidation.message || 'Please complete the required repeater rows.';
          $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${msg}`);
          $progressBarContainer.addClass('hidden');
          $submitButton.prop('disabled', false)
            .removeAttr('aria-busy aria-disabled')
            .removeClass('opacity-60 cursor-not-allowed')
            .text(defaultSubmitButtonText);
          inFlight = false;

          if (repeaterValidation.$block && repeaterValidation.$block.length) {
            try {
              repeaterValidation.$block[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
            } catch (_) {}
          }

          return;
        }

        const $firstMissingAgreement = $form.find('input[data-afb-agreement="1"][required]').filter(':enabled:visible').filter(function () {
          return !this.checked;
        }).first();
        if ($firstMissingAgreement.length) {
          const agreementText = $firstMissingAgreement.closest('label').find('span').last().text().trim();
          const msg = agreementText
            ? `Please confirm: "${agreementText}".`
            : 'Please confirm the required agreement checkbox.';
          $statusText.removeClass('text-blue-600').addClass('text-red-600').text(`❌ ${msg}`);
          $progressBarContainer.addClass('hidden');
          $submitButton.prop('disabled', false)
            .removeAttr('aria-busy aria-disabled')
            .removeClass('opacity-60 cursor-not-allowed')
            .text(defaultSubmitButtonText);
          inFlight = false;
          return;
        }

        // Resolve submitter identity field GIDs from the admin Global Field Map.
        const gidMap = (() => {
          const out = { first_name: '', last_name: '', work_email: '' };
          const buckets = { first_name: [], last_name: [], work_email: [], email: [] };
          const mapRoot = (typeof GlobalFieldMap === 'object' && GlobalFieldMap) ? GlobalFieldMap : {};

          const walk = (node) => {
            if (!node || typeof node !== 'object') return;
            Object.keys(node).forEach((key) => {
              const value = node[key];
              if (value && typeof value === 'object') {
                walk(value);
                return;
              }

              if (!['first_name', 'last_name', 'work_email', 'email'].includes(String(key))) {
                return;
              }

              const gid = String(value || '').replace(/\D+/g, '');
              if (!gid) return;
              buckets[String(key)].push(gid);
            });
          };

          walk(mapRoot);

          const pickFirst = (list) => {
            for (let i = 0; i < list.length; i += 1) {
              const gid = String(list[i] || '').trim();
              if (gid) return gid;
            }
            return '';
          };

          out.first_name = pickFirst(buckets.first_name);
          out.last_name = pickFirst(buckets.last_name);
          out.work_email = pickFirst(buckets.work_email) || pickFirst(buckets.email);
          return out;
        })();

        // Triage state for submitter + answers
        const triageState       = getTriageStateSnapshot();
        const triageUser        = triageState.user || triageState.contact || triageState.meta || {};
        const triageAnswersSend = triageState.answers || {};

        const fName = (
          $form.find(`input[name="custom_fields[${gidMap.first_name}]"]`).val() ||
          triageUser.first_name ||
          triageAnswersSend.first_name ||
          ''
        ).trim();

        const lName = (
          $form.find(`input[name="custom_fields[${gidMap.last_name}]"]`).val() ||
          triageUser.last_name ||
          triageAnswersSend.last_name ||
          ''
        ).trim();

        const full = (fName + ' ' + lName).trim() || fName || '';

        const email = (
          $form.find(`input[name="custom_fields[${gidMap.work_email}]"]`).val() ||
          triageUser.email ||
          triageUser.work_email ||
          triageAnswersSend.work_email ||
          $form.find('input[type="email"]').val() ||
          ''
        ).trim();

        $form.find('#submitter_name').val(full);
        $form.find('#submitter_email').val(email);

        // Build sanitized triage summary
        function sanitizeSummary(html, first) {
          let out = String(html || '');
          const g1 = /^\uFEFF?(?:\s|&nbsp;)*(?:<(?:p|div)[^>]*>)?\s*(?:Thanks(?:,|\s+(?:there|team|all|\w+))?|Hi|Hello|Dear)\b.*?(?:<\/(?:p|div)>|<br\s*\/?>)?\s*/i;
          out = out.replace(g1, '');
          if (first) {
            out = out.replace(
              new RegExp(
                '^\\uFEFF?(?:\\s|&nbsp;)*(?:<(?:p|div)[^>]*>)?\\s*Thanks\\s*,?\\s*' +
                  first.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +
                  '.*?(?:<\\/(?:p|div)>|<br\\s*\\/?>)?\\s*',
                'i'
              ),
              ''
            );
          }
          out = out.replace(/<h[1-6][^>]*>\s*Triage\s+Summary\s*<\/h[1-6]>\s*/gi, '');
          out = out.replace(/^\uFEFF?(?:\s|&nbsp;)*Triage\s+Summary\s*[:\-]?\s*/i, '');
          out = out.replace(/^(?:\s|&nbsp;|<br\s*\/?>)+/i, '').replace(/(?:\s*<br\s*\/?>\s*){3,}/gi, '<br><br>');
          return out.trim();
        }

        let triageHTML = '';
        try {
          triageHTML = window.AFB_TriageSummary?.buildHTML?.() || '';
        } catch (_) {}
        if (!triageHTML) {
          if (triageAnswersSend.triage_summary_html) {
            triageHTML = triageAnswersSend.triage_summary_html;
          } else if (triageAnswersSend.triage_summary) {
            triageHTML = String(triageAnswersSend.triage_summary).replace(/\n/g, '<br>');
          }
        }
        const sanitizedSummary = sanitizeSummary(triageHTML, fName);

        // Collect files (optional)
        const uploads = afbGetSelectedFiles($form);
        const uploadCount = uploads.length;

        // Collect custom fields
        const customFieldsPayload = collectCustomFields($form);
        const descriptionInputsPayload = collectDescriptionInputs($form);
        const agreementChecksPayload = collectAgreementChecks($form);
        const dueDatePayload = collectDueDateInput($form);
        const repeaterRowsPayload = collectRepeaterRows($form);
        const taskNameFieldValuesPayload = collectTaskNameFieldValues($form);

        // Snapshot triage_answers for the server
        const triageAnswersPayload = (triageState.answers && typeof triageState.answers === 'object')
          ? triageState.answers
          : {};

        // Submitted copy from answers (no sessionStorage)
        const submittedCopyHtml =
          triageAnswersPayload.submitted_copy_html ||
          '';
        const submittedCopyPlain =
          triageAnswersPayload.submitted_copy_plain ||
          '';

        // ✅ Notes textarea (HTML notes → Asana task description)
        const notesHtml = ($form.find('[name="asana_html_notes"]').val() || '').trim();


        try {
          // Phase 1: CREATE
          if (!window.afbFlowCreate?.afbCreateTaskFast) throw new Error('Create module not loaded');
          const requestId = pendingSubmissionRequestId || (
            window.afbFlowCreate?.afbMakeRequestId
              ? window.afbFlowCreate.afbMakeRequestId()
              : `cli_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`
          );
          pendingSubmissionRequestId = requestId;

          $statusText.text('Creating task…');
          $progressBar.addClass('afb-progress-bar--indeterminate');

          let createData = null;
          try {
            createData = await window.afbFlowCreate.afbCreateTaskFast({
              ajaxUrl: afb_ajax.ajax_url,
              projectId: $form.find('.project-picker').val(),
              taskName:  $form.find('input[name="task_name"]').val() || '',
              uploadCount,
              requestId,
              payload: {
                custom_fields:       JSON.stringify(customFieldsPayload),
                job_number:          $form.find('input[name="job_number"]').val(), // send IC-...
                ai_summary:          sanitizedSummary,
                asana_html_notes: notesHtml,
                date: dueDatePayload.value,
                due_date_field_id: dueDatePayload.fieldId || '',
                description_inputs: descriptionInputsPayload.length
                  ? JSON.stringify(descriptionInputsPayload)
                  : '',
                agreement_checks: Object.keys(agreementChecksPayload).length
                  ? JSON.stringify(agreementChecksPayload)
                  : '',
                repeater_rows: repeaterRowsPayload.length
                  ? JSON.stringify(repeaterRowsPayload)
                  : '',
                task_name_field_values: Object.keys(taskNameFieldValuesPayload).length
                  ? JSON.stringify(taskNameFieldValuesPayload)
                  : '',

                // structured triage state → PHP
                triage_state:   Object.keys(triageState).length
                                  ? JSON.stringify(triageState)
                                  : '',
                triage_answers: Object.keys(triageAnswersPayload).length
                                  ? JSON.stringify(triageAnswersPayload)
                                  : '',

                submitted_copy_html:  submittedCopyHtml,
                submitted_copy_plain: submittedCopyPlain,
                submitter_email:      email,
                submitter_name:       full,
                workspace_gid:        '9536715027185'
              }
            });
          } catch (createErr) {
            const fallbackProbe = String((createErr && createErr.message) || '');
            const isRecoverable = !!(
              requestId &&
              ((createErr && createErr.isTransient) || /timed\s*out|timeout|gateway|connection/i.test(fallbackProbe))
            );

            if (!isRecoverable || !window.afbFlowStatus?.afbWaitForSubmissionReady) {
              throw createErr;
            }

            $statusText.text('Connection was slow. Verifying submission…');
            $progressBar.addClass('afb-progress-bar--indeterminate');

            try {
              const recoveredStatus = await window.afbFlowStatus.afbWaitForSubmissionReady({
                ajaxUrl: afb_ajax.ajax_url,
                requestId,
                intervalMS: 1200,
                timeoutMS: 45000
              });

              createData = {
                request_id: requestId,
                task_gid: String(recoveredStatus.task_gid || ''),
                gateway_creation_mode: recoveredStatus.gateway_creation_mode || ''
              };
            } catch (_) {
              throw createErr;
            }
          }

          const { request_id, task_gid } = createData;
          const isAutomaticGatewayCreation = createData.gateway_creation_mode === 'automatic';

          // Phase 2: UPLOAD (packetised)
          if (uploadCount > 0) {
            if (!window.afbFlowUpload?.afbUploadPacket) throw new Error('Upload module not loaded');
            let done = 0;
            const uploadFailures = [];
            $statusText.text(`Uploading files… 0/${uploadCount}`);
            $progressBar.removeClass('afb-progress-bar--indeterminate').css('width', '1%');

            for (const uploadPacket of uploads) {
              const file = uploadPacket && uploadPacket.file ? uploadPacket.file : null;
              if (!file) {
                done++;
                continue;
              }

              try {
                await window.afbFlowUpload.afbUploadPacket({
                  ajaxUrl: afb_ajax.ajax_url,
                  requestId: request_id,
                  taskGid: task_gid,
                  file,
                  uploadSig: uploadPacket.uploadSig || '',
                  uploadFieldId: uploadPacket.uploadFieldId || ''
                });
              } catch (uploadErr) {
                const label = file && file.name ? file.name : 'Unknown file';
                const detail = uploadErr && uploadErr.message ? uploadErr.message : 'Upload failed';
                uploadFailures.push(`${label}: ${detail}`);
              }
              done++;
              const pct = Math.max(5, Math.round((done / uploadCount) * 100));
              $statusText.text(`Uploading files… ${done}/${uploadCount}`);
              $progressBar.css('width', `${pct}%`);
            }

            if (uploadFailures.length) {
              throw new Error(`Some uploads failed and were not sent to Asana: ${uploadFailures.join(' | ')}`);
            }
          }

          // Phase 2.5: explicitly trigger enrichment now
          try {
            const fd = new FormData();
            fd.append('action', 'afb_enrich_now');
            fd.append('request_id', request_id);
            await fetch(afb_ajax.ajax_url, { method: 'POST', body: fd });
          } catch (_) {}

          // Phase 3: STATUS (poll until done)
          if (!window.afbFlowStatus?.afbPollStatus) throw new Error('Status module not loaded');
          await window.afbFlowStatus.afbPollStatus({
            ajaxUrl: afb_ajax.ajax_url,
            requestId: request_id,
            onTick: (st) => {
              if (st.phase === 'enrich') {
                $statusText.text(isAutomaticGatewayCreation ? 'Creating client gateway…' : 'Finalising…');
                $progressBar.addClass('afb-progress-bar--indeterminate');
              }
              if (st.phase === 'done')   { $statusText.text('Completed.'); }
              if (st.phase === 'error')  { $statusText.removeClass('text-blue-600').addClass('text-red-600').text('❌ A server error occurred.'); }
            },
            intervalMS: 1000,
            timeoutMS: 180000
          });

          // Success UI
          $progressBar.removeClass('afb-progress-bar--indeterminate').css('width', '100%');
          setTimeout(() => $progressBarContainer.addClass('hidden'), 300);
          $form.closest('.asana-form-wrapper').hide();
          $form.closest('.asana-form-container').find('.asana-title-wrap').hide();
          pendingSubmissionRequestId = '';

          /* ────────────────  PURGE triage cache for this tab ──────────────── */
          try {
            window.__triage?.clear?.();
            if (window.afb_ajax && window.afb_ajax.triage_state) {
              delete window.afb_ajax.triage_state;
            }
            if (window.sessionStorage) {
              try { sessionStorage.clear(); } catch (_) {}
            }
          } catch (_) { /* no-op */ }

          /* ----- wipe triage state on the server too ----- */
          try {
            const ajaxUrl =
              (window.triageAjax && triageAjax.ajax_url) ||
              (window.afb_ajax && afb_ajax.ajax_url);

            if (ajaxUrl) {
              const params = new URLSearchParams({
                action: 'triage_gpt_reset_state',
                nonce:  (window.triageAjax && triageAjax.nonce) ? triageAjax.nonce : ''
              });

              fetch(ajaxUrl, {
                method: 'POST',
                credentials: 'same-origin',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: params
              });
            }
          } catch (_) { /* silent — we don’t block the UX */ }

          const displayJobNumber = createData.job_number || $form.find('input[name="job_number"]').val() || '';
          const taskGid          = createData.task_gid;
          const homeUrl          = (window.afb_ajax && afb_ajax.home_url) ? String(afb_ajax.home_url) : '/';

          const escapeHtml = (value) => String(value || '').replace(/[&<>"']/g, (char) => {
            const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
            return map[char] || char;
          });

          const $successBox = $(`
            <section class="afb-success-card" role="status" aria-live="polite">
              <div class="afb-success-brand-row">
                <a class="afb-success-home-btn" href="${escapeHtml(homeUrl)}" aria-label="Go to Home">
                  <span class="afb-success-brand-icon" aria-hidden="true">
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                      <path d="M4.913 2.658c2.075-.27 4.19-.408 6.337-.408 2.147 0 4.262.139 6.337.408 1.922.25 3.291 1.861 3.405 3.727a4.403 4.403 0 00-1.032-.211 50.89 50.89 0 00-8.42 0c-2.358.196-4.04 2.19-4.04 4.434v4.286a4.47 4.47 0 002.433 3.984L7.28 21.53A.75.75 0 016 21v-4.03a48.527 48.527 0 01-1.087-.128C2.905 16.58 1.5 14.833 1.5 12.862V6.638c0-1.97 1.405-3.718 3.413-3.979z"/>
                      <path d="M15.75 7.5c-1.376 0-2.739.057-4.086.169C10.124 7.797 9 9.103 9 10.609v4.285c0 1.507 1.128 2.814 2.67 2.94 1.243.102 2.5.157 3.768.165l2.782 2.781a.75.75 0 001.28-.53v-2.39l.33-.026c1.542-.125 2.67-1.433 2.67-2.94v-4.286c0-1.505-1.125-2.811-2.664-2.94A49.392 49.392 0 0015.75 7.5z"/>
                    </svg>
                  </span>
                  <span>Home</span>
                </a>
                <span class="afb-success-brand-name"></span>
              </div>

              <div class="afb-success-headline-wrap">
                <p class="afb-success-kicker">Task created successfully.</p>
              </div>

              <div class="afb-success-request-block">
                <p class="afb-success-request-label">Request number</p>
                <p class="afb-success-request-id" id="job-number-copy">${escapeHtml(displayJobNumber)}</p>
                <div class="afb-success-action-row">
                  <button type="button" id="copy-job-number-btn" class="afb-success-secondary-btn">Copy request number</button>
                  <span id="copy-feedback" class="afb-success-feedback hidden">Copied!</span>
                </div>
              </div>

              <div class="afb-success-copy">
                <p>We’ve received your submission.</p>
                <p>Your request has been sent to the relevant team and will be actioned accordingly. Only contact the team if you have questions or concerns. A copy of your request will be sent to your email automatically.</p>
              </div>

              ${
                (email && taskGid)
                  ? `<p id="email-feedback" class="afb-success-feedback">Preparing your email copy...</p>`
                  : `<p class="afb-success-warning">Unable to send automatic email copy - missing task ID or email address.</p>`
              }
            </section>
          `);

          $form.closest('.asana-form-container').append($successBox);

          $('#copy-job-number-btn').on('click', function () {
            const toCopy = $('#job-number-copy').text().trim();
            const $copyFeedback = $('#copy-feedback');
            const ta = document.createElement('textarea');
            ta.value = toCopy;
            document.body.appendChild(ta);
            ta.select();
            try {
              document.execCommand('copy');
              $copyFeedback.removeClass('hidden');
              setTimeout(() => $copyFeedback.addClass('hidden'), 2000);
            } finally {
              document.body.removeChild(ta);
            }
          });

          if (email && taskGid) {
            const $emailFeedback = $('#email-feedback');
            const setEmailFeedback = (message, isError = false) => {
              $emailFeedback
                .text(message)
                .toggleClass('afb-success-warning', !!isError);
            };

            const parseAjaxPayload = (raw) => {
              const text = String(raw || '').trim();
              if (!text) return null;

              try {
                return JSON.parse(text);
              } catch (_) {
                // Some environments prepend warnings/notices before JSON.
                const braceIndex = text.indexOf('{');
                if (braceIndex >= 0) {
                  const candidate = text.slice(braceIndex);
                  try {
                    return JSON.parse(candidate);
                  } catch (_) {}
                }
              }

              return null;
            };

            const applyEmailResult = (raw, fallbackMessage) => {
              const payload = parseAjaxPayload(raw);
              if (payload && payload.success) {
                setEmailFeedback('A copy of your request has been emailed to you.');
                return;
              }

              const message =
                (payload && payload.data && payload.data.message) ||
                (payload && payload.message) ||
                fallbackMessage ||
                'Could not send your email copy automatically. Please contact support.';

              setEmailFeedback(`⚠️ ${message}`, true);
            };

            setEmailFeedback('Sending task details to your email...');

            $.ajax({
              url: afb_ajax.ajax_url,
              method: 'POST',
              dataType: 'text',
              data: {
                action: 'afb_send_email_request',
                send_email_to: email,
                task_gid: taskGid,
                job_number: displayJobNumber || ''
              }
            })
              .done((raw) => {
                applyEmailResult(raw, 'Could not send your email copy automatically. Please contact support.');
              })
              .fail((xhr) => {
                applyEmailResult(
                  xhr && xhr.responseText,
                  'Could not send your email copy automatically due to a server error.'
                );
              });
          }

        } catch (err) {
          // Unified failure UI
          setCompactSubmitView(false);
          $progressBarContainer.addClass('hidden');
          $statusText.removeClass('text-blue-600').addClass('text-red-600')
            .text(`❌ ${err && err.message ? err.message : 'Submission failed'}`);
          $errorDetails.removeClass('hidden').text(String(err && (err.stack || err.message || err)));

        } finally {
          // Restore button state
          $submitButton
            .prop('disabled', false)
            .removeAttr('aria-busy aria-disabled')
            .removeClass('opacity-60 cursor-not-allowed')
            .text(defaultSubmitButtonText);
          inFlight = false;
        }
      });

      /* ---------- INITIAL LOAD (if pre-selected) ---------- */
      if (projectId) loadProjectFields(projectId, projectPat);
    });
  }

  $(document).ready(initAsanaDynamicForms);
  window.afbInitDynamicForms = initAsanaDynamicForms;

})(jQuery);
