jQuery(function ($) {
  // --- Google & eBay search buttons ---
  if (typeof CLIPON_VARS !== 'undefined' && (CLIPON_VARS.searchBrand || CLIPON_VARS.searchModel)) {
    var searchTerm = (CLIPON_VARS.searchBrand + ' ' + CLIPON_VARS.searchModel).trim();
    var googleUrl  = 'https://www.google.com/search?q=' + encodeURIComponent(searchTerm);
    var ebayUrl    = 'https://www.ebay.com/sch/i.html?_nkw=' + encodeURIComponent(searchTerm);

    var $googleBtn = $('<a>', {
      href: googleUrl,
      target: '_blank',
      class: 'clipon-search-btn clipon-search-btn-google',
      html: '<svg width="14" height="14" viewBox="0 0 18 18" style="vertical-align:middle;margin-right:5px"><path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z"/><path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z"/><path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"/><path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z"/></svg>' + searchTerm
    });

    var $ebayBtn = $('<a>', {
      href: ebayUrl,
      target: '_blank',
      class: 'clipon-search-btn clipon-search-btn-ebay',
      html: '<span class="ebay-e">e</span><span class="ebay-b">b</span><span class="ebay-a">a</span><span class="ebay-y">y</span><span class="ebay-term"> ' + searchTerm + '</span>'
    });

    var $wrap = $('<span class="clipon-search-btns-wrap">');
    $wrap.append($googleBtn).append($ebayBtn);
    $('.wrap h1.wp-heading-inline').after($wrap);
  }
  const MIRROR_SWITCH_ID = 'clipon_mirror_toggle';
  const BASE_COLOR = '#cccccc';

  function isColorSwatchSelected() {
    return $('select.postform').val() === 'color';
  }

  function insertMirrorSwitch() {
    if ($(`#${MIRROR_SWITCH_ID}`).length) return;

    const html = `
      <div id="mirror-options" style="margin-top: 10px;">
        <label>
          <input type="checkbox" id="${MIRROR_SWITCH_ID}"> Mirrored
        </label>
      </div>
    `;

    $('select.postform').closest('td, .form-field').append(html);

    $(`#${MIRROR_SWITCH_ID}`).on('change', function () {
      if (this.checked) {
        addMirrorPreview();
      } else {
        $('#mirror-preview-canvas, #mirrored_preview, .mirror-preview-image').remove();
      }
    });
  }

function getMirrorColor() {
  const $input = $('.woo-color');
  return $input.length && $input.val().startsWith('#') ? $input.val() : '#666666';
}


  function addMirrorPreview() {
    const $originalPicker = $('.colorSelector').last().closest('.form-field');

    if ($('#mirror-preview-canvas').length) return;

    const html = `
      <canvas id="mirror-preview-canvas" width="90" height="90" style="display:none;"></canvas>
      <input type="hidden" name="mirrored_preview" id="mirrored_preview" value="">
      <div class="mirror-preview-image"></div>
    `;

    $originalPicker.after(html);

    bindColorChange();
    updateMirrorPreview();
  }

function bindColorChange() {
  // Listen to standard change events
  $('.woo-color')
    .off('.clipon')
    .on('change.clipon input.clipon blur.clipon keyup.clipon', updateMirrorPreview);

  // Also monitor DOM changes in case the plugin updates the value silently
  const input = document.querySelector('.woo-color');
  if (input) {
    const observer = new MutationObserver(updateMirrorPreview);
    observer.observe(input, { attributes: true, attributeFilter: ['value'] });
  }
}




function updateMirrorPreview() {
  const mirrorColor = getMirrorColor();

  const canvas = document.getElementById('mirror-preview-canvas');
  if (!canvas) return;

  const ctx = canvas.getContext('2d');
  const grad = ctx.createRadialGradient(27, 27, 10, 45, 45, 90);
  grad.addColorStop(0, '#cccccc');
  grad.addColorStop(1, mirrorColor);

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  const dataURL = canvas.toDataURL('image/png');
  $('#mirrored_preview').val(dataURL);
  $('.mirror-preview-image').html(`<img src="${dataURL}" width="90" height="90">`);
}


  function initMirrorSupport() {
    $('select.postform').on('change', function () {
      if (isColorSwatchSelected()) {
        insertMirrorSwitch();
      } else {
        $('#mirror-options, #mirror-preview-canvas, #mirrored_preview, .mirror-preview-image').remove();
      }
    });

    if (isColorSwatchSelected()) {
      insertMirrorSwitch();
    }
  }

  initMirrorSupport();
});

jQuery(function ($) {
  // Only run this on the lens-color attribute list page
  if (window.location.href.includes('taxonomy=pa_lens-color')) {
    $('tr[data-mirrored="1"]').each(function () {
      const $row = $(this);
      const termId = $row.attr('id').replace('tag-', '');
      const previewData = $row.find('td .mirror-preview-image img').attr('src') ||
                          $('input[name="mirrored_preview"]').val();

      if (previewData) {
        const $thumbnailCell = $row.find('td.column-thumbnail');
        $thumbnailCell.html(`<img src="${previewData}" width="30" height="30" style="border-radius:50%; border:1px solid #ccc;">`);
      }
    });
  }
});

jQuery(function ($) {
  const previewValue = $('#mirrored_preview').val();

  // Only run on taxonomy list screen
  if (window.location.href.includes('taxonomy=pa_lens-color') && previewValue) {
    // Find the last table row (newly added term) and inject thumbnail
    const $lastRow = $('table.wp-list-table tbody tr').first(); // or .last() if ordered differently
    const $thumbCell = $lastRow.find('td.column-thumbnail');

    if ($thumbCell.length) {
      $thumbCell.html(`<img src="${previewValue}" width="30" height="30" style="border-radius:50%; border:1px solid #ccc;">`);
    }
  }
});

// Customizer

jQuery(function ($) {
  // ---------- Helpers ----------
  const $root   = $('#cliponRoot');
  if (!$root.length) return; // Not a product page — exit early
  const $box    = $root.find('.clipon-box');
  const $wrap   = $box.find('.clipon-image-wrapper');
  const $img    = $('#clipon-single-image');
  const $uploader = $box.find('.clipon-upload-single .clipon-upload-btn');

  const $guides = $wrap.find('.clipon-guides');
  const $v1 = $guides.find('.v1'), $v2=$guides.find('.v2'), $v3=$guides.find('.v3');
  const $h1 = $guides.find('.h1'), $h2=$guides.find('.h2'), $h3=$guides.find('.h3'), $h4=$guides.find('.h4');

  const $editBtn  = $wrap.find('.clipon-edit');
  const $traceBtn = $wrap.find('.clipon-trace');
  const $resetTabBtn = $root.find('#clipon-reset-right');
  const $resetBtn = $wrap.find('.clipon-reset');

  const $tabs     = $root.find('.clipon-tab');
  const $tabCal   = $tabs.filter('[data-panel="cal"]');
  const $tabTrace = $tabs.filter('[data-panel="trace"]');
  const $panels   = { cal: $('#panel-cal'), trace: $('#panel-trace') };

  const $gear     = $panels.cal.find('.clipon-gear');
  const $settings = $panels.cal.find('.clipon-settings');
  const $calibrationSwitch = $panels.cal.find('.calibration-mode-switch');

  const $incStep  = $settings.find('.step');
  const $incZoom  = $settings.find('.zoomInc');
  const $incSX    = $settings.find('.sxInc');
  const $incSY    = $settings.find('.syInc');
  const $incRot   = $settings.find('.rotInc');

  const $frame = {
    A:  $panels.cal.find('.clipon-eye-a'),
    B:  $panels.cal.find('.clipon-eye-b'),
    Br: $panels.cal.find('.clipon-bridge'),
    H:  $panels.cal.find('.clipon-height'),
    Mat:$panels.cal.find('.clipon-material'),
    DPI:$panels.cal.find('.clipon-dpi'),
    Col:$panels.cal.find('.clipon-color'),
  };

  const $tracePanel = $panels.trace;
  const $traceMsg   = $tracePanel.find('.clipon-trace-msg');
  const $modeSwitch = $tracePanel.find('.clipon-mode-switch');
  const $drillToggle= $tracePanel.find('.clipon-drill-toggle');
  const $drillPanel = $tracePanel.find('.clipon-drill-panel');
  const $drLeftTop    = $tracePanel.find('.clipon-drill-left-top');
  const $drLeftBottom = $tracePanel.find('.clipon-drill-left-bottom');
  const $drBridge   = $tracePanel.find('.clipon-drill-bridge');
  const $drFactorSpring = $tracePanel.find('.clipon-drill-factor-spring');
  const $drFactorMag = $tracePanel.find('.clipon-drill-factor-mag');
  const $magOnly    = $tracePanel.find('.mag-only');
  const $springOnly = $tracePanel.find('.spring-only');

  const $svg     = $('#clipon-trace-svg');
  const $path    = $('#clipon-trace-path');
  const $gDrills = $('#clipon-drills');
  const $gAnch   = $('#clipon-trace-handles');
  const $gLines  = $('#clipon-control-lines');
  const $gCtrl   = $('#clipon-control-handles');
  const $mask    = $wrap.find('.trace-mask');

  const $download = $root.find('.clipon-download');
const $virtualBtn = $root.find('.clipon-virtual-btn');
// Virtual Clip-On popup handlers
  const $virtualPopup = $root.find('.clipon-virtual-popup');
  const $virtualClose = $virtualPopup.find('.clipon-virtual-close');
  const $virtualOverlay = $virtualPopup.find('.clipon-virtual-overlay');

  $virtualBtn.on('click', function() {
	renderVirtualClipon();  
    $virtualPopup.show();
  });

  $virtualClose.on('click', function() {
    $virtualPopup.hide();
  });

  $virtualOverlay.on('click', function() {
    $virtualPopup.hide();
  });
 // Handle background toggle change
$(document).on('change', '#clipon-show-background', function() {
  renderVirtualClipon();
});
$('#clipon-generate-variations').on('click', function() {
  generateVariationImages();
});

function generateVariationImages() {
  const $btn = $('#clipon-generate-variations');
  const $progress = $('#clipon-generation-progress');
  const $progressText = $('#clipon-progress-text');
  
  const key = activeSize || '_single';
  const ts = traceBySize.get(key);
  
  if (!ts) {
    alert('No trace data available');
    return;
  }
  
  $btn.prop('disabled', true);
  $progress.show();
  $progressText.text('Fetching variations...');
  
  // Get product variations
  $.ajax({
    url: CLIPON_VARS.ajaxUrl,
    type: 'POST',
    data: {
      action: 'clipon_get_variations',
      nonce: CLIPON_VARS.uploadNonce,
      post_id: CLIPON_VARS.postId
    },
    success: function(response) {
      if (response.success && response.data) {
        // Filter by current bridge mode
        const currentMode = ts.magMode || 'spring';
        const filtered = response.data.filter(v => v.bridge_type === currentMode);
        
        console.log('All variations:', response.data);
        console.log('Current mode:', currentMode);
        console.log('Filtered variations:', filtered);
        
        if (filtered.length === 0) {
          alert('No variations found for ' + currentMode + ' bridge');
          $btn.prop('disabled', false);
          $progress.hide();
          return;
        }
        
        processVariations(filtered, $progressText, function() {
          $btn.prop('disabled', false);
          $progress.hide();
          alert('Generated ' + filtered.length + ' ' + currentMode + ' variation images successfully!');
        });
      } else {
        alert('Failed to fetch variations');
        $btn.prop('disabled', false);
        $progress.hide();
      }
    },
    error: function() {
      alert('Error fetching variations');
      $btn.prop('disabled', false);
      $progress.hide();
    }
  });
}

function processVariations(variations, $progressText, callback) {
  console.log('=== PROCESSING VARIATIONS ===');
  console.log('Total variations to process:', variations.length);
  variations.forEach((v, i) => {
    console.log(`${i}: ID=${v.id}, Bridge=${v.bridge_type}, Color=${v.lens_color_name}, File=${v.filename}`);
  });
  
  let index = 0;
  
  function processNext() {
    if (index >= variations.length) {
      console.log('=== ALL VARIATIONS PROCESSED ===');
      callback();
      return;
    }
    
    const variation = variations[index];
    console.log(`>>> Processing variation ${index + 1}/${variations.length}: ${variation.filename}`);
    $progressText.text(`Generating ${index + 1} of ${variations.length}...`);
    
    generateVariationImage(variation, function() {
      console.log(`<<< Completed variation ${index + 1}: ${variation.filename}`);
      index++;
      setTimeout(processNext, 100);
    });
  }
  
  processNext();
}

function generateVariationImage(variation, callback) {
  console.log('=== Generating variation ===');
  console.log('Variation ID:', variation.id);
  console.log('Filename:', variation.filename);
  
  const canvas = document.getElementById('clipon-virtual-canvas');
  if (!canvas) {
    callback();
    return;
  }
  
  const ctx = canvas.getContext('2d');
  const canvasWidth = 800;
  const canvasHeight = 400;
  
  // Clear canvas
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, canvasWidth, canvasHeight);
  
  const img = $img[0];
  const pathD = $path.attr('d');
  const key = activeSize || '_single';
  const ts = traceBySize.get(key);
  
  if (!img || !img.complete || !pathD || !ts) {
    callback();
    return;
  }
  
  const wrapWidth = $wrap.width();
  const wrapHeight = $wrap.height();
  const scale = Math.min((canvasWidth / 2) / wrapWidth, canvasHeight / wrapHeight);
  const dpi = parseFloat($frame.DPI.val()) || 350;
  const pxPerMm = dpi / 25.4;
  
  const clipWidthPx = 2.5 * pxPerMm;
  const clipLengthPx = 5.3 * pxPerMm;
  const stickOutPx = 1 * pxPerMm;
  const clipCenterOffset = stickOutPx - (clipLengthPx / 2);
  const springWidthPx = 19 * pxPerMm;
  const magneticWidthPx = 13 * pxPerMm;
  
  const lensColorData = variation.lens_color;
  const isImagePattern = lensColorData && (lensColorData.startsWith('http') || lensColorData.startsWith('/'));
  const bridgeType = variation.bridge_type || 'spring';
  
  console.log('Is image pattern?', isImagePattern);
  
  // Prevent multiple callback calls
  let callbackCalled = false;
  function safeCallback() {
    if (!callbackCalled) {
      callbackCalled = true;
      callback();
    }
  }
  
  // Set timeout for entire image loading process (8 seconds)
  const loadTimeout = setTimeout(function() {
    console.error('TIMEOUT loading images for:', variation.filename);
    safeCallback();
  }, 8000);
  
  // Load images
  const clipImg = new Image();
  clipImg.crossOrigin = 'anonymous';
  
  const bridgeImg = new Image();
  bridgeImg.crossOrigin = 'anonymous';
  
  let lensImg = null;
  if (isImagePattern) {
    lensImg = new Image();
    lensImg.crossOrigin = 'anonymous';
  }
  
  let imagesLoaded = {
    clip: false,
    bridge: false,
    lens: !isImagePattern
  };
  
  function checkAllLoaded() {
    if (imagesLoaded.clip && imagesLoaded.bridge && imagesLoaded.lens) {
      clearTimeout(loadTimeout);
      console.log('All images loaded, drawing...');
      
      const capturedLensImg = lensImg;
      drawBothSides(capturedLensImg);
    }
  }
  
  // Clip image handlers
  clipImg.onerror = function() {
    console.error('Clip image failed to load');
    clearTimeout(loadTimeout);
    safeCallback();
  };
  
  clipImg.onload = function() {
    console.log('Clip image loaded');
    imagesLoaded.clip = true;
    checkAllLoaded();
  };
  
  // Bridge image handlers
  bridgeImg.onerror = function() {
    console.error('Bridge image failed to load');
    clearTimeout(loadTimeout);
    safeCallback();
  };
  
  bridgeImg.onload = function() {
    console.log('Bridge image loaded');
    imagesLoaded.bridge = true;
    checkAllLoaded();
  };
  
  // Lens image handlers
  if (lensImg) {
    lensImg.onerror = function() {
      console.error('Lens image failed to load:', lensColorData);
      // Continue without lens image (will use fallback)
      imagesLoaded.lens = true;
      checkAllLoaded();
    };
    
    lensImg.onload = function() {
      console.log('Lens image loaded:', lensColorData, lensImg.naturalWidth, 'x', lensImg.naturalHeight);
      imagesLoaded.lens = true;
      checkAllLoaded();
    };
  }
  
  // Start loading images
  clipImg.src = 'https://cliponexpress.com/wp-content/uploads/2015/12/clip.png';
if (bridgeType === 'spring') {
  bridgeImg.src = 'https://cliponexpress.com/wp-content/uploads/2015/12/spring.png';
} else {
  // Read magnetic bridge type - target specifically the bridge field container
  const magneticBridgeSelect = $root.find('.clipon-bridge-field .clipon-bridge-select');
  const magneticBridgeValue = $root.find('.clipon-bridge-field .clipon-position-value');
  
  let magneticBridgeType = '';
  if (magneticBridgeSelect.length > 0) {
    magneticBridgeType = magneticBridgeSelect.val() || '';
  } else if (magneticBridgeValue.length > 0) {
    magneticBridgeType = magneticBridgeValue.text().trim();
  }
  
  console.log('Magnetic bridge type from form:', magneticBridgeType);
  
  // Check if "Curved" appears anywhere in the string
  const isCurved = magneticBridgeType.toLowerCase().includes('curved');
  
  bridgeImg.src = isCurved
    ? 'https://cliponexpress.com/wp-content/uploads/icons/curvedmagnetic.png'
    : 'https://cliponexpress.com/wp-content/uploads/2015/12/straightmagnet.png';
  
  console.log('Is curved?', isCurved, '- Using bridge image:', bridgeImg.src);
}
  if (lensImg) {
    lensImg.src = lensColorData;
  }
  
  function drawBothSides(passedLensImage) {
    console.log('=== drawBothSides START ===');
    
    try {
      drawSideForVariation(false, lensColorData, bridgeType, bridgeImg, passedLensImage);
      drawSideForVariation(true, lensColorData, bridgeType, bridgeImg, passedLensImage);
      
      console.log('Both sides drawn, creating blob...');
      
      // Export as JPEG
      canvas.toBlob(function(blob) {
        if (!blob) {
          console.error('Blob creation failed');
          safeCallback();
          return;
        }
        
        const reader = new FileReader();
        reader.onloadend = function() {
          uploadVariationImage(variation, reader.result, safeCallback);
        };
        reader.onerror = function() {
          console.error('FileReader error');
          safeCallback();
        };
        reader.readAsDataURL(blob);
      }, 'image/jpeg', 0.90);
    } catch(error) {
      console.error('Error in drawBothSides:', error);
      safeCallback();
    }
  }
  
  function drawSideForVariation(mirror, colorData, mode, bImg, lensImage) {
    ctx.save();
    
    ctx.beginPath();
    if (mirror) {
      ctx.rect(canvasWidth / 2, 0, canvasWidth / 2, canvasHeight);
    } else {
      const extraSpace = 50;
      ctx.rect(0, 0, (canvasWidth / 2) + extraSpace, canvasHeight);
    }
    ctx.clip();
    ctx.closePath();
    
    if (mirror) {
      ctx.translate(canvasWidth * 3 / 4, canvasHeight / 2);
      ctx.scale(-scale, scale);
    } else {
      ctx.translate(canvasWidth / 4, canvasHeight / 2);
      ctx.scale(scale, scale);
    }
    ctx.translate(-wrapWidth / 2, -wrapHeight / 2);
    
// Draw image only if background toggle is checked
const showBackground = $('#clipon-show-background').is(':checked');
if (showBackground) {
  ctx.drawImage(img, 0, 0, wrapWidth, wrapHeight);
}    
    // Fill path with lens color (image pattern or solid color) at 80% opacity
    const path2D = new Path2D(pathD);
    ctx.globalAlpha = 0.8;
    
    if (lensImage && lensImage.complete && lensImage.naturalWidth > 0) {
      // Use image as pattern with scaled size, centered on lens
      const pathEl = document.getElementById('clipon-trace-path');
      if (pathEl) {
        const bbox = pathEl.getBBox();
        const centerX = bbox.x + bbox.width / 2;
        const centerY = bbox.y + bbox.height / 2;
        
        const tempCanvas = document.createElement('canvas');
        const tempCtx = tempCanvas.getContext('2d');
        
        const scaleFactor = 3;
        tempCanvas.width = lensImage.naturalWidth * scaleFactor;
        tempCanvas.height = lensImage.naturalHeight * scaleFactor;
        
        tempCtx.drawImage(lensImage, 0, 0, tempCanvas.width, tempCanvas.height);
        
        const pattern = ctx.createPattern(tempCanvas, 'repeat');
        
        if (pattern) {
          const patternOffsetX = centerX % tempCanvas.width - tempCanvas.width / 2;
          const patternOffsetY = centerY % tempCanvas.height - tempCanvas.height / 2;
          
          const matrix = new DOMMatrix();
          matrix.translateSelf(patternOffsetX, patternOffsetY);
          pattern.setTransform(matrix);
          
          ctx.fillStyle = pattern;
        } else {
          ctx.fillStyle = 'rgba(0, 0, 0, 1)';
        }
      } else {
        ctx.fillStyle = 'rgba(0, 0, 0, 1)';
      }
    } else {
      const rgb = hexToRgb(colorData || '#000000');
      ctx.fillStyle = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
    }
    
    ctx.fill(path2D);
    ctx.globalAlpha = 1.0;
    
    // Draw clips and bridge
    if (ts.drillData) {
      const pathEl = document.getElementById('clipon-trace-path');
      if (pathEl) {
        const bbox = pathEl.getBBox();
        const centerX = bbox.x + bbox.width / 2;
        const centerY = bbox.y + bbox.height / 2;
        const leftTopX = ts.drillData.leftTopX || 20;
        const leftBottomX = ts.drillData.leftBottomX || 20;
        const targetTopXpx = centerX - (leftTopX * pxPerMm);
        const targetBotXpx = centerX - (leftBottomX * pxPerMm);
        
        const topPt = samplePathForTargetXY(pathEl, targetTopXpx, 'x', -1, centerX, centerY);
        const botPt = samplePathForTargetXY(pathEl, targetBotXpx, 'x', +1, centerX, centerY);
        
        // Draw clips
        [topPt, botPt].forEach(pt => {
          if (pt) {
            const n = inwardNormalUnitAt(pathEl, pt.s, centerX, centerY);
            ctx.save();
            ctx.translate(pt.x - n.x * clipCenterOffset, pt.y - n.y * clipCenterOffset);
            ctx.rotate(Math.atan2(-n.y, -n.x) + Math.PI / 2);
            ctx.drawImage(clipImg, -clipWidthPx / 2, -clipLengthPx / 2, clipWidthPx, clipLengthPx);
            ctx.restore();
          }
        });
        
        // Draw bridge
        if (mode === 'spring') {
          const frameData = readFrameInputs($root);
          const material = frameData.material;
          const matFactor = materialFactorForSpring(material);
          const factorSpring = (ts.drillData && ts.drillData.factorSpring) ? ts.drillData.factorSpring : 44;
          const bridgeVal = frameData.bridge || parseFloat($frame.Br.val()) || 10;
          const deltaMm = (factorSpring - (bridgeVal - matFactor)) / 2;
          const rightEdgePx = bbox.x + bbox.width;
          const startXpx = rightEdgePx - (deltaMm * pxPerMm);
          const startPx = samplePathForTargetXY(pathEl, startXpx, 'x', -1, centerX, centerY);
          
          if (startPx) {
            const holeX = startPx.x - (0.2 * pxPerMm) - (0.5 * pxPerMm);
            const holeY = startPx.y + (0.2 * pxPerMm) + (2.5 * pxPerMm);
            const springHeightPx = bImg.height * (springWidthPx / bImg.width);
            const anchorOffsetY = springHeightPx - (1 * pxPerMm);
            const springX = wrapWidth - springWidthPx;
            
            ctx.save();
            ctx.drawImage(bImg, springX, holeY - anchorOffsetY + springBridgeVisualOffset, springWidthPx, springHeightPx);
            ctx.restore();
          }
    } else {
  // Magnetic mode - right end aligned with mirror line (right edge of wrap)
  const bridgeMm = ts.drillData.bridge || 10;
  const magneticHeightPx = bImg.height * (magneticWidthPx / bImg.width);
  
  // Position: right end at right edge of wrap (mirror line)
  const bridgeX = wrapWidth - (magneticWidthPx / 2);
  const bridgeY = centerY - (bridgeMm * pxPerMm);
  
  ctx.save();
  ctx.drawImage(bImg, bridgeX - magneticWidthPx / 2, bridgeY - magneticHeightPx / 2, magneticWidthPx, magneticHeightPx);
  ctx.restore();
}
      }
    }
    
    ctx.restore();
  }
}

function hexToRgb(hex) {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : {r: 0, g: 0, b: 0};
}

function uploadVariationImage(variation, imageData, callback) {
  $.ajax({
    url: CLIPON_VARS.ajaxUrl,
    type: 'POST',
    data: {
      action: 'clipon_upload_variation_image',
      nonce: CLIPON_VARS.uploadNonce,
      variation_id: variation.id,
      filename: variation.filename,
      image_data: imageData
    },
    success: function(response) {
      console.log('Uploaded:', variation.filename);
      callback();
    },
    error: function() {
      console.error('Failed:', variation.filename);
      callback();
    }
  });
}	
	
var springBridgeVisualOffset = 0;

$('#clipon-bridge-up').on('click', function() {
  springBridgeVisualOffset -= 2;
  $('#clipon-bridge-offset-value').text(springBridgeVisualOffset);
  renderVirtualClipon();
});

$('#clipon-bridge-down').on('click', function() {
  springBridgeVisualOffset += 2;
  $('#clipon-bridge-offset-value').text(springBridgeVisualOffset);
  renderVirtualClipon();
});

function renderVirtualClipon() {
  const canvas = document.getElementById('clipon-virtual-canvas');
  if (!canvas) return;
  
  const ctx = canvas.getContext('2d');
  const canvasWidth = 800;
  const canvasHeight = 400;
  
  // Clear canvas
  ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, canvasWidth, canvasHeight);
  
  // Get current image and path
  const img = $img[0];
  const pathD = $path.attr('d');
  const key = activeSize || '_single';
  const ts = traceBySize.get(key);
  
  if (!img || !img.complete || !pathD || !ts) {
    ctx.fillStyle = '#666';
    ctx.font = '16px Arial';
    ctx.textAlign = 'center';
    ctx.fillText('No image or path available', canvasWidth / 2, canvasHeight / 2);
    return;
  }
  
  // Calculate scaling to fit canvas (half width for mirrored display)
  const wrapWidth = $wrap.width();
  const wrapHeight = $wrap.height();
  const scale = Math.min((canvasWidth / 2) / wrapWidth, canvasHeight / wrapHeight);
  const dpi = parseFloat($frame.DPI.val()) || 350;
  const pxPerMm = dpi / 25.4;
  
  // Clip dimensions: 2.5mm width, 5.3mm length, 1mm sticks out, 4.3mm goes in
  const clipWidthPx = 2.5 * pxPerMm;
  const clipLengthPx = 5.3 * pxPerMm;
  const stickOutPx = 1 * pxPerMm;
  const clipCenterOffset = stickOutPx - (clipLengthPx / 2);
  
  // Bridge dimensions
  const springWidthPx = 19 * pxPerMm;
  const magneticWidthPx = 13 * pxPerMm;
  
  // Load images
  const clipImg = new Image();
  clipImg.crossOrigin = 'anonymous';
  clipImg.src = 'https://cliponexpress.com/wp-content/uploads/2015/12/clip.png';
  
  const springImg = new Image();
  springImg.crossOrigin = 'anonymous';
  springImg.src = 'https://cliponexpress.com/wp-content/uploads/2015/12/spring.png';
  
// Determine which magnetic bridge to use based on attribute
const frameData = readFrameInputs($root);
	// Add debugging:
console.log('=== DEBUGGING MAGNETIC BRIDGE ===');
console.log('All select elements:', $('select[name*="magnetic"]').length);
$('select[name*="magnetic"]').each(function() {
  console.log('Found select:', $(this).attr('name'), '=', $(this).val());
});
console.log('All inputs with magnetic:', $('input[name*="magnetic"]').length);
$('input[name*="magnetic"]').each(function() {
  console.log('Found input:', $(this).attr('name'), '=', $(this).val());
});
console.log('Checking attribute_pa_magnetic-bridge:', $('select[name="attribute_pa_magnetic-bridge"]').val());
console.log('=== END DEBUG ===');
// Read magnetic bridge type - target specifically the bridge field container
const magneticBridgeSelect = $root.find('.clipon-bridge-field .clipon-bridge-select');
const magneticBridgeValue = $root.find('.clipon-bridge-field .clipon-position-value');

let magneticBridgeType = '';
if (magneticBridgeSelect.length > 0) {
  magneticBridgeType = magneticBridgeSelect.val() || '';
} else if (magneticBridgeValue.length > 0) {
  magneticBridgeType = magneticBridgeValue.text().trim();
}

console.log('Virtual clipon - Magnetic bridge type:', magneticBridgeType);

// Check if "Curved" appears anywhere in the string
const isCurvedMagnetic = magneticBridgeType.toLowerCase().includes('curved');

const magneticImg = new Image();
magneticImg.crossOrigin = 'anonymous';
magneticImg.src = isCurvedMagnetic 
  ? 'https://cliponexpress.com/wp-content/uploads/icons/curvedmagnetic.png'
  : 'https://cliponexpress.com/wp-content/uploads/2015/12/straightmagnet.png';

console.log('Virtual clipon - Is curved?', isCurvedMagnetic, '- Using bridge URL:', magneticImg.src);
  let loadedCount = 0;
  const totalImages = 3;
  
  function onImageLoad() {
    loadedCount++;
    if (loadedCount === totalImages) {
      drawSide(false); // Left side
      drawSide(true);  // Right side (mirrored)
    }
  }
  
  clipImg.onload = onImageLoad;
  springImg.onload = onImageLoad;
  magneticImg.onload = onImageLoad;
  
  function drawSide(mirror) {
    ctx.save();
    
  // Clip to half of canvas to prevent overlap at center
  ctx.beginPath();
  if (mirror) {
    ctx.rect(canvasWidth / 2, 0, canvasWidth / 2, canvasHeight);
  } else {
    ctx.rect(0, 0, canvasWidth / 2, canvasHeight);
  }
  ctx.clip();
  ctx.closePath();
	  
    if (mirror) {
      ctx.translate(canvasWidth * 3 / 4, canvasHeight / 2);
      ctx.scale(-scale, scale);
    } else {
      ctx.translate(canvasWidth / 4, canvasHeight / 2);
      ctx.scale(scale, scale);
    }
    ctx.translate(-wrapWidth / 2, -wrapHeight / 2);
    
    // Draw image
  // Draw image only if background toggle is checked
const showBackground = $('#clipon-show-background').is(':checked');
if (showBackground) {
  ctx.drawImage(img, 0, 0, wrapWidth, wrapHeight);
}
    
    // Fill path with black 80% opacity
    const path2D = new Path2D(pathD);
    ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
    ctx.fill(path2D);
    
    // Draw clips and bridge at drill mark positions
    if (ts.drillData) {
      const pathEl = document.getElementById('clipon-trace-path');
      if (pathEl) {
        const bbox = pathEl.getBBox();
        const centerX = bbox.x + bbox.width / 2;
        const centerY = bbox.y + bbox.height / 2;
        const frameData = readFrameInputs($root);
        const material = frameData.material;
        const matFactor = materialFactorForSpring(material);
        
        // Get left X positions for drill marks (independent top and bottom)
        const leftTopX = ts.drillData.leftTopX || 20;
        const leftBottomX = ts.drillData.leftBottomX || 20;
        const targetTopXpx = centerX - (leftTopX * pxPerMm);
        const targetBotXpx = centerX - (leftBottomX * pxPerMm);
        
        // Sample path at these X positions to get Y positions
        const topPt = samplePathForTargetXY(pathEl, targetTopXpx, 'x', -1, centerX, centerY);
        const botPt = samplePathForTargetXY(pathEl, targetBotXpx, 'x', +1, centerX, centerY);
        
        // Draw side clips
        if (topPt) {
          const n = inwardNormalUnitAt(pathEl, topPt.s, centerX, centerY);
          ctx.save();
          ctx.translate(topPt.x - n.x * clipCenterOffset, topPt.y - n.y * clipCenterOffset);
          ctx.rotate(Math.atan2(-n.y, -n.x) + Math.PI / 2);
          ctx.drawImage(clipImg, -clipWidthPx / 2, -clipLengthPx / 2, clipWidthPx, clipLengthPx);
          ctx.restore();
        }
        
        if (botPt) {
          const n = inwardNormalUnitAt(pathEl, botPt.s, centerX, centerY);
          ctx.save();
          ctx.translate(botPt.x - n.x * clipCenterOffset, botPt.y - n.y * clipCenterOffset);
          ctx.rotate(Math.atan2(-n.y, -n.x) + Math.PI / 2);
          ctx.drawImage(clipImg, -clipWidthPx / 2, -clipLengthPx / 2, clipWidthPx, clipLengthPx);
          ctx.restore();
        }
        
        // Draw bridge based on mode
        const mode = ts.magMode || 'spring';
        
        if (mode === 'spring') {
          // Find spring drill hole position
          const factorSpring = (ts.drillData && ts.drillData.factorSpring) ? ts.drillData.factorSpring : 44;
          const bridgeVal = frameData.bridge || parseFloat($frame.Br.val()) || 10;
          const deltaMm = (factorSpring - (bridgeVal - matFactor)) / 2;
          const rightEdgePx = bbox.x + bbox.width;
          const startXpx = rightEdgePx - (deltaMm * pxPerMm);
          const startPx = samplePathForTargetXY(pathEl, startXpx, 'x', -1, centerX, centerY);
          
   		if (startPx) {
  // Hole is 0.5mm left and 2.5mm down from slot start
  const holeX = startPx.x - (0.2 * pxPerMm) - (0.5 * pxPerMm);
  const holeY = startPx.y + (0.2 * pxPerMm) + (2.5 * pxPerMm);
  
  // Spring bridge: right end at right edge of wrap (center line between displays)
  const springHeightPx = springImg.height * (springWidthPx / springImg.width);
  const anchorOffsetY = springHeightPx - (1 * pxPerMm);
  
  // Position right end at right edge of wrap area
  const springX = wrapWidth - springWidthPx;
  
  ctx.save();
  ctx.drawImage(springImg, springX, holeY - anchorOffsetY + springBridgeVisualOffset, springWidthPx, springHeightPx);
  ctx.restore();
}
 } else {
  // Magnetic mode - right end aligned with mirror line (right edge of wrap)
  const bridgeMm = ts.drillData.bridge || 10;
  const magneticHeightPx = magneticImg.height * (magneticWidthPx / magneticImg.width);
  
  // Position: right end at right edge of wrap (mirror line)
  const bridgeX = wrapWidth - (magneticWidthPx / 2);
  const bridgeY = centerY - (bridgeMm * pxPerMm);
  
  ctx.save();
  // Draw centered at position, always horizontal (no rotation)
  ctx.drawImage(magneticImg, bridgeX - magneticWidthPx / 2, bridgeY - magneticHeightPx / 2, magneticWidthPx, magneticHeightPx);
  ctx.restore();
}
      }
    }
    
    ctx.restore();
  }
  
  // If all images are already loaded, draw immediately
  if (clipImg.complete && springImg.complete && magneticImg.complete) {
    drawSide(false);
    drawSide(true);
  }
}
	
  // === SVG helpers (namespace-safe) ===
  const NS = 'http://www.w3.org/2000/svg';
  function svgEl(tag, attrs={}) { const el = document.createElementNS(NS, tag); for (const [k,v] of Object.entries(attrs)) el.setAttribute(k, String(v)); return el; }
  function svgAppend($g, el)    { ($g && $g[0] ? $g[0] : $g).appendChild(el); }
  function svgShow($el)         { $el.each(function(){ this.style.display='inline'; }); }
  function svgHide($el)         { $el.each(function(){ this.style.display='none';   }); }
  function svgClear($el)        { $el.each(function(){ while (this.firstChild) this.removeChild(this.firstChild); }); }

  // Pointer behavior: off in Calibrate, on in Trace
  $svg.css({ zIndex: 30, pointerEvents: 'none' });

  // Hidden inputs
  const $hidden = $('.clipon-hidden');
  const $hid = {
    id:  $hidden.find('input[name="_clipon_single_image_id"]'),
    url: $hidden.find('input[name="_clipon_single_image_url"]'),
    transform: $hidden.find('input[name="_clipon_transform"]'),
    frame:     $hidden.find('input[name="_clipon_frame"]'),
    path:      $hidden.find('input[name="_clipon_trace_path"]'),
    points:    $hidden.find('input[name="_clipon_trace_points"]'),
    handles:   $hidden.find('input[name="_clipon_trace_handles"]'),
    drill:     $hidden.find('input[name="_clipon_drill"]'),
    sizesMap:  $hidden.find('input[name="_clipon_sizes_map"]'),
    activeSz:  $hidden.find('input[name="_clipon_active_size"]'),
    doubleImgs:$hidden.find('input[name="_clipon_double_images"]'),
  };

  // Size tabs (may be absent)
  const $sizeTabs = $root.find('.clipon-size-tab');
  let activeSize = $sizeTabs.filter('.is-active').data('size') || '';

  // ---------- State ----------
  const state = { tx:0, ty:0, scale:1, sx:1, sy:1, rot:0, step:10, zoomInc:0.1, sxInc:0.1, syInc:0.1, rotInc:1 };
  let calibrationMode = 'fine'; // 'fine' or 'coarse'
  let imageUploaded = !!$img.attr('src');
  let baseSizeOnUpload = activeSize || '';
  let baseAOnUpload = 0;
  let baseCalibrated = false;

  // Per-size trace: { mode:'placing'|'edit', points:[{x,y}], handles:[{hin:{x,y}, hout:{x,y}}], drillOpen, magMode:'spring'|'magnetic', springBackup }
  const traceBySize = new Map();

  const HOLE_COLOR = '#ef4444'; // matches your CSS var --hole
  const TR_INDEX = 3; // br, bl, tl, tr → tr is index 3 (initial only)
  
  // Helper: Find the actual TR point (rightmost point on top half)
  // This ensures the original TR stays TR even when points are added
  function findTRIndex(ts) {
    if (!ts || !ts.points || ts.points.length < 4) return 3;
    
    // Find center Y of all points
    var minY = Math.min(...ts.points.map(p => p.y));
    var maxY = Math.max(...ts.points.map(p => p.y));
    var centerY = (minY + maxY) / 2;
    
    // Find rightmost point in the upper half
    var trIndex = 0;
    var maxX = -Infinity;
    
    for (var i = 0; i < ts.points.length; i++) {
      if (ts.points[i].y < centerY && ts.points[i].x > maxX) {
        maxX = ts.points[i].x;
        trIndex = i;
      }
    }
    
    return trIndex;
  }

  // ---------- Prevent form submit ----------
  $(document).on('click', '.clipon-ui button, .clipon-controls button, .clipon-tabsbar button, .strip button, .dpad button, .clipon-drill-toggle, .clipon-download-btn', function (e) {
    e.preventDefault(); e.stopPropagation();
  });

  // ---------- Helper Functions for Download ----------
  function pxPerMmFromDpi(dpi) {
    return dpi / 25.4;
  }

  function pxToMmPoint(px, py, centerX, centerY, dpi) {
    const pxPerMm = pxPerMmFromDpi(dpi);
    return {
      X: (px - centerX) / pxPerMm,
      Y: -(py - centerY) / pxPerMm  // negative Y for coordinate system flip
    };
  }

  function fmt1(num) {
    return Number(num).toFixed(1);
  }

  function readFrameInputs($section) {
    return {
      material: $section.find('.clipon-material').val() || 'plastic',
      bridge: parseFloat($section.find('.clipon-bridge').val()) || 0,
      dpi: parseFloat($section.find('.clipon-dpi').val()) || 350,
      eyeA: parseFloat($section.find('.clipon-eye-a').val()) || 0,
	  magneticBridge: $section.find('.magnetic-bridge').val() || ''	
    };
  }

  function materialFactorForSpring(material) {
    switch (material) {
      case 'metal': return 0.4;
      case 'semi-rimless': return 0;
      case 'plastic':
      default: return 0.8;
    }
  }

  // Simple fallback functions for brand/model/eye data
function cliponGetBrandFromCategories() {
  // Get product categories
  var categories = [];
$('#taxonomy-product_cat input:checked').each(function() {
  // Get only the direct text node, excluding WordPress action links like "Make primary"
  var labelText = $(this).closest('label').clone().children().remove().end().text().trim();
  categories.push(labelText);
});
  
  // Return first category as brand, or check for specific brand taxonomy
  return categories[0] || 'Generic';
}

function cliponGetModelFromAttributes(debug = false) {
  let model = '';

  // 1) Prefer the explicit pa_model taxonomy select in this product
  let $sel = jQuery('select.attribute_values[data-taxonomy="pa_model"]');
  if ($sel.length) {
    // Try option text first
    const optTxt = $sel.find('option:selected').map(function () {
      return jQuery(this).text();
    }).get().join(' ').trim();

    if (optTxt) {
      model = optTxt;
      if (debug) console.log('[clipon] model via option:selected =', model);
      return model;
    }

    // Fallback: Select2 chips rendered as a sibling of THIS select
    const chipsTxt = $sel.nextAll('.select2').first()
      .find('.select2-selection__choice')
      .map(function () { return jQuery(this).attr('title') || jQuery(this).text(); })
      .get().join(' ').replace(/×/g, '').trim();

    if (chipsTxt) {
      model = chipsTxt;
      if (debug) console.log('[clipon] model via select2 chips =', model);
      return model;
    }
  }

  // 2) Fallback: look for a custom "model" attribute row
  const $row = jQuery('#product_attributes .woocommerce_attribute').filter(function () {
    const tax  = (jQuery(this).find('select.attribute_taxonomy').val() || '').toLowerCase();
    const name = (jQuery(this).find('input.attribute_name').val()      || '').toLowerCase();
    return tax === 'pa_model' || name === 'model' || /(^|[_-])model$/.test(name);
  }).first();

  if ($row.length) {
    // chips inside that row (custom setups)
    const rowChips = $row.find('.select2-selection__choice').map(function () {
      return jQuery(this).attr('title') || jQuery(this).text();
    }).get().join(' ').replace(/×/g, '').trim();
    if (rowChips) {
      model = rowChips;
      if (debug) console.log('[clipon] model via row chips =', model);
      return model;
    }

    // plain select inside that row
    const rowOpt = $row.find('select.attribute_values option:selected').map(function () {
      return jQuery(this).text();
    }).get().join(' ').trim();
    if (rowOpt) {
      model = rowOpt;
      if (debug) console.log('[clipon] model via row option =', model);
      return model;
    }

    // free-text (custom attribute)
    const free = ($row.find('textarea.attribute_value, input.attribute_value').val() || '').toString().trim();
    if (free) {
      model = free;
      if (debug) console.log('[clipon] model via row free text =', model);
      return model;
    }
  }

  if (debug) console.log('[clipon] final model (empty) = ""');
  return '';
}


  function cliponGetEyeAndBridge() {
    // Get from current frame inputs
    const frameData = readFrameInputs($root);
    return {
      eyeA: frameData.eyeA || '50',
      bridge: frameData.bridge || '15'
    };
  }

  // ---------- Media (single) ----------
  function openMediaSingle(cb) {
    const frame = wp.media({ title: 'Select Image', multiple: false, library: {type:'image'} });
    frame.on('select', function () {
      const att = frame.state().get('selection').first().toJSON();
      cb({id: att.id, url: att.url});
    });
    frame.open();
  }

  $uploader.on('click', function(){
    openMediaSingle(function(p){
      $hid.id.val(p.id); $hid.url.val(p.url);
      $wrap.show(); $img.attr('src', p.url).show();
      imageUploaded = true;
      baseSizeOnUpload = activeSize || '';
      baseAOnUpload = parseAB(activeSize).a || parseFloat($frame.A.val()) || 0;
      baseCalibrated = false;
      resetTransform(); applyTransform(); updateGuides();
    });
  });

  // ---------- Transform ----------
  function applyTransform() {
    const t = `translate(-50%,-50%) translate(${state.tx}px,${state.ty}px) rotate(${state.rot}deg) scale(${(state.sx*state.scale).toFixed(4)}, ${(state.sy*state.scale).toFixed(4)})`;
    $img.css({ transform: t, transformOrigin: 'center center' });
    persistTransform();
	syncPathWithImage();
  }
  function resetTransform(){ Object.assign(state, { tx:0, ty:0, scale:1, sx:1, sy:1, rot:0 }); }
  function isDefaultTransform() {
    return Math.abs(state.tx)<1e-3 && Math.abs(state.ty)<1e-3 && Math.abs(state.rot)<1e-3 &&
           Math.abs(state.scale-1)<1e-3 && Math.abs(state.sx-1)<1e-3 && Math.abs(state.sy-1)<1e-3;
  }
  
function transformPathCoordinates(ts, transform) {
  const centerX = $wrap.width() / 2;
  const centerY = $wrap.height() / 2;
  const radians = transform.rot * Math.PI / 180;
  const cos = Math.cos(radians);
  const sin = Math.sin(radians);
  const scaleX = transform.sx * transform.scale;
  const scaleY = transform.sy * transform.scale;
  
  function transformPoint(p) {
    // Start from point position
    let x = p.x - centerX;
    let y = p.y - centerY;
    
    // Apply translation first (before rotation)
    x += transform.tx;
    y += transform.ty;
    
    // Apply rotation
    const rotX = x * cos - y * sin;
    const rotY = x * sin + y * cos;
    
    // Apply scale
    const scaledX = rotX * scaleX;
    const scaledY = rotY * scaleY;
    
    // Translate back from origin
    return {
      x: scaledX + centerX,
      y: scaledY + centerY
    };
  }
  
  // Transform all points
  ts.points = ts.points.map(transformPoint);
  
  // Transform all handles
  if (ts.handles) {
    ts.handles = ts.handles.map(h => ({
      hin: transformPoint(h.hin),
      hout: transformPoint(h.hout)
    }));
  }
}
  
  function getIncrements() {
    const divisor = (calibrationMode === 'coarse') ? 10 : 1;
    return {
      step: state.step / divisor,
      zoom: state.zoomInc / divisor,
      sx: state.sxInc / divisor,
      sy: state.syInc / divisor,
      rot: state.rotInc / divisor
    };
  }
function maybeEnableTrace(){ 
  if (imageUploaded && !isDefaultTransform()) {
    $tabTrace.prop('disabled', false).removeAttr('disabled');
  }
}
function applyCalibrationToPath() {
  const key = (activeSize||'_single');
  const ts = traceBySize.get(key);
  if (!ts || !ts.points || ts.points.length === 0) return;
  
  // Only apply if there are actual transforms to apply
  if (isDefaultTransform()) return;
  
  const centerX = $wrap.width() / 2;
  const centerY = $wrap.height() / 2;
  const scaleX = state.sx * state.scale;
  const scaleY = state.sy * state.scale;
  const rotation = state.rot * Math.PI / 180; // Convert to radians
  
  // Transform each point
  ts.points = ts.points.map(p => {
    // Translate to origin
    let x = p.x - centerX;
    let y = p.y - centerY;
    
    // Apply rotation
    const rotatedX = x * Math.cos(rotation) - y * Math.sin(rotation);
    const rotatedY = x * Math.sin(rotation) + y * Math.cos(rotation);
    
    // Apply scale
    x = rotatedX * scaleX;
    y = rotatedY * scaleY;
    
    // Translate back and apply translation
    return {
      x: x + centerX + state.tx,
      y: y + centerY + state.ty
    };
  });
  
  // Transform handles the same way
  if (ts.handles) {
    ts.handles = ts.handles.map(h => {
      const transformPoint = (point) => {
        let x = point.x - centerX;
        let y = point.y - centerY;
        
        const rotatedX = x * Math.cos(rotation) - y * Math.sin(rotation);
        const rotatedY = x * Math.sin(rotation) + y * Math.cos(rotation);
        
        x = rotatedX * scaleX;
        y = rotatedY * scaleY;
        
        return {
          x: x + centerX + state.tx,
          y: y + centerY + state.ty
        };
      };
      
      return {
        hin: transformPoint(h.hin),
        hout: transformPoint(h.hout)
      };
    });
  }
  
  // Reset transform state since it's now baked into the path
  resetTransform();
}	

function syncPathWithImage() {
  if (!$tabCal.hasClass('is-active') || !($path.attr('d')||'').trim()) {
    return;
  }
  
  const svgTransform = `translate(${state.tx}px,${state.ty}px) rotate(${state.rot}deg) scale(${state.scale}, ${state.scale}) scale(${state.sx}, ${state.sy})`;
  
  $svg.css({
    transform: svgTransform,
    transformOrigin: 'center center',
    display: 'block',
    pointerEvents: 'none'
  });
}
	
  // Gear / increments
  $gear.on('click', function(){ $settings.toggle(); });
  $settings.on('input', '.clipon-inc', function(){
    state.step    = parseFloat($incStep.val()) || state.step;
    state.zoomInc = parseFloat($incZoom.val()) || state.zoomInc;
    state.sxInc   = parseFloat($incSX.val())   || state.sxInc;
    state.syInc   = parseFloat($incSY.val())   || state.syInc;
    state.rotInc  = parseFloat($incRot.val())  || state.rotInc;
  });

	// Frame/Clip-On tabs
  const $frameTabBtn = $panels.cal.find('.frametab');
  const $cliponTabBtn = $panels.cal.find('.clipontab');
  const $frameFieldsBlock = $panels.cal.find('.frame-fields-block');
  const $cliponFieldsBlock = $panels.cal.find('.clipon-fields-block');
  
  $frameTabBtn.on('click', function() {
    $frameTabBtn.addClass('is-active');
    $cliponTabBtn.removeClass('is-active');
    $frameFieldsBlock.show();
    $cliponFieldsBlock.hide();
  });
  
  $cliponTabBtn.on('click', function() {
    $cliponTabBtn.addClass('is-active');
    $frameTabBtn.removeClass('is-active');
    $cliponFieldsBlock.show();
    $frameFieldsBlock.hide();
  });
	
  // Calibration mode switch
  $calibrationSwitch.on('click', function() {
    const wasOn = $(this).hasClass('is-on');
    $(this).toggleClass('is-on');
    calibrationMode = wasOn ? 'fine' : 'coarse';
  });

  // D-pad
  $('#panel-cal .dpad [data-act]').on('click', function(){
    const a = $(this).data('act');
    const inc = getIncrements();
    if (a==='up') state.ty -= inc.step;
    else if (a==='down') state.ty += inc.step;
    else if (a==='left') state.tx -= inc.step;
    else if (a==='right') state.tx += inc.step;
    else if (a==='center') { state.tx=0; state.ty=0; }
    applyTransform();
    if ((activeSize||'')===baseSizeOnUpload && imageUploaded) baseCalibrated = !isDefaultTransform();
    maybeEnableTrace();
  });

  // Strips
  $('#panel-cal .strip').on('click', 'button', function(){
    const $strip = $(this).closest('.strip');
    const plus = $(this).hasClass('plus');
    const kind = $strip.data('strip');
    const inc = getIncrements();
    if (kind==='zoom')   state.scale = clamp(state.scale + (plus? inc.zoom : -inc.zoom), 0.05, 50);
    if (kind==='scaleX') state.sx    = clamp(state.sx    + (plus? inc.sx   : -inc.sx),   0.05, 50);
    if (kind==='scaleY') state.sy    = clamp(state.sy    + (plus? inc.sy   : -inc.sy),   0.05, 50);
    if (kind==='rotate') {
      const v = plus ? state.rot + inc.rot : state.rot - inc.rot;
      state.rot = ((v + 180) % 360 + 360) % 360 - 180;
    }
    applyTransform();
    if ((activeSize||'')===baseSizeOnUpload && imageUploaded) baseCalibrated = !isDefaultTransform();
    maybeEnableTrace();
  });

  // ---------- Guides ----------
  function mmToPx(mm){ const dpi = getDpi(); return (mm||0) * (dpi/25.4); }
  function getDpi(){ const v = parseFloat($frame.DPI.val()); return (isFinite(v)&&v>0)? v : 350; }
  function anyGuideValues(){ return ['A','B','Br','H'].some(k => parseFloat($frame[k].val())>0); }
  function updateGuides(){
    if (!$wrap.is(':visible')) return;
    const show = $tabCal.hasClass('is-active') && anyGuideValues();
    $guides.toggle(show); if (!show) return;

    const a  = parseFloat($frame.A.val()) || 0;
    const b  = parseFloat($frame.B.val()) || 0;
    const br = parseFloat($frame.Br.val())|| 0;
    const h  = parseFloat($frame.H.val()) || 0;
    const mat= ($frame.Mat.val()||'plastic').toLowerCase();
    const col= $frame.Col.val() || '#008000';
    $guides.find('.g').css('background', col);

    const H = $box.innerHeight();
    const centerY = H/2;

    const spacing12 = (mat==='plastic') ? 1.9 : (mat==='metal' ? 1.2 : 1);
    const aOffset   = (mat==='plastic') ? 0.8 : (mat==='metal' ? 0.4 : 0);

    const v1 = br ? mmToPx(br/2) : null;
    const v2 = (v1!=null) ? v1 + mmToPx(spacing12) : null;
    const v3 = (v2!=null && a) ? v2 + mmToPx(Math.max(a - aOffset, 0)) : null;

    placeV($v1, v1); placeV($v2, v2); placeV($v3, v3);

    const bOff = b ? mmToPx(b/2) : null;
    const hOff = h ? mmToPx(h/2) : null;

    placeH($h1, b ? centerY - bOff : null);
    placeH($h2, b ? centerY + bOff : null);
    placeH($h3, h ? centerY - hOff : null);
    placeH($h4, h ? centerY + hOff : null);
  }
  function placeV($el, rightPx){ if(!$el.length){return;} if(rightPx==null){$el.hide();return;} $el.show().css({right: Math.round(rightPx)+'px', left:'auto', top:0, height:'100%'}); }
  function placeH($el, topPx){ if(!$el.length){return;} if(topPx==null){$el.hide();return;} $el.show().css({top: Math.round(topPx)+'px', left:0, width:'100%'}); }

  // Snap to the right-most vertical guideline (v1)
  function v1X(){
    const wrapEl = $wrap[0];
    if (!wrapEl) return 0;

    // Prefer actual DOM position if v1 is visible
    if ($v1.length && $v1[0].offsetParent) {
      const wrapRect = wrapEl.getBoundingClientRect();
      const v1Rect   = $v1[0].getBoundingClientRect();
      return Math.max(0, v1Rect.left - wrapRect.left);
    }

    // Fallback: compute from Bridge (br/2) if guides are hidden
    const W   = wrapEl.getBoundingClientRect().width;
    const br  = parseFloat($frame.Br.val()) || 0;
    if (!(br > 0)) return W; // no bridge -> far right
    const xFromRight = mmToPx(br / 2);
    return Math.max(0, W - xFromRight);
  }

  $panels.cal.on('input change', '.clipon-eye-a, .clipon-eye-b, .clipon-bridge, .clipon-height, .clipon-material, .clipon-dpi, .clipon-color', updateGuides);

  // ---------- Tabs (Calibrate / Trace) ----------
function captureTransformedImage(forceOverwrite) {
  const currentSize = activeSize || 'base';
  const $sizeImagesInput = $hidden.find('input[name="_clipon_size_images"]');
  const sizeImages = readJSON($sizeImagesInput.val(), {});
  
  // Check if image already exists and we're not forcing overwrite
  if (sizeImages[currentSize] && !forceOverwrite && !isDefaultTransform()) {
    console.log('Image already exists for', currentSize, '- skipping bake');
    activateTab('trace');
    return;
  }
  
  if (isDefaultTransform() && sizeImages[currentSize]) {
    activateTab('trace');
    return;
  }
  
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const img = new Image();
  
  img.onload = function() {
    canvas.width = $wrap.width();
    canvas.height = $wrap.height();
    
    // Fill with white background for JPEG
    ctx.fillStyle = '#FFFFFF';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    ctx.save();
    ctx.translate(canvas.width/2, canvas.height/2);
    ctx.translate(state.tx, state.ty);
    ctx.rotate(state.rot * Math.PI / 180);
    ctx.scale(state.sx * state.scale, state.sy * state.scale);
    ctx.translate(-img.width/2, -img.height/2);
    ctx.drawImage(img, 0, 0);
    ctx.restore();
    
    canvas.toBlob(function(blob) {
      if (!blob) return;
      
      const reader = new FileReader();
      reader.onloadend = function() {
        const base64data = reader.result;
        
        $.ajax({
          url: CLIPON_VARS.ajaxUrl,
          type: 'POST',
          data: {
            action: 'clipon_upload_size_image',
            nonce: CLIPON_VARS.uploadNonce,
            post_id: CLIPON_VARS.postId,
            size: currentSize,
            image_data: base64data
          },
          success: function(response) {
            if (response.success && response.data && response.data.url) {
              sizeImages[currentSize] = response.data.url;
              $sizeImagesInput.val(JSON.stringify(sizeImages));
              
              $img.attr('src', response.data.url);
              $hid.url.val(response.data.url);
              $hid.id.val(response.data.attachment_id);
              console.log('✓ Baked image for size:', currentSize, '→', response.data.url);
				
              
              // Save current transform before resetting
              const savedTransform = {
                tx: state.tx,
                ty: state.ty,
                rot: state.rot,
                scale: state.scale,
                sx: state.sx,
                sy: state.sy
              };
              
              // Reset transforms
              resetTransform();
              applyTransform();
              
              // Transform path coordinates using the saved transform
              const key = currentSize || '_single';
              const ts = traceBySize.get(key);
              if (ts && ts.points && ts.points.length > 0) {
                transformPathCoordinates(ts, savedTransform);
                persistAll();
              }
              
              activateTab('trace');
            }
          },
          error: function() {
            console.error('Failed to upload transformed image');
          }
        });
      };
      reader.readAsDataURL(blob);
    }, 'image/jpeg', 0.85);
  };
  
  img.src = $hid.url.val();
}
function activateTab(key){
  // If entering Trace mode from Calibrate with transforms, bake image first
if ($tabCal.hasClass('is-active') && key === 'trace') {
  const hasTransforms = !isDefaultTransform();
  
  // If transforms exist, always bake and transform coordinates
  if (hasTransforms) {
    const $sizeImagesInput = $hidden.find('input[name="_clipon_size_images"]');
    const sizeImages = readJSON($sizeImagesInput.val(), {});
    const currentSize = activeSize || 'base';
    const imageExists = !!sizeImages[currentSize];
    captureTransformedImage(imageExists); // forceOverwrite if image exists
    return;
  }
}
  
  $tabs.removeClass('is-active');
  $tabs.filter('[data-panel="'+key+'"]').addClass('is-active');
  Object.keys($panels).forEach(k => $panels[k].toggleClass('is-active', k===key));
  
  if (key==='cal') { 
    $guides.toggle(anyGuideValues()); 
    $svg.css('pointer-events','none');
    syncPathWithImage();
    showIdleView();
  }
else if (key==='trace') {
  $guides.hide(); 
  $svg.css('pointer-events','auto');
  $svg.css('transform', 'none');
  
  const ts = traceBySize.get(activeSize||'_single');
  if (!ts) {
    beginPlacingTrace(activeSize||'_single');
  } else {
    // Path already exists - just show it with edit controls, don't redraw
    $svg.css('display', 'block');
    updateTraceVisibility();
  }
}
  else {
    $guides.hide(); 
    $svg.css('pointer-events','auto');
    showIdleView();
  }
}
  $tabs.on('click', function () { if (!$(this).is('[disabled]')) activateTab($(this).data('panel')); });
  if ($editBtn.length)  $editBtn.on('click',  ()=>activateTab('cal'));
  if ($traceBtn.length) $traceBtn.on('click', ()=>{ if (!$traceBtn.is('[disabled]')) activateTab('trace'); });

  // ---------- Size tabs ----------
  $sizeTabs.on('click', function(){
    const $t = $(this);
    if ($t.hasClass('is-active')) return;
    
    // Auto-generate paths for all sizes before switching (if current size has a completed path)
    const currentKey = (activeSize || '_single');
    const currentTs = traceBySize.get(currentKey);
    if (currentTs && currentTs.points && currentTs.points.length >= 4 && $sizeTabs.length > 1) {
      // Check if we haven't already generated for all sizes
      let needsGeneration = false;
      $sizeTabs.each(function() {
        const sz = $(this).data('size');
        if (sz !== activeSize && !traceBySize.has(sz)) {
          needsGeneration = true;
          return false; // break
        }
      });
      
      if (needsGeneration) {
        generatePathsForAllSizes(currentKey);
      }
    }
    
    // Generate scaled images for other sizes if current size has a baked image
    const $sizeImagesInput = $hidden.find('input[name="_clipon_size_images"]');
    const sizeImages = readJSON($sizeImagesInput.val(), {});
    const currentSize = activeSize || 'base';
    if (sizeImages[currentSize]) {
      generateScaledImagesForOtherSizes();
    }
    
    // Only persist if there's an image uploaded (avoid JSON parse errors on empty data)
    if (imageUploaded) {
      persistAll();
    }
    
    $sizeTabs.removeClass('is-active').attr('aria-selected','false');
    $t.addClass('is-active').attr('aria-selected','true');
    const next = $t.data('size');
    $hid.activeSz.val(next);
    activeSize = next;
    loadSize(next);
  });

function generateScaledImagesForOtherSizes() {
  const $sizeImagesInput = $hidden.find('input[name="_clipon_size_images"]');
  const sizeImages = readJSON($sizeImagesInput.val(), {});
  const currentSize = activeSize || 'base';
  
  // Get all size tabs
  const $sizeTabs = $root.find('.clipon-size-tab');
  const sizes = [];
  $sizeTabs.each(function() {
    const sz = $(this).data('size');
    if (sz && sz !== currentSize && !sizeImages[sz]) {
      sizes.push(sz);
    }
  });
  
  if (sizes.length === 0) {
    console.log('All size images already exist');
    return;
  }
  
  // Get current size A value
  const currentAB = parseAB(currentSize);
  const currentA = currentAB.a || baseAOnUpload || parseFloat($frame.A.val()) || 0;
  
  if (currentA <= 0) {
    console.log('Cannot generate sized images: current A value is 0');
    return;
  }
  
  const currentImageUrl = sizeImages[currentSize] || $hid.url.val();
  if (!currentImageUrl) {
    console.log('No source image to generate from');
    return;
  }
  
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const img = new Image();
  
  img.onload = function() {
    canvas.width = $wrap.width();
    canvas.height = $wrap.height();
    
    // Fill with white background for JPEG
    ctx.fillStyle = '#FFFFFF';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
    
    const uploadPromises = [];
    
    sizes.forEach(function(sizeLabel) {
      const targetAB = parseAB(sizeLabel);
      const targetA = targetAB.a;
      if (!targetA || targetA <= 0) return;
      
      const scaleFactor = targetA / currentA;
      
      const scaledCanvas = document.createElement('canvas');
      const scaledCtx = scaledCanvas.getContext('2d');
      scaledCanvas.width = canvas.width;
      scaledCanvas.height = canvas.height;
      
      // Fill with white background for JPEG
      scaledCtx.fillStyle = '#FFFFFF';
      scaledCtx.fillRect(0, 0, scaledCanvas.width, scaledCanvas.height);
      
      scaledCtx.save();
      scaledCtx.translate(canvas.width / 2, canvas.height / 2);
      scaledCtx.scale(scaleFactor, scaleFactor);
      scaledCtx.translate(-canvas.width / 2, -canvas.height / 2);
      scaledCtx.drawImage(canvas, 0, 0);
      scaledCtx.restore();
      
      scaledCanvas.toBlob(function(blob) {
        const reader = new FileReader();
        reader.onloadend = function() {
          uploadPromises.push(
            uploadImageToMediaLibrary(sizeLabel, reader.result).then(function(result) {
              if (result.success && result.data && result.data.url) {
                sizeImages[sizeLabel] = result.data.url;
                console.log('✓ Generated image for size:', sizeLabel, '→', result.data.url);
              }
            }).catch(function(err) {
              console.error('Failed to upload image for size:', sizeLabel, err);
            })
          );
        };
        reader.readAsDataURL(blob);
      }, 'image/jpeg', 0.85);
    });
    
    Promise.all(uploadPromises).then(function() {
      $sizeImagesInput.val(JSON.stringify(sizeImages));
      console.log('✓ All size images generated');
    });
  };
  
  img.src = currentImageUrl;
}

function uploadImageToMediaLibrary(size, imageData) {
  return new Promise(function(resolve, reject) {
    $.ajax({
      url: CLIPON_VARS.ajaxUrl,
      type: 'POST',
      data: {
        action: 'clipon_upload_size_image',
        nonce: CLIPON_VARS.uploadNonce,
        post_id: CLIPON_VARS.postId,
        size: size,
        image_data: imageData
      },
      success: function(response) {
        resolve(response);
      },
      error: function(xhr, status, error) {
        reject(error);
      }
    });
  });
}

function parseAB(lbl){
  // Handle the specific square symbol ▢ used in your size labels
  const m = String(lbl||'').match(/(\d+(?:\.\d+)?)\s*▢\s*(\d+(?:\.\d+)?)/u);
  return m ? { a:+m[1], bridge:+m[2] } : {a:0, bridge:0};
}

  const sizeMap = (function(){
    try { return JSON.parse($hid.sizesMap.val() || '{}') || {}; }
    catch(_){ return {}; }
  })();

  function persistAll(){
    persistTransform(); persistFrame(); persistTrace(); persistDrill(); persistAttributes();
    const key = (activeSize||'_single');
    
    // Safely parse JSON, use defaults if parsing fails
    try {
      sizeMap[key] = {
        transform: JSON.parse($hid.transform.val() || '{}') || {},
        frame:     JSON.parse($hid.frame.val() || '{}') || {},
        path:      $hid.path.val() || '',
        points:    JSON.parse($hid.points.val() || '[]') || [],
        handles:   JSON.parse($hid.handles.val() || '[]') || [],
        drill:     JSON.parse($hid.drill.val() || '{}') || {},
        attributes: readAttributes()
      };
      $hid.sizesMap.val(JSON.stringify(sizeMap));
    } catch(e) {
      console.warn('Error persisting data:', e);
    }
  }
  
function readAttributes() {
  const attrs = {};
  
  // Read all position fields (both selects and static values)
  $('.clipon-position-field').each(function() {
    const $field = $(this);
    const pos = $field.data('position');
    if (!pos) return;
    
    // Try to read from select dropdown first
    const $select = $field.find('.clipon-position-select');
    if ($select.length) {
      const val = $select.val();
      if (val) attrs[pos] = val;
    } else {
      // If no select, read from static value div
      const $valueDiv = $field.find('.clipon-position-value');
      if ($valueDiv.length) {
        const val = $valueDiv.text().trim();
        if (val && val !== '—') attrs[pos] = val;
      }
    }
  });
  
  // Read magnetic bridge (select or static)
  const $bridgeField = $('.clipon-bridge-field');
  const $bridgeSelect = $bridgeField.find('.clipon-bridge-select');
  if ($bridgeSelect.length) {
    const val = $bridgeSelect.val();
    if (val) attrs['magnetic-bridge'] = val;
  } else {
    const $bridgeValue = $bridgeField.find('.clipon-position-value');
    if ($bridgeValue.length) {
      const val = $bridgeValue.text().trim();
      if (val && val !== '—') attrs['magnetic-bridge'] = val;
    }
  }
  
  return attrs;
}
  
  function applyAttributes(attrs) {
    if (!attrs) return;
    $('.clipon-position-select').each(function() {
      const pos = $(this).data('position');
      if (attrs[pos]) $(this).val(attrs[pos]);
    });
    if (attrs['magnetic-bridge']) {
      $('.clipon-bridge-select').val(attrs['magnetic-bridge']);
    }
  }
  
  function persistAttributes() {
    // Attributes are saved in persistAll -> sizeMap
  }

  // ---- controls format helpers (convert old -> per-anchor {hin,hout}) ----
  function normalizeControls(handles, points){
    if (!handles) return null;
    const n = (points||[]).length;
    if (!n) return null;

    // already per-anchor?
    if (Array.isArray(handles) && handles.length && handles[0] && (handles[0].hin && handles[0].hout)) return handles;

    // legacy: [{cp1,cp2}] meaning cp1 is outgoing from P_i, cp2 is incoming to P_(i+1)
    if (Array.isArray(handles) && handles[0] && handles[0].cp1 && handles[0].cp2) {
      return points.map((_, i) => ({
        hin:  { x:+handles[(i-1+n)%n].cp2.x, y:+handles[(i-1+n)%n].cp2.y },
        hout: { x:+handles[i].cp1.x,         y:+handles[i].cp1.y }
      }));
    }

    // earlier segment form {c1Seg, c2Seg}
    if (handles.c1Seg && handles.c2Seg) {
      return points.map((_, i) => ({
        hin:  { x:+handles.c2Seg[(i-1+n)%n].x, y:+handles.c2Seg[(i-1+n)%n].y },
        hout: { x:+handles.c1Seg[i].x,         y:+handles.c1Seg[i].y }
      }));
    }
    return null;
  }

  // Build per-anchor handles from Catmull-Rom (natural cubic)
  function catmullHandles(points, tension = 1/6){
    const n = points.length;
    const c1Seg = new Array(n), c2Seg = new Array(n);
    for (let i=0;i<n;i++){
      const p0 = points[(i-1+n)%n], p1 = points[i], p2 = points[(i+1)%n], p3 = points[(i+2)%n];
      c1Seg[i] = { x: p1.x + (p2.x - p0.x)*tension, y: p1.y + (p2.y - p0.y)*tension };
      c2Seg[i] = { x: p2.x - (p3.x - p1.x)*tension, y: p2.y - (p3.y - p1.y)*tension };
    }
    const per = new Array(n);
    for (let i=0;i<n;i++){
      per[i] = {
        hin:  { x:c2Seg[(i-1+n)%n].x, y:c2Seg[(i-1+n)%n].y },
        hout: { x:c1Seg[i].x,         y:c1Seg[i].y }
      };
    }
    return per;
  }

  function buildPathD(points, handles){
    if (!points || points.length<3 || !handles || !handles.length) return '';
    const n = points.length;
    let d = `M ${points[0].x},${points[0].y}`;
    for (let i=0;i<n;i++){
      const next = points[(i+1)%n];
      const c1 = handles[i].hout;          // outgoing from current
      const c2 = handles[(i+1)%n].hin;     // incoming to next
      d += ` C ${c1.x},${c1.y} ${c2.x},${c2.y} ${next.x},${next.y}`;
    }
    return d + ' Z';
  }

  function drawTrace(sizeKey){
    const ts = traceBySize.get(sizeKey); if (!ts) return;
    svgClear($gDrills); svgHide($gDrills);
    svgClear($gAnch);   svgHide($gAnch);
    svgClear($gCtrl);   svgHide($gCtrl);
    svgClear($gLines);  svgHide($gLines);
    $path.attr('d','');

    if (ts.mode==='placing'){
      if (ts.points.length){
        let d = '';
        ts.points.forEach((p, idx)=>{ d += (idx===0 ? `M ${p.x},${p.y}` : ` L ${p.x},${p.y}`); });
        $path.attr('d', d);
      }
      showMaskForPlaced(sizeKey);
      persistTrace();
      $svg.css({ display:'block', pointerEvents:'auto' });
      return;
    }

    // edit
    $path.attr('d', buildPathD(ts.points, ts.handles));

    const n = ts.points.length;
    for (let i=0;i<n;i++){
      const p = ts.points[i], hin = ts.handles[i].hin, hout = ts.handles[i].hout;

      // control lines
      svgAppend($gLines, svgEl('line', { x1:p.x, y1:p.y, x2:hin.x,  y2:hin.y,  class:'handle-line', 'vector-effect':'non-scaling-stroke' }));
      svgAppend($gLines, svgEl('line', { x1:p.x, y1:p.y, x2:hout.x, y2:hout.y, class:'handle-line', 'vector-effect':'non-scaling-stroke' }));

      // handles
      svgAppend($gCtrl, svgEl('circle', { cx:hin.x,  cy:hin.y,  r:5, class:'cp', 'data-index':i, 'data-kind':'hin',  'vector-effect':'non-scaling-stroke' }));
      svgAppend($gCtrl, svgEl('circle', { cx:hout.x, cy:hout.y, r:5, class:'cp', 'data-index':i, 'data-kind':'hout', 'vector-effect':'non-scaling-stroke' }));

      // Create larger invisible hitbox for easier clicking
      svgAppend($gAnch, svgEl('circle', { 
        cx:p.x, 
        cy:p.y, 
        r:15, 
        class:'anchor anchor-hitbox', 
        'data-index':i, 
        'vector-effect':'non-scaling-stroke',
        style: 'fill:transparent;stroke:none;cursor:grab;'
      }));
      
      // Create visible anchor circle
      svgAppend($gAnch, svgEl('circle', { 
        cx:p.x, 
        cy:p.y, 
        r:6, 
        class:'anchor', 
        'data-index':i, 
        'vector-effect':'non-scaling-stroke',
        style: 'pointer-events:none;'
      }));
    }

    $svg.css({ display:'block', pointerEvents:'auto' });
    svgShow($gAnch); svgShow($gCtrl); svgShow($gLines);
    enableDragging(sizeKey);
    persistTrace();

    // refresh drills if visible/needed
    if (drillsVisible(ts)) { drawDrills(); }
    updateTraceVisibility();
  }

  const QUADS = ['br','bl','tl','tr'];

  function beginPlacingTrace(sizeKey){
    traceBySize.set(sizeKey, { 
      mode:'placing', 
      points:[], 
      handles:null, 
      drillOpen:false, 
      magMode:'spring', 
      springBackup:null,
      drillData: {
        mode: 'spring',
        leftTopX: 20,
        leftBottomX: 20,
        bridge: 10,
        factorSpring: 44,
        factorMag: 2.25
      }
    });
    $path.attr('d',''); showMaskForPlaced(sizeKey); $traceMsg.show(); $download.hide();
    $svg.css({ display:'block', pointerEvents:'auto' });
  }

  function showMaskForPlaced(sizeKey){
    const ts = traceBySize.get(sizeKey);
    $mask.show().find('.quad').removeClass('revealed');
    const placed = (ts && ts.points) ? ts.points.length : 0;
    for (let i=0;i<placed;i++) $mask.find('.'+QUADS[i]).addClass('revealed');
    const next = QUADS[placed]; if (next) $mask.find('.'+next).addClass('revealed');
  }
  function hideMask(){ $mask.hide().find('.quad').removeClass('revealed'); }

  function pointerXY(e){
    const r = $wrap[0].getBoundingClientRect();
    return { x: clamp(e.clientX - r.left, 0, r.width), y: clamp(e.clientY - r.top, 0, r.height) };
  }
  function whichQuarter(clientX, clientY){
    const r = $wrap[0].getBoundingClientRect();
    const midX = r.left + r.width/2, midY = r.top + r.height/2;
    if (clientX>=midX && clientY>=midY) return 'br';
    if (clientX< midX && clientY>=midY) return 'bl';
    if (clientX< midX && clientY< midY) return 'tl';
    return 'tr';
  }

  // place points only in revealed quarter
  $wrap.on('click', function(e){
    if (!$tabTrace.hasClass('is-active')) return;
    const key = (activeSize||'_single');
    let ts = traceBySize.get(key);
    if (!ts || ts.mode!=='placing') return;

    const want = QUADS[ts.points.length];
    const q = whichQuarter(e.clientX, e.clientY);
    if (q !== want) return;

    const pt = pointerXY(e);
    ts.points.push(pt);
    drawTrace(key); persistTrace();

    if (ts.points.length>=4) {
      ts.mode='edit';
      ts.handles = catmullHandles(ts.points);
      hideMask(); $traceMsg.hide();
      drawTrace(key); persistTrace();
      $download.show();
    }
  });

  // Generate scaled paths and images for all sizes based on the base size
  function generatePathsForAllSizes(baseSizeKey) {
    const baseTs = traceBySize.get(baseSizeKey);
    if (!baseTs || !baseTs.points || baseTs.points.length < 4) return;
    
    // Get base size A value
    const baseLabel = baseSizeKey === '_single' ? activeSize : baseSizeKey;
    const baseAB = parseAB(baseLabel);
    const baseA = baseAB.a || baseAOnUpload || parseFloat($frame.A.val()) || 0;
    
    if (baseA <= 0) return;
    
    const centerX = $wrap.width() / 2;
    const centerY = $wrap.height() / 2;
    
    // Get base data from sizeMap
    const baseData = sizeMap[baseSizeKey] || {
      transform: { tx: state.tx, ty: state.ty, scale: state.scale, sx: state.sx, sy: state.sy, rot: state.rot },
      frame: {
        a: parseFloat($frame.A.val()) || 0,
        b: parseFloat($frame.B.val()) || 0,
        bridge: parseFloat($frame.Br.val()) || 0,
        h: parseFloat($frame.H.val()) || 0,
        material: $frame.Mat.val() || 'plastic',
        dpi: getDpi(),
        color: $frame.Col.val() || '#008000'
      },
      points: baseTs.points,
      handles: baseTs.handles,
      drill: baseTs.drillData || {},
		  attributes: readAttributes()

    };
    
    // Loop through all size tabs
    $sizeTabs.each(function() {
      const targetLabel = $(this).data('size');
      if (targetLabel === baseLabel) return; // Skip the base size itself
      
      // Get target A value from size label
      const targetAB = parseAB(targetLabel);
      const targetA = targetAB.a || 0;
      
      if (targetA <= 0) return;
      
      // Calculate scale factor (works for both larger AND smaller sizes)
      const k = targetA / baseA;
      
      // Scale transform
      const scaledTransform = Object.assign({}, baseData.transform || {});
      scaledTransform.scale = +((scaledTransform.scale || 1) * k).toFixed(6);
      
      // Scale points
      const scaledPoints = baseTs.points.map(p => ({
        x: centerX + (p.x - centerX) * k,
        y: centerY + (p.y - centerY) * k
      }));
      
      // Scale handles
      const scaledHandles = baseTs.handles.map(h => ({
        hin: {
          x: centerX + (h.hin.x - centerX) * k,
          y: centerY + (h.hin.y - centerY) * k
        },
        hout: {
          x: centerX + (h.hout.x - centerX) * k,
          y: centerY + (h.hout.y - centerY) * k
        }
      }));
      
      // Generate scaled path
      const scaledPath = buildPathD(scaledPoints, scaledHandles);
      
      // Create frame data for this size
      const scaledFrame = Object.assign({}, baseData.frame || {}, {
        a: targetA,
        bridge: targetAB.bridge
      });
      
      // Store in sizeMap
      sizeMap[targetLabel] = {
        transform: scaledTransform,
        frame: scaledFrame,
        path: scaledPath,
        points: scaledPoints,
        handles: scaledHandles,
        drill: baseData.drill || {},
        attributes: baseData.attributes || {}
      };
      
      // Also create in traceBySize for immediate use
      traceBySize.set(targetLabel, {
        mode: 'edit',
        points: scaledPoints,
        handles: scaledHandles,
        drillOpen: baseTs.drillOpen || false,
        magMode: baseTs.magMode || 'spring',
        springBackup: null,
        drillData: baseTs.drillData || {
          mode: 'spring',
          leftTopX: 20,
          leftBottomX: 20,
          bridge: 10,
          factorSpring: 44,
          factorMag: 2.25
        }
      });
    });
    
    // Persist the updated sizeMap
    $hid.sizesMap.val(JSON.stringify(sizeMap));
    
    console.log('✓ Generated scaled paths for all sizes based on:', baseLabel);
    
    // Now generate and upload scaled images for all sizes
    generateAndUploadSizeImages(baseSizeKey);
  }

  // Generate scaled images for all sizes and upload to media library
  function generateAndUploadSizeImages(baseSizeKey) {
    console.log('Generating scaled images for all sizes...');
    
    const $sizeImagesInput = $hidden.find('input[name="_clipon_size_images"]');
    const existingSizeImages = readJSON($sizeImagesInput.val(), {});
    const sizeImages = Object.assign({}, existingSizeImages);
    
    // Get all size buttons
    const $sizeTabs = $root.find('.clipon-size-tab');
    const sizes = [];
    $sizeTabs.each(function() {
      sizes.push($(this).data('size'));
    });
    
    if (sizes.length === 0) return;
    
    // Get base size dimensions
    const baseLabel = baseSizeKey === '_single' ? activeSize : baseSizeKey;
    const baseAB = parseAB(baseLabel);
    const baseA = baseAB.a || baseAOnUpload || parseFloat($frame.A.val()) || 0;
    
    if (baseA <= 0) {
      console.log('Cannot generate sized images: base A value is 0');
      return;
    }
    
    // Get current transformed/calibrated image
    const currentImg = $img[0];
    if (!currentImg || !currentImg.src || currentImg.src.includes('data:image')) {
      console.log('Using current displayed image for size generation');
    }
    
    // Create a canvas with the current view
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    // Set canvas size to match wrapper
    const wrapWidth = $wrap.width();
    const wrapHeight = $wrap.height();
    canvas.width = wrapWidth;
    canvas.height = wrapHeight;
    
    // Fill with white background for JPEG
    ctx.fillStyle = '#FFFFFF';
    ctx.fillRect(0, 0, wrapWidth, wrapHeight);
    
    // Draw the current state (image with transformations)
    const t = readTransform();
    ctx.save();
    ctx.translate(wrapWidth / 2, wrapHeight / 2);
    ctx.rotate((t.rotation || 0) * Math.PI / 180);
    ctx.scale(t.scaleX || 1, t.scaleY || 1);
    ctx.scale(t.scale || 1, t.scale || 1);
    ctx.translate(t.tx || 0, t.ty || 0);
    ctx.translate(-wrapWidth / 2, -wrapHeight / 2);
    ctx.drawImage(currentImg, 0, 0, wrapWidth, wrapHeight);
    ctx.restore();
    
    // Now create scaled versions for each size
    const uploadPromises = [];
    
    sizes.forEach(sizeLabel => {
      const targetAB = parseAB(sizeLabel);
      const targetA = targetAB.a;
      
      if (!targetA || targetA <= 0) return;
      
      // Calculate scale factor
      const scaleFactor = targetA / baseA;
      
      // Create scaled canvas
      const scaledCanvas = document.createElement('canvas');
      const scaledCtx = scaledCanvas.getContext('2d');
      scaledCanvas.width = wrapWidth;
      scaledCanvas.height = wrapHeight;
      
      // Fill with white background for JPEG
      scaledCtx.fillStyle = '#FFFFFF';
      scaledCtx.fillRect(0, 0, wrapWidth, wrapHeight);
      
      // Draw scaled image
      scaledCtx.save();
      scaledCtx.translate(wrapWidth / 2, wrapHeight / 2);
      scaledCtx.scale(scaleFactor, scaleFactor);
      scaledCtx.translate(-wrapWidth / 2, -wrapHeight / 2);
      scaledCtx.drawImage(canvas, 0, 0);
      scaledCtx.restore();
      
      // Convert to blob and upload
      scaledCanvas.toBlob(function(blob) {
        if (!blob) return;
        
        // Convert blob to base64
        const reader = new FileReader();
        reader.onloadend = function() {
          const base64data = reader.result;
          
          // Upload to media library
          uploadPromises.push(
            uploadImageToMediaLibrary(sizeLabel, base64data).then(function(result) {
              if (result.success && result.data && result.data.url) {
                sizeImages[sizeLabel] = result.data.url;
                console.log('✓ Uploaded image for size:', sizeLabel, '→', result.data.url);
              }
            }).catch(function(err) {
              console.error('Failed to upload image for size:', sizeLabel, err);
            })
          );
        };
        reader.readAsDataURL(blob);
      }, 'image/jpeg', 0.85);
    });
    
    // Wait for all uploads to complete and save
    Promise.all(uploadPromises).then(function() {
      $sizeImagesInput.val(JSON.stringify(sizeImages));
      console.log('✓ All size images uploaded and saved');
    });
  }

  // Upload image to WordPress media library via AJAX
  function uploadImageToMediaLibrary(size, imageData) {
    return new Promise(function(resolve, reject) {
      $.ajax({
        url: CLIPON_VARS.ajaxUrl,
        type: 'POST',
        data: {
          action: 'clipon_upload_size_image',
          nonce: CLIPON_VARS.uploadNonce,
          post_id: CLIPON_VARS.postId,
          size: size,
          image_data: imageData
        },
        success: function(response) {
          resolve(response);
        },
        error: function(xhr, status, error) {
          reject(error);
        }
      });
    });
  }


  function enableDragging(sizeKey){
    const ts = traceBySize.get(sizeKey); if (!ts) return;
    let drag = null;
    let lastClickTime = 0;
    let lastClickIndex = null;

    // Start drag (or detect double-click)
    $svg.off('.dragAnch').on('pointerdown.dragAnch', '.anchor, .cp', function (e) {
      const idx  = +this.getAttribute('data-index');
      const kind = this.classList.contains('anchor') ? 'anchor' : this.getAttribute('data-kind'); // 'hin'|'hout'
      
      // Double-click detection for anchor points
      if (kind === 'anchor') {
        const now = Date.now();
        const timeSinceLastClick = now - lastClickTime;
        
        // If same anchor clicked within 300ms, it's a double-click
        if (idx === lastClickIndex && timeSinceLastClick < 300) {
          e.preventDefault();
          e.stopPropagation();
          
          // Don't allow removing the first 4 points
          if (idx >= 4) {
            removePointFromPath(idx, sizeKey);
          }
          
          // Reset click tracking
          lastClickTime = 0;
          lastClickIndex = null;
          return;
        }
        
        // Track this click
        lastClickTime = now;
        lastClickIndex = idx;
      }
      
      // Normal drag behavior
      drag = { kind, idx };
      e.preventDefault();
    });

    // Dragging
    $(window).off('.dragAnch').on('pointermove.dragAnch', function (e) {
      if (!drag) return;

      const i  = drag.idx;
      const pt = pointerXY(e);

      if (drag.kind === 'anchor') {
        let newX = pt.x;
        let newY = pt.y;

        // TR in Magnetic: X locked to v1X(), Y free
        var trIdx = findTRIndex(ts);
        if (ts.magMode === 'magnetic' && i === trIdx) {
          newX = v1X();
        }

        const old = ts.points[i];
        const dx  = newX - old.x;
        const dy  = newY - old.y;

        ts.points[i] = { x: newX, y: newY };
        ts.handles[i].hin  = { x: ts.handles[i].hin.x  + dx, y: ts.handles[i].hin.y  + dy };
        ts.handles[i].hout = { x: ts.handles[i].hout.x + dx, y: ts.handles[i].hout.y + dy };

      } else {
        // dragging a control point
        let x = pt.x, y = pt.y;

        // In Magnetic mode, keep TR outgoing control ('hout') on v1 vertical
        var trIdx = findTRIndex(ts);
        const isTRMagneticHout = (ts.magMode === 'magnetic' && i === trIdx && drag.kind === 'hout');
        const isTRMagnetic = (ts.magMode === 'magnetic' && i === trIdx);
        
        if (isTRMagneticHout) {
          x = v1X();
        }
        
        // Update the dragged handle
        ts.handles[i][drag.kind] = { x, y };
        
        // CONSTRAINT: Keep handles collinear (on the same line through anchor)
        // Exception: Skip for TR point in magnetic mode (both handles should be free)
        if (!isTRMagnetic) {
          const anchor = ts.points[i];
          const oppositeKind = drag.kind === 'hin' ? 'hout' : 'hin';
          const oppositeHandle = ts.handles[i][oppositeKind];
          
          // Calculate vector from anchor to dragged handle
          const dx = x - anchor.x;
          const dy = y - anchor.y;
          const dragDist = Math.sqrt(dx * dx + dy * dy);
          
          // Calculate original distance of opposite handle from anchor
          const oppDx = oppositeHandle.x - anchor.x;
          const oppDy = oppositeHandle.y - anchor.y;
          const oppDist = Math.sqrt(oppDx * oppDx + oppDy * oppDy);
          
          // Mirror the direction: opposite handle stays on same line but opposite side
          if (dragDist > 0) {
            ts.handles[i][oppositeKind] = {
              x: anchor.x - (dx / dragDist) * oppDist,
              y: anchor.y - (dy / dragDist) * oppDist
            };
          }
        }
      }

      // Redraw path + handles
      $path.attr('d', buildPathD(ts.points, ts.handles));

      svgClear($gLines); svgClear($gCtrl); svgClear($gAnch);
      const n = ts.points.length;
      for (let k=0;k<n;k++){
        const p    = ts.points[k];
        const hin  = ts.handles[k].hin;
        const hout = ts.handles[k].hout;

        svgAppend($gLines, svgEl('line',   { x1:p.x, y1:p.y, x2:hin.x,  y2:hin.y,  class:'handle-line', 'vector-effect':'non-scaling-stroke' }));
        svgAppend($gLines, svgEl('line',   { x1:p.x, y1:p.y, x2:hout.x, y2:hout.y, class:'handle-line', 'vector-effect':'non-scaling-stroke' }));
        svgAppend($gCtrl,  svgEl('circle', { cx:hin.x,  cy:hin.y,  r:5, class:'cp', 'data-index':k, 'data-kind':'hin',  'vector-effect':'non-scaling-stroke' }));
        svgAppend($gCtrl,  svgEl('circle', { cx:hout.x, cy:hout.y, r:5, class:'cp', 'data-index':k, 'data-kind':'hout', 'vector-effect':'non-scaling-stroke' }));

        // Create larger invisible hitbox for easier clicking/double-clicking
        svgAppend($gAnch, svgEl('circle', { 
          cx:p.x, 
          cy:p.y, 
          r:15, 
          class:'anchor anchor-hitbox', 
          'data-index':k, 
          'vector-effect':'non-scaling-stroke',
          style: 'fill:transparent;stroke:none;cursor:grab;'
        }));
        
        // Create visible anchor circle
        const anchorAttrs = { cx:p.x, cy:p.y, r:6, class:'anchor', 'data-index':k, 'vector-effect':'non-scaling-stroke', style:'pointer-events:none;' };
        var trIdx = findTRIndex(ts);
        if (ts.magMode === 'magnetic' && k === trIdx) {
          anchorAttrs.style = 'cursor:ns-resize;pointer-events:none;';
        }
        svgAppend($gAnch, svgEl('circle', anchorAttrs));
      }

      persistTrace();
      if (drillsVisible(ts)) { drawDrills(); }

    }).on('pointerup.dragAnch pointercancel.dragAnch', function(){ drag=null; });
  }

  // ---------- Double-Click to Add/Remove Points ----------
  
  // Double-click on path to add point
  $path.on('dblclick', function(e) {
    e.preventDefault();
    e.stopPropagation();
    
    const key = (activeSize || '_single');
    const ts = traceBySize.get(key);
    if (!ts || ts.mode !== 'edit' || !ts.points || ts.points.length < 4) return;
    
    const pt = pointerXY(e);
    addPointToPath(pt.x, pt.y, key);
  });
  
  // Double-click on anchor points to remove (only points after the first 4)
  $svg.on('dblclick', '.anchor', function(e) {
    e.preventDefault();
    e.stopPropagation();
    
    const idx = +$(this).attr('data-index');
    const key = (activeSize || '_single');
    const ts = traceBySize.get(key);
    
    // Don't allow removing the first 4 points
    if (!ts || idx < 4) return;
    
    removePointFromPath(idx, key);
  });
  
  // ---------- Context Menu for Anchor Points ----------
  
  // Create context menu HTML (only once)
  if ($('#clipon-context-menu').length === 0) {
    var menuHtml = '<div id="clipon-context-menu" class="clipon-context-menu" style="display:none;">' +
                   '<button class="clipon-context-btn" id="clipon-remove-point">Remove Point</button>' +
                   '</div>';
    $('body').append(menuHtml);
    console.log('Context menu created');
  }
  
  var $contextMenu = $('#clipon-context-menu');
  var contextMenuIndex = null;
  var contextMenuSizeKey = null;
  
  // Right-click on anchor points to show context menu
  $svg.on('contextmenu', '.anchor', function(e) {
    console.log('Right-click detected on anchor');
    e.preventDefault();
    e.stopPropagation();
    
    var idx = +$(this).attr('data-index');
    console.log('Anchor index:', idx);
    
    // Don't show menu for the first 4 core points
    if (idx < 4) {
      console.log('Skipping - core point');
      return;
    }
    
    contextMenuIndex = idx;
    contextMenuSizeKey = activeSize || '_single';
    
    // Position menu at click location
    console.log('Showing menu at:', e.clientX, e.clientY);
    $contextMenu.css({
      left: e.clientX + 'px',
      top: e.clientY + 'px',
      display: 'block'
    });
  });
  
  // Click on Remove Point button
  $('#clipon-remove-point').on('click', function(e) {
    console.log('Remove clicked');
    e.preventDefault();
    e.stopPropagation();
    
    if (contextMenuIndex !== null && contextMenuSizeKey !== null) {
      removePointFromPath(contextMenuIndex, contextMenuSizeKey);
    }
    
    $contextMenu.hide();
    contextMenuIndex = null;
    contextMenuSizeKey = null;
  });
  
  // Hide context menu when clicking elsewhere
  $(document).on('click', function(e) {
    if (!$(e.target).closest('#clipon-context-menu').length) {
      $contextMenu.hide();
    }
  });
  
  // Also hide when scrolling
  $(window).on('scroll', function() {
    $contextMenu.hide();
  });
  
  function addPointToPath(x, y, sizeKey) {
    const ts = traceBySize.get(sizeKey);
    if (!ts || !ts.points || ts.points.length < 4) return;
    
    // Find the closest point ON the curve (not clicked position)
    const pathEl = document.getElementById('clipon-trace-path');
    const closestInfo = findClosestPointOnCurve(pathEl, x, y);
    
    if (!closestInfo) return;
    
    // Use the exact point on the curve
    const exactPoint = closestInfo.point;
    const insertIdx = closestInfo.segmentIndex + 1;
    
    // Get tangent at this point to calculate handles
    const tangent = tangentUnitAt(pathEl, closestInfo.lengthOnPath);
    
    // Calculate handle length based on neighboring points
    const prevIdx = closestInfo.segmentIndex;
    const nextIdx = (insertIdx) % ts.points.length;
    const prevPoint = ts.points[prevIdx];
    const nextPoint = ts.points[nextIdx];
    
    // Average distance to neighbors
    const distToPrev = Math.hypot(exactPoint.x - prevPoint.x, exactPoint.y - prevPoint.y);
    const distToNext = Math.hypot(exactPoint.x - nextPoint.x, exactPoint.y - nextPoint.y);
    const avgDist = (distToPrev + distToNext) / 2;
    
    // Handle length proportional to distance (typically 1/3 to 1/2)
    const handleLength = avgDist * 0.35;
    
    // Create handles along the tangent
    const newHandles = {
      hin: {
        x: exactPoint.x - tangent.x * handleLength,
        y: exactPoint.y - tangent.y * handleLength
      },
      hout: {
        x: exactPoint.x + tangent.x * handleLength,
        y: exactPoint.y + tangent.y * handleLength
      }
    };
    
    // Insert the new point
    ts.points.splice(insertIdx, 0, exactPoint);
    ts.handles.splice(insertIdx, 0, newHandles);
    
    // Update both spring and magnetic paths if they exist
    if (ts.springPath) {
      ts.springPath.points = JSON.parse(JSON.stringify(ts.points));
      ts.springPath.handles = JSON.parse(JSON.stringify(ts.handles));
    }
    if (ts.magneticPath) {
      ts.magneticPath.points = JSON.parse(JSON.stringify(ts.points));
      ts.magneticPath.handles = JSON.parse(JSON.stringify(ts.handles));
    }
    
    // Redraw
    drawTrace(sizeKey);
  }
  
  function removePointFromPath(index, sizeKey) {
    const ts = traceBySize.get(sizeKey);
    if (!ts || !ts.points || index < 4 || index >= ts.points.length) return;
    
    // Remove the point and its handles
    ts.points.splice(index, 1);
    ts.handles.splice(index, 1);
    
    // Only regenerate handles for neighboring points to minimize curve change
    // Regenerate for the previous point (now at index-1) and next point (now at index, which was index+1)
    const numPoints = ts.points.length;
    if (numPoints >= 4) {
      // Use catmullHandles to regenerate just the affected handles
      const allHandles = catmullHandles(ts.points);
      
      // Update only the affected handles (previous and next points)
      const prevIdx = (index - 1 + numPoints) % numPoints;
      const currIdx = index % numPoints; // This is the point that was after the removed one
      
      ts.handles[prevIdx] = allHandles[prevIdx];
      if (currIdx < ts.handles.length) {
        ts.handles[currIdx] = allHandles[currIdx];
      }
    }
    
    // Update both spring and magnetic paths if they exist
    if (ts.springPath) {
      ts.springPath.points = JSON.parse(JSON.stringify(ts.points));
      ts.springPath.handles = JSON.parse(JSON.stringify(ts.handles));
    }
    if (ts.magneticPath) {
      ts.magneticPath.points = JSON.parse(JSON.stringify(ts.points));
      ts.magneticPath.handles = JSON.parse(JSON.stringify(ts.handles));
    }
    
    // Redraw
    drawTrace(sizeKey);
  }
  
  function findClosestPointOnCurve(pathEl, x, y) {
    if (!pathEl) return null;
    
    const totalLength = pathEl.getTotalLength();
    const samples = 200;
    let minDist = Infinity;
    let bestLength = 0;
    
    // Sample the path to find closest point
    for (let i = 0; i <= samples; i++) {
      const len = (totalLength * i) / samples;
      const pt = pathEl.getPointAtLength(len);
      const dist = Math.hypot(pt.x - x, pt.y - y);
      
      if (dist < minDist) {
        minDist = dist;
        bestLength = len;
      }
    }
    
    // Get the exact point on the curve at this length
    const exactPoint = pathEl.getPointAtLength(bestLength);
    
    // Find which segment this corresponds to
    const ts = traceBySize.get(activeSize || '_single');
    if (!ts || !ts.points) return null;
    
    const numPoints = ts.points.length;
    const segmentIndex = Math.floor((bestLength / totalLength) * numPoints) % numPoints;
    
    return { 
      point: exactPoint,
      lengthOnPath: bestLength,
      segmentIndex: segmentIndex,
      distance: minDist 
    };
  }

  // ---------- Drill maths + drawing ----------
  function tangentUnitAt(pathEl, s){
    const L = pathEl.getTotalLength();
    const h = Math.max(0.5, L/2000);
    const s1 = Math.max(0, s - h);
    const s2 = Math.min(L, s + h);
    const p1 = pathEl.getPointAtLength(s1);
    const p2 = pathEl.getPointAtLength(s2);
    let tx = p2.x - p1.x, ty = p2.y - p1.y;
    const len = Math.hypot(tx, ty) || 1e-6;
    return { x: tx/len, y: ty/len };
  }
  function inwardNormalUnitAt(pathEl, s, centerX, centerY){
    const t  = tangentUnitAt(pathEl, s);
    const n1 = { x: -t.y, y:  t.x };
    const n2 = { x:  t.y, y: -t.x };
    const p  = pathEl.getPointAtLength(s);
    const toC= { x: centerX - p.x, y: centerY - p.y };
    const dot1 = n1.x*toC.x + n1.y*toC.y;
    const dot2 = n2.x*toC.x + n2.y*toC.y;
    return (dot1 >= dot2) ? n1 : n2;
  }
  function samplePathForTargetXY(pathEl, target, axis, sign, centerX, centerY){
    const N = 2000, L = pathEl.getTotalLength();
    let best = null, bestErr = Infinity;
    for(let i=0;i<=N;i++){
      const s  = (L*i)/N;
      const pt = pathEl.getPointAtLength(s);
      const v  = (axis==='x') ? pt.x : pt.y;
      const otherRel = (axis==='x') ? (pt.y - centerY) : (pt.x - centerX);
      if (sign && Math.sign(otherRel) !== sign) continue;
      const err = Math.abs(v - target);
      if (err < bestErr){ bestErr = err; best = { x:pt.x, y:pt.y, s }; }
    }
    return best;
  }

  // Step 1–4: compute drills only when visible
  function drillsVisible(ts){
    const hasPath      = !!($path.attr('d') || '').trim();
    const isTracePanel = $tabTrace.hasClass('is-active');
    return hasPath && (ts.drillOpen || !isTracePanel);
  }

  function drawDrills(){
    const ts = traceBySize.get(activeSize||'_single');
    svgClear($gDrills);
    if (!ts || !drillsVisible(ts)) return;

    const d = ($path.attr('d')||'').trim();
    if (!d) return;

    const pathEl = $path[0];
    const bbox = pathEl.getBBox();
    if (!bbox || !bbox.width || !bbox.height) return;

    const centerX = bbox.x + bbox.width/2;
    const centerY = bbox.y + bbox.height/2;

    const dpi = getDpi();
    const pxPerMm = dpi / 25.4;

    const leftTopXmm  = parseFloat($drLeftTop.val()) || 20;
    const leftBottomXmm  = parseFloat($drLeftBottom.val()) || 20;
    const bridgeMm = parseFloat($drBridge.val()) || 10;
    const isMag    = (ts.magMode === 'magnetic');

    // LEFT slots (top & bottom) — draw 1.0mm inward along normal at independent X positions
    const targetTopXpx = centerX - (leftTopXmm * pxPerMm);
    const targetBotXpx = centerX - (leftBottomXmm * pxPerMm);
    const topPt = samplePathForTargetXY(pathEl, targetTopXpx, 'x', -1, centerX, centerY);
    const botPt = samplePathForTargetXY(pathEl, targetBotXpx, 'x', +1, centerX, centerY);

    function drawNormalSlotAt(sample, mmLen){
      if(!sample) return;
      const n  = inwardNormalUnitAt(pathEl, sample.s, centerX, centerY);
      const dx = n.x * (mmLen * pxPerMm);
      const dy = n.y * (mmLen * pxPerMm);
      svgAppend($gDrills, svgEl('line', { x1:sample.x, y1:sample.y, x2:sample.x+dx, y2:sample.y+dy, stroke:HOLE_COLOR, 'stroke-width':2 }));
    }
    drawNormalSlotAt(topPt, 1.0);
    drawNormalSlotAt(botPt, 1.0);

    // RIGHT
    if (isMag) {
      // Magnetic → horizontal slot at y = centerY - bridgeMm, ending at v1X()
      const y = centerY - (bridgeMm * pxPerMm);
      const x = v1X();
      const lenPx = 2.0 * pxPerMm;
      svgAppend($gDrills, svgEl('line', { x1:x - lenPx, y1:y, x2:x, y2:y, stroke:HOLE_COLOR, 'stroke-width':2 }));
    } else {
      // Spring → short slot + small hole near right
      const rightEdgePx = bbox.x + bbox.width;
      const startXpx    = rightEdgePx - (10.5 * pxPerMm);
      const startPt     = samplePathForTargetXY(pathEl, startXpx, 'x', -1, centerX, centerY);
      if (startPt){
        const ex = startPt.x - (0.2 * pxPerMm);
        const ey = startPt.y + (0.2 * pxPerMm);
        svgAppend($gDrills, svgEl('line', { x1:startPt.x, y1:startPt.y, x2:ex, y2:ey, stroke:HOLE_COLOR, 'stroke-width':2 }));
        const hx = ex - (0.5 * pxPerMm);
        const hy = ey + (2.5 * pxPerMm);
        svgAppend($gDrills, svgEl('circle', { cx:hx, cy:hy, r:(1.3/2) * pxPerMm, fill:'#fff', stroke:HOLE_COLOR, 'stroke-width':2 }));
      }
    }
  }

  // ---------- Drill toggle / mode ----------
  // (Step 1–4 wired correctly, namespaced, no accidental nesting)

  $drillToggle.off('.clipon').on('click.clipon', function(){
    const key = (activeSize||'_single');
    const ts = traceBySize.get(key); if (!ts || ts.mode!=='edit') return;
    ts.drillOpen = !ts.drillOpen;
    $drillPanel.toggle(ts.drillOpen);
	  $drillToggle.text(ts.drillOpen ? 'Edit Path' : 'Drill');

    if (ts.drillOpen) {
      if (drillsVisible(ts)) { drawDrills(); svgShow($gDrills); }
    } else {
      svgClear($gDrills); svgHide($gDrills);
    }
    updateTraceVisibility();
  });

  $drLeftTop.add($drLeftBottom).add($drBridge).add($drFactorSpring).add($drFactorMag).off('.clipon').on('input.clipon change.clipon', function(){
    const key = (activeSize||'_single');
    const ts = traceBySize.get(key); if (!ts) return;

    // Bridge change in Magnetic → re-position TR & redraw path
    if ($(this).is($drBridge) && ts.magMode==='magnetic') {
      applyMagnetics(ts);
      drawTrace(key); // re-renders anchors/handles; persist inside drawTrace
    }
    
    // Factor Mag change in Magnetic → re-position TR & redraw path
    if ($(this).is($drFactorMag) && ts.magMode==='magnetic') {
      applyMagnetics(ts);
      drawTrace(key); // re-renders anchors/handles; persist inside drawTrace
    }
    
    // Factor Spring change in Spring mode → redraw drills
    if ($(this).is($drFactorSpring) && ts.magMode==='spring') {
      console.log('Spring factor changed to:', $drFactorSpring.val());
      // Persist first to update drillData, then redraw
      persistDrill();
      console.log('After persist, ts.drillData:', ts.drillData);
      if (drillsVisible(ts)) { 
        drawDrills(); 
        svgShow($gDrills); 
      }
      return; // Exit early since we already called persistDrill
    }

    persistDrill();

    if (drillsVisible(ts)) { drawDrills(); svgShow($gDrills); }
    else { svgHide($gDrills); }

    updateTraceVisibility();
  });

  $modeSwitch
    .off('.clipon')
    .on('click.clipon', function(){
      const key = (activeSize||'_single');
      const ts  = traceBySize.get(key); if (!ts) return;
      const nowMag = !(ts.magMode==='magnetic');
      setMode(ts, nowMag ? 'magnetic' : 'spring');
    })
    .on('keydown.clipon', function(e){ if (e.key==='Enter' || e.key===' ') { e.preventDefault(); $(this).trigger('click'); } });

  function setMode(ts, mode){
    ts.magMode = mode;
    const isMag = (mode==='magnetic');
    $modeSwitch.toggleClass('is-on', isMag).attr('aria-checked', isMag ? 'true' : 'false');
    $magOnly.toggle(isMag);
    $springOnly.toggle(!isMag);

    if (isMag) applyMagnetics(ts); else restoreSpring(ts);

    const key = (activeSize||'_single');
    drawTrace(key);          // redraw path + handles (+persistTrace inside)
    persistDrill();

    if (drillsVisible(ts)) { drawDrills(); svgShow($gDrills); }
    else { svgHide($gDrills); }

    updateTraceVisibility();
  }

  function applyMagnetics(ts){
    var trIdx = findTRIndex(ts);
    
    if (!ts.springBackup) {
      ts.springBackup = {
        anchor: { ...ts.points[trIdx] },
        hin:    { ...ts.handles[trIdx].hin },
        hout:   { ...ts.handles[trIdx].hout }
      };
    }

    // factorMag mm above bridge line, x at v1
    const factorMag = (ts.drillData && ts.drillData.factorMag) ? ts.drillData.factorMag : 2.25;
    const yLine = $box.innerHeight()/2 - mmToPx(parseFloat($drBridge.val()) || 10);
    const newY  = yLine - mmToPx(factorMag);
    const newX  = v1X();

    const old  = ts.points[trIdx];
    const dx   = newX - old.x;
    const dy   = newY - old.y;

    ts.points[trIdx] = { x:newX, y:newY };
    ts.handles[trIdx].hin = {
      x: ts.handles[trIdx].hin.x + dx,
      y: ts.handles[trIdx].hin.y + dy
    };

    const L = Math.hypot(
      ts.handles[trIdx].hout.x - old.x,
      ts.handles[trIdx].hout.y - old.y
    );
    ts.handles[trIdx].hout = { x:newX, y:newY + L };
  }

  function restoreSpring(ts){
    if (!ts.springBackup) return;
    var trIdx = findTRIndex(ts);
    ts.points[trIdx]  = { ...ts.springBackup.anchor };
    ts.handles[trIdx] = { hin:{ ...ts.springBackup.hin }, hout:{ ...ts.springBackup.hout } };
    ts.springBackup = null;
  }

  // ---------- Visibility helpers ----------
  function updateTraceVisibility(){
    const ts = traceBySize.get(activeSize||'_single'); if (!ts) return;
    const isTraceActive = $tabTrace.hasClass('is-active');
    const hasPath = !!($path.attr('d')||'').trim();
    const showHoles = drillsVisible(ts);
    const hideEdit  = (ts.drillOpen && isTraceActive) || !isTraceActive;

    (hideEdit || !hasPath) ? svgHide($gAnch) : svgShow($gAnch);
    (hideEdit || !hasPath) ? svgHide($gCtrl) : svgShow($gCtrl);
    (hideEdit || !hasPath) ? svgHide($gLines): svgShow($gLines);

    // Only show/hide drills; drawDrills() is called elsewhere when needed
    showHoles ? svgShow($gDrills) : svgHide($gDrills);

    $path.toggle(hasPath);
    $download.toggle(hasPath && (!ts.mode || ts.mode==='edit'));
	$virtualBtn.toggle(hasPath && (!ts.mode || ts.mode==='edit'));  
	  $drillToggle.text(ts.drillOpen ? 'Edit Path' : 'Drill');
  }

  function showIdleView(){
    const ts = traceBySize.get(activeSize || '_single');
    const hasPath = !!($path.attr('d')||'').trim();

    if (hasPath) {
      $svg.css({ display:'block', pointerEvents:'none' });
      svgHide($gAnch); svgHide($gCtrl); svgHide($gLines);
      if (ts && drillsVisible(ts)) { drawDrills(); svgShow($gDrills); } else { svgHide($gDrills); }
      $download.show();
    } else {
      svgHide($gDrills); svgHide($gAnch); svgHide($gCtrl); svgHide($gLines);
      $svg.css('display','none'); $download.hide();
    }
  }

  // ---------- Persist ----------
  function persistTransform() {
    $hid.transform.val(JSON.stringify({ tx:state.tx, ty:state.ty, scale:state.scale, sx:state.sx, sy:state.sy, rot:state.rot }));
  }
  function persistFrame(){
    $hid.frame.val(JSON.stringify({
      a:parseFloat($frame.A.val())||0, b:parseFloat($frame.B.val())||0, bridge:parseFloat($frame.Br.val())||0,
      h:parseFloat($frame.H.val())||0, material:($frame.Mat.val()||'plastic'), dpi:getDpi(), color:$frame.Col.val()||'#008000'
    }));
  }
  function persistTrace(){
    $hid.path.val($path.attr('d') || '');
    const key = (activeSize||'_single');
    const ts = traceBySize.get(key);
    $hid.points.val(JSON.stringify(ts ? (ts.points||[]) : []));
    $hid.handles.val(JSON.stringify(ts ? (ts.handles||[]) : [])); // per-anchor {hin,hout}
  }
  function persistDrill(){
    const key = (activeSize||'_single');
    const ts = traceBySize.get(key);
    if (!ts) return;
    
    // Store drill data in the traceBySize object (size-specific)
    ts.drillData = {
      mode: ts.magMode,
      leftTopX: parseFloat($drLeftTop.val()) || 20,
      leftBottomX: parseFloat($drLeftBottom.val()) || 20,
      bridge: parseFloat($drBridge.val()) || 10,
      factorSpring: parseFloat($drFactorSpring.val()) || 41,
      factorMag: parseFloat($drFactorMag.val()) || 2.25
    };
    
    // Also persist to hidden field for database storage
    $hid.drill.val(JSON.stringify(ts.drillData));
  }

  // Persist on post submit
  $('#post').on('submit', function(){ persistAll(); });

  // ---------- Download Functions ----------

  // Build and download OMA file
  function buildAndDownloadOMA($section, mode) {
    const key = (activeSize || '_single');
    const ts = traceBySize.get(key);
    if (!ts) {
      alert('No trace data found for current size');
      return;
    }
    
    const pathEl = document.getElementById('clipon-trace-path');
    if(!pathEl){ 
      alert('No trace path found. Click "Trace" and place/edit your path first.'); 
      return; 
    }

    const pathData = pathEl.getAttribute('d');
    if (!pathData || !pathData.trim()) {
      alert('No trace path data found. Please create a trace first.');
      return;
    }

    const bbox = pathEl.getBBox();
    if (!bbox || !bbox.width || !bbox.height) { 
      alert('Path bounds invalid. Adjust your path.'); 
      return; 
    }

    const centerX = bbox.x + bbox.width/2;
    const centerY = bbox.y + bbox.height/2;

    // Get DPI from the frame inputs
    const frameData = readFrameInputs($section);
    const dpi = frameData.dpi;
    const pxPerMm = pxPerMmFromDpi(dpi);
    const omaPerPx = 2540 / dpi;

    // Build radii array for TRCFMT
    const totalSamples = 4000;
    const radii = new Array(400).fill(0);
    const angleStep = 2*Math.PI/400;
    const totalLen = pathEl.getTotalLength();
    
    for (let i = 0; i < totalSamples; i++) {
      const len = (totalLen * i) / totalSamples;
      const pt = pathEl.getPointAtLength(len);
      const x = (pt.x - centerX) * omaPerPx;
      const y = -(pt.y - centerY) * omaPerPx;
      const angle = (Math.atan2(y, x) + 2 * Math.PI) % (2 * Math.PI);
      const idx = Math.floor(angle / angleStep) % 400;
      const dist = Math.hypot(x, y);
      radii[idx] = Math.max(radii[idx], Math.round(dist));
    }

    // Build OMA file content
    const lines = ['TRCFMT=1;400;E;R;F'];
    for (let i = 0; i < 400; i += 10) {
      lines.push('R=' + radii.slice(i, i + 10).join(';'));
    }
    lines.push('ZFMT=0');

    // Add DRILLE records (drill holes)
    const leftTopX = parseFloat($section.find('.clipon-drill-left-top').val()) || 20;
    const leftBottomX = parseFloat($section.find('.clipon-drill-left-bottom').val()) || 20;
    const bridgeMm = parseFloat($section.find('.clipon-drill-bridge').val()) || 10;
    const material = frameData.material;
    const matFactor = materialFactorForSpring(material);

    // Left-side drill holes (top and bottom at independent X positions)
    const targetTopXpx = centerX - (leftTopX * pxPerMm);
    const targetBotXpx = centerX - (leftBottomX * pxPerMm);
    const topPt = samplePathForTargetXY(pathEl, targetTopXpx, 'x', -1, centerX, centerY);
    const botPt = samplePathForTargetXY(pathEl, targetBotXpx, 'x', +1, centerX, centerY);

    function normalSlotMM(sample){
      if (!sample) return null;
      const n = inwardNormalUnitAt(pathEl, sample.s, centerX, centerY);
      const A = pxToMmPoint(sample.x, sample.y, centerX, centerY, dpi);
      const B = pxToMmPoint(sample.x + n.x*(1.0*pxPerMm), sample.y + n.y*(1.0*pxPerMm), centerX, centerY, dpi);
      return { A: {X:fmt1(A.X), Y:fmt1(A.Y)}, B: {X:fmt1(B.X), Y:fmt1(B.Y)} };
    }

    if(topPt){
      const {A,B} = normalSlotMM(topPt);
      // lines.push(`DRILLE=B;C;${A.X};${A.Y};1.0;${B.X};${B.Y};;2;`);
		lines.push(`DRILLE=B;C;${A.X};${A.Y};1.0;`);
    }
    if(botPt){
      const {A,B} = normalSlotMM(botPt);
      // lines.push(`DRILLE=B;C;${A.X};${A.Y};1.0;${B.X};${B.Y};;2;`);
		lines.push(`DRILLE=B;C;${A.X};${A.Y};1.0;`);
    }

    if (mode === 'magnetic') {
      // Right slot: horizontal 2.0 mm to the left at Y = bridgeMm
      const targetYpx = centerY - (bridgeMm * pxPerMm);
      const rightPx = samplePathForTargetXY(pathEl, targetYpx, 'y', +1, centerX, centerY);
      if (rightPx){
        const A = pxToMmPoint(rightPx.x, rightPx.y, centerX, centerY, dpi);
        const B = pxToMmPoint(rightPx.x - (2.0*pxPerMm), rightPx.y, centerX, centerY, dpi);
        const Af = {X:fmt1(A.X), Y:fmt1(A.Y)};
        const Bf = {X:fmt1(B.X), Y:fmt1(B.Y)};
        // enforce same Y (horizontal)
        Bf.Y = Af.Y;
        lines.push(`DRILLE=B;C;${Af.X};${Af.Y};1.0;${Bf.X};${Bf.Y};;2;`);
      }
    } else {
      // Spring: short slot + hole
      const factorSpring = (ts.drillData && ts.drillData.factorSpring) ? ts.drillData.factorSpring : 44;
      console.log('drawDrills spring mode - factorSpring:', factorSpring, 'from ts.drillData:', ts.drillData);
      const deltaMm = (factorSpring - (frameData.bridge - matFactor)) / 2;
      console.log('deltaMm calculated:', deltaMm, 'bridge:', frameData.bridge, 'matFactor:', matFactor);
      const rightEdgePx = bbox.x + bbox.width;
      const startXpx = rightEdgePx - (deltaMm * pxPerMm);
      const startPx = samplePathForTargetXY(pathEl, startXpx, 'x', -1, centerX, centerY);
      if(startPx){
        // slot: 0.2 mm left & 0.2 mm down
        const A = pxToMmPoint(startPx.x, startPx.y, centerX, centerY, dpi);
        const B = pxToMmPoint(startPx.x - (0.2*pxPerMm), startPx.y + (0.2*pxPerMm), centerX, centerY, dpi);
        const Af = {X:fmt1(A.X), Y:fmt1(A.Y)};
        const Bf = {X:fmt1(B.X), Y:fmt1(B.Y)};
        lines.push(`DRILLE=B;C;${Af.X};${Af.Y};2;${Bf.X};${Bf.Y};;2;`);

        // hole: 0.6 mm left & 2.5 mm below end, dia 1.4 mm
        const HpxX = (startPx.x - (0.2*pxPerMm)) - (0.6*pxPerMm);
        const HpxY = (startPx.y + (0.2*pxPerMm)) + (2.5*pxPerMm);
        const H = pxToMmPoint(HpxX, HpxY, centerX, centerY, dpi);
        const Hf = {X:fmt1(H.X), Y:fmt1(H.Y)};
        lines.push(`DRILLE=B;C;${Hf.X};${Hf.Y};${fmt1(1.4)};`);
      }
    }

    // Generate filename
    const brand = cliponGetBrandFromCategories();
    const model = cliponGetModelFromAttributes(false);
    const fb = cliponGetEyeAndBridge();
    const suffix = (mode === 'magnetic') ? 'M' : 'S';
    
    const clean = s => (s || '')
      .toString()
      .replace(/×/g,'')
      .replace(/\(\d+\)\s*$/,'')
      .replace(/\s+/g,' ')
      .trim()
      .replace(/[\s_\-]+/g,'')
      .replace(/[^A-Za-z0-9]/g,'');
    
    const base = (clean(brand) + clean(model) + clean(fb.eyeA) + clean(fb.bridge)) || 'CliponTrace';
	const filename = base.replace(/maketermprimary/gi, '') + suffix + '.oma';
    // Create and download file
    const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    setTimeout(() => {
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }, 0);
  }

  // ---------- Download dropdown wiring ----------
  
  // Grab the existing download UI from your PHP markup
  const $dlWrap = $root.find('.clipon-download');
  const $dlMain = $dlWrap.find('.clipon-download-main');
  const $dlMenu = $dlWrap.find('.clipon-download-menu');
  const $dlMag  = $dlWrap.find('.clipon-download-mag'); // Magnetic
  const $dlSpr  = $dlWrap.find('.clipon-download-spr'); // Spring

  // Helper: do we have a path?
  function cliponHasPath() {
    const d = ($path.attr('d') || '').trim();
    return !!d;
  }

  // Toggle menu on main button click
  $dlMain.off('click.clipon').on('click.clipon', function (e) {
    e.preventDefault();
    e.stopPropagation();
    if (!cliponHasPath()) {
      alert('No trace path found. Click "Trace" and place/edit your path first.');
      return;
    }
    $dlMenu.toggle(); // simple show/hide
  });

  // Close the menu when clicking outside
  $(document).off('click.cliponCloseMenu').on('click.cliponCloseMenu', function () {
    $dlMenu.hide();
  });
  
  // Keep clicks inside the menu from closing it immediately
  $dlMenu.off('click.stopClose').on('click.stopClose', function (e) { 
    e.stopPropagation(); 
  });

  // Wire the two options to the working builders
  $dlMag.off('click.clipon').on('click.clipon', function (e) {
    e.preventDefault();
    if (!cliponHasPath()) return;
    try { 
      buildAndDownloadOMA($root, 'magnetic'); 
    } finally { 
      $dlMenu.hide(); 
    }
  });

  $dlSpr.off('click.clipon').on('click.clipon', function (e) {
    e.preventDefault();
    if (!cliponHasPath()) return;
    try { 
      buildAndDownloadOMA($root, 'spring'); 
    } finally { 
      $dlMenu.hide(); 
    }
  });

  // Optional: keyboard support
  $dlMain.attr('aria-haspopup', 'true').attr('aria-expanded', 'false');
  $dlMain.on('click.cliponA11y', function(){
    $dlMain.attr('aria-expanded', $dlMenu.is(':visible') ? 'true' : 'false');
  });
  $dlMain.on('keydown.cliponA11y', function(e){
    if (e.key === 'ArrowDown' && $dlMenu.is(':hidden') && cliponHasPath()) {
      e.preventDefault();
      $dlMenu.show();
      $dlMag.trigger('focus');
    }
  });
function setMaterialFromTags() {
  setTimeout(function() {
    try {
      var tagElements = document.querySelectorAll('.tagchecklist li');
      console.log('setMaterialFromTags: Found', tagElements.length, 'tag elements');
      
      if (tagElements.length === 0) {
        console.log('setMaterialFromTags: No tags found yet, retrying in 2 seconds...');
        setTimeout(setMaterialFromTags, 2000);
        return;
      }
      
      var tags = [];
      
      for (var i = 0; i < tagElements.length; i++) {
        var rawText = tagElements[i].textContent;
        
        // Extract the actual tag name from "Remove term: TagName TagName"
        var match = rawText.match(/Remove term:\s*([\w-]+)/);
        if (match) {
          var tagText = match[1].toLowerCase().trim();
          if (tagText && tags.indexOf(tagText) === -1) {
            tags.push(tagText);
          }
        }
      }
      
      console.log('setMaterialFromTags: Extracted tags:', tags);
      
      // Material options to match against
      var materialOptions = ['plastic', 'metal', 'semi-rimless'];
      var foundMaterial = null;
      
      // Check if any tag matches a material option
      for (var j = 0; j < materialOptions.length; j++) {
        if (tags.indexOf(materialOptions[j]) !== -1) {
          foundMaterial = materialOptions[j];
          break;
        }
      }
      
      if (foundMaterial) {
        console.log('setMaterialFromTags: Found material:', foundMaterial);
        
        // Set the material dropdown
        $frame.Mat.val(foundMaterial);
        
        // Update all size maps with the selected material
        Object.keys(sizeMap).forEach(function(sizeKey) {
          if (sizeMap[sizeKey] && sizeMap[sizeKey].frame) {
            sizeMap[sizeKey].frame.material = foundMaterial;
          }
        });
        
        // Persist the changes
        persistFrame();
        $hid.sizesMap.val(JSON.stringify(sizeMap));
        
        // Disable the dropdown
        $frame.Mat.prop('disabled', true).css('opacity', '0.6');
        
        // Update guides
        updateGuides();
        
        console.log('setMaterialFromTags: Material set and dropdown disabled');
      } else {
        console.log('setMaterialFromTags: No material tag found in tags:', tags);
      }
    } catch(e) {
      console.error('setMaterialFromTags error:', e);
    }
  }, 3000);  // Increased to 3 seconds
}
  // ---------- Double section ----------
  const $dblList = $('.clipon-double-list');
  
  // Boot: Load saved double images from database
  const savedDoubleImages = readJSON($hid.doubleImgs.val(), []);
  savedDoubleImages.forEach(img => {
    const $B = $(`<div class="clipon-double-box" data-attachment-id="${img.id}">
      <img src="${img.url}">
      <div class="clipon-controls">
        <button type="button" class="button button-small clipon-reset" data-scope="double" data-attachment-id="${img.id}">Remove</button>
      </div>
    </div>`);
    $dblList.append($B);
  });
  
  $('.clipon-upload-double').on('click', function(){
    const frame = wp.media({ title:'Add Images', multiple:true, library:{type:'image'} });
    frame.on('select', function(){
      const out = [];
      frame.state().get('selection').each(a => {
        const j = a.toJSON(); out.push({ id:j.id, url:j.url });
        const $B = $(`<div class="clipon-double-box" data-attachment-id="${j.id}">
            <img src="${j.url}"><div class="clipon-controls"><button type="button" class="button button-small clipon-reset" data-scope="double" data-attachment-id="${j.id}">Remove</button></div>
          </div>`);
        $dblList.append($B);
      });
      const curr = readJSON($hid.doubleImgs.val(), []);
      $hid.doubleImgs.val(JSON.stringify(curr.concat(out)));
    });
    frame.open();
  });
  $dblList.on('click', '.clipon-reset[data-scope="double"]', function(){
    const id = parseInt($(this).data('attachment-id'), 10);
    const list = readJSON($hid.doubleImgs.val(), []).filter(it => parseInt(it.id,10)!==id);
    $hid.doubleImgs.val(JSON.stringify(list));
    $(this).closest('.clipon-double-box').remove();
  });

  // ---------- Utilities ----------
  function readJSON(s, fb){ try{ return JSON.parse(s||''); } catch(_){ return fb; } }
  function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); }

// ---------- Boot ----------
// FORCE ENABLE: Enable size tabs aggressively at multiple points
function forceEnableSizeTabs() {
  if (!$sizeTabs || !$sizeTabs.length) return;
  $sizeTabs.each(function() {
    this.disabled = false;
    this.removeAttribute('disabled');
    $(this).prop('disabled', false)
           .removeAttr('disabled')
           .css({
             'pointer-events': 'auto',
             'cursor': 'pointer',
             'opacity': '1'
           });
  });
  console.log('✓ Force enabled', $sizeTabs.length, 'size tabs');
}

// Enable immediately
if ($sizeTabs.length) {
  forceEnableSizeTabs();
}
	
if (imageUploaded) {
  $wrap.show(); $img.show();
  try {
    const t = readJSON($hid.transform.val(), null); if (t) Object.assign(state, t);
    applyTransform();
    const f = readJSON($hid.frame.val(), null);
    if (f) {
      $frame.A.val(f.a||''); $frame.B.val(f.b||''); $frame.Br.val(f.bridge||''); $frame.H.val(f.h||'');
      $frame.Mat.val(f.material||'plastic'); $frame.DPI.val(f.dpi||350); $frame.Col.val(f.color||'#008000');
    }
    const map = readJSON($hid.sizesMap.val(), {});
    Object.keys(map||{}).forEach(k=>{
      const r = map[k]||{};
      const points  = r.points||[];
      const handles = normalizeControls(r.handles, points);
      const drill = r.drill || {};
      
      if (points.length && handles) {
        traceBySize.set(k, { 
          mode:'edit', 
          points, 
          handles, 
          drillOpen:false, 
          magMode: drill.mode || 'spring', 
          springBackup:null,
          drillData: drill
        });
      }
    });
    if (activeSize) loadSize(activeSize);
    updateGuides(); showIdleView();
    // Call material detection after everything is loaded
    setTimeout(setMaterialFromTags, 100);
  } catch(e){}
} else {
  $wrap.hide();
  // Initialize frame fields from active size tab when no image is uploaded
  if ($sizeTabs.length) {
    const $activeTab = $sizeTabs.filter('.is-active');
    if ($activeTab.length) {
      const sizeLabel = $activeTab.data('size') || $activeTab.text().trim();
      console.log('Active size label:', sizeLabel); // Debug log
      const ab = parseAB(sizeLabel);
      console.log('Parsed AB values:', ab); // Debug log
      
      if (ab && ab.a > 0) {
        $frame.A.val(ab.a);
        $frame.Br.val(ab.bridge);
        console.log('Set A:', ab.a, 'Bridge:', ab.bridge); // Debug log
        updateGuides();
      }
    }
  }
  // Call material detection after DOM is ready
  setTimeout(setMaterialFromTags, 100);
}

if ($resetBtn.length) $resetBtn.on('click', fullReset);
if ($resetTabBtn.length) $resetTabBtn.on('click', fullReset);	

// Re-enable size tabs after everything is initialized
forceEnableSizeTabs();
setTimeout(forceEnableSizeTabs, 100);
setTimeout(forceEnableSizeTabs, 500);

  // ---- size loader ----
  function loadSize(label){
    activeSize = label;
    const rec = sizeMap[label];
    let use = rec;

  if (!use && imageUploaded && baseCalibrated && baseSizeOnUpload && label !== baseSizeOnUpload) {
  const base = sizeMap[baseSizeOnUpload];
  const targetAB = parseAB(label);
  const baseAB   = parseAB(baseSizeOnUpload);
  const baseA = baseAB.a || baseAOnUpload || 0;
  const targetA = targetAB.a || 0;

  if (base && baseA>0 && targetA>0) {
    const k = targetA / baseA;
    const t = Object.assign({}, base.transform || {});
    t.scale = +( (t.scale||1) * k ).toFixed(6);
    
    // Scale the trace path if it exists
    let scaledPoints = [];
    let scaledHandles = [];
    let scaledPath = '';
    
    if (base.points && base.points.length > 0) {
      // Get the center of the image wrapper for scaling reference
      const centerX = $wrap.width() / 2;
      const centerY = $wrap.height() / 2;
      
      // Scale points around the center
      scaledPoints = base.points.map(p => ({
        x: centerX + (p.x - centerX) * k,
        y: centerY + (p.y - centerY) * k
      }));
      
      // Scale handles around the center
      if (base.handles && base.handles.length > 0) {
        scaledHandles = base.handles.map(h => ({
          hin: { 
            x: centerX + (h.hin.x - centerX) * k, 
            y: centerY + (h.hin.y - centerY) * k 
          },
          hout: { 
            x: centerX + (h.hout.x - centerX) * k, 
            y: centerY + (h.hout.y - centerY) * k 
          }
        }));
      }
      
      // Generate scaled path
      scaledPath = buildPathD(scaledPoints, scaledHandles);
    }
    
    use = {
      transform: t,
      frame: Object.assign({}, base.frame || {}, { a: targetA, bridge: targetAB.bridge }),
      path: scaledPath,
      points: scaledPoints,
      handles: scaledHandles,
      drill: base.drill || {}
    };
    sizeMap[label] = use;
  }
}

    if (use) {
      $hid.transform.val(JSON.stringify(use.transform||{}));
      $hid.frame.val(JSON.stringify(use.frame||{}));
      $hid.path.val(use.path||'');
      $hid.points.val(JSON.stringify(use.points||[]));
      $hid.handles.val(JSON.stringify(use.handles||[]));
      $hid.drill.val(JSON.stringify(use.drill||{}));

      Object.assign(state, use.transform||{}); applyTransform();

      const f = use.frame||{};
      $frame.A.val(f.a||''); $frame.B.val(f.b||''); $frame.Br.val(f.bridge||''); $frame.H.val(f.h||'');
      $frame.Mat.val(f.material||'plastic'); $frame.DPI.val(f.dpi||350); $frame.Col.val(f.color||'#008000');
      updateGuides();

      const key = (label||'_single');
      const points  = use.points||[];
      const handles = normalizeControls(use.handles, points);
      const drill = use.drill || {};
      
      // Load drill data into input fields
      $drLeftTop.val(drill.leftTopX || 20);
      $drLeftBottom.val(drill.leftBottomX || 20);
      $drBridge.val(drill.bridge || 10);
      $drFactorSpring.val(drill.factorSpring || 44);
      $drFactorMag.val(drill.factorMag || 2.25);
      
      // Load attributes for this size
      if (use.attributes) {
        applyAttributes(use.attributes);
      }
      
      if (points.length && handles) {
        const ts = { 
          mode: 'edit', 
          points, 
          handles, 
          drillOpen: false, 
          magMode: drill.mode || 'spring', 
          springBackup: null,
          drillData: drill
        };
        traceBySize.set(key, ts);
        
        // Set the mode UI to match loaded drill data
        setMode(ts, ts.magMode);
        
        drawTrace(label);
      } else {
        clearPathAndMask();
      }
    } else {
      const ab = parseAB(label);
      $frame.A.val(ab.a||''); $frame.Br.val(ab.bridge||''); $frame.DPI.val($frame.DPI.val()||350);
      resetTransform(); applyTransform(); updateGuides(); clearPathAndMask();
      
      // Reset attributes
      $('.clipon-position-select').val('');
      $('.clipon-bridge-select').val('');
    }
    
    // Load size-specific image if available
    loadSizeImage(label);
    
    showIdleView();
  }
  
  // Load size-specific image from uploaded media library URLs
  function loadSizeImage(sizeLabel) {
    const $sizeImagesInput = $hidden.find('input[name="_clipon_size_images"]');
    const sizeImages = readJSON($sizeImagesInput.val(), {});
    
    if (sizeImages[sizeLabel]) {
      const imageUrl = sizeImages[sizeLabel];
      console.log('✓ Loading size-specific image for', sizeLabel);
      $img.attr('src', imageUrl);
      $hid.url.val(imageUrl);
      $wrap.show();
      imageUploaded = true;
    } else {
      console.log('No size-specific image for', sizeLabel);
    }
  }
  
  function clearPathAndMask(){ $path.attr('d',''); svgClear($gAnch); svgClear($gCtrl); svgClear($gLines); hideMask(); }

  // ---------- Reset ----------
  function fullReset(){
    $img.attr('src','').hide();
    $wrap.hide();
    imageUploaded = false;
    baseCalibrated = false;
    baseSizeOnUpload = '';
    baseAOnUpload = 0;
    resetTransform(); applyTransform();
    $traceBtn.prop('disabled', true);
    clearPathAndMask(); svgClear($gDrills); $download.hide();
    $hid.id.val(''); $hid.url.val('');
    $hid.transform.val(''); $hid.frame.val('');
    $hid.path.val(''); $hid.points.val(''); $hid.handles.val('');
    $hid.drill.val('');
  }
  
  // ---------- Page Load: Enable Trace tab if data exists ----------
  (function initializeTraceTab() {
    // Check if we have an image (either from single_image_url or size_images)
    const hasSingleImage = !!$hid.url.val();
    const $sizeImagesInput = $hidden.find('input[name="_clipon_size_images"]');
    const sizeImages = readJSON($sizeImagesInput.val(), {});
    const hasSizeImages = Object.keys(sizeImages).length > 0;
    const hasImage = hasSingleImage || hasSizeImages;
    
    // Check if we have path data
    const sizesMap = readJSON($hid.sizesMap.val(), {});
    const hasPathData = Object.keys(sizesMap).length > 0 && Object.values(sizesMap).some(function(sizeData) {
      return sizeData && (sizeData.path || (sizeData.points && sizeData.points.length > 0));
    });
    
    // Enable Trace tab if both conditions are met
    if (hasImage && hasPathData) {
      $tabTrace.removeClass('disabled').prop('disabled', false);
      console.log('✓ Trace tab enabled on page load (image + path data found)');
    } else {
      console.log('Trace tab disabled on page load:', { hasImage, hasPathData });
    }
  })();
});


// Final failsafe: Force enable size tabs after everything loads
jQuery(document).ready(function($) {
  setTimeout(function() {
    $('.clipon-size-tab').each(function() {
      this.disabled = false;
      this.removeAttribute('disabled');
      $(this).prop('disabled', false)
             .removeAttr('disabled')
             .css({
               'pointer-events': 'auto',
               'cursor': 'pointer'
             });
    });
    console.log('✓ Final failsafe: Size tabs forcibly enabled');
  }, 1000);
});

// On WordPress update, ensure attributes are saved
jQuery(function($) {
$('form#post').on('submit', function(e) {
  const $hidden = $('#clipon-hidden');
  const $sizesMapInput = $hidden.find('input[name="_clipon_sizes_map"]');
  const activeSize = $hidden.find('input[name="_clipon_active_size"]').val() || '_single';
  
  // Parse current sizeMap
  let sizeMap;
  try {
    sizeMap = JSON.parse($sizesMapInput.val() || '{}') || {};
  } catch(err) {
    sizeMap = {};
  }
  
  // Get current attributes from UI
  const attrs = {};
  $('.clipon-position-field').each(function() {
    const $field = $(this);
    const pos = $field.data('position');
    if (!pos) return;
    
    const $select = $field.find('.clipon-position-select');
    if ($select.length) {
      const val = $select.val();
      if (val) attrs[pos] = val;
    } else {
      const $valueDiv = $field.find('.clipon-position-value');
      if ($valueDiv.length) {
        const val = $valueDiv.text().trim();
        if (val && val !== '—') attrs[pos] = val;
      }
    }
  });
  
  const $bridgeField = $('.clipon-bridge-field');
  const $bridgeSelect = $bridgeField.find('.clipon-bridge-select');
  if ($bridgeSelect.length) {
    const val = $bridgeSelect.val();
    if (val) attrs['magnetic-bridge'] = val;
  } else {
    const $bridgeValue = $bridgeField.find('.clipon-position-value');
    if ($bridgeValue.length) {
      const val = $bridgeValue.text().trim();
      if (val && val !== '—') attrs['magnetic-bridge'] = val;
    }
  }
  
  // Save attributes to the active size
  if (sizeMap[activeSize]) {
    sizeMap[activeSize].attributes = attrs;
  } else {
    // Create minimal entry for active size
    sizeMap[activeSize] = { attributes: attrs };
  }
  
  // Save back to hidden input
  $sizesMapInput.val(JSON.stringify(sizeMap));
  
  console.log('Pre-submit: Saved attributes for', activeSize, attrs);
});
}); // end jQuery wrapper — form#post submit


// Product Navigation Buttons
jQuery(document).ready(function($) {
  if (typeof cliponNavData !== 'undefined') {
    var $buttons = $('<div class="product-nav-buttons"></div>');
    
    if (cliponNavData.prevUrl) {
      $buttons.append('<a href="' + cliponNavData.prevUrl + '" class="button">← Prev</a>');
    }
    
    if (cliponNavData.nextUrl) {
      $buttons.append('<a href="' + cliponNavData.nextUrl + '" class="button">Next →</a>');
    }
    
    $('.wrap h1').first().css('position', 'relative').append($buttons);
  }
});
// ── Admin Lightbox ── (Order pages)
jQuery(function ($) {
  if (!$('#order_line_items, .woocommerce_order_items, #woocommerce-order-items').length) return;

  if (!$('.clipon-admin-lightbox-overlay').length) {
    $('body').append(
      '<div class="clipon-admin-lightbox-overlay">' +
        '<div class="clipon-admin-lightbox">' +
          '<button class="clipon-lightbox-close">&times;</button>' +
          '<button class="clipon-lightbox-nav clipon-lightbox-prev">&#8249;</button>' +
          '<img src="" alt="View image">' +
          '<button class="clipon-lightbox-nav clipon-lightbox-next">&#8250;</button>' +
          '<div class="clipon-lightbox-counter"><span class="current">1</span> / <span class="total">1</span></div>' +
        '</div>' +
      '</div>'
    );
  }

  var images = [];
  var currentIndex = 0;

  function collectImages() {
    images = [];
    $('.woocommerce_order_items .clipon-view-image, #order_line_items .clipon-view-image').each(function () {
      images.push($(this).data('image'));
    });
  }

  function showLightbox(imageUrl) {
    collectImages();
    currentIndex = images.indexOf(imageUrl);
    if (currentIndex === -1) currentIndex = 0;
    updateLightboxImage();
    $('.clipon-admin-lightbox-overlay').addClass('active');
  }

  function updateLightboxImage() {
    if (!images.length) return;
    $('.clipon-admin-lightbox img').attr('src', images[currentIndex]);
    $('.clipon-lightbox-counter .current').text(currentIndex + 1);
    $('.clipon-lightbox-counter .total').text(images.length);
    $('.clipon-lightbox-prev, .clipon-lightbox-next').toggle(images.length > 1);
  }

  $('.clipon-lightbox-prev').on('click', function (e) {
    e.stopPropagation();
    currentIndex = (currentIndex - 1 + images.length) % images.length;
    updateLightboxImage();
  });

  $('.clipon-lightbox-next').on('click', function (e) {
    e.stopPropagation();
    currentIndex = (currentIndex + 1) % images.length;
    updateLightboxImage();
  });

  $('.clipon-lightbox-close, .clipon-admin-lightbox-overlay').on('click', function (e) {
    if (e.target === this) $('.clipon-admin-lightbox-overlay').removeClass('active');
  });

  $(document).on('keydown', function (e) {
    if (!$('.clipon-admin-lightbox-overlay').hasClass('active')) return;
    if (e.key === 'ArrowLeft')  $('.clipon-lightbox-prev').click();
    if (e.key === 'ArrowRight') $('.clipon-lightbox-next').click();
    if (e.key === 'Escape')     $('.clipon-admin-lightbox-overlay').removeClass('active');
  });

  $(document).on('click', '.clipon-view-image', function (e) {
    e.preventDefault();
    showLightbox($(this).data('image'));
  });
});

// ── Create Product Wizard ── (Order pages)
jQuery(function ($) {
  if (!$('#order_line_items, .woocommerce_order_items, #woocommerce-order-items').length) return;

  $('#order_line_items tr.item, .woocommerce_order_items tbody tr.item').each(function () {
    var $row = $(this);
    var productId = '';

    var $productLink = $row.find('.wc-order-item-name a');
    if ($productLink.length) {
      var href = $productLink.attr('href') || '';
      var match = href.match(/post=(\d+)/);
      if (match) productId = match[1];
    }

    if (!productId) {
      var sku = ($row.find('.wc-order-item-sku').text() || '').replace('SKU:', '').trim();
      if (sku.indexOf('-') > -1) productId = sku.split('-')[0];
    }

    if (productId != '1120') return;

    var brand = '', model = '', sizes = '', lensHeight = '', lensPhoto = '', topPhoto = '', bottomPhoto = '';

    $row.find('.display_meta tbody tr, .wc-order-item-meta tbody tr').each(function () {
      var label = $(this).find('th').text().replace(':', '').trim();
      var $td = $(this).find('td');
      var value = $td.find('p').text().trim() || $td.text().trim();
      if (label === 'Brand')       brand      = value;
      if (label === 'Model')       model      = value;
      if (label === 'Sizes')       sizes      = value;
      if (label === 'Lens Height') lensHeight = value;
      var $link = $td.find('.clipon-view-image');
      if (label === 'Lens')   lensPhoto   = $link.data('image') || '';
      if (label === 'Top')    topPhoto    = $link.data('image') || '';
      if (label === 'Bottom') bottomPhoto = $link.data('image') || '';
    });

    if (!brand || !model) return;

    $.ajax({
      url: ajaxurl, type: 'POST',
      data: { action: 'check_clipon_product_exists', brand: brand, model: model },
      success: function (response) {
        if (response.success && !response.data.exists) {
          var searchTerm = brand + ' ' + model;
          var googleUrl  = 'https://www.google.com/search?q=' + encodeURIComponent(searchTerm);
          var ebayUrl    = 'https://www.ebay.com/sch/i.html?_nkw=' + encodeURIComponent(searchTerm);

          var $metaView = $row.find('.display_meta, .wc-order-item-meta, td.item_cost').first();
          var buttonHtml =
            '<div style="margin-top:10px;padding:10px;border-top:1px solid #ddd;display:flex;align-items:center;gap:8px;overflow-x:auto;">' +
              '<button type="button" class="clipon-create-product-btn"' +
                ' data-brand="' + brand + '" data-model="' + model + '"' +
                ' data-sizes="' + sizes + '" data-lens-height="' + lensHeight + '"' +
                ' data-lens-photo="' + lensPhoto + '" data-top-photo="' + topPhoto + '" data-bottom-photo="' + bottomPhoto + '">' +
                'Add ' + brand + ' ' + model +
              '</button>' +
              '<a href="' + googleUrl + '" target="_blank" class="clipon-search-btn clipon-search-btn-google">' +
                '<svg width="14" height="14" viewBox="0 0 18 18"><path fill="#4285F4" d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z"/><path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z"/><path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z"/><path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 6.29C4.672 4.163 6.656 3.58 9 3.58z"/></svg>' +
                searchTerm +
              '</a>' +
              '<a href="' + ebayUrl + '" target="_blank" class="clipon-search-btn clipon-search-btn-ebay">' +
                '<span class="ebay-e">e</span><span class="ebay-b">b</span><span class="ebay-a">a</span><span class="ebay-y">y</span>' +
                '<span class="ebay-term"> ' + searchTerm + '</span>' +
              '</a>' +
            '</div>';
          $metaView.append(buttonHtml);
        }
      }
    });
  });

  $(document).on('click', '.clipon-create-product-btn', function () {
    var $btn = $(this);

    if ($btn.hasClass('product-created')) {
      var url = $btn.data('edit-url');
      if (url) window.open(url, '_blank');
      return;
    }

    var brand      = $btn.data('brand');
    var model      = $btn.data('model');
    var sizes      = $btn.data('sizes');
    var lensHeight = $btn.data('lens-height');
    var lensPhoto  = $btn.data('lens-photo');
    var topPhoto   = $btn.data('top-photo');
    var bottomPhoto = $btn.data('bottom-photo');

    var popupHtml =
      '<div class="clipon-create-popup-overlay">' +
        '<div class="clipon-create-popup">' +
          '<div class="ccp-header">' +
            '<h2>Create Clip-On Product</h2>' +
            '<div class="ccp-steps">' +
              '<div class="ccp-step active" data-step="1"><div class="ccp-step-num">1</div>Frame Type</div>' +
              '<div class="ccp-step-line"></div>' +
              '<div class="ccp-step" data-step="2"><div class="ccp-step-num">2</div>Frame Info</div>' +
              '<div class="ccp-step-line"></div>' +
              '<div class="ccp-step" data-step="3"><div class="ccp-step-num">3</div>Clip-on Parts</div>' +
            '</div>' +
          '</div>' +
          '<div class="ccp-body">' +
            '<div class="ccp-step-panel" data-panel="1">' +
              '<div class="ccp-frame-types">' +
                '<button type="button" class="ccp-frame-type-btn" data-material="plastic"><img src="https://cliponexpress.com/wp-content/uploads/2026/06/plasticframe.png" class="ccp-type-img" alt="Plastic">Plastic</button>' +
                '<button type="button" class="ccp-frame-type-btn" data-material="metal"><img src="https://cliponexpress.com/wp-content/uploads/2026/06/metal.png" class="ccp-type-img" alt="Metal">Metal</button>' +
                '<button type="button" class="ccp-frame-type-btn" data-material="semi-rimless"><img src="https://cliponexpress.com/wp-content/uploads/2026/06/semirimless.png" class="ccp-type-img" alt="Semi-Rimless">Semi-Rimless</button>' +
                '<button type="button" class="ccp-frame-type-btn" data-material="rimless"><img src="https://cliponexpress.com/wp-content/uploads/2026/06/rimless.png" class="ccp-type-img" alt="Rimless">Rimless</button>' +
              '</div>' +
            '</div>' +
            '<div class="ccp-step-panel" data-panel="2" style="display:none;">' +
              '<div class="ccp-field"><label>Brand</label><input type="text" id="popup-brand" value="' + (brand || '') + '" placeholder="e.g. Ray-Ban" /></div>' +
              '<div class="ccp-field"><label>Model</label><input type="text" id="popup-model" value="' + (model || '') + '" placeholder="e.g. RB5154" /></div>' +
              '<div class="ccp-field"><label>Sizes</label><input type="text" id="popup-sizes" value="' + (sizes || '') + '" placeholder="e.g. 51 ▢ 21" /></div>' +
              '<div class="ccp-field"><label>Lens Height</label><input type="text" id="popup-lens-height" value="' + (lensHeight || '') + '" placeholder="mm" /></div>' +
              '<div class="ccp-field"><label>Lens Photo URL</label><input type="text" id="popup-lens-photo" value="' + (lensPhoto || '') + '" /></div>' +
              '<div class="ccp-field"><label>Top Photo URL</label><input type="text" id="popup-top-photo" value="' + (topPhoto || '') + '" /></div>' +
              '<div class="ccp-field"><label>Bottom Photo URL</label><input type="text" id="popup-bottom-photo" value="' + (bottomPhoto || '') + '" /></div>' +
            '</div>' +
            '<div class="ccp-step-panel" data-panel="3" style="display:none;">' +
              '<div class="ccp-sync-btns">' +
                '<button type="button" class="ccp-sync-btn" id="ccp-sync-all">All same</button>' +
                '<button type="button" class="ccp-sync-btn" id="ccp-sync-pairs">Top &amp; bottom pairs</button>' +
                '<button type="button" class="ccp-sync-btn is-active" id="ccp-sync-diff">All different</button>' +
              '</div>' +
              '<div class="ccp-field"><label>Top Left</label><input type="text" id="popup-top-left" placeholder="e.g. 4-4" /></div>' +
              '<div class="ccp-field"><label>Top Right</label><input type="text" id="popup-top-right" placeholder="e.g. 4-4" /></div>' +
              '<div class="ccp-field"><label>Bottom Left</label><input type="text" id="popup-bottom-left" placeholder="e.g. 4-4" /></div>' +
              '<div class="ccp-field"><label>Bottom Right</label><input type="text" id="popup-bottom-right" placeholder="e.g. 4-4" /></div>' +
              '<div class="ccp-field"><label>Magnetic Bridge</label><input type="text" id="popup-magnetic-bridge" placeholder="e.g. 14-8-1.5" /></div>' +
            '</div>' +
          '</div>' +
          '<div class="ccp-footer">' +
            '<button type="button" class="ccp-btn ccp-btn-secondary" id="ccp-back">Cancel</button>' +
            '<span class="ccp-step-count">Step <span id="ccp-step-label">1</span> of 3</span>' +
            '<button type="button" class="ccp-btn ccp-btn-primary" id="ccp-next">Next \u2192</button>' +
          '</div>' +
        '</div>' +
      '</div>';

    $('body').append(popupHtml);
    var currentStep = 1;

    function ccpUpdateUI() {
      $('.ccp-step-panel').hide();
      $('.ccp-step-panel[data-panel="' + currentStep + '"]').show();
      $('.ccp-step').each(function () {
        var s = +$(this).data('step');
        $(this).removeClass('active done');
        if (s === currentStep) $(this).addClass('active');
        if (s < currentStep)  $(this).addClass('done');
      });
      $('.ccp-step-line').each(function (i) { $(this).toggleClass('done', i < currentStep - 1); });
      $('#ccp-step-label').text(currentStep);
      $('#ccp-back').text(currentStep === 1 ? 'Cancel' : '\u2190 Back');
      $('#ccp-next').text(currentStep === 3 ? 'Create Product' : 'Next \u2192');
    }

    $(document).on('click', '.ccp-frame-type-btn', function () {
      $('.ccp-frame-type-btn').removeClass('selected');
      $(this).addClass('selected');
    });

    // Sync mode: 'all' | 'pairs' | 'different' (default)
    var syncMode = 'different';

    $(document).on('click', '#ccp-sync-all, #ccp-sync-pairs, #ccp-sync-diff', function () {
      $('.ccp-sync-btn').removeClass('is-active');
      $(this).addClass('is-active');
      var id = $(this).attr('id');
      if      (id === 'ccp-sync-all')   syncMode = 'all';
      else if (id === 'ccp-sync-pairs') syncMode = 'pairs';
      else                              syncMode = 'different';
    });

    // Live field syncing based on active mode
    $(document).on('input', '#popup-top-left, #popup-top-right, #popup-bottom-left, #popup-bottom-right', function () {
      var val = $(this).val();
      var id  = $(this).attr('id');
      if (syncMode === 'all') {
        $('#popup-top-left, #popup-top-right, #popup-bottom-left, #popup-bottom-right').not(this).val(val);
      } else if (syncMode === 'pairs') {
        if (id === 'popup-top-left' || id === 'popup-top-right') {
          $('#popup-top-left, #popup-top-right').not(this).val(val);
        } else {
          $('#popup-bottom-left, #popup-bottom-right').not(this).val(val);
        }
      }
      // 'different': no action
    });

    $('#ccp-back').on('click', function () {
      if (currentStep === 1) { $('.clipon-create-popup-overlay').remove(); }
      else { currentStep--; ccpUpdateUI(); }
    });

    $('.clipon-create-popup-overlay').on('click', function (e) {
      if (e.target === this) $('.clipon-create-popup-overlay').remove();
    });

    $('#ccp-next').on('click', function () {
      if (currentStep === 1) {
        if (!$('.ccp-frame-type-btn.selected').length) { alert('Please select a frame type.'); return; }
        currentStep = 2; ccpUpdateUI();
      } else if (currentStep === 2) {
        if (!$('#popup-brand').val() || !$('#popup-model').val()) { alert('Brand and Model are required.'); return; }
        currentStep = 3; ccpUpdateUI();
      } else if (currentStep === 3) {
        var data = {
          action: 'create_clipon_product',
          brand:           $('#popup-brand').val(),
          model:           $('#popup-model').val(),
          sizes:           $('#popup-sizes').val(),
          lens_height:     $('#popup-lens-height').val(),
          lens_photo:      $('#popup-lens-photo').val(),
          top_photo:       $('#popup-top-photo').val(),
          bottom_photo:    $('#popup-bottom-photo').val(),
          top_left:        $('#popup-top-left').val(),
          top_right:       $('#popup-top-right').val(),
          bottom_left:     $('#popup-bottom-left').val(),
          bottom_right:    $('#popup-bottom-right').val(),
          magnetic_bridge: $('#popup-magnetic-bridge').val(),
          material_type:   $('.ccp-frame-type-btn.selected').data('material') || ''
        };
        $('#ccp-next').prop('disabled', true).text('Creating...');
        $.ajax({
          url: ajaxurl, type: 'POST', data: data,
          success: function (response) {
            if (response.success) {
              $('.clipon-create-popup-overlay').remove();
              $btn.text('Created! View Product').css('background', '#46b450').addClass('product-created').data('edit-url', response.data.edit_url);
            } else {
              alert('Error: ' + response.data.message);
              $('#ccp-next').prop('disabled', false).text('Create Product');
            }
          },
          error: function () {
            alert('Error creating product');
            $('#ccp-next').prop('disabled', false).text('Create Product');
          }
        });
      }
    });
  });
});

// ── Addon Toggle + Image Upload ── (Product data panels)
jQuery(function ($) {
  if (!$('#custom_addon_form_data').length) return;

  // Toggle wrapper click
  $('.addon-toggle-wrapper').on('click', function (e) {
    if (e.target.tagName !== 'INPUT') {
      var $cb = $(this).find('input[type="checkbox"]');
      $cb.prop('checked', !$cb.is(':checked'));
    }
  });

  // WP Media uploader for placeholder images
  $(document).on('click', '.addon-upload-image', function (e) {
    e.preventDefault();
    var $button  = $(this);
    var $field   = $button.closest('.addon-image-field');
    var $input   = $field.find('.addon-image-url');
    var $preview = $field.find('.addon-image-preview');

    var mediaUploader = wp.media({
      title: 'Select Placeholder Image',
      button: { text: 'Use this image' },
      multiple: false
    });

    mediaUploader.on('select', function () {
      var attachment = mediaUploader.state().get('selection').first().toJSON();
      $input.val(attachment.url);
      $preview.html('<img src="' + attachment.url + '" style="max-width:150px;height:auto;border-radius:4px;">');
      if (!$field.find('.addon-remove-image').length) {
        $button.after('<button type="button" class="button addon-remove-image">Remove</button>');
      }
    });

    mediaUploader.open();
  });

  $(document).on('click', '.addon-remove-image', function (e) {
    e.preventDefault();
    var $field = $(this).closest('.addon-image-field');
    $field.find('.addon-image-url').val('');
    $field.find('.addon-image-preview').html('<span style="color:#999;">No image set</span>');
    $(this).remove();
  });
});

// ── Category Prefix Field Mover ── (product_cat taxonomy pages)
jQuery(function ($) {
  if (!window.location.href.includes('taxonomy=product_cat')) return;
  var $prefixRow = $('input#category_prefix').closest('tr, .form-field');
  var $nameRow   = $('.form-field.term-name-wrap').closest('tr, .form-field');
  if ($prefixRow.length && $nameRow.length) {
    $prefixRow.insertAfter($nameRow);
  }
});

// ── Bulk Discounts Table ── (WooCommerce settings page)
jQuery(function ($) {
  if (!$('#bulk-discount-table').length) return;

  $('#btn-add-rule').on('click', function () {
    var row =
      '<tr class="bulk-discount-row">' +
        '<td><input type="number" name="bulk_qty[]" min="2" step="1" value="2" required /></td>' +
        '<td><input type="number" name="bulk_discount[]" min="0.01" max="100" step="0.01" value="10" required /> %</td>' +
        '<td><button type="button" class="btn-remove-rule" title="Remove">\u2715</button></td>' +
      '</tr>';
    $('#bulk-discount-tbody').append(row);
  });

  $(document).on('click', '.btn-remove-rule', function () {
    if ($('.bulk-discount-row').length > 1) {
      $(this).closest('tr').remove();
    }
  });
});<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet type="text/xsl" href="//cliponexpress.com/main-sitemap.xsl"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
	<sitemap>
		<loc>https://cliponexpress.com/post-sitemap.xml</loc>
		<lastmod>2023-02-06T22:36:37+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://cliponexpress.com/page-sitemap.xml</loc>
		<lastmod>2026-04-06T12:36:21+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://cliponexpress.com/product-sitemap.xml</loc>
		<lastmod>2026-06-27T01:12:10+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://cliponexpress.com/category-sitemap.xml</loc>
		<lastmod>2023-02-06T22:36:37+00:00</lastmod>
	</sitemap>
	<sitemap>
		<loc>https://cliponexpress.com/product_cat-sitemap.xml</loc>
		<lastmod>2026-06-27T01:12:10+00:00</lastmod>
	</sitemap>
</sitemapindex>
<!-- XML Sitemap generated by Rank Math SEO Plugin (c) Rank Math - rankmath.com -->